From 994102b16e0c16144d60d2658a614d23d481c3b7 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 7 Sep 2012 16:02:00 -0400 Subject: [PATCH 01/82] Move QueryCountDebugMiddleware inclusion to dev.py instead of common.py -- it was too chatty in the logs --- lms/envs/common.py | 1 - lms/envs/dev.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 55fa5b457c..1d33100ee7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -394,7 +394,6 @@ MIDDLEWARE_CLASSES = ( # 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django_comment_client.utils.ViewNameMiddleware', - 'django_comment_client.utils.QueryCountDebugMiddleware', ) ############################### Pipeline ####################################### diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 50befeb875..b90cd97130 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -133,7 +133,8 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True ################################ DEBUG TOOLBAR ################################# INSTALLED_APPS += ('debug_toolbar',) -MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) +MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware',) INTERNAL_IPS = ('127.0.0.1',) DEBUG_TOOLBAR_PANELS = ( From 03ad061a2f91c8b47eb0252999470cba8350dc32 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Thu, 13 Sep 2012 20:10:45 -0400 Subject: [PATCH 02/82] style for submission feedback --- common/lib/xmodule/xmodule/css/capa/display.scss | 13 +++++++++++++ lms/templates/problem.html | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 0591a01843..63b9991a9a 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -432,6 +432,19 @@ section.problem { input.save { @extend .blue-button; } + + .submission_feedback { + // background: #F3F3F3; + // border: 1px solid #ddd; + // @include border-radius(3px); + // padding: 8px 12px; + // margin-top: 10px; + @include inline-block; + font-style: italic; + margin: 8px 0 0 10px; + color: #777; + -webkit-font-smoothing: antialiased; + } } .detailed-solution { diff --git a/lms/templates/problem.html b/lms/templates/problem.html index 65b8193df9..2e94bc963f 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -13,7 +13,7 @@ % if check_button: - + % endif % if reset_button: @@ -26,7 +26,7 @@ % endif % if attempts_allowed :
- You have used ${ attempts_used } of ${ attempts_allowed } submissions + You have used ${ attempts_used } of ${ attempts_allowed } submissions
% endif From 0170fcc01c3466f3fb8a8cb558d7070a0d6ef5cc Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 14 Sep 2012 13:25:55 -0400 Subject: [PATCH 03/82] styles for a slightly better multiple choice and solution span --- .../lib/capa/capa/templates/choicegroup.html | 22 +++++++------ .../lib/xmodule/xmodule/css/capa/display.scss | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index cb11350a63..106694ea4a 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -1,14 +1,4 @@
- - % for choice_id, choice_description in choices: - - % endfor - - % if state == 'unsubmitted': % elif state == 'correct': @@ -18,4 +8,16 @@ % elif state == 'incomplete': % endif + +
+ % for choice_id, choice_description in choices: + + % endfor + +
+
diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 0591a01843..ba3d7e716c 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -36,9 +36,24 @@ section.problem { } .choicegroup { + @include clearfix; + label.choicegroup_correct:after { content: url('../images/correct-icon.png'); } + + > span { + padding-right: 20px; + float: left; + background-position: 0 0 !important; + } + + fieldset { + @include box-sizing(border-box); + float: left; + border-left: 1px solid #ddd; + padding-left: 20px; + } } ol.enumerate { @@ -52,6 +67,23 @@ section.problem { } } + .solution-span { + span { + margin: 20px 0; + display: block; + border: 1px solid #ddd; + padding: 9px 15px 20px; + background: #FFF; + position: relative; + @include box-shadow(inset 0 0 0 1px #eee); + @include border-radius(3px); + + &:empty { + display: none; + } + } + } + div { p { &.answer { From 6472e0ebe338584dd83959df0c27fd70d73159d8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 14 Sep 2012 14:29:31 -0400 Subject: [PATCH 04/82] If there's a ?next param, redirect there instead of dashboard after login --- lms/templates/login_modal.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lms/templates/login_modal.html b/lms/templates/login_modal.html index d7d327178c..0b19399fc0 100644 --- a/lms/templates/login_modal.html +++ b/lms/templates/login_modal.html @@ -46,7 +46,11 @@ (function() { $(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) { if(json.success) { - location.href="${reverse('dashboard')}"; + % if request.REQUEST.get('next', False): + location.href="${request.REQUEST.get('next')}"; + % else: + location.href="${reverse('dashboard')}"; + % endif } else { if($('#login_error').length == 0) { $('#login_form').prepend(''); From a788db53e90de9475dfa889ee883ac4f04a257e0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 14 Sep 2012 14:31:04 -0400 Subject: [PATCH 05/82] Show login modal if there's a next param to index, university index * also replace the external-auth-related popping up of the signup form with jquery. Presumably it takes care of the browser specific stuff. --- common/djangoapps/student/views.py | 2 ++ lms/djangoapps/courseware/views.py | 3 +++ lms/templates/index.html | 30 ++++++------------------------ 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index cbb12e44cc..a70349bec3 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -82,6 +82,8 @@ def index(request, extra_context={}, user=None): domain=domain) context = {'universities': universities, 'entries': entries} context.update(extra_context) + if request.REQUEST.get('next', False): + context['show_login_immediately'] = True return render_to_response('index.html', context) def course_from_id(course_id): diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index c474da8d8b..7da5d06741 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -404,6 +404,9 @@ def university_profile(request, org_id): context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() + if request.REQUEST.get('next', False): + context['show_login_immediately'] = True + return render_to_response(template_file, context) def render_notifications(request, course, notifications): diff --git a/lms/templates/index.html b/lms/templates/index.html index 0ee00b57c0..8cabe62f09 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -147,28 +147,10 @@ % if show_signup_immediately is not UNDEFINED: -% endif +% elif show_login_immediately is not UNDEFINED: + +% endif \ No newline at end of file From 210da2c4f04c29073c5603c7880e986f0f4fe37a Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 14 Sep 2012 16:53:23 -0400 Subject: [PATCH 06/82] Start to remember caption state --- common/lib/xmodule/xmodule/js/src/video/display.coffee | 6 ++++++ .../xmodule/js/src/video/display/video_caption.coffee | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index 3880091661..90e465d0ec 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -9,6 +9,12 @@ class @Video @parseVideos @el.data('streams') @fetchMetadata() @parseSpeed() + + if $.cookie('hide_captions') == 'true' + @el.addClass('closed') + else + @el.removeClass('closed') + $("#video_#{@id}").data('video', this).addClass('video-load-complete') if YT.Player diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee index f65debb1a2..ae6b0c8ed1 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee @@ -1,6 +1,6 @@ class @VideoCaption extends Subview initialize: -> - @loaded = false + @loaded = false bind: -> $(window).bind('resize', @resize) @@ -49,7 +49,7 @@ class @VideoCaption extends Subview @$('.subtitles').html(container.html()) @$('.subtitles li[data-index]').click @seekPlayer - # prepend and append an empty
  • for cosmatic reason + # prepend and append an empty
  • for cosmetic reason @$('.subtitles').prepend($('
  • ').height(@topSpacingHeight())) .append($('
  • ').height(@bottomSpacingHeight())) @@ -131,10 +131,12 @@ class @VideoCaption extends Subview toggle: (event) => event.preventDefault() if @el.hasClass('closed') + $.cookie('hide_captions', 'false', expires: 3650, path: '/') @$('.hide-subtitles').attr('title', 'Turn off captions') @el.removeClass('closed') @scrollCaption() else + $.cookie('hide_captions', 'true', expires: 3650, path: '/') @$('.hide-subtitles').attr('title', 'Turn on captions') @el.addClass('closed') From d893a14a531eb6c636ca2a3c56bcbdae14812a50 Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 14 Sep 2012 17:24:15 -0400 Subject: [PATCH 07/82] Load player before turning off caption --- common/lib/xmodule/xmodule/js/src/video/display.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index 90e465d0ec..e226072b50 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -10,11 +10,6 @@ class @Video @fetchMetadata() @parseSpeed() - if $.cookie('hide_captions') == 'true' - @el.addClass('closed') - else - @el.removeClass('closed') - $("#video_#{@id}").data('video', this).addClass('video-load-complete') if YT.Player @@ -24,6 +19,11 @@ class @Video $('.course-content .video').each -> $(this).data('video').embed() + if $.cookie('hide_captions') == 'true' + @el.addClass('closed') + else + @el.removeClass('closed') + youtubeId: (speed)-> @videos[speed || @speed] From 1625125e6fa4c539fc5e973101f9a876adf19b37 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 15 Sep 2012 16:55:43 -0400 Subject: [PATCH 08/82] Respect object boundaries --- common/lib/xmodule/xmodule/js/src/video/display.coffee | 7 ++----- .../xmodule/js/src/video/display/video_player.coffee | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index e226072b50..2d7cb5fcfa 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -12,6 +12,8 @@ class @Video $("#video_#{@id}").data('video', this).addClass('video-load-complete') + @hide_captions = $.cookie('hide_captions') == 'true' + if YT.Player @embed() else @@ -19,11 +21,6 @@ class @Video $('.course-content .video').each -> $(this).data('video').embed() - if $.cookie('hide_captions') == 'true' - @el.addClass('closed') - else - @el.removeClass('closed') - youtubeId: (speed)-> @videos[speed || @speed] diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 4b265d20c8..79d27d2633 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -1,5 +1,6 @@ class @VideoPlayer extends Subview initialize: -> + console.log(@['video'].hide_captions) # Define a missing constant of Youtube API YT.PlayerState.UNSTARTED = -1 From 439ca1dd59fce07c46ec450d45b6e4f6ed6df7dd Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 15 Sep 2012 17:18:21 -0400 Subject: [PATCH 09/82] Write setter, toggle uses setter --- .../js/src/video/display/video_caption.coffee | 19 ++++++++++++------- .../js/src/video/display/video_player.coffee | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee index ae6b0c8ed1..b1e41afc3c 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee @@ -130,16 +130,21 @@ class @VideoCaption extends Subview toggle: (event) => event.preventDefault() - if @el.hasClass('closed') - $.cookie('hide_captions', 'false', expires: 3650, path: '/') + if @el.hasClass('closed') # Captions are "closed" e.g. turned off + @hideCaptions(false) + else # Captions are on + @hideCaptions(true) + + hideCaptions: (hide_captions) => + if hide_captions + @$('.hide-subtitles').attr('title', 'Turn on captions') + @el.addClass('closed') + else @$('.hide-subtitles').attr('title', 'Turn off captions') @el.removeClass('closed') @scrollCaption() - else - $.cookie('hide_captions', 'true', expires: 3650, path: '/') - @$('.hide-subtitles').attr('title', 'Turn on captions') - @el.addClass('closed') - + $.cookie('hide_captions', hide_captions, expires: 3650, path: '/') + captionHeight: -> if @el.hasClass('fullscreen') $(window).height() - @$('.video-controls').height() diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 79d27d2633..bb89def63d 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -1,6 +1,5 @@ class @VideoPlayer extends Subview initialize: -> - console.log(@['video'].hide_captions) # Define a missing constant of Youtube API YT.PlayerState.UNSTARTED = -1 @@ -46,6 +45,7 @@ class @VideoPlayer extends Subview events: onReady: @onReady onStateChange: @onStateChange + @caption.hideCaptions(@['video'].hide_captions) addToolTip: -> @$('.add-fullscreen, .hide-subtitles').qtip From 68e0701113616d03779974fc9b20bde4f7fc969a Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 15 Sep 2012 17:23:41 -0400 Subject: [PATCH 10/82] Remove extra line --- common/lib/xmodule/xmodule/js/src/video/display.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index 2d7cb5fcfa..6587f05899 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -9,7 +9,6 @@ class @Video @parseVideos @el.data('streams') @fetchMetadata() @parseSpeed() - $("#video_#{@id}").data('video', this).addClass('video-load-complete') @hide_captions = $.cookie('hide_captions') == 'true' From 1a02cfec8350dd8bc98beeef70aaa632ea9b2021 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 15 Sep 2012 23:25:26 -0400 Subject: [PATCH 11/82] Reset CapaProblem when state HTML is corrupt --- common/lib/xmodule/xmodule/capa_module.py | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 8bf1a56404..a344d2981d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -131,17 +131,17 @@ class CapaModule(XModule): self.weight = None if self.rerandomize == 'never': - seed = 1 + self.seed = 1 elif self.rerandomize == "per_student" and hasattr(self.system, 'id'): - seed = system.id + self.seed = system.id else: - seed = None + self.seed = None try: # TODO (vshnayder): move as much as possible of this work and error # checking to descriptor load time self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), - instance_state, seed=seed, system=self.system) + instance_state, seed=self.seed, system=self.system) except Exception as err: msg = 'cannot create LoncapaProblem {loc}: {err}'.format( loc=self.location.url(), err=err) @@ -160,7 +160,7 @@ class CapaModule(XModule): (self.location.url(), msg)) self.lcp = LoncapaProblem( problem_text, self.location.html_id(), - instance_state, seed=seed, system=self.system) + instance_state, seed=self.seed, system=self.system) else: # add extra info and raise raise Exception(msg), None, sys.exc_info()[2] @@ -220,9 +220,10 @@ class CapaModule(XModule): try: html = self.lcp.get_html() except Exception, err: + log.exception(err) + # TODO (vshnayder): another switch on DEBUG. if self.system.DEBUG: - log.exception(err) msg = ( '[courseware.capa.capa_module] ' 'Failed to generate HTML for problem %s' % @@ -231,7 +232,28 @@ class CapaModule(XModule): msg += '

    %s

    ' % traceback.format_exc().replace('<', '<') html = msg else: - raise + # We're in non-debug mode, and possibly even in production. We want + # to avoid bricking of problem as much as possible + + # Presumably, student submission has corrupted LoncapaProblem HTML. + # So, let's try generate a fresh LoncapaProblem + self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), + state=None, # Tabula rasa + seed=self.seed, system=self.system) + + # Prepend a scary warning to the student + warning = '
    ' + warning += '

    Problem state was corruped by invalid input. ' + warning += 'Problem reset to initial state! ' + warning += 'If problem persists, please contact the course staff.

    ' + warning += '
    ' + + html = warning + try: + html += self.lcp.get_html() + except Exception, err: # Couldn't do it. Give up + log.exception(err) + raise content = {'name': self.display_name, 'html': html, From 827e211ac75f59ff0c19373f6b31cde06e2f55c2 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 00:27:41 -0400 Subject: [PATCH 12/82] Reveal student input at state of problem reset back to student --- common/lib/xmodule/xmodule/capa_module.py | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a344d2981d..894d23c342 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -1,3 +1,4 @@ +import cgi import datetime import dateutil import dateutil.parser @@ -236,16 +237,33 @@ class CapaModule(XModule): # to avoid bricking of problem as much as possible # Presumably, student submission has corrupted LoncapaProblem HTML. - # So, let's try generate a fresh LoncapaProblem + # First, pull down all student answers + student_answers = self.lcp.student_answers + answer_ids = student_answers.keys() + + # Some inputtypes, such as dynamath, have additional "hidden" state that + # is not exposed to the student. Keep those hidden + hidden_state_keywords = ['dynamath'] + for answer_id in answer_ids: + for hidden_state_keyword in hidden_state_keywords: + if answer_id.find(hidden_state_keyword) >= 0: + student_answers.pop(answer_id) + + # Next, generate a fresh LoncapaProblem self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), state=None, # Tabula rasa seed=self.seed, system=self.system) # Prepend a scary warning to the student warning = '
    ' - warning += '

    Problem state was corruped by invalid input. ' - warning += 'Problem reset to initial state! ' - warning += 'If problem persists, please contact the course staff.

    ' + warning += '

    Problem reset to initial state!

    ' + warning += '

    Problem state was corruped by invalid submission. The submission consisted of:

    ' + warning += '
      ' + for student_answer in student_answers.values(): + if student_answer != '': + warning += '
    • ' + cgi.escape(student_answer) + '
    • ' + warning += '
    ' + warning += '

    If problem persists, please contact the course staff.

    ' warning += '
    ' html = warning From eac8037808c8c480ce3c34d6e39df06e21c2acdf Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 00:29:48 -0400 Subject: [PATCH 13/82] Add TODO comment --- common/lib/xmodule/xmodule/capa_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 894d23c342..9eeaa57896 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -243,6 +243,7 @@ class CapaModule(XModule): # Some inputtypes, such as dynamath, have additional "hidden" state that # is not exposed to the student. Keep those hidden + # TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id hidden_state_keywords = ['dynamath'] for answer_id in answer_ids: for hidden_state_keyword in hidden_state_keywords: From f5d6f080c7dc05e78dafe6062dcd20e9049393df Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 00:47:40 -0400 Subject: [PATCH 14/82] Add basic styles --- common/lib/xmodule/xmodule/capa_module.py | 14 +++++++------- common/lib/xmodule/xmodule/css/capa/display.scss | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9eeaa57896..f6ba0d278d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -256,16 +256,16 @@ class CapaModule(XModule): seed=self.seed, system=self.system) # Prepend a scary warning to the student - warning = '
    ' - warning += '

    Problem reset to initial state!

    ' - warning += '

    Problem state was corruped by invalid submission. The submission consisted of:

    ' - warning += '
      ' + warning = '
      '\ + '

      Warning: Problem reset to initial state!

      '\ + 'Problem state was corruped by invalid submission. The submission consisted of:'\ + '
        ' for student_answer in student_answers.values(): if student_answer != '': warning += '
      • ' + cgi.escape(student_answer) + '
      • ' - warning += '
      ' - warning += '

      If problem persists, please contact the course staff.

      ' - warning += '
      ' + warning += '
    '\ + 'If problem persists, please contact the course staff.'\ + '
    ' html = warning try: diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 0591a01843..944904ca54 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -465,6 +465,22 @@ section.problem { margin-top: 10px; } + div.capa_reset { + padding: 25px; + border: 1px solid #EBE8BF; + border-radius: 3px; + background: #FFFCDD; + font-size: 1em; + margin-top: 10px; + margin-bottom: 10px; + } + .capa_reset>h2 { + color: #FF0000; + } + .capa_reset li { + font-size: 0.9em; + } + .hints { border: 1px solid #ccc; From 2c239f87e890467111c1933b585a066c6aa658f5 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 00:48:53 -0400 Subject: [PATCH 15/82] Change wording of warning slightly --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index f6ba0d278d..17b5e2921a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -264,7 +264,7 @@ class CapaModule(XModule): if student_answer != '': warning += '
  • ' + cgi.escape(student_answer) + '
  • ' warning += ''\ - 'If problem persists, please contact the course staff.'\ + 'If the problem persists, please contact the course staff.'\ '' html = warning From 7682663e1b90a1566bab37987df02b817863415a Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Sat, 15 Sep 2012 22:07:10 -0700 Subject: [PATCH 16/82] Make broken problem error a bit scarier and fix typo in message --- common/lib/xmodule/xmodule/capa_module.py | 7 ++++--- common/lib/xmodule/xmodule/css/capa/display.scss | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 17b5e2921a..a891474581 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -257,14 +257,15 @@ class CapaModule(XModule): # Prepend a scary warning to the student warning = '
    '\ - '

    Warning: Problem reset to initial state!

    '\ - 'Problem state was corruped by invalid submission. The submission consisted of:'\ + '

    Warning: The problem has been reset to its initial state!

    '\ + 'The problem\'s state was corrupted by an invalid submission. ' \ + 'The submission consisted of:'\ '
      ' for student_answer in student_answers.values(): if student_answer != '': warning += '
    • ' + cgi.escape(student_answer) + '
    • ' warning += '
    '\ - 'If the problem persists, please contact the course staff.'\ + 'If this error persists, please contact the course staff.'\ '
    ' html = warning diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 944904ca54..f349de31b5 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -467,15 +467,15 @@ section.problem { div.capa_reset { padding: 25px; - border: 1px solid #EBE8BF; + border: 1px solid $error-red; + background-color: lighten($error-red, 25%); border-radius: 3px; - background: #FFFCDD; font-size: 1em; margin-top: 10px; margin-bottom: 10px; } .capa_reset>h2 { - color: #FF0000; + color: #AA0000; } .capa_reset li { font-size: 0.9em; From 44d5bfa9d0a41e586543eef1c33e073298fb4cc0 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 10:27:37 -0400 Subject: [PATCH 17/82] Trivial fix to detailed-solution style --- common/lib/xmodule/xmodule/css/capa/display.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index f349de31b5..3e90b3790c 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -437,7 +437,7 @@ section.problem { .detailed-solution { border: 1px solid #ddd; padding: 9px 15px 20px; - margin-bottom: 10px; + margin: 20px 10px; background: #FFF; position: relative; @include box-shadow(inset 0 0 0 1px #eee); From 1dfd222b9850dccf90cebd8434a8f8ef95df1503 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 14:50:40 -0400 Subject: [PATCH 18/82] Escape quotations, lt/gt, ampersand --- common/lib/capa/capa/inputtypes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 187d2fd422..9ae63fb43a 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -333,6 +333,10 @@ def textline_dynamath(element, value, status, render_template, msg=''): if '' in preprocessor.values(): preprocessor = None + # Escape characters in student input for safe XML parsing + escapedict = {'"': '"'} + value = saxutils.escape(value, escapedict) + context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden, 'preprocessor': preprocessor, From 9e01b51f968387b814441672463b31f93d7da3df Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 16 Sep 2012 16:46:19 -0400 Subject: [PATCH 19/82] Tiny fix to style --- common/lib/xmodule/xmodule/css/capa/display.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 3e90b3790c..5f918ff563 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -443,7 +443,7 @@ section.problem { @include box-shadow(inset 0 0 0 1px #eee); @include border-radius(3px); - p:first-child { + > p:first-child { font-size: 0.9em; font-weight: bold; font-style: normal; From 2133ec9686ba41c3d725e7d79276b6a8050e561d Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 17 Sep 2012 11:55:37 -0400 Subject: [PATCH 20/82] fixed word wrap issue on discussions --- lms/static/sass/_discussion.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 78bda06442..c97972333f 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -1260,6 +1260,10 @@ body.discussion { padding: 40px; min-height: 468px; + a { + word-wrap: break-word; + } + h1 { margin-bottom: 10px; font-size: 28px; From d016c0b20aad6651256e4cfc4b504daf75e831a4 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 17 Sep 2012 12:00:08 -0400 Subject: [PATCH 21/82] fixed discussion search cursor bug --- lms/static/sass/_discussion.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index c97972333f..79c853f42d 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -940,7 +940,7 @@ body.discussion { display: block; width: 100%; height: 30px; - padding: 0; + padding: 0 0 0 30px; margin: 14px auto; @include box-sizing(border-box); border: 1px solid #acacac; @@ -951,7 +951,6 @@ body.discussion { font-weight: 400; font-size: 13px; line-height: 20px; - text-indent: 30px; color: #333; outline: 0; cursor: pointer; From 261948f38a7709e1972d5a140f2fd6b7470d3351 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:19:24 -0400 Subject: [PATCH 22/82] Make textbooks in the course definition be stored as data, rather than objects, and turn them into objects during module instatiation --- common/lib/xmodule/xmodule/course_module.py | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 7aa904205d..5ea0c13d65 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -22,9 +22,6 @@ class CourseDescriptor(SequenceDescriptor): self.book_url = book_url self.table_of_contents = self._get_toc_from_s3() - @classmethod - def from_xml_object(cls, xml_object): - return cls(xml_object.get('title'), xml_object.get('book_url')) @property def table_of_contents(self): @@ -57,10 +54,18 @@ class CourseDescriptor(SequenceDescriptor): return table_of_contents - def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) - self.textbooks = self.definition['data']['textbooks'] + + self.textbooks = [] + for title, book_url in self.definition['data']['textbooks']: + try: + self.textbooks.append(self.Textbook(title, book_url)) + except: + # If we can't get to S3 (e.g. on a train with no internet), don't break + # the rest of the courseware. + log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url)) + continue self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course @@ -82,7 +87,6 @@ class CourseDescriptor(SequenceDescriptor): # disable the syllabus content for courses that do not provide a syllabus self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) - def set_grading_policy(self, policy_str): """Parse the policy specified in policy_str, and save it""" try: @@ -94,19 +98,11 @@ class CourseDescriptor(SequenceDescriptor): # the error log. self._grading_policy = {} - @classmethod def definition_from_xml(cls, xml_object, system): textbooks = [] for textbook in xml_object.findall("textbook"): - try: - txt = cls.Textbook.from_xml_object(textbook) - except: - # If we can't get to S3 (e.g. on a train with no internet), don't break - # the rest of the courseware. - log.exception("Couldn't load textbook") - continue - textbooks.append(txt) + textbooks = (textbook.get('title'), textbook.get('book_url')) xml_object.remove(textbook) #Load the wiki tag if it exists @@ -116,7 +112,7 @@ class CourseDescriptor(SequenceDescriptor): wiki_slug = wiki_tag.attrib.get("slug", default=None) xml_object.remove(wiki_tag) - definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system) + definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system) definition.setdefault('data', {})['textbooks'] = textbooks definition['data']['wiki_slug'] = wiki_slug From 34541b5e7c4b597b5f87231345640c64602398cb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:49:42 -0400 Subject: [PATCH 23/82] Make a list of textbooks, don't just store a single one --- common/lib/xmodule/xmodule/course_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5ea0c13d65..3af65d1bd6 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -102,7 +102,7 @@ class CourseDescriptor(SequenceDescriptor): def definition_from_xml(cls, xml_object, system): textbooks = [] for textbook in xml_object.findall("textbook"): - textbooks = (textbook.get('title'), textbook.get('book_url')) + textbooks.append((textbook.get('title'), textbook.get('book_url'))) xml_object.remove(textbook) #Load the wiki tag if it exists From 0a55e1eae13789cc3cad1f693cfc072351f44af5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:51:33 -0400 Subject: [PATCH 24/82] Add sub-environments for the lms that is running alongside the lms for debugging --- lms/envs/cms/__init__.py | 0 lms/envs/{with_cms.py => cms/aws.py} | 0 lms/envs/cms/dev.py | 19 +++++++++++++++++++ rakefile | 4 ++-- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lms/envs/cms/__init__.py rename lms/envs/{with_cms.py => cms/aws.py} (100%) create mode 100644 lms/envs/cms/dev.py diff --git a/lms/envs/cms/__init__.py b/lms/envs/cms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/envs/with_cms.py b/lms/envs/cms/aws.py similarity index 100% rename from lms/envs/with_cms.py rename to lms/envs/cms/aws.py diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py new file mode 100644 index 0000000000..6e4697cccb --- /dev/null +++ b/lms/envs/cms/dev.py @@ -0,0 +1,19 @@ +""" +Settings for the LMS that runs alongside the CMS on AWS +""" + +from ..dev import * + +MODULESTORE = { + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'xmodule', + 'collection': 'modulestore', + 'fs_root': DATA_DIR, + 'render_template': 'mitxmako.shortcuts.render_to_string', + } + } +} diff --git a/rakefile b/rakefile index 9e0bbcbfa4..be5ef1d9e7 100644 --- a/rakefile +++ b/rakefile @@ -125,8 +125,8 @@ TEST_TASKS = [] end # Per environment tasks - Dir["#{system}/envs/*.py"].each do |env_file| - env = File.basename(env_file).gsub(/\.py/, '') + Dir["#{system}/envs/**/*.py"].each do |env_file| + env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors" task "#{system}:check_settings:#{env}" => :predjango do sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") From d17e2aada90f69e30940345037f87d76d3c1b033 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 14:19:07 -0400 Subject: [PATCH 25/82] Remove trailing '/' from ajax_urls --- cms/djangoapps/contentstore/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index d701db33a3..505b897497 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -207,7 +207,7 @@ def preview_module_system(request, preview_id, descriptor): descriptor: An XModuleDescriptor """ return ModuleSystem( - ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']), + ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), # TODO (cpennington): Do we want to track how instructors are using the preview problems? track_function=lambda type, event: None, filestore=descriptor.system.resources_fs, From c0ed79397647f90458fb80a7552f1980fd2679d3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 14:19:23 -0400 Subject: [PATCH 26/82] Add an empty get_errored_courses function for the mongo modulestore --- common/lib/xmodule/xmodule/modulestore/mongo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 7aa05e474f..33901947a6 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -316,3 +316,9 @@ class MongoModuleStore(ModuleStoreBase): {'_id': True}) return [i['_id'] for i in items] + def get_errored_courses(self): + """ + This function doesn't make sense for the mongo modulestore, as courses + are loaded on demand, rather than up front + """ + return {} From 53ec558f115f80eb2bb654ddfdd251de24cf333e Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 17 Sep 2012 14:38:19 -0400 Subject: [PATCH 27/82] fixed solution set glitch --- common/lib/xmodule/xmodule/css/capa/display.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 8a6664b08f..4302e0b92e 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -68,7 +68,7 @@ section.problem { } .solution-span { - span { + > span { margin: 20px 0; display: block; border: 1px solid #ddd; From 3bf8ae8f090a7d26c4840c6d8316f7709c11f706 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 17 Sep 2012 14:44:32 -0400 Subject: [PATCH 28/82] double border fix --- common/lib/xmodule/xmodule/css/capa/display.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 4302e0b92e..8ed67ff20e 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -67,8 +67,7 @@ section.problem { } } - .solution-span { - > span { + span.solution-span { margin: 20px 0; display: block; border: 1px solid #ddd; @@ -81,7 +80,6 @@ section.problem { &:empty { display: none; } - } } div { From 0acc75e3a4f1bb8f7f753d33cf92f425086a576e Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 17 Sep 2012 14:51:20 -0400 Subject: [PATCH 29/82] fixed detailed-solution bug --- .../lib/xmodule/xmodule/css/capa/display.scss | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 8ed67ff20e..d7ffa198af 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -67,18 +67,20 @@ section.problem { } } - span.solution-span { - margin: 20px 0; - display: block; - border: 1px solid #ddd; - padding: 9px 15px 20px; - background: #FFF; - position: relative; - @include box-shadow(inset 0 0 0 1px #eee); - @include border-radius(3px); + .solution-span { + > span { + margin: 20px 0; + display: block; + border: 1px solid #ddd; + padding: 9px 15px 20px; + background: #FFF; + position: relative; + @include box-shadow(inset 0 0 0 1px #eee); + @include border-radius(3px); - &:empty { - display: none; + &:empty { + display: none; + } } } @@ -478,14 +480,6 @@ section.problem { } .detailed-solution { - border: 1px solid #ddd; - padding: 9px 15px 20px; - margin: 20px 10px; - background: #FFF; - position: relative; - @include box-shadow(inset 0 0 0 1px #eee); - @include border-radius(3px); - > p:first-child { font-size: 0.9em; font-weight: bold; From abf2e508939d808f714375814dc1407a9b8fecf8 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 17 Sep 2012 15:15:15 -0400 Subject: [PATCH 30/82] homebrew changed the installation url --- create-dev-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create-dev-env.sh b/create-dev-env.sh index 3664129775..d28a5891d9 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -223,7 +223,7 @@ EO command -v brew &>/dev/null || { output "Installing brew" - /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)" + /usr/bin/ruby <(curl -fsSkL raw.github.com/mxcl/homebrew/go) } command -v git &>/dev/null || { output "Installing git" From f45fa578a26c6274e21391c6698be9ff7e1c356d Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 17 Sep 2012 15:56:09 -0400 Subject: [PATCH 31/82] onreset rerandomize option --- common/lib/xmodule/xmodule/capa_module.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a891474581..0d810af87a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -179,6 +179,8 @@ class CapaModule(XModule): return "per_student" elif rerandomize == "never": return "never" + elif rerandomize == "onreset": + return "onreset" else: raise Exception("Invalid rerandomize attribute " + rerandomize) @@ -307,7 +309,7 @@ class CapaModule(XModule): save_button = False # Only show the reset button if pressing it will show different values - if self.rerandomize != 'always': + if self.rerandomize not in ["always", "onreset"]: reset_button = False # User hasn't submitted an answer yet -- we don't want resets @@ -617,7 +619,7 @@ class CapaModule(XModule): return "Refresh the page and make an attempt before resetting." self.lcp.do_reset() - if self.rerandomize == "always": + if self.rerandomize in ["always", "onreset"]: # reset random number generator seed (note the self.lcp.get_state() # in next line) self.lcp.seed = None From 90f37b3344274db67cfc118f0f377c09227f5cdc Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 17 Sep 2012 14:10:28 -0700 Subject: [PATCH 32/82] Don't use the search API to retrieve all threads. --- .../django_comment_client/forum/views.py | 20 ------------------- .../views/discussion_thread_list_view.coffee | 15 +++++++++++++- lms/templates/discussion/_accordion.html | 0 3 files changed, 14 insertions(+), 21 deletions(-) delete mode 100644 lms/templates/discussion/_accordion.html diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 573badbb92..0abcbf5fbb 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -28,26 +28,6 @@ PAGES_NEARBY_DELTA = 2 escapedict = {'"': '"'} log = logging.getLogger("edx.discussions") -def _general_discussion_id(course_id): - return course_id.replace('/', '_').replace('.', '_') - -def _should_perform_search(request): - return bool(request.GET.get('text', False) or \ - request.GET.get('tags', False)) - -def render_accordion(request, course, discussion_id): - # TODO: Delete if obsolete - discussion_info = utils.get_categorized_discussion_info(request, course) - - context = { - 'course': course, - 'discussion_info': discussion_info, - 'active': discussion_id, - 'csrf': csrf(request)['csrf_token'], - } - - return render_to_string('discussion/_accordion.html', context) - def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise cc.utils.CommentClientError or diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index d4c144f808..6b8abbdc91 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -244,7 +244,8 @@ if Backbone? item = $(event.target).closest('li') if item.find("span.board-name").data("discussion_id") == "#all" @discussionIds = "" - @clearSearch() + @$(".post-search-field").val("") + @retrieveAllThreads() else discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id @retrieveDiscussions(discussionIds) @@ -277,6 +278,18 @@ if Backbone? Content.loadContentInfos(response.content_info) @displayedCollection.reset(@collection.models) + retrieveAllThreads: () -> + url = DiscussionUtil.urlFor("threads") + DiscussionUtil.safeAjax + url: url + type: "GET" + success: (response, textStatus) => + @collection.current_page = response.page + @collection.pages = response.num_pages + @collection.reset(response.discussion_data) + Content.loadContentInfos(response.content_info) + @displayedCollection.reset(@collection.models) + sortThreads: (event) -> @$(".sort-bar a").removeClass("active") $(event.target).addClass("active") diff --git a/lms/templates/discussion/_accordion.html b/lms/templates/discussion/_accordion.html deleted file mode 100644 index e69de29bb2..0000000000 From aaa4db8d299a2bfb4678994f2d493fa35b711eae Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 17 Sep 2012 15:54:24 -0700 Subject: [PATCH 33/82] Fixes image uploades in firefox --- lms/static/js/jquery.ajaxfileupload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/js/jquery.ajaxfileupload.js b/lms/static/js/jquery.ajaxfileupload.js index d761c5ae3c..e588d3555a 100644 --- a/lms/static/js/jquery.ajaxfileupload.js +++ b/lms/static/js/jquery.ajaxfileupload.js @@ -79,7 +79,7 @@ jQuery.extend({ try { if(io.contentWindow){ xml.responseText = io.contentWindow.document.body ? - io.contentWindow.document.body.innerText : null; + io.contentWindow.document.body.textContent || io.contentWindow.document.body.innerText : null; xml.responseXML = io.contentWindow.document.XMLDocument ? io.contentWindow.document.XMLDocument : io.contentWindow.document; From 0fdd0a005fdfab2679bb9fca5b11161d4c1dce1c Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 17 Sep 2012 20:40:06 -0400 Subject: [PATCH 34/82] Add docs to xml_format --- doc/xml-format.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/xml-format.md b/doc/xml-format.md index 29c60fea99..0dd2c59417 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -274,6 +274,7 @@ __Inherited:__ * `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. * `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". * `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) + 'onreset' (students see a different version of the problem when they reset, but are not forced to reset after each check) 'never' (all students see the same version of the problem) 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) Default: 'always'. Optional. From 5d4c60a880c9877615a4839b574995b9e8e0c000 Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 16:20:00 -0700 Subject: [PATCH 35/82] Move new post button handler to DiscussionRouter. --- .../coffee/src/discussion/discussion_router.coffee | 10 ++++++++++ lms/static/js/discussions-temp.js | 11 ----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lms/static/coffee/src/discussion/discussion_router.coffee b/lms/static/coffee/src/discussion/discussion_router.coffee index b2e41cd5af..a5219a68b9 100644 --- a/lms/static/coffee/src/discussion/discussion_router.coffee +++ b/lms/static/coffee/src/discussion/discussion_router.coffee @@ -14,6 +14,9 @@ if Backbone? @newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion) @nav.on "thread:created", @navigateToThread + @newPost = $('.new-post-article') + $('.new-post-btn').bind "click", @showNewPost + $('.new-post-cancel').bind "click", @hideNewPost allThreads: -> @nav.updateSidebar() @@ -43,3 +46,10 @@ if Backbone? navigateToAllThreads: => @navigate("", trigger: true) + + showNewPost: (event) => + @newPost.slideDown(300) + $('.new-post-title').focus() + + hideNewPost: (event) => + @newPost.slideUp(300) diff --git a/lms/static/js/discussions-temp.js b/lms/static/js/discussions-temp.js index dd6af6ef14..ec7c189938 100644 --- a/lms/static/js/discussions-temp.js +++ b/lms/static/js/discussions-temp.js @@ -55,8 +55,6 @@ $(document).ready(function() { // $topicDrop.bind('click', setTopic); $formTopicDropBtn.bind('click', showFormTopicDrop); $formTopicDropMenu.bind('click', setFormTopic); - $('.new-post-btn').bind('click', newPost); - $('.new-post-cancel').bind('click', closeNewPost); $body.delegate('[data-tooltip]', { 'mouseover': showTooltip, @@ -266,15 +264,6 @@ function setTopic(e) { showBrowse(); } -function newPost(e) { - $newPost.slideDown(300); - $('.new-post-title').focus(); -} - -function closeNewPost(e) { - $newPost.slideUp(300); -} - function showFormTopicDrop(e) { $formTopicDropBtn.addClass('is-dropped'); $formTopicDropMenu.show(); From 680e681feef421bc6de0cc41c8754c2a24c57c31 Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 17:13:06 -0700 Subject: [PATCH 36/82] Removed a lot of dead code and moved tooltips into their own file. --- .../src/discussion/views/new_post_view.coffee | 3 - lms/static/js/discussions-temp.js | 222 +----------------- lms/static/js/tooltips.js | 56 +++++ .../discussion/_js_head_dependencies.html | 1 + 4 files changed, 61 insertions(+), 221 deletions(-) create mode 100644 lms/static/js/tooltips.js diff --git a/lms/static/coffee/src/discussion/views/new_post_view.coffee b/lms/static/coffee/src/discussion/views/new_post_view.coffee index 2400a3b18e..ac5d43b60d 100644 --- a/lms/static/coffee/src/discussion/views/new_post_view.coffee +++ b/lms/static/coffee/src/discussion/views/new_post_view.coffee @@ -120,9 +120,6 @@ if Backbone? anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked") follow = false || @$("input.discussion-follow").is(":checked") - $formTopicDropBtn.bind('click', showFormTopicDrop) - $formTopicDropMenu.bind('click', setFormTopic) - url = DiscussionUtil.urlFor('create_thread', @topicId) DiscussionUtil.safeAjax diff --git a/lms/static/js/discussions-temp.js b/lms/static/js/discussions-temp.js index ec7c189938..9ca1a9ba8f 100644 --- a/lms/static/js/discussions-temp.js +++ b/lms/static/js/discussions-temp.js @@ -2,30 +2,16 @@ var $body; var $browse; var $search; var $searchField; -var $topicDrop; var $currentBoard; -var $tooltip; + var $newPost; -var $thread; var $sidebar; var $sidebarWidthStyles; -var $formTopicDropBtn; -var $formTopicDropMenu; var $postListWrapper; -var $dropFilter; -var $topicFilter; var $discussionBody; var sidebarWidth; -var sidebarHeight; -var sidebarHeaderHeight; var sidebarXOffset; var scrollTop; -var discussionsBodyTop; -var discussionsBodyBottom; -var tooltipTimer; -var tooltipCoords; -var SIDEBAR_PADDING = 10; -var SIDEBAR_HEADER_HEIGHT = 87; $(document).ready(function() { @@ -35,13 +21,11 @@ $(document).ready(function() { $searchField = $('.post-search-field'); //$topicDrop = $('.browse-topic-drop-menu-wrapper'); $currentBoard = $('.current-board'); - $tooltip = $('
    '); + $newPost = $('.new-post-article'); $sidebar = $('.sidebar'); $discussionBody = $('.discussion-body'); $postListWrapper = $('.post-list-wrapper'); - $formTopicDropBtn = $('.new-post-article .form-topic-drop-btn'); - $formTopicDropMenu = $('.new-post-article .form-topic-drop-menu-wrapper'); // $dropFilter = $('.browse-topic-drop-search-input'); // $topicFilter = $('.topic-drop-search-input'); $sidebarWidthStyles = $(''); @@ -53,15 +37,8 @@ $(document).ready(function() { //$browse.bind('click', showTopicDrop); //$search.bind('click', showSearch); // $topicDrop.bind('click', setTopic); - $formTopicDropBtn.bind('click', showFormTopicDrop); - $formTopicDropMenu.bind('click', setFormTopic); - - $body.delegate('[data-tooltip]', { - 'mouseover': showTooltip, - 'mousemove': moveTooltip, - 'mouseout': hideTooltip, - 'click': hideTooltip - }); +// $formTopicDropBtn.bind('click', showFormTopicDrop); +// $formTopicDropMenu.bind('click', setFormTopic); $body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop); }); @@ -112,7 +89,6 @@ function filterDrop(e) { /* * single query */ - var $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper'); var query = $(this).val(); var $items = $drop.find('a'); @@ -147,193 +123,3 @@ function filterDrop(e) { } }); } - -function showTooltip(e) { - var tooltipText = $(this).attr('data-tooltip'); - $tooltip.html(tooltipText); - $body.append($tooltip); - $(this).children().css('pointer-events', 'none'); - - tooltipCoords = { - x: e.pageX - ($tooltip.outerWidth() / 2), - y: e.pageY - ($tooltip.outerHeight() + 15) - }; - - $tooltip.css({ - 'left': tooltipCoords.x, - 'top': tooltipCoords.y - }); - - tooltipTimer = setTimeout(function() { - $tooltip.show().css('opacity', 1); - - tooltipTimer = setTimeout(function() { - hideTooltip(); - }, 3000); - }, 500); -} - -function moveTooltip(e) { - tooltipCoords = { - x: e.pageX - ($tooltip.outerWidth() / 2), - y: e.pageY - ($tooltip.outerHeight() + 15) - }; - - $tooltip.css({ - 'left': tooltipCoords.x, - 'top': tooltipCoords.y - }); -} - -function hideTooltip(e) { - $tooltip.hide().css('opacity', 0); - clearTimeout(tooltipTimer); -} - -function showBrowse(e) { - $browse.addClass('is-open'); - $search.removeClass('is-open'); - $searchField.val(''); -} - -function showSearch(e) { - $search.addClass('is-open'); - $browse.removeClass('is-open'); - setTimeout(function() { - $searchField.focus(); - }, 200); -} - -function showTopicDrop(e) { - e.preventDefault(); - - $browse.addClass('is-dropped'); - - if(!$topicDrop[0]) { - $topicDrop = $('.browse-topic-drop-menu-wrapper'); - } - - $topicDrop.show(); - $browse.unbind('click', showTopicDrop); - $body.bind('keyup', setActiveDropItem); - $browse.bind('click', hideTopicDrop); - setTimeout(function() { - $body.bind('click', hideTopicDrop); - }, 0); -} - -function hideTopicDrop(e) { - if(e.target == $('.browse-topic-drop-search-input')[0]) { - return; - } - - $browse.removeClass('is-dropped'); - $topicDrop.hide(); - $body.unbind('click', hideTopicDrop); - $browse.bind('click', showTopicDrop); -} - -function setTopic(e) { - if(e.target == $('.browse-topic-drop-search-input')[0]) { - return; - } - - var $item = $(e.target).closest('a'); - var boardName = $item.find('.board-name').html(); - - $item.parents('ul').not('.browse-topic-drop-menu').each(function(i) { - boardName = $(this).siblings('a').find('.board-name').html() + ' / ' + boardName; - }); - - if(!$currentBoard[0]) { - $currentBoard = $('.current-board'); - } - $currentBoard.html(boardName); - - var fontSize = 16; - $currentBoard.css('font-size', '16px'); - - while($currentBoard.width() > (sidebarWidth * .8) - 40) { - fontSize--; - if(fontSize < 11) { - break; - } - $currentBoard.css('font-size', fontSize + 'px'); - } - - showBrowse(); -} - -function showFormTopicDrop(e) { - $formTopicDropBtn.addClass('is-dropped'); - $formTopicDropMenu.show(); - $formTopicDropBtn.unbind('click', showFormTopicDrop); - $formTopicDropBtn.bind('click', hideFormTopicDrop); - - setTimeout(function() { - $body.bind('click', hideFormTopicDrop); - }, 0); - -} - -function hideFormTopicDrop(e) { - if(e.target == $('.topic-drop-search-input')[0]) { - return; - } - - $formTopicDropBtn.removeClass('is-dropped'); - $formTopicDropMenu.hide(); - $body.unbind('click', hideFormTopicDrop); - $formTopicDropBtn.unbind('click', hideFormTopicDrop); - $formTopicDropBtn.bind('click', showFormTopicDrop); -} - -function setFormTopic(e) { - if(e.target == $('.topic-drop-search-input')[0]) { - return; - } - $formTopicDropBtn.removeClass('is-dropped'); - hideFormTopicDrop(e); - - var $item = $(e.target); - var boardName = $item.html(); - $item.parents('ul').not('.form-topic-drop-menu').each(function(i) { - boardName = $(this).siblings('a').html() + ' / ' + boardName; - }); - $formTopicDropBtn.html(boardName + ' '); -} - -function updateSidebar(e) { - // determine page scroll attributes - scrollTop = $(window).scrollTop(); - discussionsBodyTop = $discussionBody.offset().top; - discussionsBodyBottom = discussionsBodyTop + $discussionBody.height(); - var windowHeight = $(window).height(); - - // toggle fixed positioning - if(scrollTop > discussionsBodyTop - SIDEBAR_PADDING) { - $sidebar.addClass('fixed'); - $sidebar.css('top', SIDEBAR_PADDING + 'px'); - } else { - $sidebar.removeClass('fixed'); - $sidebar.css('top', '0'); - } - - // set sidebar width - var sidebarWidth = .32 * $discussionBody.width() - 10; - $sidebar.css('width', sidebarWidth + 'px'); - - // show the entire sidebar at all times - var sidebarHeight = windowHeight - (scrollTop < discussionsBodyTop - SIDEBAR_PADDING ? discussionsBodyTop - scrollTop : SIDEBAR_PADDING) - SIDEBAR_PADDING - (scrollTop + windowHeight > discussionsBodyBottom + SIDEBAR_PADDING ? scrollTop + windowHeight - discussionsBodyBottom - SIDEBAR_PADDING : 0); - $sidebar.css('height', sidebarHeight > 400 ? sidebarHeight : 400 + 'px'); - - // update the list height - if(!$postListWrapper[0]) { - $postListWrapper = $('.post-list-wrapper'); - } - $postListWrapper.css('height', (sidebarHeight - SIDEBAR_HEADER_HEIGHT - 4) + 'px'); - - // update title wrappers - var titleWidth = sidebarWidth - 115; - $sidebarWidthStyles.html('.discussion-body .post-list a .title { width: ' + titleWidth + 'px !important; }'); -} diff --git a/lms/static/js/tooltips.js b/lms/static/js/tooltips.js new file mode 100644 index 0000000000..3cc771d241 --- /dev/null +++ b/lms/static/js/tooltips.js @@ -0,0 +1,56 @@ +var $body; +var $tooltip; +var tooltipTimer; +var tooltipCoords; +$(document).ready(function() { + $body = $('body'); + $tooltip = $('
    '); + $body.delegate('[data-tooltip]', { + 'mouseover': showTooltip, + 'mousemove': moveTooltip, + 'mouseout': hideTooltip, + 'click': hideTooltip + }); +}); + +function showTooltip(e) { + var tooltipText = $(this).attr('data-tooltip'); + $tooltip.html(tooltipText); + $body.append($tooltip); + $(this).children().css('pointer-events', 'none'); + + tooltipCoords = { + x: e.pageX - ($tooltip.outerWidth() / 2), + y: e.pageY - ($tooltip.outerHeight() + 15) + }; + + $tooltip.css({ + 'left': tooltipCoords.x, + 'top': tooltipCoords.y + }); + + tooltipTimer = setTimeout(function() { + $tooltip.show().css('opacity', 1); + + tooltipTimer = setTimeout(function() { + hideTooltip(); + }, 3000); + }, 500); +} + +function moveTooltip(e) { + tooltipCoords = { + x: e.pageX - ($tooltip.outerWidth() / 2), + y: e.pageY - ($tooltip.outerHeight() + 15) + }; + + $tooltip.css({ + 'left': tooltipCoords.x, + 'top': tooltipCoords.y + }); +} + +function hideTooltip(e) { + $tooltip.hide().css('opacity', 0); + clearTimeout(tooltipTimer); +} diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index bde873fee1..e2adea7899 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -12,6 +12,7 @@ + From bb6f4cdb2eeac002c7809aa7f888b4fa5cf54d1b Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 18:07:52 -0700 Subject: [PATCH 37/82] Move dropdown filter into a more clearly named file, discussion-filter.js, and move the event bindings into the proper views. --- .../views/discussion_thread_list_view.coffee | 1 + .../src/discussion/views/new_post_view.coffee | 1 + ...scussions-temp.js => discussion-filter.js} | 49 ++----------------- .../discussion/_js_head_dependencies.html | 1 + lms/templates/discussion/index.html | 2 - lms/templates/discussion/single_thread.html | 1 - 6 files changed, 6 insertions(+), 49 deletions(-) rename lms/static/js/{discussions-temp.js => discussion-filter.js} (57%) diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index 6b8abbdc91..d998763f50 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -9,6 +9,7 @@ if Backbone? "click .browse-topic-drop-search-input": "ignoreClick" "click .post-list .list-item a": "threadSelected" "click .post-list .more-pages a": "loadMorePages" + 'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop initialize: -> @displayedCollection = new Discussion(@collection.models, pages: @collection.pages) diff --git a/lms/static/coffee/src/discussion/views/new_post_view.coffee b/lms/static/coffee/src/discussion/views/new_post_view.coffee index ac5d43b60d..1c49fdbc8e 100644 --- a/lms/static/coffee/src/discussion/views/new_post_view.coffee +++ b/lms/static/coffee/src/discussion/views/new_post_view.coffee @@ -21,6 +21,7 @@ if Backbone? "click .topic_dropdown_button": "toggleTopicDropdown" "click .topic_menu_wrapper": "setTopic" "click .topic_menu_search": "ignoreClick" + "keyup .form-topic-drop-search-input": DiscussionFilter.filterDrop # Because we want the behavior that when the body is clicked the menu is # closed, we need to ignore clicks in the search field and stop propagation. diff --git a/lms/static/js/discussions-temp.js b/lms/static/js/discussion-filter.js similarity index 57% rename from lms/static/js/discussions-temp.js rename to lms/static/js/discussion-filter.js index 9ca1a9ba8f..31ef3a8a07 100644 --- a/lms/static/js/discussions-temp.js +++ b/lms/static/js/discussion-filter.js @@ -1,49 +1,6 @@ -var $body; -var $browse; -var $search; -var $searchField; -var $currentBoard; +var DiscussionFilter = DiscussionFilter || {}; -var $newPost; -var $sidebar; -var $sidebarWidthStyles; -var $postListWrapper; -var $discussionBody; -var sidebarWidth; -var sidebarXOffset; -var scrollTop; - - -$(document).ready(function() { - $body = $('body'); - //$browse = $('.browse-search .browse'); - //$search = $('.browse-search .search'); - $searchField = $('.post-search-field'); - //$topicDrop = $('.browse-topic-drop-menu-wrapper'); - $currentBoard = $('.current-board'); - - $newPost = $('.new-post-article'); - $sidebar = $('.sidebar'); - $discussionBody = $('.discussion-body'); - $postListWrapper = $('.post-list-wrapper'); - // $dropFilter = $('.browse-topic-drop-search-input'); - // $topicFilter = $('.topic-drop-search-input'); - $sidebarWidthStyles = $(''); - $body.append($sidebarWidthStyles); - - sidebarWidth = $('.sidebar').width(); - sidebarXOffset = $sidebar.offset().top; - - //$browse.bind('click', showTopicDrop); - //$search.bind('click', showSearch); - // $topicDrop.bind('click', setTopic); -// $formTopicDropBtn.bind('click', showFormTopicDrop); -// $formTopicDropMenu.bind('click', setFormTopic); - - $body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop); -}); - -function filterDrop(e) { +DiscussionFilter.filterDrop = function (e) { /* * multiple queries */ @@ -90,7 +47,7 @@ function filterDrop(e) { * single query */ var $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper'); - var query = $(this).val(); + var query = $(e.target).val(); var $items = $drop.find('a'); if(query.length == 0) { diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index e2adea7899..c3a764439c 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -13,6 +13,7 @@ + diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index fc96640a35..8f04470506 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -21,8 +21,6 @@ <%include file="_new_post.html" /> - -
    diff --git a/lms/templates/discussion/single_thread.html b/lms/templates/discussion/single_thread.html index 9ec05b1534..c890c7350f 100644 --- a/lms/templates/discussion/single_thread.html +++ b/lms/templates/discussion/single_thread.html @@ -16,7 +16,6 @@ <%block name="js_extra"> <%include file="_js_body_dependencies.html" /> <%static:js group='discussion'/> - From daa38af58c8ea52c42a13f2ffab6bef81f6da2dc Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 18:55:52 -0700 Subject: [PATCH 38/82] Convert DiscussionFilter to coffeescript. --- .../src/discussion/discussion_filter.coffee | 28 +++++++ lms/static/js/discussion-filter.js | 82 ------------------- 2 files changed, 28 insertions(+), 82 deletions(-) create mode 100644 lms/static/coffee/src/discussion/discussion_filter.coffee delete mode 100644 lms/static/js/discussion-filter.js diff --git a/lms/static/coffee/src/discussion/discussion_filter.coffee b/lms/static/coffee/src/discussion/discussion_filter.coffee new file mode 100644 index 0000000000..6b3ab03689 --- /dev/null +++ b/lms/static/coffee/src/discussion/discussion_filter.coffee @@ -0,0 +1,28 @@ +class @DiscussionFilter + @filterDrop: (e) -> + $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper') + query = $(e.target).val() + $items = $drop.find('a') + + if(query.length == 0) + $items.removeClass('hidden') + return; + + $items.addClass('hidden') + $items.each (i) -> + thisText = $(this).not('.unread').text() + $(this).parents('ul').siblings('a').not('.unread').each (i) -> + thisText = thisText + ' ' + $(this).text(); + + test = true + terms = thisText.split(' ') + + if(thisText.toLowerCase().search(query.toLowerCase()) == -1) + test = false + + if(test) + $(this).removeClass('hidden') + # show children + $(this).parent().find('a').removeClass('hidden'); + # show parents + $(this).parents('ul').siblings('a').removeClass('hidden'); diff --git a/lms/static/js/discussion-filter.js b/lms/static/js/discussion-filter.js deleted file mode 100644 index 31ef3a8a07..0000000000 --- a/lms/static/js/discussion-filter.js +++ /dev/null @@ -1,82 +0,0 @@ -var DiscussionFilter = DiscussionFilter || {}; - -DiscussionFilter.filterDrop = function (e) { - /* - * multiple queries - */ - - // var $drop = $(e.target).parents('.form-topic-drop-menu-wrapper, .browse-topic-drop-menu-wrapper'); - // var queries = $(this).val().split(' '); - // var $items = $drop.find('a'); - - // if(queries.length == 0) { - // $items.show(); - // return; - // } - - // $items.hide(); - // $items.each(function(i) { - // var thisText = $(this).children().not('.unread').text(); - // $(this).parents('ul').siblings('a').not('.unread').each(function(i) { - // thisText = thisText + ' ' + $(this).text(); - // }); - - // var test = true; - // var terms = thisText.split(' '); - - // for(var i = 0; i < queries.length; i++) { - // if(thisText.toLowerCase().search(queries[i].toLowerCase()) == -1) { - // test = false; - // } - // } - - // if(test) { - // $(this).show(); - - // // show children - // $(this).parent().find('a').show(); - - // // show parents - // $(this).parents('ul').siblings('a').show(); - // } - // }); - - - - /* - * single query - */ - var $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper'); - var query = $(e.target).val(); - var $items = $drop.find('a'); - - if(query.length == 0) { - $items.removeClass('hidden'); - return; - } - - $items.addClass('hidden'); - $items.each(function(i) { - var thisText = $(this).not('.unread').text(); - $(this).parents('ul').siblings('a').not('.unread').each(function(i) { - thisText = thisText + ' ' + $(this).text(); - }); - - var test = true; - var terms = thisText.split(' '); - - if(thisText.toLowerCase().search(query.toLowerCase()) == -1) { - test = false; - } - - if(test) { - $(this).removeClass('hidden'); - - // show children - $(this).parent().find('a').removeClass('hidden'); - - // show parents - $(this).parents('ul').siblings('a').removeClass('hidden'); - } - }); -} From aa301a805efb1ef9c4ff3868ad016de9c3d3014a Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 19:19:09 -0700 Subject: [PATCH 39/82] Converted tooltips to coffee too, and removed both converted files from the included js header. --- .../coffee/src/discussion/tooltips.coffee | 47 ++++++++++++++++ lms/static/js/tooltips.js | 56 ------------------- .../discussion/_js_head_dependencies.html | 2 - 3 files changed, 47 insertions(+), 58 deletions(-) create mode 100644 lms/static/coffee/src/discussion/tooltips.coffee delete mode 100644 lms/static/js/tooltips.js diff --git a/lms/static/coffee/src/discussion/tooltips.coffee b/lms/static/coffee/src/discussion/tooltips.coffee new file mode 100644 index 0000000000..d2aa6e4857 --- /dev/null +++ b/lms/static/coffee/src/discussion/tooltips.coffee @@ -0,0 +1,47 @@ +$ -> + new Tooltips + +class @Tooltips + constructor: () -> + @$body = $('body') + @$tooltip = $('
    ') + @$body.delegate '[data-tooltip]', + 'mouseover': @showTooltip, + 'mousemove': @moveTooltip, + 'mouseout': @hideTooltip, + 'click': @hideTooltip + + showTooltip: (e) => + tooltipText = $(e.target).attr('data-tooltip') + @$tooltip.html(tooltipText) + @$body.append(@$tooltip) + $(e.target).children().css('pointer-events', 'none') + + tooltipCoords = + x: e.pageX - (@$tooltip.outerWidth() / 2) + y: e.pageY - (@$tooltip.outerHeight() + 15) + + @$tooltip.css + 'left': tooltipCoords.x, + 'top': tooltipCoords.y + + @tooltipTimer = setTimeout ()=> + @$tooltip.show().css('opacity', 1) + + @tooltipTimer = setTimeout ()=> + @hideTooltip() + , 3000 + , 500 + + moveTooltip: (e) => + tooltipCoords = + x: e.pageX - (@$tooltip.outerWidth() / 2) + y: e.pageY - (@$tooltip.outerHeight() + 15) + + @$tooltip.css + 'left': tooltipCoords.x + 'top': tooltipCoords.y + + hideTooltip: (e) => + @$tooltip.hide().css('opacity', 0) + clearTimeout(@tooltipTimer) diff --git a/lms/static/js/tooltips.js b/lms/static/js/tooltips.js deleted file mode 100644 index 3cc771d241..0000000000 --- a/lms/static/js/tooltips.js +++ /dev/null @@ -1,56 +0,0 @@ -var $body; -var $tooltip; -var tooltipTimer; -var tooltipCoords; -$(document).ready(function() { - $body = $('body'); - $tooltip = $('
    '); - $body.delegate('[data-tooltip]', { - 'mouseover': showTooltip, - 'mousemove': moveTooltip, - 'mouseout': hideTooltip, - 'click': hideTooltip - }); -}); - -function showTooltip(e) { - var tooltipText = $(this).attr('data-tooltip'); - $tooltip.html(tooltipText); - $body.append($tooltip); - $(this).children().css('pointer-events', 'none'); - - tooltipCoords = { - x: e.pageX - ($tooltip.outerWidth() / 2), - y: e.pageY - ($tooltip.outerHeight() + 15) - }; - - $tooltip.css({ - 'left': tooltipCoords.x, - 'top': tooltipCoords.y - }); - - tooltipTimer = setTimeout(function() { - $tooltip.show().css('opacity', 1); - - tooltipTimer = setTimeout(function() { - hideTooltip(); - }, 3000); - }, 500); -} - -function moveTooltip(e) { - tooltipCoords = { - x: e.pageX - ($tooltip.outerWidth() / 2), - y: e.pageY - ($tooltip.outerHeight() + 15) - }; - - $tooltip.css({ - 'left': tooltipCoords.x, - 'top': tooltipCoords.y - }); -} - -function hideTooltip(e) { - $tooltip.hide().css('opacity', 0); - clearTimeout(tooltipTimer); -} diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index c3a764439c..bde873fee1 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -12,8 +12,6 @@ - - From 7fb757c86c2e2c93c8c71cc2d1cef6f18de0d930 Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Mon, 17 Sep 2012 23:03:36 -0700 Subject: [PATCH 40/82] Rename Tooltips => TooltipManager. --- .../discussion/{tooltips.coffee => tooltip_manager.coffee} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename lms/static/coffee/src/discussion/{tooltips.coffee => tooltip_manager.coffee} (96%) diff --git a/lms/static/coffee/src/discussion/tooltips.coffee b/lms/static/coffee/src/discussion/tooltip_manager.coffee similarity index 96% rename from lms/static/coffee/src/discussion/tooltips.coffee rename to lms/static/coffee/src/discussion/tooltip_manager.coffee index d2aa6e4857..695ad52e3d 100644 --- a/lms/static/coffee/src/discussion/tooltips.coffee +++ b/lms/static/coffee/src/discussion/tooltip_manager.coffee @@ -1,7 +1,7 @@ $ -> - new Tooltips + new TooltipManager -class @Tooltips +class @TooltipManager constructor: () -> @$body = $('body') @$tooltip = $('
    ') From d9219c608610f123b4c345b170e3676194c4dea8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sun, 16 Sep 2012 21:55:05 -0400 Subject: [PATCH 41/82] Custom tabs * specify in tabs list in course policy - active page tracking now done in tabs.py - properly handle the fact that there may be multiple textbooks * Still need: - wiki pages - (if that's delayed, special-case syllabus support) --- common/lib/xmodule/xmodule/course_module.py | 11 +- common/lib/xmodule/xmodule/modulestore/xml.py | 1 - doc/xml-format.md | 47 +++- lms/djangoapps/courseware/tabs.py | 233 ++++++++++++++++++ lms/djangoapps/staticbook/views.py | 6 +- .../courseware/course_navigation.html | 49 +--- lms/templates/staticbook.html | 2 +- 7 files changed, 301 insertions(+), 48 deletions(-) create mode 100644 lms/djangoapps/courseware/tabs.py diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 7aa904205d..f0751c5462 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,9 +1,9 @@ from fs.errors import ResourceNotFoundError -import time import logging -import requests from lxml import etree from path import path # NOTE (THK): Only used for detecting presence of syllabus +import requests +import time from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy @@ -134,6 +134,13 @@ class CourseDescriptor(SequenceDescriptor): def grade_cutoffs(self): return self._grading_policy['GRADE_CUTOFFS'] + @property + def tabs(self): + """ + Return the tabs config, as a python object, or None if not specified. + """ + return self.metadata.get('tabs') + @property def show_calculator(self): return self.metadata.get("show_calculator", None) == "Yes" diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 82b0abd8ab..874c7d3d7f 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -414,7 +414,6 @@ class XMLModuleStore(ModuleStoreBase): policy_str = self.read_grading_policy(paths, tracker) course_descriptor.set_grading_policy(policy_str) - log.debug('========> Done with course import from {0}'.format(course_dir)) return course_descriptor diff --git a/doc/xml-format.md b/doc/xml-format.md index 29c60fea99..181a814069 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -219,6 +219,13 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}. * The order in which things appear does not matter, though it may be helpful to organize the file in the same order as things appear in the content. * NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This can be irritating at first. +Supported fields at the course level: + +* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00". +* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start". +* "tabs" -- have custom tabs in the courseware. See below for details on config. +* TODO: there are others + ### Grading policy file contents TODO: This needs to be improved, but for now here's a sketch of how grading works: @@ -340,7 +347,45 @@ If you look at some older xml, you may see some tags or metadata attributes that # Static links -if your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this in `YOUR_COURSE_DIR/blah/ponies.jpg`. Note that this is not looking in a `static/` subfolder in your course dir. This may (should?) change at some point. Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. +If your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this... + +* If your course dir has a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/static/blah/ponies.jpg`. This is the prefered organization, as it does not expose anything except what's in `static/` to the world. +* If your course dir does not have a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/blah/ponies.jpg`. This is the old organization, and requires that the web server allow access to everything in the couse dir. To switch to the new organization, move all your static content into a new `static/` dir (e.g. if you currently have things in `images/`, `css/`, and `special/`, create a dir called `static/`, and move `images/, css/, and special/` there). + +Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. + +# Tabs + +If you want to customize the courseware tabs displayed for your course, specify a "tabs" list in the course-level policy. e.g.: + +"tabs" : [ +{ "type": "courseware"}, # no name--always "Courseware" for consistency between courses +{"name": "Course Info", + "type": course_info"}, +{"name": "My Discussion", + "type": external_"link", + "link": "http://www.mydiscussion.org/blah"}, +{"name": "Progress", +"type": "Progress"}, +{"name": "Wonderwiki", + "type": "wiki"}, +{"type": "textbooks"} # generates one tab per textbook, taking names from the textbook titles +] + + +* If you specify any tabs, you must specify all tabs. They will appear in the order given. +* The first two tabs must have types "courseware" and "course_info", in that order. Otherwise, we'll refuse to load the course. +* An Instructor tab will be automatically added at the end for course staff users. + +## Supported tab types: + +* "courseware". No other parameters. +* "course_info". Parameter "name". +* "wiki". Parameter "name". +* "discussion". Parameter "name". +* "external_link". Parameters "name", "link". +* "textbooks". No parameters--generates tab names from book titles. +* "progress". Parameter "name". # Tips for content developers diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py new file mode 100644 index 0000000000..01753625b8 --- /dev/null +++ b/lms/djangoapps/courseware/tabs.py @@ -0,0 +1,233 @@ +""" +Tabs configuration. By the time the tab is being rendered, it's just a name, +link, and css class (CourseTab tuple). Tabs are specified in course policy. +Each tab has a type, and possibly some type-specific parameters. + +To add a new tab type, add a TabImpl to the VALID_TAB_TYPES dict below--it will +contain a validation function that checks whether config for the tab type is +valid, and a generator function that takes the config, user, and course, and +actually generates the CourseTab. +""" + +from collections import namedtuple +import logging + +from django.conf import settings +from django.core.urlresolvers import reverse + +from courseware.access import has_access + +log = logging.getLogger(__name__) + +class InvalidTabsException(Exception): + """ + A complaint about invalid tabs. + """ + pass + +CourseTab = namedtuple('CourseTab', 'name link is_active') + +# encapsulate implementation for a tab: +# - a validation function: takes the config dict and raises +# InvalidTabsException if required fields are missing or otherwise +# wrong. (e.g. "is there a 'name' field?). Validators can assume +# that the type field is valid. +# +# - a function that takes a config, a user, and a course, and active_page and +# return a list of CourseTabs. (e.g. "return a CourseTab with specified +# name, link to courseware, and is_active=True/False"). The function can +# assume that it is only called with configs of the appropriate type that +# have passed the corresponding validator. +TabImpl = namedtuple('TabImpl', 'validator generator') + + +##### Generators for various tabs. + +def _tab(name, view_name, is_active, extra_args=[]): + """Return a CourseTab when link is reverse of css class with course_id""" + return CourseTab(name, reverse(class_name, args=[course.id]), class_name) + +def _courseware(tab, user, course, active_page): + link = reverse('courseware', args=[course.id]) + return [CourseTab('Courseware', link, active_page == "courseware")] + +def _course_info(tab, user, course, active_page): + link = reverse('info', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == "info")] + +def _progress(tab, user, course, active_page): + if user.is_authenticated(): + link = reverse('progress', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == "progress")] + return [] + +def _wiki(tab, user, course, active_page): + if settings.WIKI_ENABLED: + link = reverse('course_wiki', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == 'wiki')] + return [] + +def _discussion(tab, user, course, active_page): + """ + This tab format only supports the new Berkeley discussion forums. + """ + if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): + link = reverse('django_comment_client.forum.views.forum_form_discussion', + args=[course.id]) + return [CourseTab(tab['name'], link, active_page=='discussion')] + return [] + +def _external_link(tab, user, course, active_page): + # external links are never active + return [CourseTab(tab['name'], tab['link'], False)] + + +def _textbooks(tab, user, course, active_page): + """ + Generates one tab per textbook. Only displays if user is authenticated. + """ + if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): + # since there can be more than one textbook, active_page is e.g. "book/0". + return [CourseTab(textbook.title, reverse('book', args=[course.id, index]), + active_page=="textbook/{0}".format(index)) + for index, textbook in enumerate(course.textbooks)] + return [] + +#### Validators + + +def key_checker(expected_keys): + """ + Returns a function that checks that specified keys are present in a dict + """ + def check(d): + for k in expected_keys: + if k not in d: + raise InvalidTabsException("Key {0} not present in {1}" + .format(k, d)) + return check + + +need_name = key_checker(['name']) + +def null_validator(d): + """ + Don't check anything--use for tabs that don't need any params. (e.g. textbook) + """ + pass + +##### The main tab config dict. + +# type -> TabImpl +VALID_TAB_TYPES = { + 'courseware': TabImpl(null_validator, _courseware), + 'course_info': TabImpl(need_name, _course_info), + 'wiki': TabImpl(need_name, _wiki), + 'discussion': TabImpl(need_name, _discussion), + 'external_link': TabImpl(key_checker(['name', 'link']), _external_link), + 'textbooks': TabImpl(null_validator, _textbooks), + 'progress': TabImpl(need_name, _progress), + } + + +### External interface below this. + +def validate_tabs(course): + """ + Check that the tabs set for the specified course is valid. If it + isn't, raise InvalidTabsException with the complaint. + + Specific rules checked: + - if no tabs specified, that's fine + - if tabs specified, first two must have type 'courseware' and 'course_info', in that order. + - All the tabs must have a type in VALID_TAB_TYPES. + + """ + tabs = course.tabs + if tabs is None: + return + + if len(tabs) < 2: + raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs)) + if tabs[0]['type'] != 'courseware': + raise InvalidTabsException( + "Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs)) + if tabs[1]['type'] != 'course_info': + raise InvalidTabsException( + "Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs)) + for t in tabs: + if t['type'] not in VALID_TAB_TYPES: + raise InvalidTabsException("Unknown tab type {0}. Known types: {1}" + .format(t['type'], VALID_TAB_TYPES)) + # the type-specific validator checks the rest of the tab config + VALID_TAB_TYPES[t['type']].validator(t) + + # Possible other checks: make sure tabs that should only appear once (e.g. courseware) + # are actually unique (otherwise, will break active tag code) + + +def get_course_tabs(user, course, active_page): + """ + Return the tabs to show a particular user, as a list of CourseTab items. + """ + if not course.tabs: + return get_default_tabs(user, course, active_page) + + # TODO (vshnayder): There needs to be a place to call this right after course + # load, but not from inside xmodule, since that doesn't (and probably + # shouldn't) know about the details of what tabs are supported, etc. + validate_tabs(course) + + tabs = [] + for tab in course.tabs: + # expect handlers to return lists--handles things that are turned off + # via feature flags, and things like 'textbook' which might generate + # multiple tabs. + gen = VALID_TAB_TYPES[tab['type']].generator + tabs.extend(gen(tab, user, course, active_page)) + + # Instructor tab is special--automatically added if user is staff for the course + if has_access(user, course, 'staff'): + tabs.append(CourseTab('Instructor', + reverse('instructor_dashboard', args=[course.id]), + active_page == 'instructor')) + return tabs + + +def get_default_tabs(user, course, active_page): + + # When calling the various _tab methods, can omit the 'type':'blah' from the + # first arg, since that's only used for dispatch + tabs = [] + tabs.extend(_courseware({''}, user, course, active_page)) + tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page)) + + if hasattr(course, 'syllabus_present') and course.syllabus_present: + link = reverse('syllabus', args=[course.id]) + tabs.append(CourseTab('Syllabus', link, active_page=='syllabus')) + + tabs.extend(_textbooks({}, user, course, active_page)) + + ## If they have a discussion link specified, use that even if we feature + ## flag discussions off. Disabling that is mostly a server safety feature + ## at this point, and we don't need to worry about external sites. + if course.discussion_link: + tabs.append(CourseTab('Discussion', course.discussion_link, active_page == 'discussion')) + elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): + link = reverse('django_comment_client.forum.views.forum_form_discussion', + args=[course.id]) + tabs.append(CourseTab('Discussion', link, active_page == 'discussion')) + elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION'): + ## This is Askbot, which we should be retiring soon... + tabs.append(CourseTab('Discussion', reverse('questions'), active_page == 'discussion')) + + tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page)) + + if user.is_authenticated() and not course.hide_progress_tab: + tabs.extend(_progress({'name': 'Progress'}, user, course, active_page)) + + if has_access(user, course, 'staff'): + link = reverse('instructor_dashboard', args=[course.id]) + tabs.append(CourseTab('Instructor', link, active_page=='instructor')) + + return tabs diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index d68117dd8a..37087af597 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -11,11 +11,13 @@ def index(request, course_id, book_index, page=0): course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') - textbook = course.textbooks[int(book_index)] + book_index = int(book_index) + textbook = course.textbooks[book_index] table_of_contents = textbook.table_of_contents return render_to_response('staticbook.html', - {'page': int(page), 'course': course, 'book_url': textbook.book_url, + {'book_index': book_index, 'page': int(page), + 'course': course, 'book_url': textbook.book_url, 'table_of_contents': table_of_contents, 'staff_access': staff_access}) diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index ffa8b0cadd..5ae69908fb 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -6,54 +6,21 @@ if active_page == None and active_page_context is not UNDEFINED: # If active_page is not passed in as an argument, it may be in the context as active_page_context active_page = active_page_context -def url_class(url): - if url == active_page: +def url_class(is_active): + if is_active: return "active" return "" %> -<%! from django.core.urlresolvers import reverse %> -<%! from courseware.access import has_access %> +<%! from courseware.tabs import get_course_tabs %>
    -
    + From da4206f170c24088aec0a388315ead69907e2d5e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 18 Sep 2012 15:31:32 -0400 Subject: [PATCH 55/82] Remove scratch pad ui WIPs, until we decide we actually want it --- cms/templates/widgets/navigation.html | 25 ----------- cms/templates/widgets/sequence-edit.html | 55 ------------------------ 2 files changed, 80 deletions(-) diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index ce18e867bd..7da9cf9a3d 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -68,31 +68,6 @@ %endfor -
  • -
    -

    Course Scratch Pad

    -
    - - -
  • diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html index d92ccbb7a7..2b58323177 100644 --- a/cms/templates/widgets/sequence-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -48,61 +48,6 @@
    - -
    -
      -
    1. - <%include file="new-module.html"/> -
    2. -
    3. -
      -

      Section Scratch

      -
      - -
    4. -
    5. -
      -

      Course Scratch

      -
      - - -
    6. -
    -
    From 3af4ce887f1e1ec7ee8d39ac3a8e46d647dd9807 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 18 Sep 2012 16:01:31 -0400 Subject: [PATCH 56/82] removed extraneous comments --- lms/static/sass/_discussion.scss | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 582ca9871c..4c8476f301 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -1171,17 +1171,6 @@ body.discussion { } } - // .title { - // display: block; - // float: left; - // width: 70%; - // margin: 6px 0; - // line-height: 1.4; - // white-space: nowrap; - // text-overflow: ellipsis; - // overflow: hidden; - // } - .votes-count, .comments-count { display: block; From d401a3270932195c458052e96718903ab5e56835 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 18 Sep 2012 16:39:58 -0400 Subject: [PATCH 57/82] Don't break on empty answer string --- common/lib/capa/capa/responsetypes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 5b89b78867..0909deea3a 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1627,6 +1627,10 @@ class ImageResponse(LoncapaResponse): for aid in self.answer_ids: # loop through IDs of fields in our stanza given = student_answers[aid] # this should be a string of the form '[x,y]' + if not given: # No answer to parse. Mark as incorrect and move on + correct_map.set(aid, 'incorrect') + continue + # parse expected answer # TODO: Compile regexp on file load m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', '')) From 0b3ae9052afef1414317ac54384b714c37abb68e Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 18 Sep 2012 19:19:08 -0400 Subject: [PATCH 58/82] Make the default topic a dict so it doesn't error on courses that don't specify topics --- lms/djangoapps/django_comment_client/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 305f3d0929..476d5b0a6a 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -189,7 +189,7 @@ def initialize_discussion_info(course): "sort_key": entry["sort_key"], "start_date": entry["start_date"]} - default_topics = {'General': course.location.html_id()} + default_topics = {'General': {'id' :course.location.html_id()}} discussion_topics = course.metadata.get('discussion_topics', default_topics) for topic, entry in discussion_topics.items(): category_map['entries'][topic] = {"id": entry["id"], From 97c880a7149651262d3f073723e564f199d61db0 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 18 Sep 2012 20:39:42 -0400 Subject: [PATCH 59/82] Fix CSV downloads when there are non-ASCII chars in student data, and prefetch groups info to speed up the grades page --- lms/djangoapps/instructor/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index d812791c3d..312a46142a 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -85,7 +85,8 @@ def instructor_dashboard(request, course_id): writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: - writer.writerow(datarow) + encoded_row = [unicode(s).encode('utf-8') for s in datarow] + writer.writerow(encoded_row) return response def get_staff_group(course): @@ -250,7 +251,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, If get_raw_scores=True, then instead of grade summaries, the raw grades for all graded modules are returned. ''' - enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username') + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] if get_grades: From eab1737e83232fb20b4eb725f38e777bf170185a Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Wed, 19 Sep 2012 05:57:30 -0700 Subject: [PATCH 60/82] Prepare lms for upcoming discussion service changes. --- lms/lib/comment_client/thread.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index bda032bbdf..8719d4166c 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -8,8 +8,9 @@ class Thread(models.Model): accessible_fields = [ 'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', - 'created_at', 'updated_at', 'comments_count', 'at_position_list', - 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed' + 'created_at', 'updated_at', 'comments_count', 'unread_comments_count', + 'at_position_list', 'children', 'type', 'highlighted_title', + 'highlighted_body', 'endorsed', 'unread' ] updatable_fields = [ From f6efa99f6c17cab1cf3bfb4b73eec440f4c12041 Mon Sep 17 00:00:00 2001 From: Rocky Duan Date: Tue, 18 Sep 2012 00:33:16 -0700 Subject: [PATCH 61/82] unread count tracking --- .../django_comment_client/forum/views.py | 32 ++++++++++++++++--- lms/djangoapps/django_comment_client/utils.py | 3 +- lms/lib/comment_client/models.py | 4 +-- lms/lib/comment_client/thread.py | 5 +-- lms/lib/comment_client/user.py | 11 +++++++ .../src/discussion/discussion_router.coffee | 2 ++ .../views/discussion_thread_list_view.coffee | 2 ++ lms/static/sass/_discussion.scss | 5 +++ .../discussion/_underscore_templates.html | 2 +- 9 files changed, 55 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 0abcbf5fbb..7a80de194b 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -21,6 +21,8 @@ from django_comment_client.utils import merge_dict, extract, strip_none, strip_b import django_comment_client.utils as utils import comment_client as cc import xml.sax.saxutils as saxutils +import datetime +from django.utils.timezone import utc THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20 @@ -43,6 +45,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG 'tags': '', 'commentable_id': discussion_id, 'course_id': course_id, + 'user_id': request.user.id, } if not request.GET.get('sort_key'): @@ -166,12 +169,21 @@ def single_thread(request, course_id, discussion_id, thread_id): if request.is_ajax(): course = get_course_with_access(request.user, course_id, 'load') - user_info = cc.User.from_django_user(request.user).to_dict() + cc_user = cc.User.from_django_user(request.user) + user_info = cc_user.to_dict() try: - thread = cc.Thread.find(thread_id).retrieve(recursive=True) + last_read_time = datetime.datetime.utcnow().replace(tzinfo=utc).strftime('%Y-%m-%dT%H:%M:%S%z') + cc_user.update_read_states(course_id, thread_id, last_read_time) + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + # TODO log error + pass + + try: + thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: raise Http404 + courseware_context = get_courseware_context(thread, course) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) @@ -190,9 +202,20 @@ def single_thread(request, course_id, discussion_id, thread_id): else: course = get_course_with_access(request.user, course_id, 'load') category_map = utils.get_discussion_category_map(course) + + cc_user = cc.User.from_django_user(request.user) + user_info = cc_user.to_dict() + + try: + last_read_time = datetime.datetime.utcnow().replace(tzinfo=utc).strftime('%Y-%m-%dT%H:%M:%S%z') + cc_user.update_read_states(course_id, thread_id, last_read_time) + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + # TODO log error + pass + try: threads, query_params = get_threads(request, course_id) - thread = cc.Thread.find(thread_id).retrieve(recursive=True) + thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) threads.append(thread.to_dict()) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: raise Http404 @@ -216,8 +239,7 @@ def single_thread(request, course_id, discussion_id, thread_id): # course_id, #) - user_info = cc.User.from_django_user(request.user).to_dict() - + def infogetter(thread): return utils.get_annotated_content_infos(course_id, thread, request.user, user_info) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 476d5b0a6a..76a784a8dc 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -336,7 +336,8 @@ def safe_content(content): 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', - 'courseware_title', 'courseware_url', 'tags' + 'courseware_title', 'courseware_url', 'tags', 'unread_comments_count', + 'viewed', ] if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): diff --git a/lms/lib/comment_client/models.py b/lms/lib/comment_client/models.py index 3ce3858d2d..3f1ad35cd7 100644 --- a/lms/lib/comment_client/models.py +++ b/lms/lib/comment_client/models.py @@ -72,8 +72,8 @@ class Model(object): for k, v in kwargs.items(): if k in self.accessible_fields: self.__setattr__(k, v) - else: - raise AttributeError("Field {0} does not exist".format(k)) + #else: + # raise AttributeError("Field {0} does not exist".format(k)) def updatable_attributes(self): return extract(self.attributes, self.updatable_fields) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index bda032bbdf..2ae72f8fd7 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -9,7 +9,8 @@ class Thread(models.Model): 'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', 'created_at', 'updated_at', 'comments_count', 'at_position_list', - 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed' + 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed', + 'unread_comments_count', 'viewed', ] updatable_fields = [ @@ -61,5 +62,5 @@ class Thread(models.Model): def _retrieve(self, *args, **kwargs): url = self.url(action='get', params=self.attributes) - response = perform_request('get', url, {'recursive': kwargs.get('recursive')}) + response = perform_request('get', url, {'recursive': kwargs.get('recursive'), 'user_id': kwargs.get('user_id')}) self.update_attributes(**response) diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 9813e9a199..91b570c10e 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -2,6 +2,7 @@ from utils import * import models import settings +import json class User(models.Model): @@ -73,6 +74,13 @@ class User(models.Model): response = perform_request('get', url, retrieve_params) self.update_attributes(**response) + def update_read_states(self, course_id, thread_id, last_read_time): + url = _url_for_read_states(self.id) + response = perform_request('put', url, { "course_id": course_id, + "thread_id": thread_id, + "last_read_time": last_read_time, + }) + def _url_for_vote_comment(comment_id): return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id) @@ -82,5 +90,8 @@ def _url_for_vote_thread(thread_id): def _url_for_subscription(user_id): return "{prefix}/users/{user_id}/subscriptions".format(prefix=settings.PREFIX, user_id=user_id) +def _url_for_read_states(user_id): + return "{prefix}/users/{user_id}/read_states".format(prefix=settings.PREFIX, user_id=user_id) + def _url_for_user_active_threads(user_id): return "{prefix}/users/{user_id}/active_threads".format(prefix=settings.PREFIX, user_id=user_id) diff --git a/lms/static/coffee/src/discussion/discussion_router.coffee b/lms/static/coffee/src/discussion/discussion_router.coffee index a5219a68b9..b02ffbf868 100644 --- a/lms/static/coffee/src/discussion/discussion_router.coffee +++ b/lms/static/coffee/src/discussion/discussion_router.coffee @@ -27,6 +27,8 @@ if Backbone? showThread: (forum_name, thread_id) -> @thread = @discussion.get(thread_id) + @thread.set("unread_comments_count", 0) + @thread.set("viewed", true) @setActiveThread() if(@main) @main.cleanup() diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index d998763f50..1028e6a8fb 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -130,6 +130,8 @@ if Backbone? content.addClass("followed") if thread.get('endorsed') content.addClass("resolved") + if thread.get('viewed') + content.addClass("viewed") @highlight(content) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 4c8476f301..42af55dacc 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -1018,6 +1018,7 @@ body.discussion { text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 1px rgba(0, 0, 0, .2) inset; } + } } @@ -1134,6 +1135,10 @@ body.discussion { background: url(../images/following-flag.png) no-repeat; } + &.viewed { + @include linear-gradient(top, white, #ddd); + } + &.active { @include linear-gradient(top, #96e0fd, #61c7fc); border-color: #4697c1; diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 86e950d42e..7a9f8a9e9b 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -128,5 +128,5 @@ From 2c4b335f98e3b7f491fa5cfb59c97011a24c8a21 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 19 Sep 2012 11:02:45 -0400 Subject: [PATCH 62/82] polished multiple choice question styles --- common/lib/xmodule/xmodule/css/capa/display.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index d7ffa198af..1a49baf106 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -53,6 +53,17 @@ section.problem { float: left; border-left: 1px solid #ddd; padding-left: 20px; + margin: 20px 0; + } + + input[type="radio"] { + float: left; + margin: 4px 8px 0 0; + } + + text { + display: block; + margin-left: 25px; } } From 10a838cb0134574f05dd16103f2cc4aa1373b99a Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 19 Sep 2012 11:13:47 -0400 Subject: [PATCH 63/82] remove hardcoding of 6.002x in textbook page --- lms/templates/staticbook.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/staticbook.html b/lms/templates/staticbook.html index c31cf0e7b1..6eec836c3a 100644 --- a/lms/templates/staticbook.html +++ b/lms/templates/staticbook.html @@ -1,6 +1,6 @@ <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> -<%block name="title">Textbook – MITx 6.002x +<%block name="title">${course.number} Textbook <%block name="headextra"> <%static:css group='course'/> From 82ab2e5a6771661272771e0be22a0135b3781b28 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 19 Sep 2012 14:03:56 -0400 Subject: [PATCH 64/82] fixed styles for checkboxes --- common/lib/xmodule/xmodule/css/capa/display.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 1a49baf106..ce0d2d9bf7 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -56,7 +56,8 @@ section.problem { margin: 20px 0; } - input[type="radio"] { + input[type="radio"], + input[type="checkbox"] { float: left; margin: 4px 8px 0 0; } From f9a49ece6f98aaa0e71dcc2f760fd2ae803eb017 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 19 Sep 2012 17:12:08 -0400 Subject: [PATCH 65/82] Dynamically find the start and end pages of a textbook instead of hardcoding to 6.002 values --- common/lib/xmodule/xmodule/course_module.py | 10 +++++++++- lms/djangoapps/staticbook/views.py | 7 ++++++- lms/templates/staticbook.html | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 54e532cb24..71c0d761ef 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -21,7 +21,15 @@ class CourseDescriptor(SequenceDescriptor): self.title = title self.book_url = book_url self.table_of_contents = self._get_toc_from_s3() - + self.start_page = int(self.table_of_contents[0].attrib['page']) + + # The last page should be the last element in the table of contents, + # but it may be nested. So recurse all the way down the last element + last_el = self.table_of_contents[-1] + while last_el.getchildren(): + last_el = last_el[-1] + + self.end_page = int(last_el.attrib['page']) @property def table_of_contents(self): diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index 37087af597..fabb8b861c 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -7,7 +7,7 @@ from courseware.courses import get_course_with_access from lxml import etree @login_required -def index(request, course_id, book_index, page=0): +def index(request, course_id, book_index, page=None): course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') @@ -15,10 +15,15 @@ def index(request, course_id, book_index, page=0): textbook = course.textbooks[book_index] table_of_contents = textbook.table_of_contents + if page is None: + page = textbook.start_page + return render_to_response('staticbook.html', {'book_index': book_index, 'page': int(page), 'course': course, 'book_url': textbook.book_url, 'table_of_contents': table_of_contents, + 'start_page' : textbook.start_page, + 'end_page' : textbook.end_page, 'staff_access': staff_access}) def index_shifted(request, course_id, page): diff --git a/lms/templates/staticbook.html b/lms/templates/staticbook.html index 6eec836c3a..ab92d22257 100644 --- a/lms/templates/staticbook.html +++ b/lms/templates/staticbook.html @@ -38,14 +38,14 @@ function goto_page(n) { function prev_page() { var newpage=page-1; - if(newpage<0) newpage=0; + if(newpage< ${start_page}) newpage=${start_page}; goto_page(newpage); log_event("book", {"type":"prevpage","new":page}); } function next_page() { var newpage=page+1; - if(newpage>1008) newpage=1008; + if(newpage> ${end_page}) newpage=${end_page}; goto_page(newpage); log_event("book", {"type":"nextpage","new":page}); } From 3cde783bdf6722b58428d6e3fbbb30540fc27cdb Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 19 Sep 2012 17:37:15 -0400 Subject: [PATCH 66/82] Display the last top level element in the Table of Contents as expandable. This isn't a great fix, but I'm confused what's going on. It's something in JQuery.TreeView. We write out the HTML correctly, but the lastExpandable style causes the list item to not be expandable. This didn't bite us in 6.002x because the last item was an appendix that isn't expandable anyway. It does affect 6.00x, where the last item is Chapter 5 at the moment. --- lms/templates/staticbook.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lms/templates/staticbook.html b/lms/templates/staticbook.html index ab92d22257..f99bfb6d92 100644 --- a/lms/templates/staticbook.html +++ b/lms/templates/staticbook.html @@ -97,6 +97,10 @@ $("#open_close_accordion a").click(function(){ % for entry in table_of_contents: ${print_entry(entry)} % endfor + + ## Don't delete this empty list item. Without it, Jquery.TreeView won't + ## render the last list item as expandable. +
  • From 32d224c4d015239774160fbcdefc9df62da37006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 19 Sep 2012 18:10:13 -0400 Subject: [PATCH 67/82] Update doc/xml-format.md --- doc/xml-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/xml-format.md b/doc/xml-format.md index b8aaf78b44..e7919b85d5 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -281,7 +281,7 @@ __Inherited:__ * `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. * `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". * `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) - 'onreset' (students see a different version of the problem when they reset, but are not forced to reset after each check) + 'onreset' (randomize question when reset button is pressed by the student) 'never' (all students see the same version of the problem) 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) Default: 'always'. Optional. From 7a88223ef7958cfffe8df2dcd67c6620f105d647 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 20 Sep 2012 00:51:23 -0400 Subject: [PATCH 68/82] custom tabs * still needs better error checking and testing --- doc/xml-format.md | 5 +++- lms/djangoapps/courseware/tabs.py | 37 ++++++++++++++++++++++++ lms/djangoapps/courseware/views.py | 26 +++++++++++++++++ lms/templates/courseware/static_tab.html | 16 ++++++++++ lms/urls.py | 4 +++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 lms/templates/courseware/static_tab.html diff --git a/doc/xml-format.md b/doc/xml-format.md index e7919b85d5..9d07f432c9 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -365,12 +365,15 @@ If you want to customize the courseware tabs displayed for your course, specify {"type": "external_link", "name": "My Discussion", "link": "http://www.mydiscussion.org/blah"}, {"type": "progress", "name": "Progress"}, {"type": "wiki", "name": "Wonderwiki"}, + {"type": "static_tab", "url_slug": "news", "name": "Exciting news"}, {"type": "textbooks"} # generates one tab per textbook, taking names from the textbook titles ] * If you specify any tabs, you must specify all tabs. They will appear in the order given. -* The first two tabs must have types "courseware" and "course_info", in that order. Otherwise, we'll refuse to load the course. +* The first two tabs must have types `"courseware"` and `"course_info"`, in that order. Otherwise, we'll refuse to load the course. +* for static tabs, the url_slug will be the url that points to the tab. It can not be one of the existing courseware url types (even if those aren't used in your course). The static content will come from `tabs/{course_url_name}/{url_slug}.html`, or `tabs/{url_slug}.html` if that doesn't exist. + * An Instructor tab will be automatically added at the end for course staff users. ## Supported tab types: diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 8ecd8137df..2dfa7afed3 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -16,6 +16,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from courseware.access import has_access +from static_replace import replace_urls log = logging.getLogger(__name__) @@ -77,6 +78,11 @@ def _external_link(tab, user, course, active_page): # external links are never active return [CourseTab(tab['name'], tab['link'], False)] +def _static_tab(tab, user, course, active_page): + link = reverse('static_tab', args=[course.id, tab['url_slug']]) + active_str = 'static_tab_{0}'.format(tab['url_slug']) + return [CourseTab(tab['name'], link, active_page==active_str)] + def _textbooks(tab, user, course, active_page): """ @@ -123,6 +129,7 @@ VALID_TAB_TYPES = { 'external_link': TabImpl(key_checker(['name', 'link']), _external_link), 'textbooks': TabImpl(null_validator, _textbooks), 'progress': TabImpl(need_name, _progress), + 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), } @@ -227,3 +234,33 @@ def get_default_tabs(user, course, active_page): tabs.append(CourseTab('Instructor', link, active_page=='instructor')) return tabs + +def get_static_tab_by_slug(tabs, tab_slug): + """ + Look for a tab with type 'static_tab' and the specified 'tab_slug'. Returns + the tab (a config dict), or None if not found. + """ + for tab in tabs: + # if the tab is misconfigured, this will blow up. The validation code should check... + if tab['type'] == 'static_tab' and tab['url_slug'] == tab_slug: + return tab + + return None + + +def get_static_tab_contents(course, tab): + """ + Given a course and a static tab config dict, load the tab contents, + returning None if not found. + + Looks in tabs/{course_url_name}/{tab_slug}.html first, then tabs/{tab_slug}.html. + """ + slug = tab['url_slug'] + paths = ['tabs/{0}/{1}.html'.format(course.url_name, slug), 'tabs/{0}.html'.format(slug)] + fs = course.system.resources_fs + for p in paths: + if fs.exists(p): + with fs.open(p) as tabfile: + # TODO: redundant with module_render.py. Want to be helper methods in static_replace or something. + contents = replace_urls(tabfile.read(), course.metadata['data_dir']) + return replace_urls(contents, staticfiles_prefix='/courses/'+course.id, replace_prefix='/course/') diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7da5d06741..b03c1c932e 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -22,6 +22,7 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access from courseware.courses import (get_course_with_access, get_courses_by_university) +import courseware.tabs as tabs from models import StudentModuleCache from module_render import toc_for_course, get_module, get_instance_module from student.models import UserProfile @@ -343,6 +344,30 @@ def course_info(request, course_id): return render_to_response('courseware/info.html', {'course': course, 'staff_access': staff_access,}) +@ensure_csrf_cookie +def static_tab(request, course_id, tab_slug): + """ + Display the courses tab with the given name. + + Assumes the course_id is in a valid format. + """ + course = get_course_with_access(request.user, course_id, 'load') + + tab = tabs.get_static_tab_by_slug(course.tabs, tab_slug) + if tab is None: + raise Http404 + + contents = tabs.get_static_tab_contents(course, tab) + if contents is None: + raise Http404 + + staff_access = has_access(request.user, course, 'staff') + return render_to_response('courseware/static_tab.html', + {'course': course, + 'tab': tab, + 'tab_contents': contents, + 'staff_access': staff_access,}) + # TODO arjun: remove when custom tabs in place, see courseware/syllabus.py @ensure_csrf_cookie def syllabus(request, course_id): @@ -357,6 +382,7 @@ def syllabus(request, course_id): return render_to_response('courseware/syllabus.html', {'course': course, 'staff_access': staff_access,}) + def registered_for_course(course, user): '''Return CourseEnrollment if user is registered for course, else False''' if user is None: diff --git a/lms/templates/courseware/static_tab.html b/lms/templates/courseware/static_tab.html new file mode 100644 index 0000000000..7a832d6a4c --- /dev/null +++ b/lms/templates/courseware/static_tab.html @@ -0,0 +1,16 @@ +<%inherit file="/main.html" /> +<%namespace name='static' file='/static_content.html'/> + +<%block name="headextra"> + <%static:css group='course'/> + + +<%block name="title">${course.number} ${tab['name']} + +<%include file="/courseware/course_navigation.html" args="active_page='static_tab_{0}'.format(tab['url_slug'])" /> + +
    +
    + ${tab_contents} +
    +
    diff --git a/lms/urls.py b/lms/urls.py index 49febaf84e..6741113507 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -164,6 +164,10 @@ if settings.COURSEWARE_ENABLED: 'instructor.views.grade_summary', name='grade_summary'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/enroll_students$', 'instructor.views.enroll_students', name='enroll_students'), + + # This MUST be the last view in the courseware--it's a catch-all for custom tabs. + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/(?P.*)$', + 'courseware.views.static_tab', name="static_tab"), ) # discussion forums live within courseware, so courseware must be enabled first From 4a162dadae728913c31f0d2c84a712e13dd01020 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 20 Sep 2012 01:11:59 -0400 Subject: [PATCH 69/82] bugfixes for static tabs - move courseware wiki views above catch-all rule - handle courses with no custom tabs --- lms/djangoapps/courseware/tabs.py | 6 ++-- lms/djangoapps/courseware/views.py | 2 +- lms/urls.py | 50 ++++++++++++++++-------------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 2dfa7afed3..c05a872382 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -235,12 +235,14 @@ def get_default_tabs(user, course, active_page): return tabs -def get_static_tab_by_slug(tabs, tab_slug): +def get_static_tab_by_slug(course, tab_slug): """ Look for a tab with type 'static_tab' and the specified 'tab_slug'. Returns the tab (a config dict), or None if not found. """ - for tab in tabs: + if course.tabs is None: + return None + for tab in course.tabs: # if the tab is misconfigured, this will blow up. The validation code should check... if tab['type'] == 'static_tab' and tab['url_slug'] == tab_slug: return tab diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index b03c1c932e..269977e458 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -353,7 +353,7 @@ def static_tab(request, course_id, tab_slug): """ course = get_course_with_access(request.user, course_id, 'load') - tab = tabs.get_static_tab_by_slug(course.tabs, tab_slug) + tab = tabs.get_static_tab_by_slug(course, tab_slug) if tab is None: raise Http404 diff --git a/lms/urls.py b/lms/urls.py index 6741113507..6b44cf9b43 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -97,6 +97,33 @@ urlpatterns = ('', if settings.PERFSTATS: urlpatterns += (url(r'^reprofile$','perfstats.views.end_profile'),) + + +# Multicourse wiki (Note: wiki urls must be above the courseware ones because of +# the custom tab catch-all) +if settings.WIKI_ENABLED: + from wiki.urls import get_pattern as wiki_pattern + from django_notify.urls import get_pattern as notify_pattern + + # Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update + # them together. + urlpatterns += ( + # First we include views from course_wiki that we use to override the default views. + # They come first in the urlpatterns so they get resolved first + url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'), + + + url(r'^wiki/', include(wiki_pattern())), + url(r'^notify/', include(notify_pattern())), + + # These urls are for viewing the wiki in the context of a course. They should + # never be returned by a reverse() so they come after the other url patterns + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/course_wiki/?$', + 'course_wiki.views.course_wiki_redirect', name="course_wiki"), + url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())), + ) + + if settings.COURSEWARE_ENABLED: urlpatterns += ( # Hook django-masquerade, allowing staff to view site as other users @@ -180,29 +207,6 @@ if settings.COURSEWARE_ENABLED: include('django_comment_client.urls')) ) - # Multicourse wiki -if settings.WIKI_ENABLED: - from wiki.urls import get_pattern as wiki_pattern - from django_notify.urls import get_pattern as notify_pattern - - # Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update - # them together. - urlpatterns += ( - # First we include views from course_wiki that we use to override the default views. - # They come first in the urlpatterns so they get resolved first - url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'), - - - url(r'^wiki/', include(wiki_pattern())), - url(r'^notify/', include(notify_pattern())), - - # These urls are for viewing the wiki in the context of a course. They should - # never be returned by a reverse() so they come after the other url patterns - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/course_wiki/?$', - 'course_wiki.views.course_wiki_redirect', name="course_wiki"), - url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())), - ) - if settings.QUICKEDIT: urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'dogfood.views.quickedit'),) urlpatterns += (url(r'^dogfood/(?P[^/]*)$', 'dogfood.views.df_capa_problem'),) From 27cd9c9f82b83ee2da9096c76be8bdb527b79f5a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 20 Sep 2012 01:12:33 -0400 Subject: [PATCH 70/82] Remove unused 'module' parameter from replace_{static,course}_urls --- cms/djangoapps/contentstore/views.py | 2 +- common/djangoapps/xmodule_modifiers.py | 4 ++-- lms/djangoapps/courseware/module_render.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index a9e2602307..6f428ee3e8 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -247,7 +247,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module.get_html = replace_static_urls( wrap_xmodule(module.get_html, module, "xmodule_display.html"), - module.metadata['data_dir'], module + module.metadata['data_dir'] ) save_preview_state(request, preview_id, descriptor.location.url(), module.get_instance_state(), module.get_shared_state()) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 80514cf8d4..7ea6778af6 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template): return _get_html -def replace_course_urls(get_html, course_id, module): +def replace_course_urls(get_html, course_id): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /course/... @@ -46,7 +46,7 @@ def replace_course_urls(get_html, course_id, module): return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') return _get_html -def replace_static_urls(get_html, prefix, module): +def replace_static_urls(get_html, prefix): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index b033660c17..a65d73e4bc 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -254,12 +254,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi module.get_html = replace_static_urls( wrap_xmodule(module.get_html, module, 'xmodule_display.html'), - module.metadata['data_dir'], module - ) + module.metadata['data_dir']) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course - module.get_html = replace_course_urls(module.get_html, course_id, module) + module.get_html = replace_course_urls(module.get_html, course_id) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff'): From 117c0b692710d5bff637df97338b2b659eecd311 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 00:24:36 -0700 Subject: [PATCH 71/82] Mark threads as read once they are fetched; change viewed to read --- .../django_comment_client/forum/views.py | 29 +++++-------------- lms/djangoapps/django_comment_client/utils.py | 2 +- lms/lib/comment_client/models.py | 4 +-- lms/lib/comment_client/thread.py | 7 +++-- lms/lib/comment_client/user.py | 10 ------- 5 files changed, 16 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 7a80de194b..4d02d94903 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -84,6 +84,7 @@ def inline_discussion(request, course_id, discussion_id): # TODO (vshnayder): since none of this code seems to be aware of the fact that # sometimes things go wrong, I suspect that the js client is also not # checking for errors on request. Check and fix as needed. + log.error("Error loading inline discussion threads.") raise Http404 def infogetter(thread): @@ -117,6 +118,7 @@ def forum_form_discussion(request, course_id): unsafethreads, query_params = get_threads(request, course_id) # This might process a search query threads = [utils.safe_content(thread) for thread in unsafethreads] except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + log.error("Error loading forum discussion threads: %s" % str(err)) raise Http404 user_info = cc.User.from_django_user(request.user).to_dict() @@ -167,21 +169,16 @@ def forum_form_discussion(request, course_id): @login_required def single_thread(request, course_id, discussion_id, thread_id): - if request.is_ajax(): - course = get_course_with_access(request.user, course_id, 'load') - cc_user = cc.User.from_django_user(request.user) - user_info = cc_user.to_dict() + course = get_course_with_access(request.user, course_id, 'load') + cc_user = cc.User.from_django_user(request.user) + user_info = cc_user.to_dict() - try: - last_read_time = datetime.datetime.utcnow().replace(tzinfo=utc).strftime('%Y-%m-%dT%H:%M:%S%z') - cc_user.update_read_states(course_id, thread_id, last_read_time) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: - # TODO log error - pass + if request.is_ajax(): try: thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + log.error("Error loading single thread.") raise Http404 courseware_context = get_courseware_context(thread, course) @@ -200,24 +197,14 @@ def single_thread(request, course_id, discussion_id, thread_id): }) else: - course = get_course_with_access(request.user, course_id, 'load') category_map = utils.get_discussion_category_map(course) - cc_user = cc.User.from_django_user(request.user) - user_info = cc_user.to_dict() - - try: - last_read_time = datetime.datetime.utcnow().replace(tzinfo=utc).strftime('%Y-%m-%dT%H:%M:%S%z') - cc_user.update_read_states(course_id, thread_id, last_read_time) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: - # TODO log error - pass - try: threads, query_params = get_threads(request, course_id) thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) threads.append(thread.to_dict()) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + log.error("Error loading single thread.") raise Http404 course = get_course_with_access(request.user, course_id, 'load') diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 76a784a8dc..cd323e56c1 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -337,7 +337,7 @@ def safe_content(content): 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'courseware_title', 'courseware_url', 'tags', 'unread_comments_count', - 'viewed', + 'read', ] if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): diff --git a/lms/lib/comment_client/models.py b/lms/lib/comment_client/models.py index 3f1ad35cd7..3ce3858d2d 100644 --- a/lms/lib/comment_client/models.py +++ b/lms/lib/comment_client/models.py @@ -72,8 +72,8 @@ class Model(object): for k, v in kwargs.items(): if k in self.accessible_fields: self.__setattr__(k, v) - #else: - # raise AttributeError("Field {0} does not exist".format(k)) + else: + raise AttributeError("Field {0} does not exist".format(k)) def updatable_attributes(self): return extract(self.attributes, self.updatable_fields) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 0e0aa97744..2f118374d6 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -10,7 +10,7 @@ class Thread(models.Model): 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', 'created_at', 'updated_at', 'comments_count', 'unread_comments_count', 'at_position_list', 'children', 'type', 'highlighted_title', - 'highlighted_body', 'endorsed', 'unread' + 'highlighted_body', 'endorsed', 'read' ] updatable_fields = [ @@ -60,7 +60,10 @@ class Thread(models.Model): else: return super(Thread, cls).url(action, params) + # TODO: This is currently overriding Model._retrieve only to add parameters + # for the request. Model._retrieve should be modified to handle this such + # that subclasses don't need to override for this. def _retrieve(self, *args, **kwargs): url = self.url(action='get', params=self.attributes) - response = perform_request('get', url, {'recursive': kwargs.get('recursive'), 'user_id': kwargs.get('user_id')}) + response = perform_request('get', url, {'recursive': kwargs.get('recursive'), 'user_id': kwargs.get('user_id'), 'mark_as_read': kwargs.get('mark_as_read', True)}) self.update_attributes(**response) diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 91b570c10e..101a7f2ff6 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -74,13 +74,6 @@ class User(models.Model): response = perform_request('get', url, retrieve_params) self.update_attributes(**response) - def update_read_states(self, course_id, thread_id, last_read_time): - url = _url_for_read_states(self.id) - response = perform_request('put', url, { "course_id": course_id, - "thread_id": thread_id, - "last_read_time": last_read_time, - }) - def _url_for_vote_comment(comment_id): return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id) @@ -90,8 +83,5 @@ def _url_for_vote_thread(thread_id): def _url_for_subscription(user_id): return "{prefix}/users/{user_id}/subscriptions".format(prefix=settings.PREFIX, user_id=user_id) -def _url_for_read_states(user_id): - return "{prefix}/users/{user_id}/read_states".format(prefix=settings.PREFIX, user_id=user_id) - def _url_for_user_active_threads(user_id): return "{prefix}/users/{user_id}/active_threads".format(prefix=settings.PREFIX, user_id=user_id) From 93d5a6690e29e9c0258b45a883cec03edbfb24e3 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 00:25:43 -0700 Subject: [PATCH 72/82] Change unread to read; makes some things clearer both on the comment service and in the django client --- lms/lib/comment_client/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 8719d4166c..342711c5fe 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -10,7 +10,7 @@ class Thread(models.Model): 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', 'created_at', 'updated_at', 'comments_count', 'unread_comments_count', 'at_position_list', 'children', 'type', 'highlighted_title', - 'highlighted_body', 'endorsed', 'unread' + 'highlighted_body', 'endorsed', 'read' ] updatable_fields = [ From 6132b9b20707a43bf122cd32c69e86a695f834ba Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 02:11:27 -0700 Subject: [PATCH 73/82] Alter some css; don't send the user if there is no user when retrieivng a thread --- lms/lib/comment_client/thread.py | 13 ++++++++++++- lms/lib/comment_client/user.py | 1 - .../coffee/src/discussion/discussion_router.coffee | 2 +- .../views/discussion_thread_list_view.coffee | 4 ++-- lms/static/sass/_discussion.scss | 9 +++------ lms/templates/discussion/_underscore_templates.html | 10 +++++++++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 2f118374d6..424250033e 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -65,5 +65,16 @@ class Thread(models.Model): # that subclasses don't need to override for this. def _retrieve(self, *args, **kwargs): url = self.url(action='get', params=self.attributes) - response = perform_request('get', url, {'recursive': kwargs.get('recursive'), 'user_id': kwargs.get('user_id'), 'mark_as_read': kwargs.get('mark_as_read', True)}) + + request_params = { + 'recursive': kwargs.get('recursive'), + 'user_id': kwargs.get('user_id'), + 'mark_as_read': kwargs.get('mark_as_read', True), + } + + # user_id may be none, in which case it shouldn't be part of the + # request. + request_params = strip_none(request_params) + + response = perform_request('get', url, request_params) self.update_attributes(**response) diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 101a7f2ff6..9813e9a199 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -2,7 +2,6 @@ from utils import * import models import settings -import json class User(models.Model): diff --git a/lms/static/coffee/src/discussion/discussion_router.coffee b/lms/static/coffee/src/discussion/discussion_router.coffee index b02ffbf868..50c14b20de 100644 --- a/lms/static/coffee/src/discussion/discussion_router.coffee +++ b/lms/static/coffee/src/discussion/discussion_router.coffee @@ -28,7 +28,7 @@ if Backbone? showThread: (forum_name, thread_id) -> @thread = @discussion.get(thread_id) @thread.set("unread_comments_count", 0) - @thread.set("viewed", true) + @thread.set("read", true) @setActiveThread() if(@main) @main.cleanup() diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index 1028e6a8fb..cca6502158 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -130,8 +130,8 @@ if Backbone? content.addClass("followed") if thread.get('endorsed') content.addClass("resolved") - if thread.get('viewed') - content.addClass("viewed") + if thread.get('read') + content.addClass("read") @highlight(content) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 42af55dacc..53b1b5eb9b 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -1135,10 +1135,6 @@ body.discussion { background: url(../images/following-flag.png) no-repeat; } - &.viewed { - @include linear-gradient(top, white, #ddd); - } - &.active { @include linear-gradient(top, #96e0fd, #61c7fc); border-color: #4697c1; @@ -1209,12 +1205,13 @@ body.discussion { background-position: 0 -5px; } - &.new { - @include linear-gradient(top, #84d7fe, #99e0fe); + &.unread { + @include linear-gradient(top, #84d7fe, #60a8d6); color: #333; &:after { color: #99e0fe; + background-position: 0 0px; } } } diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 7a9f8a9e9b..fc497a37b2 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -128,5 +128,13 @@ From 3567e23a9d7b9a4ceb7783bc70932ffc2fc1349b Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 02:13:35 -0700 Subject: [PATCH 74/82] Unneeded imports --- lms/djangoapps/django_comment_client/forum/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 4d02d94903..6858957b4f 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -21,8 +21,6 @@ from django_comment_client.utils import merge_dict, extract, strip_none, strip_b import django_comment_client.utils as utils import comment_client as cc import xml.sax.saxutils as saxutils -import datetime -from django.utils.timezone import utc THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20 From 55a9da933aafd256a92565e5ba47e9d3e4a269a1 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 02:15:51 -0700 Subject: [PATCH 75/82] DRY things slightly --- lms/djangoapps/django_comment_client/forum/views.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 6858957b4f..925c59a03b 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -171,13 +171,13 @@ def single_thread(request, course_id, discussion_id, thread_id): cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() - if request.is_ajax(): + try: + thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + log.error("Error loading single thread.") + raise Http404 - try: - thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: - log.error("Error loading single thread.") - raise Http404 + if request.is_ajax(): courseware_context = get_courseware_context(thread, course) @@ -199,7 +199,6 @@ def single_thread(request, course_id, discussion_id, thread_id): try: threads, query_params = get_threads(request, course_id) - thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) threads.append(thread.to_dict()) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: log.error("Error loading single thread.") From ed034ff1032add4b24bddf5854a8ce2fbc0ea31c Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 20 Sep 2012 04:23:31 -0700 Subject: [PATCH 76/82] fix ubuntu graphviz lib install --- create-dev-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create-dev-env.sh b/create-dev-env.sh index d28a5891d9..274c84a719 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -105,7 +105,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz libgraphviz-dev" +APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz graphviz-dev" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" From afdd11342ecfbc9568c2f695a95f8f504f886c65 Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Thu, 20 Sep 2012 04:26:58 -0700 Subject: [PATCH 77/82] Show total comments instead of unread only, and use a tooltip to show the unread count. --- lms/templates/discussion/_underscore_templates.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index fc497a37b2..a6f2471b1c 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -119,7 +119,7 @@