From fb6a07c812748916061109ac1aa4f807a1069652 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Nov 2012 18:39:54 -0500 Subject: [PATCH 001/280] allow flagging for abuse and spoilers --- lms/lib/comment_client/comment.py | 2 +- lms/lib/comment_client/thread.py | 2 +- lms/static/coffee/src/discussion/content.coffee | 12 ++++++++++-- lms/templates/discussion/_underscore_templates.html | 5 ++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index 6d0bafee02..5d49c0a869 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -10,7 +10,7 @@ class Comment(models.Model): 'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id', 'closed', 'created_at', 'updated_at', 'depth', 'at_position_list', - 'type', 'commentable_id', + 'type', 'commentable_id', 'abuse_flaggers', 'spoiler_flaggers' ] updatable_fields = [ diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 424250033e..4246aabe21 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', 'read' + 'highlighted_body', 'endorsed', 'read', 'abuse_flaggers', 'spoiler_flaggers' ] updatable_fields = [ diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index 4e612dfc40..0e86064bf2 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -78,7 +78,8 @@ if Backbone? if @getContent(id) @getContent(id).updateInfo(info) $.extend @contentInfos, infos - + + class @Thread extends @Content urlMappers: 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id) @@ -119,7 +120,13 @@ if Backbone? else @get("body") - display_title: -> + display_tigetCommentsCount: -> + count = 0 + @get('comments').each (comment) -> + count += comment.getCommentsCount() + 1 + count + + class @Comments extends Backbtle: -> if @has("highlighted_title") String(@get("highlighted_title")).replace(//g, '').replace(/<\/highlight>/g, '') else @@ -134,6 +141,7 @@ if Backbone? created_at_time: -> new Date(@get("created_at")).getTime() + class @Comment extends @Content urlMappers: diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index be238811c2..7c14e5af17 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -26,7 +26,10 @@
- + ${'<%- votes["up_count"] %>'} + + + ${'<%- votes["up_count"] %>'} + + + ${'<%- abuse_flaggers.length%>'}

${'<%- title %>'}

${"<% if (obj.username) { %>"} From 57b70092e817fb236e3c32f85f542f0885e765f0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 20 Nov 2012 13:39:33 -0500 Subject: [PATCH 002/280] update front end to handle flagging --- .../django_comment_client/base/urls.py | 1 + .../django_comment_client/base/views.py | 11 +++++- .../django_comment_client/permissions.py | 1 + lms/djangoapps/django_comment_client/utils.py | 2 +- .../coffee/src/discussion/content.coffee | 9 +++++ lms/static/coffee/src/discussion/utils.coffee | 1 + .../views/discussion_thread_show_view.coffee | 37 +++++++++++++++++++ .../discussion/_underscore_templates.html | 4 +- 8 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index f2cb4ccb15..1a2715763d 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -10,6 +10,7 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'threads/(?P[\w\-]+)/reply$', 'create_comment', name='create_comment'), url(r'threads/(?P[\w\-]+)/delete', 'delete_thread', name='delete_thread'), url(r'threads/(?P[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), + url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', {'value': 'up'}, name='flag_abuse_thread'), url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'), url(r'threads/(?P[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), url(r'threads/(?P[\w\-]+)/follow$', 'follow_thread', name='follow_thread'), diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 63d69427c9..d821c939f2 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -136,7 +136,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): user = cc.User.from_django_user(request.user) user.follow(comment.thread) if request.is_ajax(): - return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_create_comment.html') + return ajax_content_response(request, course_id,comment.to_dict(), 'discussion/ajax_create_comment.html') else: return JsonResponse(utils.safe_content(comment.to_dict())) @@ -235,6 +235,15 @@ def vote_for_thread(request, course_id, thread_id, value): user.vote(thread, value) return JsonResponse(utils.safe_content(thread.to_dict())) +@require_POST +@login_required +@permitted +def flag_abuse_for_thread(request, course_id, thread_id, value): + user = cc.User.from_django_user(request.user) + thread = cc.Thread.find(thread_id) + thread.flagAbuse(thread, value) + return JsonResponse(utils.safe_content(thread.to_dict())) + @require_POST @login_required @permitted diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index b95a890dda..10a5f748e9 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -85,6 +85,7 @@ VIEW_PERMISSIONS = { 'vote_for_comment' : [['vote', 'is_open']], 'undo_vote_for_comment': [['unvote', 'is_open']], 'vote_for_thread' : [['vote', 'is_open']], + 'flag_abuse_for_thread': [['vote', 'is_open']], 'undo_vote_for_thread': [['unvote', 'is_open']], 'follow_thread' : ['follow_thread'], 'follow_commentable': ['follow_commentable'], diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index b3a1626d22..e442225c4d 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -352,7 +352,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', - 'read', + 'read', "abuse_flaggers", "spoiler_flaggers" ] if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index 0e86064bf2..6a25a07ec2 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -84,6 +84,7 @@ if Backbone? urlMappers: 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id) 'reply' : -> DiscussionUtil.urlFor('create_comment', @id) + 'flagAbuse': -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id) 'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id) 'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id) 'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id) @@ -113,6 +114,14 @@ if Backbone? unvote: -> @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 @trigger "change", @ + + flagAbuse: -> + @get("abuse_flaggers").push window.user.get('id') + @trigger "change", @ + + unflagAbuse: -> + @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 + @trigger "change", @ display_body: -> if @has("highlighted_body") diff --git a/lms/static/coffee/src/discussion/utils.coffee b/lms/static/coffee/src/discussion/utils.coffee index a032c0248f..6947eb6529 100644 --- a/lms/static/coffee/src/discussion/utils.coffee +++ b/lms/static/coffee/src/discussion/utils.coffee @@ -48,6 +48,7 @@ class @DiscussionUtil update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update" create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply" delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete" + flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse" upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote" downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote" undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote" diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index a8e95c2565..21458bcc39 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -3,6 +3,8 @@ if Backbone? events: "click .discussion-vote": "toggleVote" + "click .discussion-flag-abuse": "toggleFlagAbuse" + "click .discussion-flag-spoiler": "toggleFlagSpoiler" "click .action-follow": "toggleFollowing" "click .action-edit": "edit" "click .action-delete": "delete" @@ -57,6 +59,20 @@ if Backbone? else @vote() + toggleFlagAbuse: (event) -> + event.preventDefault() + if window.user in @model.get("abuse_flaggers") + @unFlagAbuse() + else + @flagAbuse() + + toggleFlagSpoiler: (event) -> + event.preventDefault() + if window.user in @model.abuse_flaggers + @unFlagAbuse() + else + @flagAbuse() + toggleFollowing: (event) -> $elem = $(event.target) url = null @@ -82,6 +98,16 @@ if Backbone? if textStatus == 'success' @model.set(response, {silent: true}) + flagAbuse: -> + url = @model.urlFor("flagAbuse") + DiscussionUtil.safeAjax + $elem: @$(".discussion-flag-abuse") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response, {silent: true}) + unvote: -> window.user.unvote(@model) url = @model.urlFor("unvote") @@ -93,6 +119,17 @@ if Backbone? if textStatus == 'success' @model.set(response, {silent: true}) + unFlagAbuse: -> + window.user.unvote(@model) + url = @model.urlFor("unvote") + DiscussionUtil.safeAjax + $elem: @$(".discussion-vote") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response, {silent: true}) + edit: (event) -> @trigger "thread:edit", event diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 7c14e5af17..b37f9acfa0 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -28,8 +28,10 @@

+ ${'<%- votes["up_count"] %>'} - + + ${'<%- abuse_flaggers.length%>'} + + + ${'<%- spoiler_flaggers.length%>'}

${'<%- title %>'}

${"<% if (obj.username) { %>"} From 4b017e711dd7fd94d105a95a9d4cf853561a96e5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 20 Nov 2012 17:00:16 -0500 Subject: [PATCH 003/280] updated models and vies f for flagging --- .../django_comment_client/base/urls.py | 2 +- .../django_comment_client/base/views.py | 2 +- .../django_comment_client/permissions.py | 4 ++-- lms/lib/comment_client/thread.py | 23 +++++++++++++------ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index 1a2715763d..cf0ef68916 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -10,7 +10,7 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'threads/(?P[\w\-]+)/reply$', 'create_comment', name='create_comment'), url(r'threads/(?P[\w\-]+)/delete', 'delete_thread', name='delete_thread'), url(r'threads/(?P[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), - url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', {'value': 'up'}, name='flag_abuse_thread'), + url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', {'value': 'up'}, name='flag_abuse_for_thread'), url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'), url(r'threads/(?P[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), url(r'threads/(?P[\w\-]+)/follow$', 'follow_thread', name='follow_thread'), diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index d821c939f2..9c750423ef 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -241,7 +241,7 @@ def vote_for_thread(request, course_id, thread_id, value): def flag_abuse_for_thread(request, course_id, thread_id, value): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.flagAbuse(thread, value) + thread.flagAbuse(user,thread, value) return JsonResponse(utils.safe_content(thread.to_dict())) @require_POST diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 10a5f748e9..a5e4803b97 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -69,8 +69,8 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs): return True in results elif operator == "and": return not False in results - - return test(user, permissions, operator="or") + return True + #return test(user, permissions, operator="or") VIEW_PERMISSIONS = { diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 4246aabe21..3d1945730c 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -71,10 +71,19 @@ class Thread(models.Model): '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) + + + def flagAbuse(self, user, voteable, value): + if voteable.type == 'thread': + url = _url_for_flag_abuse_thread(voteable.id) + elif voteable.type == 'comment': + url = _url_for_vote_comment(voteable.id) + else: + raise CommentClientError("Can only vote / unvote for threads or comments") + params = {'user_id': user.id, 'value': value} + request = perform_request('put', url, params) + voteable.update_attributes(request) + +def _url_for_flag_abuse_thread(thread_id): + return "{prefix}/threads/{thread_id}/abuse_flags".format(prefix=settings.PREFIX, thread_id=thread_id) + From a22dd5ebb71d78f13af7a823950cc5ce2605694c Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Nov 2012 17:31:40 -0500 Subject: [PATCH 004/280] flagging interface updates --- lms/static/sass/_discussion.scss | 16 ++++++++++++++-- .../discussion/_underscore_templates.html | 5 +---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 809c968fe6..6d84af186c 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -95,6 +95,7 @@ body.discussion { + .new-post-form-errors { display: none; background: $error-red; @@ -1261,8 +1262,8 @@ body.discussion { .discussion-article { position: relative; padding: 40px; - min-height: 468px; - + min-height: 468px; + a { word-wrap: break-word; } @@ -1315,6 +1316,9 @@ body.discussion { background-position: 0 0; } } + + + } .discussion-post { @@ -2412,3 +2416,11 @@ body.discussion { .discussion-user-threads { @extend .discussion-module } + +.flagdiv{ + font-size: 12px; + color: #888; + align:right; + font-style: italic; + + } diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index b37f9acfa0..ccbcc55da2 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -3,6 +3,7 @@ +%endif From ffb125f8a995bd544b144b5a1cd081013063fc9c Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 19:42:58 -0400 Subject: [PATCH 089/280] course_navigation for masquerade --- lms/templates/courseware/course_navigation.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 481c2761d9..4e77c96adb 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -27,14 +27,17 @@ def url_class(is_active): % endfor <%block name="extratabs" /> - %if staff_access and masquerade: + % if masquerade is not UNDEFINED: + % if staff_access and masquerade is not None:

  • Staff view
  • - %endif + % endif + % endif
    -%if staff_access and masquerade: +% if masquerade is not UNDEFINED: + % if staff_access and masquerade is not None: -%endif + % endif +% endif From 168a3aadf6eb79ce6b7d87707b34f2d08f0fa8fc Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 19:08:14 -0400 Subject: [PATCH 090/280] masquerade - remove debug line, careful with ifs in course_nav.html --- lms/djangoapps/courseware/module_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index a228c7eac3..eb819fd5a5 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -328,7 +328,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) - log.debug('user_is_staff=%s' % system.user_is_staff) _get_html = module.get_html if wrap_xmodule_display == True: From 3b4093ab7b6bc12f52462da47dbed93c1c2922ce Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 14 Apr 2013 00:43:02 +0000 Subject: [PATCH 091/280] url and mitx_settings for masquerade --- lms/envs/common.py | 2 ++ lms/urls.py | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 8654b5ebf5..efc8a382d7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -71,6 +71,8 @@ MITX_FEATURES = { 'ENABLE_LMS_MIGRATION': False, 'ENABLE_MANUAL_GIT_RELOAD': False, + 'ENABLE_MASQUERADE': True, # allow course staff to change to student view of courseware + 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests diff --git a/lms/urls.py b/lms/urls.py index 4a0608720a..c792c149e1 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -201,9 +201,6 @@ if settings.WIKI_ENABLED: if settings.COURSEWARE_ENABLED: urlpatterns += ( - # Hook django-masquerade, allowing staff to view site as other users - url(r'^masquerade/', include('masquerade.urls')), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/jump_to/(?P.*)$', 'courseware.views.jump_to', name="jump_to"), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/modx/(?P.*?)/(?P[^/]*)$', @@ -345,6 +342,13 @@ if settings.COURSEWARE_ENABLED: 'open_ended_grading.views.peer_grading', name='peer_grading'), ) + # allow course staff to change to student view of courseware + if settings.MITX_FEATURES.get('ENABLE_MASQUERADE'): + urlpatterns += ( + url(r'^masquerade/(?P.*)$','courseware.masquerade.handle_ajax', name="masquerate-switch"), + ) + + # discussion forums live within courseware, so courseware must be enabled first if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): urlpatterns += ( From af3e08e8c7910d173c234db739ebc73a07a643a1 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 14 Apr 2013 00:44:01 +0000 Subject: [PATCH 092/280] masquerade link in courseware/views.py --- lms/djangoapps/courseware/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index efd22f9985..91e7f4553b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -292,6 +292,7 @@ def index(request, course_id, chapter=None, section=None, 'init': '', 'content': '', 'staff_access': staff_access, + 'masquerade': masq, 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa') } From fdfc37e4e321454cb54103b01bdf5ed3d72aadf9 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 14 Apr 2013 01:05:52 +0000 Subject: [PATCH 093/280] masquerade pep8 --- lms/djangoapps/courseware/masquerade.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/masquerade.py b/lms/djangoapps/courseware/masquerade.py index 2fe1830940..33c3239f27 100644 --- a/lms/djangoapps/courseware/masquerade.py +++ b/lms/djangoapps/courseware/masquerade.py @@ -15,8 +15,9 @@ log = logging.getLogger(__name__) MASQ_KEY = 'masquerade_identity' + def handle_ajax(request, marg): - if marg=='toggle': + if marg == 'toggle': status = request.session.get(MASQ_KEY, '') if status is None or status in ['', 'staff']: status = 'student' @@ -37,7 +38,7 @@ def setup_masquerade(request, staff_access=False): if request.user is None: return None - if not staff_access: # can masquerade only if user has staff access to course + if not staff_access: # can masquerade only if user has staff access to course return None usertype = request.session.get(MASQ_KEY, '') @@ -45,7 +46,7 @@ def setup_masquerade(request, staff_access=False): request.session[MASQ_KEY] = 'staff' usertype = 'staff' - if usertype=='student': + if usertype == 'student': request.user.masquerade_as_student = True return usertype From 857a6e0bd0589951a3e0c31acc51445f013f81e2 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 14 Apr 2013 01:15:26 +0000 Subject: [PATCH 094/280] pylint masquerade fixes --- lms/djangoapps/courseware/access.py | 2 +- lms/djangoapps/courseware/masquerade.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 80b7af9694..f7cd74f121 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -15,7 +15,7 @@ from xmodule.modulestore import Location from xmodule.x_module import XModule, XModuleDescriptor from student.models import CourseEnrollmentAllowed -from masquerade import is_masquerading_as_student +from courseware.masquerade import is_masquerading_as_student DEBUG_ACCESS = False diff --git a/lms/djangoapps/courseware/masquerade.py b/lms/djangoapps/courseware/masquerade.py index 33c3239f27..5b8e265094 100644 --- a/lms/djangoapps/courseware/masquerade.py +++ b/lms/djangoapps/courseware/masquerade.py @@ -1,14 +1,12 @@ -#---------------------------------------- Masequerade ---------------------------------------- -# -# Allow course staff to see a student or staff view of courseware. -# Which kind of view has been selected is stored in the session state. +''' +---------------------------------------- Masequerade ---------------------------------------- +Allow course staff to see a student or staff view of courseware. +Which kind of view has been selected is stored in the session state. +''' import json import logging -from django.conf import settings -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required from django.http import HttpResponse log = logging.getLogger(__name__) @@ -17,6 +15,9 @@ MASQ_KEY = 'masquerade_identity' def handle_ajax(request, marg): + ''' + Handle ajax call from "staff view" / "student view" toggle button + ''' if marg == 'toggle': status = request.session.get(MASQ_KEY, '') if status is None or status in ['', 'staff']: @@ -53,5 +54,8 @@ def setup_masquerade(request, staff_access=False): def is_masquerading_as_student(user): + ''' + Return True if user is masquerading as a student, False otherwise + ''' masq = getattr(user, 'masquerade_as_student', False) return masq From 1c2452e401b8a2aa532bc3db00c366d436e24647 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 21:38:12 -0400 Subject: [PATCH 095/280] make is_masquerading_as_student work with Mock for tests --- lms/djangoapps/courseware/masquerade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/masquerade.py b/lms/djangoapps/courseware/masquerade.py index 5b8e265094..e98b0e1df4 100644 --- a/lms/djangoapps/courseware/masquerade.py +++ b/lms/djangoapps/courseware/masquerade.py @@ -58,4 +58,4 @@ def is_masquerading_as_student(user): Return True if user is masquerading as a student, False otherwise ''' masq = getattr(user, 'masquerade_as_student', False) - return masq + return masq==True From fac9d758d8f8cd29ce1f47b2bc40a4c16f12ad7d Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 22:14:51 -0400 Subject: [PATCH 096/280] fix typo in masquerade url name --- lms/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/urls.py b/lms/urls.py index c792c149e1..989118ddc4 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -345,7 +345,7 @@ if settings.COURSEWARE_ENABLED: # allow course staff to change to student view of courseware if settings.MITX_FEATURES.get('ENABLE_MASQUERADE'): urlpatterns += ( - url(r'^masquerade/(?P.*)$','courseware.masquerade.handle_ajax', name="masquerate-switch"), + url(r'^masquerade/(?P.*)$','courseware.masquerade.handle_ajax', name="masquerade-switch"), ) From 5ca0393e052a7e6c9a451d05ff42e0aa073d207c Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 22:30:07 -0400 Subject: [PATCH 097/280] add test for masquerade --- .../courseware/tests/test_masquerade.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 lms/djangoapps/courseware/tests/test_masquerade.py diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py new file mode 100644 index 0000000000..11cdc4c1f9 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -0,0 +1,120 @@ +""" +Unit tests for masquerade + +Based on (and depends on) unit tests for courseware. + +Notes for running by hand: + +django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware +""" + +from django.test.utils import override_settings + +from django.core.urlresolvers import reverse + +from django.contrib.auth.models import User, Group +from courseware.access import _course_staff_group_name +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from xmodule.modulestore.django import modulestore +import xmodule.modulestore.django +import json + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): + ''' + Check for staff being able to masquerade as student + ''' + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + #self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + #self.toy = modulestore().get_course("edX/toy/2012_Fall") + self.graded_course = modulestore().get_course("edX/graded/2012_Fall") + + # Create staff account + self.instructor = 'view2@test.com' + self.password = 'foo' + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.instructor) + + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(get_user(self.instructor)) + + make_instructor(self.graded_course) + + self.logout() + self.login(self.instructor, self.password) + self.enroll(self.graded_course) + # self.factory = RequestFactory() + + def get_cw_section(self): + url = reverse('courseware_section', + kwargs={'course_id': self.graded_course.id, + 'chapter': 'GradedChapter', + 'section': 'Homework1'}) + + resp = self.client.get(url) + + print "url ", url + return resp + + def test_staff_debug_for_staff(self): + resp = self.get_cw_section() + sdebug = '' + + self.assertTrue(sdebug in resp.content) + + + def toggle_masquerade(self): + ''' + Toggle masquerade state + ''' + masq_url = reverse('masquerade-switch', kwargs={'marg': 'toggle'}) + print "masq_url ", masq_url + resp = self.client.get(masq_url) + return resp + + def test_no_staff_debug_for_student(self): + togresp = self.toggle_masquerade() + print "masq now ", togresp.content + self.assertEqual(togresp.content, '{"status": "student"}', '') + + resp = self.get_cw_section() + sdebug = '' + + self.assertFalse(sdebug in resp.content) + + def get_problem(self): + pun = 'H1P1' + problem_location = "i4x://edX/graded/problem/%s" % pun + + modx_url = reverse('modx_dispatch', + kwargs={'course_id': self.graded_course.id, + 'location': problem_location, + 'dispatch': 'problem_get', }) + + resp = self.client.get(modx_url) + + print "modx_url ", modx_url + return resp + + def test_showanswer_for_staff(self): + resp = self.get_problem() + html = json.loads(resp.content)['html'] + print html + sabut = '' + self.assertTrue(sabut in html) + + def test_no_showanswer_for_student(self): + togresp = self.toggle_masquerade() + print "masq now ", togresp.content + self.assertEqual(togresp.content, '{"status": "student"}', '') + + resp = self.get_problem() + html = json.loads(resp.content)['html'] + print html + sabut = '' + self.assertFalse(sabut in html) From 5f74ccd1a0cdb46feaebe0e5e2f9486db7882c46 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 13 Apr 2013 22:52:17 -0400 Subject: [PATCH 098/280] use ENABLE_MASQUERADE in masquerade.py --- lms/djangoapps/courseware/masquerade.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lms/djangoapps/courseware/masquerade.py b/lms/djangoapps/courseware/masquerade.py index e98b0e1df4..27135f727e 100644 --- a/lms/djangoapps/courseware/masquerade.py +++ b/lms/djangoapps/courseware/masquerade.py @@ -8,6 +8,7 @@ import json import logging from django.http import HttpResponse +from django.conf import settings log = logging.getLogger(__name__) @@ -39,6 +40,9 @@ def setup_masquerade(request, staff_access=False): if request.user is None: return None + if not settings.MITX_FEATURES.get('ENABLE_MASQUERADE', False): + return None + if not staff_access: # can masquerade only if user has staff access to course return None From edfa4564838416182c3424678b1a775cd6929cb5 Mon Sep 17 00:00:00 2001 From: jmclaus Date: Mon, 15 Apr 2013 14:05:39 +0200 Subject: [PATCH 099/280] Grading works when sequence is loaded from saved state --- common/static/js/capa/edit-a-gene.js | 25 +- ...1B31BA00E7CE7B6BD63DD13A8586A45.cache.html | 652 +++++++++++++++++ ...6DBCB09BEC38A6DEE76494C6517111B.cache.html | 641 ----------------- ...57C7018CDCA52B163256408948A1722.cache.html | 651 ----------------- ...3308EE54E8033A708B414CAC05B0C32.cache.html | 642 +++++++++++++++++ ...AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html | 628 +++++++++++++++++ ...66AF633CAA7EA4DA7E906456CDEC65A.cache.html | 627 ----------------- ...F9C3F1A91187AA8391FD08BA7F8716D.cache.html | 641 ----------------- ...B4F4D4EFA24CDE2E4287CC07897F249.cache.html | 654 ++++++++++++++++++ ...016796CF7FB22261AE1160531B5CF82.cache.html | 653 ----------------- ...069AC107D79C29D6237614AC340F0C0.cache.html | 652 +++++++++++++++++ ...6220FCC8B9234FEAD8D826A73C6D2A4.cache.html | 642 +++++++++++++++++ ...28D6C3D881F6C18E3357AAB004477EF.cache.html | 651 ----------------- common/static/js/capa/genex/genex.nocache.js | 4 +- 14 files changed, 3886 insertions(+), 3877 deletions(-) create mode 100644 common/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html delete mode 100644 common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html delete mode 100644 common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html create mode 100644 common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html create mode 100644 common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html delete mode 100644 common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html delete mode 100644 common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html create mode 100644 common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html delete mode 100644 common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html create mode 100644 common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html create mode 100644 common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html delete mode 100644 common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html diff --git a/common/static/js/capa/edit-a-gene.js b/common/static/js/capa/edit-a-gene.js index eb404de881..fa0b491886 100644 --- a/common/static/js/capa/edit-a-gene.js +++ b/common/static/js/capa/edit-a-gene.js @@ -15,7 +15,7 @@ // NOTE: // Genex uses 8 global functions, all prefixed with genex: // 6 are exported from GWT: - // genexSetInitialDNASequence + // genexSetDefaultDNASequence // genexSetDNASequence // genexGetDNASequence // genexSetClickEvent @@ -36,32 +36,35 @@ genexIsReady = function() { var input_field = genexGetInputField(); var genex_saved_state = input_field.val(); - var genex_initial_dna_sequence; + var genex_default_dna_sequence; var genex_dna_sequence; - //Get the sequence from xml file - genex_initial_dna_sequence = $('#genex_dna_sequence').val(); - //Call this function to set the value used by reset button - genexSetInitialDNASequence(genex_initial_dna_sequence); + //Get the DNA sequence from xml file + genex_default_dna_sequence = $('#genex_dna_sequence').val(); + //Set the default DNA + genexSetDefaultDNASequence(genex_default_dna_sequence); + //Now load problem + var genex_problem_number = $('#genex_problem_number').val(); + genexSetProblemNumber(genex_problem_number); + + //Set the DNA sequence that is displayed if (genex_saved_state === '') { //Load DNA sequence from xml file - genex_dna_sequence = genex_initial_dna_sequence; + genex_dna_sequence = genex_default_dna_sequence; } else { //Load DNA sequence from saved value genex_saved_state = JSON.parse(genex_saved_state); genex_dna_sequence = genex_saved_state.genex_dna_sequence; } - genexSetDNASequence(genex_dna_sequence); + //Now load mouse and keyboard handlers genexSetClickEvent(); genexSetKeyEvent(); - //Now load problem - var genex_problem_number = $('#genex_problem_number').val(); - genexSetProblemNumber(genex_problem_number); }; + genexStoreAnswer = function(answer) { var input_field = genexGetInputField(); var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer}; diff --git a/common/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html b/common/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html new file mode 100644 index 0000000000..ec8170eb47 --- /dev/null +++ b/common/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html @@ -0,0 +1,652 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html b/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html deleted file mode 100644 index 054ef6c31a..0000000000 --- a/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html b/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html deleted file mode 100644 index 3862093b1b..0000000000 --- a/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html b/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html new file mode 100644 index 0000000000..952e3b5f37 --- /dev/null +++ b/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html b/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html new file mode 100644 index 0000000000..95cb962805 --- /dev/null +++ b/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html @@ -0,0 +1,628 @@ + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html b/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html deleted file mode 100644 index 472502dde2..0000000000 --- a/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html +++ /dev/null @@ -1,627 +0,0 @@ - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html b/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html deleted file mode 100644 index f488b6fcf6..0000000000 --- a/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html b/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html new file mode 100644 index 0000000000..5c828c1209 --- /dev/null +++ b/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html @@ -0,0 +1,654 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html b/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html deleted file mode 100644 index f799ecf5b7..0000000000 --- a/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html +++ /dev/null @@ -1,653 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html b/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html new file mode 100644 index 0000000000..bcf15330d9 --- /dev/null +++ b/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html @@ -0,0 +1,652 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html b/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html new file mode 100644 index 0000000000..5ab12af718 --- /dev/null +++ b/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html b/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html deleted file mode 100644 index 0c242fde9c..0000000000 --- a/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/genex.nocache.js b/common/static/js/capa/genex/genex.nocache.js index f457e80b6c..11f9714afb 100644 --- a/common/static/js/capa/genex/genex.nocache.js +++ b/common/static/js/capa/genex/genex.nocache.js @@ -1,4 +1,4 @@ -function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='46DBCB09BEC38A6DEE76494C6517111B',Rb='557C7018CDCA52B163256408948A1722',Sb='866AF633CAA7EA4DA7E906456CDEC65A',Tb='8F9C3F1A91187AA8391FD08BA7F8716D',Wb=':',pb='::',dc=' diff --git a/cms/templates/index.html b/cms/templates/index.html index 916720f4e7..0f6e982b1d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,6 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="base.html" /> -<%block name="title">My Courses +<%block name="title">${_("My Courses")} <%block name="bodyclass">is-signedin index dashboard <%block name="header_extras"> @@ -36,18 +38,18 @@
    -

    My Courses

    +

    ${_("My Courses")}

    % if user.is_active:
    - \ No newline at end of file + diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 7162dad50f..db7d5fb3f8 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,8 +1,10 @@ <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> + \ No newline at end of file + + diff --git a/cms/urls.py b/cms/urls.py index e1eae3352a..832879b51e 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -94,7 +94,7 @@ urlpatterns = ('', # noop to squelch ajax errors url(r'^event$', 'contentstore.views.event', name='event'), - url(r'^heartbeat$', include('heartbeat.urls')), + url(r'^heartbeat$', include('heartbeat.urls')) ) # User creation and updating views @@ -118,6 +118,17 @@ urlpatterns += ( ) +js_info_dict = { + 'domain': 'djangojs', + 'packages': ('cms',), + } + +urlpatterns += ( + # Serve catalog of localized strings to be rendered by Javascript + url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), + ) + + if settings.ENABLE_JASMINE: # # Jasmine urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) diff --git a/create-dev-env.sh b/create-dev-env.sh index f0ebca3ff7..f87d88401d 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -93,7 +93,7 @@ clone_repos() { ### START PROG=${0##*/} -BASE="$HOME/mitx_all" +BASE="$HOME/src/mitx_all" PYTHON_DIR="$BASE/python" RUBY_DIR="$BASE/ruby" RUBY_VER="1.9.3" @@ -290,7 +290,8 @@ source $PYTHON_DIR/bin/activate NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" -if [[ -n $compile ]]; then +if [-z "false"]; then + if [[ -n $compile ]]; then output "Downloading numpy and scipy" curl -sL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz curl -sL -o scipy.tar.gz http://downloads.sourceforge.net/project/scipy/scipy/${SCIPY_VER}/scipy-${SCIPY_VER}.tar.gz @@ -305,6 +306,7 @@ if [[ -n $compile ]]; then python setup.py install cd "$BASE" rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER} + fi fi case `uname -s` in diff --git a/i18n/converter.py b/i18n/converter.py new file mode 100644 index 0000000000..fe66ff3e74 --- /dev/null +++ b/i18n/converter.py @@ -0,0 +1,74 @@ +import re, itertools + +# Converter is an abstract class that transforms strings. +# It hides embedded tags (HTML or Python sequences) from transformation +# +# To implement Converter, provide implementation for inner_convert_string() + + +class Converter: + + # matches tags like these: + # HTML: , ,
    , + # Python: %(date)s, %(name)s + # + tag_pattern = re.compile(r'(<[-\w" .:?=/]*>)|({[^}]*})|(%\(.*\)\w)', re.I) + + + def convert (self, string): + if self.tag_pattern.search(string): + result = self.convert_tagged_string(string) + else: + result = self.inner_convert_string(string) + return result + + # convert_tagged_string(string): + # returns: a converted tagged string + # param: string (contains html tags) + # + # Don't replace characters inside tags + # + # Strategy: + # 1. extract tags embedded in the string + # a. use the index of each extracted tag to re-insert it later + # b. replace tags in string with numbers (<0>, <1>, etc.) + # c. save extracted tags in a separate list + # 2. convert string + # 3. re-insert the extracted tags + # + def convert_tagged_string (self, string): + (string, tags) = self.detag_string(string) + string = self.inner_convert_string(string) + string = self.retag_string(string, tags) + return string + + # extracts tags from string. + # + # returns (string, list) where + # string: string has tags replaced by indices (
    ... => <0>, <1>, <2>, etc.) + # list: list of the removed tags ("
    ", "", "") + def detag_string (self, string): + counter = itertools.count(0) + count = lambda m: '<%s>' % counter.next() + tags = self.tag_pattern.findall(string) + tags = [''.join(tag) for tag in tags] + (new, nfound) = self.tag_pattern.subn(count, string) + if len(tags) != nfound: + raise Exception('tags dont match:'+string) + return (new, tags) + + # substitutes each tag back into string, into occurrences of <0>, <1> etc + # + def retag_string (self, string, tags): + for (i, tag) in enumerate(tags): + p = '<%s>' % i + string = re.sub(p, tag, string, 1) + return string + + + # ------------------------------ + # Customize this in subclasses of Converter + + def inner_convert_string (self, string): + return string # do nothing by default + diff --git a/i18n/dummy.py b/i18n/dummy.py new file mode 100644 index 0000000000..a94d400ba0 --- /dev/null +++ b/i18n/dummy.py @@ -0,0 +1,186 @@ +# -*- coding: iso-8859-15 -*- + +from converter import Converter + +# This file converts string resource files. +# Java: file has name like messages_en.properties +# Flex: file has name like locales/en_US/Labels.properties + +# Creates new localization properties files in a dummy language (saved as 'vr', Vardebedian) +# Each property file is derived from the equivalent en_US file, except +# 1. Every vowel is replaced with an equivalent with extra accent marks +# 2. Every string is padded out to +30% length to simulate verbose languages (e.g. German) +# to see if layout and flows work properly +# 3. Every string is terminated with a '#' character to make it easier to detect truncation + + +# -------------------------------- +# Example use: +# >>> from dummy import Dummy +# >>> c = Dummy() +# >>> print c.convert("hello my name is Bond, James Bond") +# héllö my nämé ïs Bönd, Jämés Bönd Lorem i# +# +# >>> print c.convert('don\'t convert tag ids') +# dön't çönvért täg ïds Lorem ipsu# +# +# >>> print c.convert('don\'t convert %(name)s tags on %(date)s') +# dön't çönvért %(name)s tags on %(date)s Lorem ips# + + +# Substitute plain characters with accented lookalikes. +# http://tlt.its.psu.edu/suggestions/international/web/codehtml.html#accent +# print "print u'\\x%x'" % 207 +TABLE = {'A': u'\xC0', + 'a': u'\xE4', + 'b': u'\xDF', + 'C': u'\xc7', + 'c': u'\xE7', + 'E': u'\xC9', + 'e': u'\xE9', + 'I': U'\xCC', + 'i': u'\xEF', + 'O': u'\xD8', + 'o': u'\xF6', + 'u': u'\xFC' + } + + + +# The print industry's standard dummy text, in use since the 1500s +# see http://www.lipsum.com/ +LOREM = ' Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed ' \ + 'do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad ' \ + 'minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ' \ + 'ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate ' \ + 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat ' \ + 'cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ' + +# To simulate more verbose languages (like German), pad the length of a string +# by a multiple of PAD_FACTOR +PAD_FACTOR = 1.3 + + +class Dummy (Converter): + ''' + A string converter that generates dummy strings with fake accents + and lorem ipsum padding. + ''' + + def convert (self, string): + result = Converter.convert(self, string) + return self.pad(result) + + def inner_convert_string (self, string): + for (k,v) in TABLE.items(): + string = string.replace(k, v) + return string + + + def pad (self, string): + '''add some lorem ipsum text to the end of string''' + size = len(string) + if size < 7: + target = size*3 + else: + target = int(size*PAD_FACTOR) + return string + self.terminate(LOREM[:(target-size)]) + + def terminate (self, string): + '''replaces the final char of string with #''' + return string[:-1]+'#' + + def init_msgs (self, msgs): + ''' + Make sure the first msg in msgs has a plural property. + msgs is list of instances of pofile.Msg + ''' + if len(msgs)==0: + return + headers = msgs[0].get_property('msgstr') + has_plural = len([header for header in headers if header.find('Plural-Forms:') == 0])>0 + if not has_plural: + # Apply declaration for English pluralization rules + plural = "Plural-Forms: nplurals=2; plural=(n != 1);\\n" + headers.append(plural) + + + def convert_msg (self, msg): + ''' + Takes one Msg object and converts it (adds a dummy translation to it) + msg is an instance of pofile.Msg + ''' + source = msg.get_property('msgid') + if len(source)==1 and len(source[0])==0: + # don't translate empty string + return + plural = msg.get_property('msgid_plural') + if len(plural)>0: + # translate singular and plural + foreign_single = self.convert(merge(source)) + foreign_plural = self.convert(merge(plural)) + msg.set_property('msgstr[0]', split(foreign_single)) + msg.set_property('msgstr[1]', split(foreign_plural)) + return + else: + src_merged = merge(source) + foreign = self.convert(src_merged) + if len(source)>1: + # If last char is a newline, make sure translation + # has a newline too. + if src_merged[-2:]=='\\n': + foreign += '\\n' + msg.set_property('msgstr', split(foreign)) + + +# ---------------------------------- +# String splitting utility functions + +SPLIT_SIZE = 70 + +def merge (string_list): + '''returns a single string: concatenates string_list''' + return ''.join(string_list) + +# .po file format requires long strings to be broken +# up into several shorter (<80 char) strings. +# The first string is empty (""), which indicates +# that more are to be read on following lines. + +def split (string): + ''' + Returns string split into fragments of a given size. + If there are multiple fragments, insert "" as the first fragment. + ''' + result = [chunk for chunk in chunks(string, SPLIT_SIZE)] + if len(result)>1: + result = [''] + result + return result + +def chunks(string, size): + ''' + Generate fragments of a given size from string. Avoid breaking + the string in the middle of an escape sequence (e.g. "\n") + ''' + strlen=len(string)-1 + esc = False + last = 0 + for i,char in enumerate(string): + if not esc and char == '\\': + esc = True + continue + if esc: + esc = False + if i>=last+size-1 or i==strlen: + chunk = string[last:i+1] + last = i+1 + yield chunk + +# testing +# >>> a = "abcd\\efghijklmnopqrstuvwxyz" +# >>> SPLIT_SIZE = 5 +# >>> split(a) +# ['abcd\\e', 'fghij', 'klmno', 'pqrst', 'uvwxy', 'z'] +# >>> merge(split(a)) +# 'abcd\\efghijklmnopqrstuvwxyz' + diff --git a/i18n/googleTranslate.py b/i18n/googleTranslate.py new file mode 100644 index 0000000000..e79dbe00a2 --- /dev/null +++ b/i18n/googleTranslate.py @@ -0,0 +1,68 @@ +import urllib, urllib2, json + +# Google Translate API +# see https://code.google.com/apis/language/translate/v2/getting_started.html +# +# +# usage: translate('flower', 'fr') => 'fleur' + + +# -------------------------------------------- +# Translation limit = 100,000 chars/day (request submitted for more) +# Limit of 5,000 characters per request +# This key is personally registered to Steve Strassmann +# +#KEY = 'AIzaSyCDapmXdBtIYw3ofsvgm6gIYDNwiVmSm7g' +KEY = 'AIzaSyDOhTQokSOqqO-8ZJqUNgn12C83g-muIqA' + +URL = 'https://www.googleapis.com/language/translate/v2' + +SOURCE = 'en' # source: English + +TARGETS = ['zh-CN', 'ja', 'fr', 'de', # tier 1: Simplified Chinese, Japanese, French, German + 'es', 'it', # tier 2: Spanish, Italian + 'ru'] # extra credit: Russian + + +def translate (string, target): + return extract(fetch(string, target)) + + +# Ask Google to translate string to target language +# string: English string +# target: lang (e.g. 'fr', 'cn') +# Returns JSON object +def fetch (string, target, url=URL, key=KEY, source=SOURCE): + data = {'key':key, + 'q':string, + 'source': source, + 'target':target} + fullUrl = '%s?%s' % (url, urllib.urlencode(data)) + try: + response = urllib2.urlopen(fullUrl) + return json.loads(response.read()) + except urllib2.HTTPError as err: + if err.code == 403: + print "***" + print "*** Possible daily limit exceeded for Google Translate:" + print "***" + print "***", json.loads("".join(err.readlines())) + print "***" + raise + + + +# Extracts a translated result from a json object returned from Google +def extract (response): + data = response['data'] + translations = data['translations'] + first = translations[0] + result = first.get('translated_text', None) + if result != None: + return result + else: + result = first.get('translatedText', None) + if result != None: + return result + else: + raise Exception("Could not read translation from: %s" % translations) diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py new file mode 100755 index 0000000000..8bf9711c57 --- /dev/null +++ b/i18n/make_dummy.py @@ -0,0 +1,65 @@ +#!/usr/bin/python + +# Generate test translation files from human-readable po files. +# +# +# po files can be generated with this: +# django-admin.py makemessages --all --extension html -l en + +# Usage: +# +# $ ./make_dummy.py +# +# $ ./make_dummy.py mitx/conf/locale/en/LC_MESSAGES/django.po +# +# generates output to +# mitx/conf/locale/vr/LC_MESSAGES/django.po + +import os, sys +from pofile import PoFile +from dummy import Dummy + +# Dummy language +# two letter language codes reference: +# see http://www.loc.gov/standards/iso639-2/php/code_list.php +# +# Django will not localize in languages that django itself has not been +# localized for. So we are using a well-known language: 'fr'. + +OUT_LANG = 'fr' + +def main (file): + ''' + Takes a source po file, reads it, and writes out a new po file + containing a dummy translation. + ''' + pofile = PoFile(file) + converter = Dummy() + converter.init_msgs(pofile.msgs) + for msg in pofile.msgs: + converter.convert_msg(msg) + new_file = new_filename(file, OUT_LANG) + create_dir_if_necessary(new_file) + pofile.write(new_file) + + +def new_filename (original_filename, new_lang): + '''Returns a filename derived from original_filename, using new_lang as the locale''' + orig_dir = os.path.dirname(original_filename) + msgs_dir = os.path.basename(orig_dir) + orig_file = os.path.basename(original_filename) + return '%s/%s/%s/%s' % (os.path.abspath(orig_dir + '/../..'), + new_lang, + msgs_dir, + orig_file) + + +def create_dir_if_necessary(pathname): + dirname = os.path.dirname(pathname) + if not os.path.exists(dirname): + os.makedirs(dirname) + +if __name__ == '__main__': + if len(sys.argv)<2: + raise Exception("missing file argument") + main(sys.argv[1]) diff --git a/i18n/pofile.py b/i18n/pofile.py new file mode 100644 index 0000000000..d91f76a925 --- /dev/null +++ b/i18n/pofile.py @@ -0,0 +1,143 @@ +import re, codecs +from operator import itemgetter + +# Django stores externalized strings in .po and .mo files. +# po files are human readable and contain metadata about the strings. +# mo files are machine readable and optimized for runtime performance. + +# See https://docs.djangoproject.com/en/1.3/topics/i18n/internationalization/ +# See http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html + +# Usage: +# >>> pofile = PoFile('/path/to/file') + + +class PoFile: + + # Django requires po files to be in UTF8 with no BOM (byte order marker) + # see "Mind your charset" on this page: + # https://docs.djangoproject.com/en/1.3/topics/i18n/localization/ + + ENCODING = 'utf_8' + + def __init__ (self, pathname): + self.pathname = pathname + self.parse() + + def parse (self): + with codecs.open(self.pathname, 'r', self.ENCODING) as stream: + text = stream.read() + msgs = text.split('\n\n') + self.msgs = [Msg.parse(m) for m in msgs] + return msgs + + def write (self, out_pathname=None): + if out_pathname == None: + out_pathname = self.pathname + with codecs.open(out_pathname, 'w', self.ENCODING) as stream: + for msg in self.msgs: + msg.write(stream) + +class Msg: + + # A PoFile is parsed into a list of Msg objects, each of which corresponds + # to an externalized string entry. + + # Each Msg object may contain multiple comment lines, capturing metadata + + # Each Msg has a property list (self.props) with a dict of key-values. + # Each value is a list of strings + kwords = ['msgid', 'msgstr', 'msgctxt', 'msgid_plural'] + + # Line might begin with "msgid ..." or "msgid[2] ..." + pattern = re.compile('^(\w+)(\[(\d+)\])?') + + @classmethod + def parse (cls, string): + ''' + String is a fragment of a pofile (.po) source file. + This returns a Msg object created by parsing string. + ''' + lines = string.strip().split('\n') + msg = Msg() + msg.comments = [] + msg.props = {} + last_kword = None + for line in lines: + if line[0]=='#': + msg.comments.append(line) + elif line[0]=='"' and last_kword != None: + msg.add_string(last_kword, line) + else: + match = cls.pattern.search(line) + if match: + kword = match.group(1) + last_kword = kword + if kword in cls.kwords: + if match.group(3): + key = '%s[%s]' % (kword, match.group(3)) + msg.add_string(key, line[len(key):]) + else: + msg.add_string(kword, line[len(kword):]) + return msg + + def get_property (self, kword): + '''returns value for kword. Typically returns a list of strings''' + return self.props.get(kword, []) + + def set_property (self, kword, value): + '''sets value for kword. Typically returns a list of strings''' + self.props[kword] = value + + def add_string (self, kword, line): + '''Append line to the list of values stored for the property kword''' + props = self.props + value = self.get_property(kword) + value.append(self.cleanup_string(line)) + self.set_property(kword, value) + + def cleanup_string(self, string): + string = string.strip() + if len(string)>1 and string[0]=='"' and string[-1]=='"': + return string[1:-1] + else: + return string + + def write (self, stream): + '''Write a Msg to stream''' + for comment in self.comments: + stream.write(comment) + stream.write('\n') + for (key, values) in self.sort(self.props.items()): + stream.write(key + ' ') + for value in values: + stream.write('"'+value+'"') + stream.write('\n') + stream.write('\n') + + # Preferred ordering of key output + # Always print 'msgctxt' first, then 'msgid', etc. + KEY_ORDER = ('msgctxt', 'msgid', 'msgid_plural', 'msgstr', 'msgstr[0]', 'msgstr[1]') + + def keyword_compare (self, k1, k2): + for key in self.KEY_ORDER: + if key == k1: + return -1 + if key == k2: + return 1 + return 0 + + def sort (self, plist): + '''sorts a propertylist to bring the high-priority keys to the beginning of the list''' + return sorted(plist, key=itemgetter(0), cmp=self.keyword_compare) + + + +# Testing +# +# >>> file = 'mitx/conf/locale/en/LC_MESSAGES/django.po' +# >>> file1 = 'mitx/conf/locale/en/LC_MESSAGES/django1.po' +# >>> po = PoFile(file) +# >>> po.write(file1) +# $ diff file file1 + diff --git a/i18n/update.py b/i18n/update.py new file mode 100755 index 0000000000..8a865c2528 --- /dev/null +++ b/i18n/update.py @@ -0,0 +1,101 @@ +#!/usr/bin/python + +import os, subprocess, logging, json +from make_dummy import create_dir_if_necessary, main as dummy_main + +''' +Generate or update all translation files + Usage: + $ update.py + + + 1. extracts files from mako templates + 2. extracts files from django templates and python source files + 3. extracts files from django javascript files + 4. generates dummy text translations + 5. compiles po files to mo files + + Configuration (e.g. known languages) declared in mitx/conf/locale/config +''' + +# ----------------------------------- +# BASE_DIR is the working directory to execute django-admin commands from. +# Typically this should be the 'mitx' directory. +BASE_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))+'/..') + +# LOCALE_DIR contains the locale files. +# Typically this should be 'mitx/conf/locale' +LOCALE_DIR = BASE_DIR + '/conf/locale' + +# MSGS_DIR contains the English po files +MSGS_DIR = LOCALE_DIR + '/en/LC_MESSAGES' + +# CONFIG_FILENAME contains localization configuration in json format +CONFIG_FILENAME = LOCALE_DIR + '/config' + +# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files +BABEL_CONFIG = LOCALE_DIR + '/babel.cfg' + +# Strings from mako template files are written to BABEL_OUT +BABEL_OUT = MSGS_DIR + '/mako.po' + +# These are the shell commands invoked by main() +COMMANDS = { + 'babel_mako': 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT), + 'make_django': 'django-admin.py makemessages --all --extension html -l en', + 'make_djangojs': 'django-admin.py makemessages --all -d djangojs --extension js -l en', + 'msgcat' : 'msgcat -o merged.po django.po %s' % BABEL_OUT, + 'rename_django' : 'mv django.po django_old.po', + 'rename_merged' : 'mv merged.po django.po', + 'compile': 'django-admin.py compilemessages' + + } + +def execute (command_kwd, log, working_directory=BASE_DIR): + ''' + Executes command_kwd, which references a shell command in COMMANDS. + ''' + full_cmd = COMMANDS[command_kwd] + log.info('%s' % full_cmd) + subprocess.call(full_cmd.split(' '), cwd=working_directory) + +def make_log (): + '''returns a logger''' + log = logging.getLogger(__name__) + log.setLevel(logging.INFO) + log_handler = logging.StreamHandler() + log_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) + log.addHandler(log_handler) + return log + +def get_config (): + '''Returns data found in config file, or returns None if file not found''' + config_path = os.path.abspath(CONFIG_FILENAME) + if not os.path.exists(config_path): + return None + with open(config_path) as stream: + return json.load(stream) + +def main (): + log = make_log() + create_dir_if_necessary(LOCALE_DIR) + log.info('Executing all commands from %s' % BASE_DIR) + + # Generate or update human-readable .po files from all source code. + execute('babel_mako', log=log) + execute('make_django', log=log) + execute('make_djangojs', log=log) + execute('msgcat', log=log, working_directory=MSGS_DIR) + execute('rename_django', log=log, working_directory=MSGS_DIR) + execute('rename_merged', log=log, working_directory=MSGS_DIR) + + # Generate dummy text files from the English .po files + log.info('Generating dummy text.') + dummy_main(LOCALE_DIR + '/en/LC_MESSAGES/django.po') + dummy_main(LOCALE_DIR + '/en/LC_MESSAGES/djangojs.po') + + # Generate machine-readable .mo files + execute('compile', log) + +if __name__ == '__main__': + main() diff --git a/lms/templates/discussion/_single_thread.html b/lms/templates/discussion/_single_thread.html index 0dec32ad47..e291bc955c 100644 --- a/lms/templates/discussion/_single_thread.html +++ b/lms/templates/discussion/_single_thread.html @@ -6,7 +6,7 @@
    - %if thread['group_id'] + %if thread['group_id']:
    This post visible only to group ${cohort_dictionary[thread['group_id']]}.
    %endif @@ -35,4 +35,4 @@ -<%include file="_js_data.html" /> \ No newline at end of file +<%include file="_js_data.html" /> From e76419093d976bb2f2e91fade4926e826d3e2183 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Wed, 17 Apr 2013 12:25:23 -0400 Subject: [PATCH 133/280] uncommit unneeded files --- cms/urls.py | 2 +- create-dev-env.sh | 6 ++-- i18n/googleTranslate.py | 68 ----------------------------------------- 3 files changed, 3 insertions(+), 73 deletions(-) delete mode 100644 i18n/googleTranslate.py diff --git a/cms/urls.py b/cms/urls.py index 832879b51e..598d91b075 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -94,7 +94,7 @@ urlpatterns = ('', # noop to squelch ajax errors url(r'^event$', 'contentstore.views.event', name='event'), - url(r'^heartbeat$', include('heartbeat.urls')) + url(r'^heartbeat$', include('heartbeat.urls')), ) # User creation and updating views diff --git a/create-dev-env.sh b/create-dev-env.sh index f87d88401d..f0ebca3ff7 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -93,7 +93,7 @@ clone_repos() { ### START PROG=${0##*/} -BASE="$HOME/src/mitx_all" +BASE="$HOME/mitx_all" PYTHON_DIR="$BASE/python" RUBY_DIR="$BASE/ruby" RUBY_VER="1.9.3" @@ -290,8 +290,7 @@ source $PYTHON_DIR/bin/activate NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" -if [-z "false"]; then - if [[ -n $compile ]]; then +if [[ -n $compile ]]; then output "Downloading numpy and scipy" curl -sL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz curl -sL -o scipy.tar.gz http://downloads.sourceforge.net/project/scipy/scipy/${SCIPY_VER}/scipy-${SCIPY_VER}.tar.gz @@ -306,7 +305,6 @@ if [-z "false"]; then python setup.py install cd "$BASE" rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER} - fi fi case `uname -s` in diff --git a/i18n/googleTranslate.py b/i18n/googleTranslate.py deleted file mode 100644 index e79dbe00a2..0000000000 --- a/i18n/googleTranslate.py +++ /dev/null @@ -1,68 +0,0 @@ -import urllib, urllib2, json - -# Google Translate API -# see https://code.google.com/apis/language/translate/v2/getting_started.html -# -# -# usage: translate('flower', 'fr') => 'fleur' - - -# -------------------------------------------- -# Translation limit = 100,000 chars/day (request submitted for more) -# Limit of 5,000 characters per request -# This key is personally registered to Steve Strassmann -# -#KEY = 'AIzaSyCDapmXdBtIYw3ofsvgm6gIYDNwiVmSm7g' -KEY = 'AIzaSyDOhTQokSOqqO-8ZJqUNgn12C83g-muIqA' - -URL = 'https://www.googleapis.com/language/translate/v2' - -SOURCE = 'en' # source: English - -TARGETS = ['zh-CN', 'ja', 'fr', 'de', # tier 1: Simplified Chinese, Japanese, French, German - 'es', 'it', # tier 2: Spanish, Italian - 'ru'] # extra credit: Russian - - -def translate (string, target): - return extract(fetch(string, target)) - - -# Ask Google to translate string to target language -# string: English string -# target: lang (e.g. 'fr', 'cn') -# Returns JSON object -def fetch (string, target, url=URL, key=KEY, source=SOURCE): - data = {'key':key, - 'q':string, - 'source': source, - 'target':target} - fullUrl = '%s?%s' % (url, urllib.urlencode(data)) - try: - response = urllib2.urlopen(fullUrl) - return json.loads(response.read()) - except urllib2.HTTPError as err: - if err.code == 403: - print "***" - print "*** Possible daily limit exceeded for Google Translate:" - print "***" - print "***", json.loads("".join(err.readlines())) - print "***" - raise - - - -# Extracts a translated result from a json object returned from Google -def extract (response): - data = response['data'] - translations = data['translations'] - first = translations[0] - result = first.get('translated_text', None) - if result != None: - return result - else: - result = first.get('translatedText', None) - if result != None: - return result - else: - raise Exception("Could not read translation from: %s" % translations) From 9f158774e4ec981224c0e07a8c7dc1cc5f9bc55a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 17 Apr 2013 12:26:33 -0400 Subject: [PATCH 134/280] Disable pdb by default when running tests --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9feac06260..d01e55c7db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,4 +4,4 @@ with-xunit=1 rednose=1 # Uncomment the following line to open pdb when a test fails -pdb=1 \ No newline at end of file +#pdb=1 \ No newline at end of file From 73f5dc1cf5182c45436da8974a533e9933e4a0bf Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 17 Apr 2013 12:26:52 -0400 Subject: [PATCH 135/280] Add documentation of the setup.cfg flag for pdb --- doc/development.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/development.md b/doc/development.md index 184767a139..e89d8b19af 100644 --- a/doc/development.md +++ b/doc/development.md @@ -67,7 +67,7 @@ If if you aren't changing static files, can run `rake test` once, then run or rake fasttest_cms - + xmodule can be tested independently, with this: rake test_common/lib/xmodule @@ -90,7 +90,7 @@ To run a single nose test: nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify -Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html +Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html ### Javascript Tests @@ -105,7 +105,7 @@ To run the tests headless, you must install phantomjs (http://phantomjs.org/down rake phantomjs_jasmine_{lms,cms} If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it - + PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms} @@ -126,7 +126,7 @@ When you connect to the LMS, you need to use the public ip. Use `ifconfig` to f ## Content development -If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore. +If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore. Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course. From c4e4f73373b9c6ad67581126683e146d1cebec6d Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 17 Apr 2013 13:19:42 -0400 Subject: [PATCH 136/280] studio - adding in course name value in the share course mailto link --- cms/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index cc5dafc57b..fa6983300b 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -92,7 +92,7 @@ from contentstore import utils
    From 2f84b230479bbd5f544a7d4dff2beeb0bf12c292 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Wed, 17 Apr 2013 13:20:20 -0400 Subject: [PATCH 137/280] disable i18n unit test - it wont run without creating dummy strings first --- cms/djangoapps/contentstore/tests/test_i18n.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index 8b9fb9e16f..fba2da10dd 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -4,6 +4,7 @@ from django.test import TestCase from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.test.client import Client +from nose.tools import nottest class InternationalizationTest(TestCase): """ @@ -60,7 +61,8 @@ class InternationalizationTest(TestCase): # with actual French. This test will need to be updated with # actual French at that time. - + # Test temporarily disable since it depends on creation of dummy strings + @nottest def test_course_with_accents (self): """Test viewing the index page with no courses""" # Create a course so there is something to view From 33ba38b8dda3ca57ed4565b9bb840a5513b7d236 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 17 Apr 2013 13:38:14 -0400 Subject: [PATCH 138/280] studio - prefixed course URL with https: for individual share course mailto link on settings --- cms/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index fa6983300b..da807fd1d2 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -92,7 +92,7 @@ from contentstore import utils From b115ad46bf31f97447d66a9883cc2204f0578995 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 17 Apr 2013 13:44:23 -0400 Subject: [PATCH 139/280] studio - changed https to http in course mailto link and added that prefix to link displayed --- cms/templates/settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index da807fd1d2..9cfe557d47 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -87,12 +87,12 @@ from contentstore import utils From 423fa4781134199111c62b437279cb6ddef7d0e9 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 17 Apr 2013 13:47:54 -0400 Subject: [PATCH 140/280] studio - reverting back to https references in the course mailto link and url display --- cms/templates/settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 9cfe557d47..7ed918604a 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -87,12 +87,12 @@ from contentstore import utils From 44bf60d20811454071e0c3e6c871e159409e8a1c Mon Sep 17 00:00:00 2001 From: "Mark L. Chang" Date: Wed, 17 Apr 2013 14:03:29 -0400 Subject: [PATCH 141/280] copy changes, link text change --- cms/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 7ed918604a..23cc41d504 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -92,7 +92,7 @@ from contentstore import utils From 0f26be2f4db6beb500d470d8e5dbdb3415b2a0e6 Mon Sep 17 00:00:00 2001 From: "Mark L. Chang" Date: Wed, 17 Apr 2013 14:12:16 -0400 Subject: [PATCH 142/280] quick fix to email copy --- cms/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 23cc41d504..3923c0f905 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -92,7 +92,7 @@ from contentstore import utils From b42c4dacf73824dbbf32d1447661287548d77732 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 16 Apr 2013 11:19:11 -0400 Subject: [PATCH 143/280] Added test for "radio" choicegroup --- .../courseware/features/problems.feature | 5 ++++ .../courseware/features/problems.py | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index dc8495af60..266ffa3680 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -15,6 +15,7 @@ Feature: Answer problems | drop down | | multiple choice | | checkbox | + | radio | | string | | numerical | | formula | @@ -33,6 +34,7 @@ Feature: Answer problems | drop down | | multiple choice | | checkbox | + | radio | | string | | numerical | | formula | @@ -50,6 +52,7 @@ Feature: Answer problems | drop down | | multiple choice | | checkbox | + | radio | | string | | numerical | | formula | @@ -71,6 +74,8 @@ Feature: Answer problems | multiple choice | incorrect | | checkbox | correct | | checkbox | incorrect | + | radio | correct | + | radio | incorrect | | string | correct | | string | incorrect | | numerical | correct | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index b25d606c4e..3d538d7ae1 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -42,7 +42,13 @@ PROBLEM_FACTORY_DICT = { 'choice_type': 'checkbox', 'choices': [True, False, True, False, False], 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, - + 'radio': { + 'factory': ChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choice 3', + 'choice_type': 'radio', + 'choices': [False, False, True, False], + 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, 'string': { 'factory': StringResponseXMLFactory(), 'kwargs': { @@ -174,6 +180,12 @@ def answer_problem(step, problem_type, correctness): else: inputfield('checkbox', choice='choice_3').check() + elif problem_type == 'radio': + if correctness == 'correct': + inputfield('radio', choice='choice_2').check() + else: + inputfield('radio', choice='choice_1').check() + elif problem_type == 'string': textvalue = 'correct string' if correctness == 'correct' \ else 'incorrect' @@ -252,6 +264,14 @@ def assert_problem_has_answer(step, problem_type, answer_class): else: assert_checked('checkbox', []) + elif problem_type == "radio": + if answer_class == 'correct': + assert_checked('radio', ['choice_2']) + elif answer_class == 'incorrect': + assert_checked('radio', ['choice_1']) + else: + assert_checked('radio', []) + elif problem_type == 'string': if answer_class == 'blank': expected = '' @@ -298,6 +318,7 @@ CORRECTNESS_SELECTORS = { 'correct': {'drop down': ['span.correct'], 'multiple choice': ['label.choicegroup_correct'], 'checkbox': ['span.correct'], + 'radio': ['label.choicegroup_correct'], 'string': ['div.correct'], 'numerical': ['div.correct'], 'formula': ['div.correct'], @@ -308,6 +329,8 @@ CORRECTNESS_SELECTORS = { 'multiple choice': ['label.choicegroup_incorrect', 'span.incorrect'], 'checkbox': ['span.incorrect'], + 'radio': ['label.choicegroup_incorrect', + 'span.incorrect'], 'string': ['div.incorrect'], 'numerical': ['div.incorrect'], 'formula': ['div.incorrect'], @@ -317,6 +340,7 @@ CORRECTNESS_SELECTORS = { 'unanswered': {'drop down': ['span.unanswered'], 'multiple choice': ['span.unanswered'], 'checkbox': ['span.unanswered'], + 'radio': ['span.unanswered'], 'string': ['div.unanswered'], 'numerical': ['div.unanswered'], 'formula': ['div.unanswered'], From 754d1eb70253342e35b758a360163fade6e1a26e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 16 Apr 2013 17:22:01 -0400 Subject: [PATCH 144/280] Added tests for ChoiceGroup template --- .../capa/capa/tests/test_input_templates.py | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 common/lib/capa/capa/tests/test_input_templates.py diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py new file mode 100644 index 0000000000..30359060f0 --- /dev/null +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -0,0 +1,276 @@ +"""Tests for the logic in input type mako templates.""" +#pylint: disable=R0904 + +import unittest +import capa +import os.path +from lxml import etree +from mako.template import Template as MakoTemplate + + +class TemplateTestCase(unittest.TestCase): + """Utilitites for testing templates""" + + # Allow us to pass an extra arg to setUp to configure + # the test case. Also allow setUp as a valid method name. + #pylint: disable=W0221 + #pylint: disable=C0103 + def setUp(self, template_name): + """Load the template + + `template_name` is the file name of the template + to be loaded from capa/templates. + The template name should include the .html extension: + for example: choicegroup.html + """ + capa_path = capa.__path__[0] + self.template_path = os.path.join(capa_path, 'templates', template_name) + template_file = open(self.template_path) + self.template = MakoTemplate(template_file.read()) + template_file.close() + + # Allow us to pass **context_dict to render_unicode() + #pylint: disable=W0142 + def render_to_xml(self, context_dict): + """Render the template using the `context_dict` dict. + + Returns an `etree` XML element.""" + xml_str = self.template.render_unicode(**context_dict) + return etree.fromstring(xml_str) + + def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1): + """Asserts that the xml tree has an element satisfying `xpath`. + + `xml_root` is an etree XML element + `xpath` is an XPath string, such as `'/foo/bar'` + `context` is used to print a debugging message + `exact_num` is the exact number of matches to expect. + """ + message = ("XML does not have %d match(es) for xpath '%s'\nXML: %s\nContext: %s" + % (exact_num, str(xpath), etree.tostring(xml_root), str(context_dict))) + + self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message) + + def assert_no_xpath(self, xml_root, xpath, context_dict): + """Asserts that the xml tree does NOT have an element + satisfying `xpath`. + + `xml_root` is an etree XML element + `xpath` is an XPath string, such as `'/foo/bar'` + `context` is used to print a debugging message + """ + self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0) + + +class TestChoiceGroupTemplate(TemplateTestCase): + """Test mako template for `` input""" + + # Allow us to pass an extra arg to setUp to configure + # the test case. Also allow setUp as a valid method name. + #pylint: disable=W0221 + #pylint: disable=C0103 + def setUp(self): + choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')] + self.context = {'id': '1', + 'choices': choices, + 'status': 'correct', + 'input_type': 'checkbox', + 'name_array_suffix': '1', + 'value': '3'} + super(TestChoiceGroupTemplate, self).setUp('choicegroup.html') + + def test_problem_marked_correct(self): + """Test conditions under which the entire problem + (not a particular option) is marked correct""" + + self.context['status'] = 'correct' + self.context['input_type'] = 'checkbox' + self.context['value'] = ['1', '2'] + + # Should mark the entire problem correct + xml = self.render_to_xml(self.context) + xpath = "//div[@class='indicator_container']/span[@class='correct']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should NOT mark individual options + self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']", + self.context) + + self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']", + self.context) + + def test_problem_marked_incorrect(self): + """Test all conditions under which the entire problem + (not a particular option) is marked incorrect""" + conditions = [ + {'status': 'incorrect', 'input_type': 'radio', 'value': ''}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': []}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2']}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2', '3']}, + {'status': 'incomplete', 'input_type': 'radio', 'value': ''}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': []}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2']}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2', '3']}] + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + xpath = "//div[@class='indicator_container']/span[@class='incorrect']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should NOT mark individual options + self.assert_no_xpath(xml, + "//label[@class='choicegroup_incorrect']", + self.context) + + self.assert_no_xpath(xml, + "//label[@class='choicegroup_correct']", + self.context) + + def test_problem_marked_unanswered(self): + """Test all conditions under which the entire problem + (not a particular option) is marked unanswered""" + conditions = [ + {'status': 'unsubmitted', 'input_type': 'radio', 'value': ''}, + {'status': 'unsubmitted', 'input_type': 'radio', 'value': []}, + {'status': 'unsubmitted', 'input_type': 'checkbox', 'value': []}, + {'input_type': 'radio', 'value': ''}, + {'input_type': 'radio', 'value': []}, + {'input_type': 'checkbox', 'value': []}, + {'input_type': 'checkbox', 'value': ['1']}, + {'input_type': 'checkbox', 'value': ['1', '2']}] + + self.context['status'] = 'unanswered' + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + xpath = "//div[@class='indicator_container']/span[@class='unanswered']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should NOT mark individual options + self.assert_no_xpath(xml, + "//label[@class='choicegroup_incorrect']", + self.context) + + self.assert_no_xpath(xml, + "//label[@class='choicegroup_correct']", + self.context) + + def test_option_marked_correct(self): + """Test conditions under which a particular option + (not the entire problem) is marked correct.""" + conditions = [ + {'input_type': 'radio', 'value': '2'}, + {'input_type': 'radio', 'value': ['2']}] + + self.context['status'] = 'correct' + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + xpath = "//label[@class='choicegroup_correct']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should NOT mark the whole problem + xpath = "//div[@class='indicator_container']/span" + self.assert_no_xpath(xml, xpath, self.context) + + def test_option_marked_incorrect(self): + """Test conditions under which a particular option + (not the entire problem) is marked incorrect.""" + conditions = [ + {'input_type': 'radio', 'value': '2'}, + {'input_type': 'radio', 'value': ['2']}] + + self.context['status'] = 'incorrect' + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + xpath = "//label[@class='choicegroup_incorrect']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should NOT mark the whole problem + xpath = "//div[@class='indicator_container']/span" + self.assert_no_xpath(xml, xpath, self.context) + + def test_never_show_correctness(self): + """Test conditions under which we tell the template to + NOT show correct/incorrect, but instead show a message. + + This is used, for example, by the Justice course to ask + questions without specifying a correct answer. When + the student responds, the problem displays "Thank you + for your response" + """ + + conditions = [ + {'input_type': 'radio', 'status': 'correct', 'value': ''}, + {'input_type': 'radio', 'status': 'correct', 'value': '2'}, + {'input_type': 'radio', 'status': 'correct', 'value': ['2']}, + {'input_type': 'radio', 'status': 'incorrect', 'value': '2'}, + {'input_type': 'radio', 'status': 'incorrect', 'value': []}, + {'input_type': 'radio', 'status': 'incorrect', 'value': ['2']}, + {'input_type': 'checkbox', 'status': 'correct', 'value': []}, + {'input_type': 'checkbox', 'status': 'correct', 'value': ['2']}, + {'input_type': 'checkbox', 'status': 'incorrect', 'value': []}, + {'input_type': 'checkbox', 'status': 'incorrect', 'value': ['2']}] + + self.context['show_correctness'] = 'never' + self.context['submitted_message'] = 'Test message' + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + + # Should NOT mark the entire problem correct/incorrect + xpath = "//div[@class='indicator_container']/span[@class='correct']" + self.assert_no_xpath(xml, xpath, self.context) + + xpath = "//div[@class='indicator_container']/span[@class='incorrect']" + self.assert_no_xpath(xml, xpath, self.context) + + # Should NOT mark individual options + self.assert_no_xpath(xml, + "//label[@class='choicegroup_incorrect']", + self.context) + + self.assert_no_xpath(xml, + "//label[@class='choicegroup_correct']", + self.context) + + # Expect to see the message + message_elements = xml.xpath("//div[@class='capa_alert']") + self.assertEqual(len(message_elements), 1) + self.assertEqual(message_elements[0].text, + self.context['submitted_message']) + + def test_no_message_before_submission(self): + """Ensure that we don't show the `submitted_message` + before submitting""" + + conditions = [ + {'input_type': 'radio', 'status': 'unsubmitted', 'value': ''}, + {'input_type': 'radio', 'status': 'unsubmitted', 'value': []}, + {'input_type': 'checkbox', 'status': 'unsubmitted', 'value': []}, + + # These tests expose bug #365 + # When the bug is fixed, uncomment these cases. + #{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'}, + #{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']}, + #{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'}, + #{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']}, + #{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']}, + #{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']}] + ] + + self.context['show_correctness'] = 'never' + self.context['submitted_message'] = 'Test message' + + for test_conditions in conditions: + self.context.update(test_conditions) + xml = self.render_to_xml(self.context) + + # Expect that we do NOT see the message yet + self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context) From 52c2f3ae372e551da4278b1879831f83f4b116c2 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 17 Apr 2013 08:55:08 -0400 Subject: [PATCH 145/280] Tested additional values of rerandomize (true/false/per_student) Pylint fixes --- .../capa/capa/tests/test_input_templates.py | 33 ++-- .../xmodule/xmodule/tests/test_capa_module.py | 155 ++++++++++++------ 2 files changed, 119 insertions(+), 69 deletions(-) diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 30359060f0..7bb32acd10 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -1,5 +1,4 @@ """Tests for the logic in input type mako templates.""" -#pylint: disable=R0904 import unittest import capa @@ -11,26 +10,22 @@ from mako.template import Template as MakoTemplate class TemplateTestCase(unittest.TestCase): """Utilitites for testing templates""" - # Allow us to pass an extra arg to setUp to configure - # the test case. Also allow setUp as a valid method name. - #pylint: disable=W0221 - #pylint: disable=C0103 - def setUp(self, template_name): - """Load the template + # Subclasses override this to specify the file name of the template + # to be loaded from capa/templates. + # The template name should include the .html extension: + # for example: choicegroup.html + TEMPLATE_NAME = None - `template_name` is the file name of the template - to be loaded from capa/templates. - The template name should include the .html extension: - for example: choicegroup.html - """ + def setUp(self): + """Load the template""" capa_path = capa.__path__[0] - self.template_path = os.path.join(capa_path, 'templates', template_name) + self.template_path = os.path.join(capa_path, + 'templates', + self.TEMPLATE_NAME) template_file = open(self.template_path) self.template = MakoTemplate(template_file.read()) template_file.close() - # Allow us to pass **context_dict to render_unicode() - #pylint: disable=W0142 def render_to_xml(self, context_dict): """Render the template using the `context_dict` dict. @@ -65,10 +60,8 @@ class TemplateTestCase(unittest.TestCase): class TestChoiceGroupTemplate(TemplateTestCase): """Test mako template for `` input""" - # Allow us to pass an extra arg to setUp to configure - # the test case. Also allow setUp as a valid method name. - #pylint: disable=W0221 - #pylint: disable=C0103 + TEMPLATE_NAME = 'choicegroup.html' + def setUp(self): choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')] self.context = {'id': '1', @@ -77,7 +70,7 @@ class TestChoiceGroupTemplate(TemplateTestCase): 'input_type': 'checkbox', 'name_array_suffix': '1', 'value': '3'} - super(TestChoiceGroupTemplate, self).setUp('choicegroup.html') + super(TestChoiceGroupTemplate, self).setUp() def test_problem_marked_correct(self): """Test conditions under which the entire problem diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index bc5d342646..f948f5bdfe 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -1,17 +1,19 @@ +"""Tests of the Capa XModule""" +#pylint: disable=C0111 +#pylint: disable=R0904 +#pylint: disable=C0103 +#pylint: disable=C0302 + import datetime -import json -from mock import Mock, MagicMock, patch -from pprint import pprint +from mock import Mock, patch import unittest import random import xmodule -import capa from capa.responsetypes import StudentInputError, \ - LoncapaProblemError, ResponseError + LoncapaProblemError, ResponseError from xmodule.capa_module import CapaModule from xmodule.modulestore import Location -from lxml import etree from django.http import QueryDict @@ -384,7 +386,7 @@ class CapaModuleTest(unittest.TestCase): # what the input is, by patching CorrectMap.is_correct() # Also simulate rendering the HTML # TODO: pep8 thinks the following line has invalid syntax - with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\ + with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct, \ patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html: mock_is_correct.return_value = True mock_html.return_value = "Test HTML" @@ -435,32 +437,38 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.attempts, 3) def test_check_problem_resubmitted_with_randomize(self): - # Randomize turned on - module = CapaFactory.create(rerandomize='always', attempts=0) + rerandomize_values = ['always', 'true'] - # Simulate that the problem is completed - module.done = True + for rerandomize in rerandomize_values: + # Randomize turned on + module = CapaFactory.create(rerandomize=rerandomize, attempts=0) - # Expect that we cannot submit - with self.assertRaises(xmodule.exceptions.NotFoundError): - get_request_dict = {CapaFactory.input_key(): '3.14'} - module.check_problem(get_request_dict) + # Simulate that the problem is completed + module.done = True - # Expect that number of attempts NOT incremented - self.assertEqual(module.attempts, 0) + # Expect that we cannot submit + with self.assertRaises(xmodule.exceptions.NotFoundError): + get_request_dict = {CapaFactory.input_key(): '3.14'} + module.check_problem(get_request_dict) + + # Expect that number of attempts NOT incremented + self.assertEqual(module.attempts, 0) def test_check_problem_resubmitted_no_randomize(self): - # Randomize turned off - module = CapaFactory.create(rerandomize='never', attempts=0, done=True) + rerandomize_values = ['never', 'false', 'per_student'] - # Expect that we can submit successfully - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.check_problem(get_request_dict) + for rerandomize in rerandomize_values: + # Randomize turned off + module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True) - self.assertEqual(result['success'], 'correct') + # Expect that we can submit successfully + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) - # Expect that number of attempts IS incremented - self.assertEqual(module.attempts, 1) + self.assertEqual(result['success'], 'correct') + + # Expect that number of attempts IS incremented + self.assertEqual(module.attempts, 1) def test_check_problem_queued(self): module = CapaFactory.create(attempts=1) @@ -615,24 +623,34 @@ class CapaModuleTest(unittest.TestCase): self.assertTrue('success' in result and not result['success']) def test_save_problem_submitted_with_randomize(self): - module = CapaFactory.create(rerandomize='always', done=True) - # Try to save - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.save_problem(get_request_dict) + # Capa XModule treats 'always' and 'true' equivalently + rerandomize_values = ['always', 'true'] - # Expect that we cannot save - self.assertTrue('success' in result and not result['success']) + for rerandomize in rerandomize_values: + module = CapaFactory.create(rerandomize=rerandomize, done=True) + + # Try to save + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.save_problem(get_request_dict) + + # Expect that we cannot save + self.assertTrue('success' in result and not result['success']) def test_save_problem_submitted_no_randomize(self): - module = CapaFactory.create(rerandomize='never', done=True) - # Try to save - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.save_problem(get_request_dict) + # Capa XModule treats 'false' and 'per_student' equivalently + rerandomize_values = ['never', 'false', 'per_student'] - # Expect that we succeed - self.assertTrue('success' in result and result['success']) + for rerandomize in rerandomize_values: + module = CapaFactory.create(rerandomize=rerandomize, done=True) + + # Try to save + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.save_problem(get_request_dict) + + # Expect that we succeed + self.assertTrue('success' in result and result['success']) def test_check_button_name(self): @@ -681,21 +699,30 @@ class CapaModuleTest(unittest.TestCase): # If user submitted a problem but hasn't reset, # do NOT show the check button - # Note: we can only reset when rerandomize="always" + # Note: we can only reset when rerandomize="always" or "true" module = CapaFactory.create(rerandomize="always", done=True) self.assertFalse(module.should_show_check_button()) + module = CapaFactory.create(rerandomize="true", done=True) + self.assertFalse(module.should_show_check_button()) + # Otherwise, DO show the check button module = CapaFactory.create() self.assertTrue(module.should_show_check_button()) # If the user has submitted the problem # and we do NOT have a reset button, then we can show the check button - # Setting rerandomize to "never" ensures that the reset button + # Setting rerandomize to "never" or "false" ensures that the reset button # is not shown module = CapaFactory.create(rerandomize="never", done=True) self.assertTrue(module.should_show_check_button()) + module = CapaFactory.create(rerandomize="false", done=True) + self.assertTrue(module.should_show_check_button()) + + module = CapaFactory.create(rerandomize="per_student", done=True) + self.assertTrue(module.should_show_check_button()) + def test_should_show_reset_button(self): attempts = random.randint(1, 10) @@ -712,6 +739,14 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize="never", done=True) self.assertFalse(module.should_show_reset_button()) + # If we're NOT randomizing, then do NOT show the reset button + module = CapaFactory.create(rerandomize="per_student", done=True) + self.assertFalse(module.should_show_reset_button()) + + # If we're NOT randomizing, then do NOT show the reset button + module = CapaFactory.create(rerandomize="false", done=True) + self.assertFalse(module.should_show_reset_button()) + # If the user hasn't submitted an answer yet, # then do NOT show the reset button module = CapaFactory.create(done=False) @@ -742,13 +777,19 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize="always", done=True) self.assertFalse(module.should_show_save_button()) + module = CapaFactory.create(rerandomize="true", done=True) + self.assertFalse(module.should_show_save_button()) + # If the user has unlimited attempts and we are not randomizing, # then do NOT show a save button # because they can keep using "Check" module = CapaFactory.create(max_attempts=None, rerandomize="never", done=False) self.assertFalse(module.should_show_save_button()) - module = CapaFactory.create(max_attempts=None, rerandomize="never", done=True) + module = CapaFactory.create(max_attempts=None, rerandomize="false", done=True) + self.assertFalse(module.should_show_save_button()) + + module = CapaFactory.create(max_attempts=None, rerandomize="per_student", done=True) self.assertFalse(module.should_show_save_button()) # Otherwise, DO show the save button @@ -759,6 +800,12 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize="never", max_attempts=2, done=True) self.assertTrue(module.should_show_save_button()) + module = CapaFactory.create(rerandomize="false", max_attempts=2, done=True) + self.assertTrue(module.should_show_save_button()) + + module = CapaFactory.create(rerandomize="per_student", max_attempts=2, done=True) + self.assertTrue(module.should_show_save_button()) + # If survey question for capa (max_attempts = 0), # DO show the save button module = CapaFactory.create(max_attempts=0, done=False) @@ -788,9 +835,15 @@ class CapaModuleTest(unittest.TestCase): done=True) self.assertTrue(module.should_show_save_button()) + module = CapaFactory.create(force_save_button="true", + rerandomize="true", + done=True) + self.assertTrue(module.should_show_save_button()) + def test_no_max_attempts(self): module = CapaFactory.create(max_attempts='') html = module.get_problem_html() + self.assertTrue(html is not None) # assert that we got here without exploding def test_get_problem_html(self): @@ -875,6 +928,8 @@ class CapaModuleTest(unittest.TestCase): # Try to render the module with DEBUG turned off html = module.get_problem_html() + self.assertTrue(html is not None) + # Check the rendering context render_args, _ = module.system.render_template.call_args context = render_args[1] @@ -886,7 +941,9 @@ class CapaModuleTest(unittest.TestCase): def test_random_seed_no_change(self): # Run the test for each possible rerandomize value - for rerandomize in ['never', 'per_student', 'always', 'onreset']: + for rerandomize in ['false', 'never', + 'per_student', 'always', + 'true', 'onreset']: module = CapaFactory.create(rerandomize=rerandomize) # Get the seed @@ -896,8 +953,9 @@ class CapaModuleTest(unittest.TestCase): # If we're not rerandomizing, the seed is always set # to the same value (1) - if rerandomize == 'never': - self.assertEqual(seed, 1) + if rerandomize in ['never']: + self.assertEqual(seed, 1, + msg="Seed should always be 1 when rerandomize='%s'" % rerandomize) # Check the problem get_request_dict = {CapaFactory.input_key(): '3.14'} @@ -947,7 +1005,8 @@ class CapaModuleTest(unittest.TestCase): return success # Run the test for each possible rerandomize value - for rerandomize in ['never', 'per_student', 'always', 'onreset']: + for rerandomize in ['never', 'false', 'per_student', + 'always', 'true', 'onreset']: module = CapaFactory.create(rerandomize=rerandomize) # Get the seed @@ -959,7 +1018,7 @@ class CapaModuleTest(unittest.TestCase): # is set to 'never' -- it should still be 1 # The seed also stays the same if we're randomizing # 'per_student': the same student should see the same problem - if rerandomize in ['never', 'per_student']: + if rerandomize in ['never', 'false', 'per_student']: self.assertEqual(seed, _reset_and_get_seed(module)) # Otherwise, we expect the seed to change @@ -969,10 +1028,8 @@ class CapaModuleTest(unittest.TestCase): # Since there's a small chance we might get the # same seed again, give it 5 chances # to generate a different seed - success = _retry_and_check(5, - lambda: _reset_and_get_seed(module) != seed) + success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed) - # TODO: change this comparison to module.seed is not None? - self.assertTrue(module.seed != None) + self.assertTrue(module.seed is not None) msg = 'Could not get a new seed from reset after 5 tries' self.assertTrue(success, msg) From 841d3484c847aa31226a7a169f3e9c9fbc9bca66 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 17 Apr 2013 12:17:23 -0400 Subject: [PATCH 146/280] Added test for textline input template --- .../capa/capa/tests/test_input_templates.py | 152 +++++++++++++++++- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 7bb32acd10..1a046ddf4b 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -5,7 +5,11 @@ import capa import os.path from lxml import etree from mako.template import Template as MakoTemplate +from mako import exceptions +class TemplateError(Exception): + """Error occurred while rendering a Mako template""" + pass class TemplateTestCase(unittest.TestCase): """Utilitites for testing templates""" @@ -30,7 +34,11 @@ class TemplateTestCase(unittest.TestCase): """Render the template using the `context_dict` dict. Returns an `etree` XML element.""" - xml_str = self.template.render_unicode(**context_dict) + try: + xml_str = self.template.render_unicode(**context_dict) + except: + raise TemplateError(exceptions.text_error_template().render()) + return etree.fromstring(xml_str) def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1): @@ -56,8 +64,28 @@ class TemplateTestCase(unittest.TestCase): """ self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0) + def assert_has_text(self, xml_root, xpath, text, exact=True): + """Find the element at `xpath` in `xml_root` and assert + that its text is `text`. -class TestChoiceGroupTemplate(TemplateTestCase): + `xml_root` is an etree XML element + `xpath` is an XPath string, such as `'/foo/bar'` + `text` is the expected text that the element should contain + + If multiple elements are found, checks the first one. + If no elements are found, the assertion fails. + """ + element_list = xml_root.xpath(xpath) + self.assertTrue(len(element_list) > 0, + "Could not find element at '%s'" % str(xpath)) + + if exact: + self.assertEqual(text, element_list[0].text) + else: + self.assertIn(text, element_list[0].text) + + +class ChoiceGroupTemplateTest(TemplateTestCase): """Test mako template for `` input""" TEMPLATE_NAME = 'choicegroup.html' @@ -120,7 +148,7 @@ class TestChoiceGroupTemplate(TemplateTestCase): "//label[@class='choicegroup_correct']", self.context) - def test_problem_marked_unanswered(self): + def test_problem_marked_unsubmitted(self): """Test all conditions under which the entire problem (not a particular option) is marked unanswered""" conditions = [ @@ -234,10 +262,8 @@ class TestChoiceGroupTemplate(TemplateTestCase): self.context) # Expect to see the message - message_elements = xml.xpath("//div[@class='capa_alert']") - self.assertEqual(len(message_elements), 1) - self.assertEqual(message_elements[0].text, - self.context['submitted_message']) + self.assert_has_text(xml, "//div[@class='capa_alert']", + self.context['submitted_message']) def test_no_message_before_submission(self): """Ensure that we don't show the `submitted_message` @@ -267,3 +293,115 @@ class TestChoiceGroupTemplate(TemplateTestCase): # Expect that we do NOT see the message yet self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context) + + +class TextlineTemplateTest(TemplateTestCase): + """Test mako template for `` input""" + + # Allow us to pass an extra arg to setUp to configure + # the test case. + #pylint: disable=W0221 + def setUp(self): + self.context = {'id': '1', + 'status': 'correct', + 'value': '3', + 'preprocessor': None, + 'trailing_text': None} + super(TextlineTemplateTest, self).setUp('textline.html') + + def test_section_class(self): + cases = [ ({}, ' capa_inputtype '), + ({'do_math': True}, 'text-input-dynamath capa_inputtype '), + ({'inline': True}, ' capa_inputtype inline'), + ({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), + ] + + for (context, css_class) in cases: + base_context = self.context.copy() + base_context.update(context) + xml = self.render_to_xml(base_context) + xpath = "//section[@class='%s']" % css_class + self.assert_has_xpath(xml, xpath, self.context) + + def test_status(self): + cases = [('correct', 'correct', 'correct'), + ('unsubmitted', 'unanswered', 'unanswered'), + ('incorrect', 'incorrect', 'incorrect'), + ('incomplete', 'incorrect', 'incomplete')] + + for (context_status, div_class, status_mark) in cases: + self.context['status'] = context_status + xml = self.render_to_xml(self.context) + + # Expect that we get a
    with correct class + xpath = "//div[@class='%s ']" % div_class + self.assert_has_xpath(xml, xpath, self.context) + + # Expect that we get a

    with class="status" + # (used to by CSS to draw the green check / red x) + self.assert_has_text(xml, "//p[@class='status']", + status_mark, exact=False) + + def test_hidden(self): + self.context['hidden'] = True + xml = self.render_to_xml(self.context) + + xpath = "//div[@style='display:none;']" + self.assert_has_xpath(xml, xpath, self.context) + + xpath = "//input[@style='display:none;']" + self.assert_has_xpath(xml, xpath, self.context) + + def test_do_math(self): + self.context['do_math'] = True + xml = self.render_to_xml(self.context) + + xpath = "//input[@class='math']" + self.assert_has_xpath(xml, xpath, self.context) + + xpath = "//div[@class='equation']" + self.assert_has_xpath(xml, xpath, self.context) + + xpath = "//textarea[@id='input_1_dynamath']" + self.assert_has_xpath(xml, xpath, self.context) + + def test_size(self): + self.context['size'] = '20' + xml = self.render_to_xml(self.context) + + xpath = "//input[@size='20']" + self.assert_has_xpath(xml, xpath, self.context) + + def test_preprocessor(self): + self.context['preprocessor'] = {'class_name': 'test_class', + 'script_src': 'test_script'} + xml = self.render_to_xml(self.context) + + xpath = "//div[@class='text-input-dynamath_data' and @data-preprocessor='test_class']" + self.assert_has_xpath(xml, xpath, self.context) + + xpath = "//div[@class='script_placeholder' and @data-src='test_script']" + self.assert_has_xpath(xml, xpath, self.context) + + def test_do_inline(self): + cases = [('correct', 'correct'), + ('unsubmitted', 'unanswered'), + ('incorrect', 'incorrect'), + ('incomplete', 'incorrect')] + + self.context['inline'] = True + + for (context_status, div_class) in cases: + self.context['status'] = context_status + xml = self.render_to_xml(self.context) + + # Expect that we get a

    with correct class + xpath = "//div[@class='%s inline']" % div_class + self.assert_has_xpath(xml, xpath, self.context) + + def test_message(self): + self.context['msg'] = "Test message" + xml = self.render_to_xml(self.context) + + xpath = "//span[@class='message']" + self.assert_has_text(xml, xpath, self.context['msg']) From a57a093e73a7ecd98bdd6223ee893e58a5343532 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 17 Apr 2013 15:56:05 -0400 Subject: [PATCH 147/280] Rebased to master --- common/lib/capa/capa/tests/test_input_templates.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 1a046ddf4b..4ca020be07 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -98,7 +98,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): 'input_type': 'checkbox', 'name_array_suffix': '1', 'value': '3'} - super(TestChoiceGroupTemplate, self).setUp() + super(ChoiceGroupTemplateTest, self).setUp() def test_problem_marked_correct(self): """Test conditions under which the entire problem @@ -298,16 +298,15 @@ class ChoiceGroupTemplateTest(TemplateTestCase): class TextlineTemplateTest(TemplateTestCase): """Test mako template for `` input""" - # Allow us to pass an extra arg to setUp to configure - # the test case. - #pylint: disable=W0221 + TEMPLATE_NAME = 'textline.html' + def setUp(self): self.context = {'id': '1', 'status': 'correct', 'value': '3', 'preprocessor': None, 'trailing_text': None} - super(TextlineTemplateTest, self).setUp('textline.html') + super(TextlineTemplateTest, self).setUp() def test_section_class(self): cases = [ ({}, ' capa_inputtype '), From 3de927fe017ec22793d471016aae4bf9b517a0b6 Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 17 Apr 2013 21:22:08 -0400 Subject: [PATCH 148/280] untabify course_nagivation.html --- .../courseware/course_navigation.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 4e77c96adb..ea367873e2 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -29,9 +29,9 @@ def url_class(is_active): <%block name="extratabs" /> % if masquerade is not UNDEFINED: % if staff_access and masquerade is not None: -
  • Staff view
  • - % endif - % endif +
  • Staff view
  • + % endif + % endif
    @@ -42,11 +42,11 @@ def url_class(is_active): masq = (function(){ var el = $('#staffstatus'); var setstat = function(status){ - if (status=='student'){ - el.html('Student view'); - }else{ - el.html('Staff view'); - } + if (status=='student'){ + el.html('Student view'); + }else{ + el.html('Staff view'); + } } setstat('${masquerade}'); @@ -55,7 +55,7 @@ masq = (function(){ type: 'GET', success: function(result){ setstat(result.status); - location.reload(); + location.reload(); }, error: function() { alert('Error: cannot connect to server'); From 0ffc399f7df2fad88f49cc16a2a27a340764daf6 Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 17 Apr 2013 22:12:12 -0400 Subject: [PATCH 149/280] move masquerade call up, to make sure it is used for start date checks --- lms/djangoapps/courseware/module_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index eb819fd5a5..6f05b32778 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -165,14 +165,14 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours Actually implement get_module. See docstring there for details. """ - # Short circuit--if the user shouldn't have access, bail without doing any work - if not has_access(user, descriptor, 'load', course_id): - return None - # allow course staff to masquerade as student if has_access(user, descriptor, 'staff', course_id): setup_masquerade(request, True) + # Short circuit--if the user shouldn't have access, bail without doing any work + if not has_access(user, descriptor, 'load', course_id): + return None + # Setup system context for module instance ajax_url = reverse('modx_dispatch', kwargs=dict(course_id=course_id, From 9c7ba41880827d05e3bde0cf8a5ea8a5960a0be6 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Apr 2013 12:06:38 -0400 Subject: [PATCH 150/280] Only add the xmodule.coffee file once per class, and put it before all other coffeescript files --- common/lib/xmodule/xmodule/x_module.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index e6d367ac7a..ab3fe8c027 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -45,17 +45,13 @@ class HTMLSnippet(object): # cdodge: We've moved the xmodule.coffee script from an outside directory into the xmodule area of common # this means we need to make sure that all xmodules include this dependency which had been previously implicitly # fulfilled in a different area of code - js = cls.js + coffee = cls.js.setdefault('coffee', []) + fragment = resource_string(__name__, 'js/src/xmodule.coffee') - if js is None: - js = {} + if fragment not in coffee: + coffee.insert(0, fragment) - if 'coffee' not in js: - js['coffee'] = [] - - js['coffee'].append(resource_string(__name__, 'js/src/xmodule.coffee')) - - return js + return cls.js @classmethod def get_css(cls): From 346aee8863514f89f278c1014a0a818c3f9257df Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Apr 2013 12:23:13 -0400 Subject: [PATCH 151/280] Make django-pipeline only work with raw js and css --- cms/envs/common.py | 33 ++++++++----------------- cms/envs/jasmine.py | 2 +- common/lib/xmodule/xmodule/x_module.py | 2 +- lms/envs/common.py | 34 ++++++++++---------------- 4 files changed, 25 insertions(+), 46 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index ca08a56a70..21995be40d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -35,7 +35,7 @@ MITX_FEATURES = { 'AUTH_USE_MIT_CERTIFICATES': False, 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STAFF_EMAIL': '', # email address for staff (eg to request course creation) - 'STUDIO_NPS_SURVEY': True, + 'STUDIO_NPS_SURVEY': True, 'SEGMENT_IO': True, } ENABLE_JASMINE = False @@ -195,20 +195,21 @@ from rooted_paths import rooted_glob, remove_root write_descriptor_styles(PROJECT_ROOT / "static/sass/descriptor", [RawDescriptor, ErrorDescriptor]) write_module_styles(PROJECT_ROOT / "static/sass/module", [RawDescriptor, ErrorDescriptor]) -descriptor_js = remove_root( +descriptor_js = [path.replace('.coffee', '.js') for path in remove_root( PROJECT_ROOT / 'static', write_descriptor_js( PROJECT_ROOT / "static/coffee/descriptor", [RawDescriptor, ErrorDescriptor] ) -) -module_js = remove_root( +)] + +module_js = [path.replace('.coffee', '.js') for path in remove_root( PROJECT_ROOT / 'static', write_module_js( PROJECT_ROOT / "static/coffee/module", [RawDescriptor, ErrorDescriptor] ) -) +)] PIPELINE_CSS = { 'base-style': { @@ -216,19 +217,17 @@ PIPELINE_CSS = { 'js/vendor/CodeMirror/codemirror.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', - 'sass/base-style.scss' + 'sass/base-style.css' ], 'output_filename': 'css/cms-base-style.css', }, } -PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] - PIPELINE_JS = { 'main': { 'source_filenames': sorted( - rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + - rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') + + rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') ) + ['js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', }, @@ -237,18 +236,11 @@ PIPELINE_JS = { 'output_filename': 'js/cms-modules.js', }, 'spec': { - 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')), + 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')), 'output_filename': 'js/cms-spec.js' } } -PIPELINE_COMPILERS = [ - 'pipeline.compilers.sass.SASSCompiler', - 'pipeline.compilers.coffee.CoffeeScriptCompiler', -] - -PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) - PIPELINE_CSS_COMPRESSOR = None PIPELINE_JS_COMPRESSOR = None @@ -260,11 +252,6 @@ STATICFILES_IGNORE_PATTERNS = ( ) PIPELINE_YUI_BINARY = 'yui-compressor' -PIPELINE_SASS_BINARY = 'sass' -PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee' - -# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream -PIPELINE_COMPILE_INPLACE = True ############################ APPS ##################################### diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index 5c9be1cf9c..ac28f8fc9a 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -27,7 +27,7 @@ PIPELINE_JS['js-test-source'] = { } PIPELINE_JS['spec'] = { - 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')), + 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')), 'output_filename': 'js/cms-spec.js' } diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index ab3fe8c027..1fd0b8e138 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -337,7 +337,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): # cdodge: this is a list of metadata names which are 'system' metadata # and should not be edited by an end-user - system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', + system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'discussion_id', 'xml_attributes'] # A list of descriptor attributes that must be equal for the descriptors to diff --git a/lms/envs/common.py b/lms/envs/common.py index 8654b5ebf5..27c2cfc022 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -394,17 +394,18 @@ from xmodule.hidden_module import HiddenDescriptor from rooted_paths import rooted_glob, remove_root write_module_styles(PROJECT_ROOT / 'static/sass/module', [HiddenDescriptor]) -module_js = remove_root( + +module_js = [path.replace('.coffee', '.js') for path in remove_root( PROJECT_ROOT / 'static', write_module_js(PROJECT_ROOT / 'static/coffee/module', [HiddenDescriptor]) -) +)] courseware_js = ( [ - 'coffee/src/' + pth + '.coffee' + 'coffee/src/' + pth + '.js' for pth in ['courseware', 'histogram', 'navigation', 'time'] ] + - sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.coffee')) + sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js')) ) # 'js/vendor/RequireJS.js' - Require JS wrapper. @@ -420,13 +421,13 @@ main_vendor_js = [ 'js/vendor/jquery.ba-bbq.min.js', ] -discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) -staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) -open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.coffee')) +discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.js')) +staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js')) +open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js')) PIPELINE_CSS = { 'application': { - 'source_filenames': ['sass/application.scss'], + 'source_filenames': ['sass/application.css'], 'output_filename': 'css/lms-application.css', }, 'course': { @@ -435,24 +436,23 @@ PIPELINE_CSS = { 'css/vendor/jquery.treeview.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', - 'sass/course.scss' + 'sass/course.css' ], 'output_filename': 'css/lms-course.css', }, 'ie-fixes': { - 'source_filenames': ['sass/ie.scss'], + 'source_filenames': ['sass/ie.css'], 'output_filename': 'css/lms-ie.css', }, } -PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/course.scss'] PIPELINE_JS = { 'application': { # Application will contain all paths not in courseware_only_js 'source_filenames': sorted( - set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + - rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - + set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.js') + + rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.js')) - set(courseware_js + discussion_js + staff_grading_js + open_ended_js) ) + [ 'js/form.ext.js', @@ -510,12 +510,6 @@ if os.path.isdir(DATA_DIR): os.system("rm %s" % (js_dir / new_filename)) os.system("coffee -c %s" % (js_dir / filename)) -PIPELINE_COMPILERS = [ - 'pipeline.compilers.sass.SASSCompiler', - 'pipeline.compilers.coffee.CoffeeScriptCompiler', -] - -PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_CSS_COMPRESSOR = None PIPELINE_JS_COMPRESSOR = None @@ -526,8 +520,6 @@ STATICFILES_IGNORE_PATTERNS = ( ) PIPELINE_YUI_BINARY = 'yui-compressor' -PIPELINE_SASS_BINARY = 'sass' -PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee' # Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream PIPELINE_COMPILE_INPLACE = True From 291e772ce06bc640f0b0936c8c6aede6218a59a4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Apr 2013 12:23:31 -0400 Subject: [PATCH 152/280] Clean up cruft from rakefile --- rakefile | 64 -------------------------------------------------------- 1 file changed, 64 deletions(-) diff --git a/rakefile b/rakefile index d6571118ca..e56cbed332 100644 --- a/rakefile +++ b/rakefile @@ -21,15 +21,6 @@ COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10] BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '') BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp() -if BRANCH == "master" - DEPLOY_NAME = "#{PACKAGE_NAME}-#{BUILD_NUMBER}-#{COMMIT}" -else - DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}" -end -PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming" - -NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-') -INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME) # Set up the clean and clobber tasks CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles') CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util") @@ -360,51 +351,6 @@ task :set_staff, [:user, :system, :env] do |t, args| sh(django_admin(args.system, args.env, 'set_staff', args.user)) end -task :package do - FileUtils.mkdir_p(BUILD_DIR) - - Dir.chdir(BUILD_DIR) do - afterremove = Tempfile.new('afterremove') - afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '') - #! /bin/bash - set -e - set -x - - # to be a little safer this rm is executed - # as the makeitso user - - if [[ -d "#{INSTALL_DIR_PATH}" ]]; then - sudo rm -rf "#{INSTALL_DIR_PATH}" - fi - - AFTERREMOVE - afterremove.close() - FileUtils.chmod(0755, afterremove.path) - - args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb", - "--after-remove=#{afterremove.path}", - "--prefix=#{INSTALL_DIR_PATH}", - "--exclude=**/build/**", - "--exclude=**/rakefile", - "--exclude=**/.git/**", - "--exclude=**/*.pyc", - "--exclude=**/reports/**", - "--exclude=**/test_root/**", - "--exclude=**/.coverage/**", - "-C", "#{REPO_ROOT}", - "--provides=#{PACKAGE_NAME}", - "--name=#{NORMALIZED_DEPLOY_NAME}", - "--version=#{PKG_VERSION}", - "-a", "all", - "."] - system(*args) || raise("fpm failed to build the .deb") - end -end - -task :publish => :package do - sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}") -end - namespace :cms do desc "Clone existing MongoDB based course" task :clone do @@ -415,9 +361,7 @@ namespace :cms do raise "You must pass in a SOURCE_LOC and DEST_LOC parameters" end end -end -namespace :cms do desc "Delete existing MongoDB based course" task :delete_course do @@ -429,9 +373,7 @@ namespace :cms do raise "You must pass in a LOC parameter" end end -end -namespace :cms do desc "Import course data within the given DATA_DIR variable" task :import do if ENV['DATA_DIR'] and ENV['COURSE_DIR'] @@ -443,16 +385,12 @@ namespace :cms do "Example: \`rake cms:import DATA_DIR=../data\`" end end -end -namespace :cms do desc "Imports all the templates from the code pack" task :update_templates do sh(django_admin(:cms, :dev, :update_templates)) end -end -namespace :cms do desc "Import course data within the given DATA_DIR variable" task :xlint do if ENV['DATA_DIR'] and ENV['COURSE_DIR'] @@ -464,9 +402,7 @@ namespace :cms do "Example: \`rake cms:import DATA_DIR=../data\`" end end -end -namespace :cms do desc "Export course data to a tar.gz file" task :export do if ENV['COURSE_ID'] and ENV['OUTPUT_PATH'] From 589267599f6b7c613b0fd1ad4fd94c30fabb54b9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Apr 2013 15:29:53 -0400 Subject: [PATCH 153/280] Run coffee and sass in watch mode when running lms or cms, and oneshot before running collectstatic --- rakefile | 70 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/rakefile b/rakefile index e56cbed332..7d26c027ae 100644 --- a/rakefile +++ b/rakefile @@ -34,16 +34,42 @@ def django_admin(system, env, command, *args) return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}" end +# Runs Process.spawn, and kills the process at the end of the rake process +# Expects the same arguments as Process.spawn +def background_process(*command) + pid = Process.spawn(*command, :pgroup => true) + + at_exit do + puts "Ending process and children" + pgid = Process.getpgid(pid) + begin + Timeout.timeout(5) do + puts "Terminating process group #{pgid}" + Process.kill(:SIGTERM, -pgid) + puts "Waiting on process group #{pgid}" + Process.wait(-pgid) + puts "Done waiting on process group #{pgid}" + end + rescue Timeout::Error + puts "Killing process group #{pgid}" + Process.kill(:SIGKILL, -pgid) + puts "Waiting on process group #{pgid}" + Process.wait(-pgid) + puts "Done waiting on process group #{pgid}" + end + end +end + def django_for_jasmine(system, django_reload) if !django_reload reload_arg = '--noreload' end port = 10000 + rand(40000) - django_pid = fork do - exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', port.to_s, reload_arg).split(' ')) - end jasmine_url = "http://localhost:#{port}/_jasmine/" + + background_process(*django_admin(system, 'jasmine', 'runserver', '-v', '0', port.to_s, reload_arg).split(' ')) + up = false start_time = Time.now until up do @@ -61,16 +87,7 @@ def django_for_jasmine(system, django_reload) sleep(0.5) end end - begin - yield jasmine_url - ensure - if django_reload - Process.kill(:SIGKILL, -Process.getpgid(django_pid)) - else - Process.kill(:SIGKILL, django_pid) - end - Process.wait(django_pid) - end + yield jasmine_url end def template_jasmine_runner(lib) @@ -102,6 +119,25 @@ def report_dir_path(dir) return File.join(REPORT_DIR, dir.to_s) end +def compile_assets(watch=false) + coffee_cmd = "coffee #{watch ? '--watch' : ''} --compile */static" + sass_cmd = "sass --style compressed --require ./common/static/sass/bourbon/lib/bourbon.rb #{watch ? '--watch' : '--update'} */static" + + if watch + background_process(coffee_cmd) + background_process(sass_cmd) + else + coffee_pid = Process.spawn(coffee_cmd) + puts "Waiting for coffee to complete (pid #{coffee_pid})" + Process.wait(coffee_pid) + puts "Coffee completed" + sass_pid = Process.spawn(sass_cmd) + puts "Waiting for sass to complete (pid #{sass_pid})" + Process.wait(sass_pid) + puts "Sass completed" + end +end + task :default => [:test, :pep8, :pylint] directory REPORT_DIR @@ -182,7 +218,7 @@ end # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" - task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"] + task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collect_assets:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. @@ -201,6 +237,7 @@ end desc task system, [:env, :options] => [:predjango] do |t, args| args.with_defaults(:env => 'dev', :options => default_options[system]) + compile_assets(watch=true) sh(django_admin(system, args.env, 'runserver', args.options)) end @@ -213,8 +250,9 @@ end end desc "Run collectstatic in the specified environment" - task "#{system}:collectstatic:#{env}" => :predjango do - sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status| + task "#{system}:collect_assets:#{env}" do + compile_assets() + sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /dev/null") do |ok, status| if !ok abort "collectstatic failed!" end From afb60f790e6465603797c6555bed85b900213e58 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 16 Apr 2013 07:11:31 -0400 Subject: [PATCH 154/280] Change the name of the asset gathering task --- rakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rakefile b/rakefile index 7d26c027ae..6158fd1c77 100644 --- a/rakefile +++ b/rakefile @@ -249,8 +249,8 @@ end sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") end - desc "Run collectstatic in the specified environment" - task "#{system}:collect_assets:#{env}" do + desc "Compile coffeescript and sass, and then run collectstatic in the specified environment" + task "#{system}:gather_assets:#{env}" do compile_assets() sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /dev/null") do |ok, status| if !ok From f80353121c36341f941cb9b0e8aab32fa1674ee9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 16 Apr 2013 07:49:52 -0400 Subject: [PATCH 155/280] Don't clean pyc file and install xmodule before all arbitrary rake commands --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index 6158fd1c77..aa9b131396 100644 --- a/rakefile +++ b/rakefile @@ -378,7 +378,7 @@ end task :runserver => :lms desc "Run django-admin against the specified system and environment" -task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, args| +task "django-admin", [:action, :system, :env, :options] do |t, args| args.with_defaults(:env => 'dev', :system => 'lms', :options => '') sh(django_admin(args.system, args.env, args.action, args.options)) end From 6a36d9dba8ebe3e12c886a68661941b20201ad14 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 17 Apr 2013 09:41:04 -0400 Subject: [PATCH 156/280] Use the new gather_assets target before running tests --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index aa9b131396..3b96a2000e 100644 --- a/rakefile +++ b/rakefile @@ -218,7 +218,7 @@ end # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" - task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collect_assets:test", "fasttest_#{system}"] + task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:gather_assets:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. From 3031c1da3809d15516468cbce1abb2c027ae8b76 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 18 Apr 2013 07:18:22 -0400 Subject: [PATCH 157/280] Fix up call to spawn --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index 3b96a2000e..87f1e78317 100644 --- a/rakefile +++ b/rakefile @@ -37,7 +37,7 @@ end # Runs Process.spawn, and kills the process at the end of the rake process # Expects the same arguments as Process.spawn def background_process(*command) - pid = Process.spawn(*command, :pgroup => true) + pid = Process.spawn({}, *command, {:pgroup => true}) at_exit do puts "Ending process and children" From 91bee1a96606bef29e1f30d188cf5adefa83130f Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Thu, 18 Apr 2013 09:27:44 -0400 Subject: [PATCH 158/280] add Babel to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 1a383e6cc0..9242724ed5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -r repo-requirements.txt +Babel==0.9.6 beautifulsoup4==4.1.3 beautifulsoup==3.2.1 boto==2.6.0 From a4717aca90f07d1b7452c5776fe2c8165b08a5db Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 18 Apr 2013 09:49:05 -0400 Subject: [PATCH 159/280] Pep8 fixes --- .../capa/capa/tests/test_input_templates.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 4ca020be07..92c4d8b3b7 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -7,10 +7,12 @@ from lxml import etree from mako.template import Template as MakoTemplate from mako import exceptions + class TemplateError(Exception): """Error occurred while rendering a Mako template""" pass + class TemplateTestCase(unittest.TestCase): """Utilitites for testing templates""" @@ -23,8 +25,8 @@ class TemplateTestCase(unittest.TestCase): def setUp(self): """Load the template""" capa_path = capa.__path__[0] - self.template_path = os.path.join(capa_path, - 'templates', + self.template_path = os.path.join(capa_path, + 'templates', self.TEMPLATE_NAME) template_file = open(self.template_path) self.template = MakoTemplate(template_file.read()) @@ -309,11 +311,10 @@ class TextlineTemplateTest(TemplateTestCase): super(TextlineTemplateTest, self).setUp() def test_section_class(self): - cases = [ ({}, ' capa_inputtype '), - ({'do_math': True}, 'text-input-dynamath capa_inputtype '), - ({'inline': True}, ' capa_inputtype inline'), - ({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), - ] + cases = [({}, ' capa_inputtype '), + ({'do_math': True}, 'text-input-dynamath capa_inputtype '), + ({'inline': True}, ' capa_inputtype inline'), + ({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), ] for (context, css_class) in cases: base_context = self.context.copy() @@ -336,9 +337,9 @@ class TextlineTemplateTest(TemplateTestCase): xpath = "//div[@class='%s ']" % div_class self.assert_has_xpath(xml, xpath, self.context) - # Expect that we get a

    with class="status" + # Expect that we get a

    with class="status" # (used to by CSS to draw the green check / red x) - self.assert_has_text(xml, "//p[@class='status']", + self.assert_has_text(xml, "//p[@class='status']", status_mark, exact=False) def test_hidden(self): From bbab2d7de73138d5fc42908e25d633d29e29fdc3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 18 Apr 2013 10:09:21 -0400 Subject: [PATCH 160/280] Install gemfiles for tests --- jenkins/test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jenkins/test.sh b/jenkins/test.sh index b554e7a708..e1d44bf6b5 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -38,6 +38,8 @@ source /mnt/virtualenvs/"$JOB_NAME"/bin/activate pip install -q -r pre-requirements.txt yes w | pip install -q -r requirements.txt +bundle install + rake clobber rake pep8 > pep8.log || cat pep8.log rake pylint > pylint.log || cat pylint.log From 8a2d08bbd6625fa0e8a8cb1ca2f9e6012293b30f Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 17 Apr 2013 12:58:41 -0400 Subject: [PATCH 161/280] Refactor choosing the browser for lettuce tests to settings.py --- cms/envs/acceptance.py | 1 + common/djangoapps/terrain/browser.py | 19 +++++++++++++------ lms/envs/acceptance.py | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 26a8adc92c..1e7a32dc68 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -36,3 +36,4 @@ DATABASES = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) LETTUCE_SERVER_PORT = 8001 +LETTUCE_BROWSER = 'chrome' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c8cc0c9e4b..1d371a3242 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -1,6 +1,8 @@ from lettuce import before, after, world from splinter.browser import Browser from logging import getLogger +from django.core.management import call_command +from django.conf import settings # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -10,18 +12,14 @@ from cms import one_time_startup logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") -from django.core.management import call_command - @before.harvest def initial_setup(server): ''' Launch the browser once before executing the tests ''' - # Launch the browser app (choose one of these below) - world.browser = Browser('chrome') - # world.browser = Browser('phantomjs') - # world.browser = Browser('firefox') + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + world.browser = Browser(browser_driver) @before.each_scenario @@ -34,6 +32,15 @@ def reset_data(scenario): call_command('flush', interactive=False) +@after.each_scenario +def screenshot_on_error(scenario): + ''' + Save a screenshot to help with debugging + ''' + if scenario.failed: + world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png') + + @after.all def teardown_browser(total): ''' diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 5f416cd189..2c51dda5e6 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -67,3 +67,4 @@ MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) +LETTUCE_BROWSER = 'chrome' From 8a852f90cb15ff848066a02d0aeb9bc193be327a Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 17 Apr 2013 13:27:33 -0400 Subject: [PATCH 162/280] Fix or skip lettuce tests to run under phantomjs and firefox --- .../contentstore/features/advanced-settings.py | 13 +++++-------- .../contentstore/features/checklists.feature | 5 ++++- cms/djangoapps/contentstore/features/checklists.py | 7 +++---- common/djangoapps/terrain/steps.py | 2 ++ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 6fb102faea..ea5b24b21f 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -3,10 +3,7 @@ from lettuce import world, step from common import * -import time -from terrain.steps import reload_the_page - -from nose.tools import assert_true, assert_false, assert_equal +from nose.tools import assert_false, assert_equal """ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html @@ -18,8 +15,8 @@ VALUE_CSS = 'textarea.json' DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_VALUE = '"Robot Super Course"' -############### ACTIONS #################### +############### ACTIONS #################### @step('I select the Advanced Settings$') def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' @@ -38,7 +35,7 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): css = 'a.%s-button' % name.lower() - world.css_click_at(css) + world.css_click(css) @step(u'I edit the value of a policy key$') @@ -52,7 +49,7 @@ def edit_the_value_of_a_policy_key(step): @step(u'I edit the value of a policy key and save$') -def edit_the_value_of_a_policy_key(step): +def edit_the_value_of_a_policy_key_and_save(step): change_display_name_value(step, '"foo"') @@ -90,7 +87,7 @@ def it_is_formatted(step): @step('it is displayed as a string') -def it_is_formatted(step): +def it_is_displayed_as_string(step): assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"']) diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index bccb80b8d7..ddf1adf263 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -10,6 +10,8 @@ Feature: Course checklists Then I can check and uncheck tasks in a checklist And They are correctly selected after I reload the page + @skip-phantom + @skip-firefox Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -17,8 +19,9 @@ Feature: Course checklists And I press the browser back button Then I am brought back to the course outline in the correct state + @skip-phantom + @skip-firefox Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page Then I am brought to the help page in a new window - diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index 489544f424..d433dbbf0d 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -89,8 +89,6 @@ def i_am_brought_to_help_page_in_new_window(step): assert_equal('http://help.edge.edx.org/', world.browser.url) - - ############### HELPER METHODS #################### def verifyChecklist2Status(completed, total, percentage): def verify_count(driver): @@ -107,9 +105,11 @@ def verifyChecklist2Status(completed, total, percentage): def toggleTask(checklist, task): - world.css_click('#course-checklist' + str(checklist) +'-task' + str(task)) + world.css_click('#course-checklist' + str(checklist) + '-task' + str(task)) +# TODO: figure out a way to do this in phantom and firefox +# For now we will mark the scenerios that use this method as skipped def clickActionLink(checklist, task, actionText): # toggle checklist item to make sure that the link button is showing toggleTask(checklist, task) @@ -121,4 +121,3 @@ def clickActionLink(checklist, task, actionText): world.wait_for(verify_action_link_text) action_link.click() - diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index a2db80712f..fdab514177 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -132,6 +132,8 @@ def i_am_logged_in(step): world.create_user('robot') world.log_in('robot', 'test') world.browser.visit(django_url('/')) + # You should not see the login link + assert_equals(world.browser.find_by_css('a#login'), []) @step(u'I am an edX user$') From 0426761bede3c0938e08c438914e9cbb9a9419fb Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 17 Apr 2013 15:49:29 -0400 Subject: [PATCH 163/280] Update lettuce and factory-boy versions. --- common/djangoapps/student/tests/factories.py | 26 +++++++++----------- requirements.txt | 4 +-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index adb51954e8..952a8c016e 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -2,17 +2,17 @@ from student.models import (User, UserProfile, Registration, CourseEnrollmentAllowed, CourseEnrollment) from django.contrib.auth.models import Group from datetime import datetime -from factory import Factory, SubFactory, post_generation +from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall from uuid import uuid4 -class GroupFactory(Factory): +class GroupFactory(DjangoModelFactory): FACTORY_FOR = Group name = 'staff_MITx/999/Robot_Super_Course' -class UserProfileFactory(Factory): +class UserProfileFactory(DjangoModelFactory): FACTORY_FOR = UserProfile user = None @@ -23,19 +23,20 @@ class UserProfileFactory(Factory): goals = 'World domination' -class RegistrationFactory(Factory): +class RegistrationFactory(DjangoModelFactory): FACTORY_FOR = Registration user = None activation_key = uuid4().hex -class UserFactory(Factory): +class UserFactory(DjangoModelFactory): FACTORY_FOR = User username = 'robot' email = 'robot+test@edx.org' - password = 'test' + password = PostGenerationMethodCall('set_password', + 'test') first_name = 'Robot' last_name = 'Test' is_staff = False @@ -44,26 +45,21 @@ class UserFactory(Factory): last_login = datetime(2012, 1, 1) date_joined = datetime(2011, 1, 1) - @post_generation - def set_password(self, create, extracted, **kwargs): - self._raw_password = self.password - self.set_password(self.password) - if create: - self.save() +class AdminFactory(Factory): + FACTORY_FOR = User -class AdminFactory(UserFactory): is_staff = True -class CourseEnrollmentFactory(Factory): +class CourseEnrollmentFactory(DjangoModelFactory): FACTORY_FOR = CourseEnrollment user = SubFactory(UserFactory) course_id = 'edX/toy/2012_Fall' -class CourseEnrollmentAllowedFactory(Factory): +class CourseEnrollmentAllowedFactory(DjangoModelFactory): FACTORY_FOR = CourseEnrollmentAllowed email = 'test@edx.org' diff --git a/requirements.txt b/requirements.txt index a626ac1944..08aaecc71e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,8 +61,8 @@ sphinx==1.1.3 # Used for testing coverage==3.6 -factory_boy==1.3.0 -lettuce==0.2.15 +factory_boy==2.0.2 +lettuce==0.2.16 mock==0.8.0 nosexcover==1.0.7 pep8==1.4.5 From 29ce700de6e1b7c68dc532898f14893ed0ff6da3 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 17 Apr 2013 17:16:01 -0400 Subject: [PATCH 164/280] Tag CMS lettuce tests with @skip-phantom if they aren't working under phantomjs right now. --- .../features/advanced-settings.feature | 4 +++ .../features/course-settings.feature | 3 ++ .../contentstore/features/section.feature | 1 + .../studio-overview-togglesection.feature | 31 ++++++++++--------- .../contentstore/features/subsection.feature | 17 +++++----- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 558294e890..ca5b62e596 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + @skip-phantom Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key @@ -19,6 +20,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + @skip-phantom Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key and save @@ -26,6 +28,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + @skip-phantom Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value @@ -33,6 +36,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + @skip-phantom Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio When I create a non-JSON value not in quotes diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index e869bfe47a..fc9641cb46 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -1,17 +1,20 @@ Feature: Course Settings As a course author, I want to be able to configure my course settings. + @skip-phantom Scenario: User can set course dates Given I have opened a new course in Studio When I select Schedule and Details And I set course dates Then I see the set dates on refresh + @skip-phantom Scenario: User can clear previously set course dates (except start date) Given I have set course dates And I clear all the dates except start Then I see cleared dates on refresh + @skip-phantom Scenario: User cannot clear the course start date Given I have set course dates And I clear the course start date diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 08d38367bc..24cbeb3db9 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -3,6 +3,7 @@ Feature: Create Section As a course author I want to create and edit sections + @skip-phantom Scenario: Add a new section to a course Given I have opened a new course in Studio When I click the New Section link diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 762dea6838..a0e0a48f9e 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -1,32 +1,33 @@ Feature: Overview Toggle Section In order to quickly view the details of a course's section or to scan the inventory of sections - As a course author - I want to toggle the visibility of each section's subsection details in the overview listing + As a course author + I want to toggle the visibility of each section's subsection details in the overview listing Scenario: The default layout for the overview page is to show sections in expanded view Given I have a course with multiple sections - When I navigate to the course overview page - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + Then I see the "Collapse All Sections" link + And all sections are expanded Scenario: Expand /collapse for a course with no sections Given I have a course with no sections - When I navigate to the course overview page - Then I do not see the "Collapse All Sections" link + When I navigate to the course overview page + Then I do not see the "Collapse All Sections" link + @skip-phantom Scenario: Collapse link appears after creating first section of a course Given I have a course with no sections - When I navigate to the course overview page - And I add a section - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + And I add a section + Then I see the "Collapse All Sections" link + And all sections are expanded @skip-phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section - And I navigate to the course overview page - When I press the "section" delete icon - And I confirm the alert + And I navigate to the course overview page + When I press the "section" delete icon + And I confirm the alert Then I see the "Collapse All Sections" link Scenario: Collapsing all sections when all sections are expanded @@ -57,4 +58,4 @@ Feature: Overview Toggle Section When I expand the first section And I click the "Expand All Sections" link Then I see the "Collapse All Sections" link - And all sections are expanded \ No newline at end of file + And all sections are expanded diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index cc3b2b1cbb..28285bf8a1 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -3,13 +3,15 @@ Feature: Create Subsection As a course author I want to create and edit subsections - Scenario: Add a new subsection to a section + @skip-phantom + Scenario: Add a new subsection to a section Given I have opened a new course section in Studio When I click the New Subsection link And I enter the subsection name and click save Then I see my subsection on the Courseware page - Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) + @skip-phantom + Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) Given I have opened a new course section in Studio When I click the New Subsection link And I enter a subsection name with a quote and click save @@ -17,7 +19,7 @@ Feature: Create Subsection And I click to edit the subsection name Then I see the complete subsection name with a quote in the editor - Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258) + Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258) Given I have opened a new course section in Studio And I have added a new subsection And I mark it as Homework @@ -25,20 +27,19 @@ Feature: Create Subsection And I reload the page Then I see it marked as Homework - Scenario: Set a due date in a different year (bug #256) + @skip-phantom + Scenario: Set a due date in a different year (bug #256) Given I have opened a new subsection in Studio And I have set a release date and due date in different years Then I see the correct dates And I reload the page Then I see the correct dates - @skip-phantom - Scenario: Delete a subsection + @skip-phantom + Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection And I see my subsection on the Courseware page When I press the "subsection" delete icon And I confirm the alert Then the subsection does not exist - - From 03daefb9246e8aa8a5e936c7e89e9267ef2dbcc0 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 18 Apr 2013 14:31:51 -0400 Subject: [PATCH 165/280] Change the name of the plot button --- common/lib/capa/capa/templates/matlabinput.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 6c02e8e68e..04c91972d4 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -34,7 +34,7 @@

    - +
    - + - + @@ -97,8 +97,8 @@ from contentstore import utils
    1. - - Leeway on due dates + + Leeway on due dates (using HH:MM format)
    @@ -112,13 +112,13 @@ from contentstore import utils
      - -
    + + From ba4c0b6d5318b1cd697e2a4c5b3176b4bff3eb55 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 22 Apr 2013 09:43:57 -0400 Subject: [PATCH 191/280] studio - removing formatting suggestion from label --- cms/templates/settings_graders.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index a2f3f7022e..2e98409585 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -98,7 +98,7 @@ from contentstore import utils
  • - Leeway on due dates (using HH:MM format) + Leeway on due dates
  • From 506a9a20aaef0ce0d8d0997096e08071a4769d7d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 22 Apr 2013 10:13:33 -0400 Subject: [PATCH 192/280] when updating the list of children of sequentials, we must use the 'direct' store otherwise we end up with draft revisions of sequentials, which is bad --- cms/djangoapps/contentstore/tests/test_contentstore.py | 9 +++++++-- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index e40d7c57b9..ed95d81d67 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -9,7 +9,6 @@ from tempdir import mkdtemp_clean from fs.osfs import OSFS import copy from json import loads -import traceback from datetime import timedelta from django.contrib.auth.models import User @@ -397,7 +396,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full', - 'vertical', 'no_references', 'draft'])) + 'vertical', 'no_references', 'draft'])) for child in vertical.get_children(): draft_store.clone_item(child.location, child.location) @@ -478,6 +477,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) + # make sure that we don't have a sequential that is in draft mode + sequential = draft_store.get_item(Location(['i4x', 'edX', 'full', + 'sequential', 'Administrivia_and_Circuit_Elements', None])) + + self.assertFalse(getattr(sequential, 'is_draft', False)) + # verify that we have the private vertical test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None])) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 6355204d07..71c6983644 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -274,7 +274,7 @@ def import_from_xml(store, data_dir, course_dirs=None, # now import any 'draft' items if draft_store is not None: - import_course_draft(xml_module_store, draft_store, course_data_path, + import_course_draft(xml_module_store, store, draft_store, course_data_path, static_content_store, target_location_namespace if target_location_namespace is not None else course_location) @@ -339,7 +339,7 @@ def import_module(module, store, course_data_path, static_content_store, allow_n store.update_metadata(module.location, dict(own_metadata(module))) -def import_course_draft(xml_module_store, store, course_data_path, static_content_store, target_location_namespace): +def import_course_draft(xml_module_store, store, draft_store, course_data_path, static_content_store, target_location_namespace): ''' This will import all the content inside of the 'drafts' folder, if it exists NOTE: This is not a full course import, basically in our current application only verticals (and downwards) @@ -396,7 +396,7 @@ def import_course_draft(xml_module_store, store, course_data_path, static_conten del module.xml_attributes['parent_sequential_url'] del module.xml_attributes['index_in_children_list'] - import_module(module, store, course_data_path, static_content_store, allow_not_found=True) + import_module(module, draft_store, course_data_path, static_content_store, allow_not_found=True) for child in module.get_children(): _import_module(child) From 7994e1b344127b3a3c5d9463d6d24f1deaeadbf6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 12:53:30 -0400 Subject: [PATCH 193/280] pep8 fixes --- .../django_comment_client/base/views.py | 35 ++++++------- .../django_comment_client/forum/views.py | 19 +++---- .../management/commands/reload_forum_users.py | 8 +-- .../tests/test_mustache_helpers.py | 1 - lms/djangoapps/django_comment_client/utils.py | 32 ++++++------ lms/lib/comment_client/comment.py | 22 ++++---- lms/lib/comment_client/thread.py | 50 ++++++++++--------- 7 files changed, 84 insertions(+), 83 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 84a543868e..1f20fcbc79 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -119,7 +119,7 @@ def create_thread(request, course_id, commentable_id): #patch for backward compatibility to comments service if not 'pinned' in thread.attributes: thread['pinned'] = False - + if post.get('auto_subscribe', 'false').lower() == 'true': user = cc.User.from_django_user(request.user) user.follow(thread) @@ -174,7 +174,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): user = cc.User.from_django_user(request.user) user.follow(comment.thread) if request.is_ajax(): - return ajax_content_response(request, course_id,comment.to_dict(), 'discussion/ajax_create_comment.html') + return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_create_comment.html') else: return JsonResponse(utils.safe_content(comment.to_dict())) @@ -290,29 +290,32 @@ def vote_for_thread(request, course_id, thread_id, value): def flag_abuse_for_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.flagAbuse(user,thread) + thread.flagAbuse(user, thread) return JsonResponse(utils.safe_content(thread.to_dict())) + @require_POST @login_required @permitted def un_flag_abuse_for_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) - + thread = cc.Thread.find(thread_id) removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) - thread.unFlagAbuse(user,thread,removeAll) + thread.unFlagAbuse(user, thread, removeAll) return JsonResponse(utils.safe_content(thread.to_dict())) + @require_POST @login_required @permitted def flag_abuse_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) comment = cc.Comment.find(comment_id) - comment.flagAbuse(user,comment) + comment.flagAbuse(user, comment) return JsonResponse(utils.safe_content(comment.to_dict())) + @require_POST @login_required @permitted @@ -320,9 +323,10 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) comment = cc.Comment.find(comment_id) - comment.unFlagAbuse(user,comment, removeAll) + comment.unFlagAbuse(user, comment, removeAll) return JsonResponse(utils.safe_content(comment.to_dict())) + @require_POST @login_required @permitted @@ -332,19 +336,21 @@ def undo_vote_for_thread(request, course_id, thread_id): user.unvote(thread) return JsonResponse(utils.safe_content(thread.to_dict())) + @require_POST @login_required @permitted def pin_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.pin(user,thread_id) + thread.pin(user, thread_id) return JsonResponse(utils.safe_content(thread.to_dict())) + def un_pin_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.un_pin(user,thread_id) + thread.un_pin(user, thread_id) return JsonResponse(utils.safe_content(thread.to_dict())) @@ -491,16 +497,11 @@ def upload(request, course_id): # ajax upload file to a question or answer if not file_extension in cc_settings.ALLOWED_UPLOAD_FILE_TYPES: file_types = "', '".join(cc_settings.ALLOWED_UPLOAD_FILE_TYPES) msg = _("allowed file types are '%(file_types)s'") % \ - {'file_types': file_types} + {'file_types': file_types} raise exceptions.PermissionDenied(msg) # generate new file name - new_file_name = str( - time.time() - ).replace( - '.', - str(random.randint(0, 100000)) - ) + file_extension + new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_extension file_storage = get_storage_class()() # use default storage to store file @@ -511,7 +512,7 @@ def upload(request, course_id): # ajax upload file to a question or answer if size > cc_settings.MAX_UPLOAD_FILE_SIZE: file_storage.delete(new_file_name) msg = _("maximum upload file size is %(file_size)sK") % \ - {'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE} + {'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE} raise exceptions.PermissionDenied(msg) except exceptions.PermissionDenied, e: diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index df65d5aae6..e53a0195e3 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from mitxmako.shortcuts import render_to_response, render_to_string from courseware.courses import get_course_with_access -from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted, +from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted, get_cohorted_commentables, get_course_cohorts, get_cohort_by_id) from courseware.access import has_access from django_comment_client.models import Role @@ -43,7 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG 'course_id': course_id, 'user_id': request.user.id, } - + if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key cc_user = cc.User.from_django_user(request.user) @@ -93,11 +93,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG else: thread['group_name'] = "" thread['group_string'] = "This post visible to everyone." - + #patch for backward compatibility to comments service if not 'pinned' in thread: thread['pinned'] = False - + query_params['page'] = page query_params['num_pages'] = num_pages @@ -242,10 +242,10 @@ def single_thread(request, course_id, discussion_id, thread_id): user_info = cc_user.to_dict() try: - thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) + 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 + log.error("Error loading single thread.") + raise Http404 if request.is_ajax(): courseware_context = get_courseware_context(thread, course) @@ -302,9 +302,6 @@ def single_thread(request, course_id, discussion_id, thread_id): cohorts = get_course_cohorts(course_id) cohorted_commentables = get_cohorted_commentables(course_id) user_cohort = get_cohort_id(request.user, course_id) - - - context = { 'discussion_id': discussion_id, @@ -411,7 +408,7 @@ def followed_threads(request, course_id, user_id): 'user_info': saxutils.escape(json.dumps(user_info), escapedict), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), # 'content': content, - } + } return render_to_response('discussion/user_profile.html', context) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): diff --git a/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py index 5e7e268270..e84771d615 100644 --- a/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py +++ b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py @@ -6,10 +6,11 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import User import comment_client as cc + class Command(BaseCommand): help = 'Reload forum (comment client) users from existing users' - def adduser(self,user): + def adduser(self, user): print user try: cc_user = cc.User.from_django_user(user) @@ -22,8 +23,7 @@ class Command(BaseCommand): uset = [User.objects.get(username=x) for x in args] else: uset = User.objects.all() - + for user in uset: self.adduser(user) - - \ No newline at end of file + diff --git a/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py b/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py index 7db3ba6e86..d5a403ecb8 100644 --- a/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py +++ b/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py @@ -39,4 +39,3 @@ class CloseThreadTextTest(TestCase): self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread') ######################################################################################### - diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 05615f3870..e9efb38aed 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -105,12 +105,12 @@ def filter_unstarted_categories(category_map): result_map = {} unfiltered_queue = [category_map] - filtered_queue = [result_map] + filtered_queue = [result_map] while len(unfiltered_queue) > 0: unfiltered_map = unfiltered_queue.pop() - filtered_map = filtered_queue.pop() + filtered_map = filtered_queue.pop() filtered_map["children"] = [] filtered_map["entries"] = {} @@ -187,8 +187,7 @@ def initialize_discussion_info(course): category = " / ".join([x.strip() for x in category.split("/")]) last_category = category.split("/")[-1] discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title} - unexpanded_category_map[category].append({"title": title, "id": id, - "sort_key": sort_key, "start_date": module.lms.start}) + unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": module.lms.start}) category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)} for category_path, entries in unexpanded_category_map.items(): @@ -215,9 +214,9 @@ def initialize_discussion_info(course): level = path[-1] if level not in node: node[level] = {"subcategories": defaultdict(dict), - "entries": defaultdict(dict), - "sort_key": level, - "start_date": category_start_date} + "entries": defaultdict(dict), + "sort_key": level, + "start_date": category_start_date} else: if node[level]["start_date"] > category_start_date: node[level]["start_date"] = category_start_date @@ -297,12 +296,12 @@ class QueryCountDebugMiddleware(object): def get_ability(course_id, content, user): return { - 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), - 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), - 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False, - 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), - 'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, - 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), + 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), + 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), + 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False, + 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), + 'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, + 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), } #TODO: RENAME @@ -331,6 +330,7 @@ def get_annotated_content_infos(course_id, thread, user, user_info): Get metadata for a thread and its children """ infos = {} + def annotate(content): infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, user_info) for child in content.get('children', []): @@ -395,8 +395,8 @@ def get_courseware_context(content, course): location = id_map[id]["location"].url() title = id_map[id]["title"] - url = reverse('jump_to', kwargs={"course_id":course.location.course_id, - "location": location}) + url = reverse('jump_to', kwargs={"course_id": course.location.course_id, + "location": location}) content_info = {"courseware_url": url, "courseware_title": title} return content_info @@ -410,7 +410,7 @@ def safe_content(content): 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'courseware_title', 'courseware_url', 'tags', 'unread_comments_count', 'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers' - + ] if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index 8010aaf60f..324de7923f 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -41,7 +41,7 @@ class Comment(models.Model): return cls.url_for_comments(params) else: return super(Comment, cls).url(action, params) - + def flagAbuse(self, user, voteable): if voteable.type == 'thread': url = _url_for_flag_abuse_thread(voteable.id) @@ -51,8 +51,8 @@ class Comment(models.Model): raise CommentClientError("Can only flag/unflag threads or comments") params = {'user_id': user.id} request = perform_request('put', url, params) - voteable.update_attributes(request) - + voteable.update_attributes(request) + def unFlagAbuse(self, user, voteable, removeAll): if voteable.type == 'thread': url = _url_for_unflag_abuse_thread(voteable.id) @@ -61,12 +61,12 @@ class Comment(models.Model): else: raise CommentClientError("Can flag/unflag for threads or comments") params = {'user_id': user.id} - + if removeAll: params['all'] = True - + request = perform_request('put', url, params) - voteable.update_attributes(request) + voteable.update_attributes(request) def _url_for_thread_comments(thread_id): @@ -75,9 +75,11 @@ def _url_for_thread_comments(thread_id): def _url_for_comment(comment_id): return "{prefix}/comments/{comment_id}".format(prefix=settings.PREFIX, comment_id=comment_id) - + + def _url_for_flag_abuse_comment(comment_id): - return "{prefix}/comments/{comment_id}/abuse_flags".format(prefix=settings.PREFIX, comment_id=comment_id) - + return "{prefix}/comments/{comment_id}/abuse_flags".format(prefix=settings.PREFIX, comment_id=comment_id) + + def _url_for_unflag_abuse_comment(comment_id): - return "{prefix}/comments/{comment_id}/abuse_unflags".format(prefix=settings.PREFIX, comment_id=comment_id) + return "{prefix}/comments/{comment_id}/abuse_unflags".format(prefix=settings.PREFIX, comment_id=comment_id) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 8ecb0368be..60a68dc3ae 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -11,7 +11,6 @@ class Thread(models.Model): 'created_at', 'updated_at', 'comments_count', 'unread_comments_count', 'at_position_list', 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name', 'pinned', 'abuse_flaggers' - ] updatable_fields = [ @@ -33,7 +32,7 @@ class Thread(models.Model): 'course_id': query_params['course_id'], 'recursive': False} params = merge_dict(default_params, strip_blank(strip_none(query_params))) - + if query_params.get('text') or query_params.get('tags') or query_params.get('commentable_ids'): url = cls.url(action='search') else: @@ -56,7 +55,7 @@ class Thread(models.Model): @classmethod def url(cls, action, params={}): - + if action in ['get_all', 'post']: return cls.url_for_threads(params) elif action == 'search': @@ -70,11 +69,11 @@ class Thread(models.Model): def _retrieve(self, *args, **kwargs): url = self.url(action='get', params=self.attributes) request_params = { - 'recursive': kwargs.get('recursive'), - 'user_id': kwargs.get('user_id'), - 'mark_as_read': kwargs.get('mark_as_read', True), - } - + '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) @@ -91,8 +90,8 @@ class Thread(models.Model): raise CommentClientError("Can only flag/unflag threads or comments") params = {'user_id': user.id} request = perform_request('put', url, params) - voteable.update_attributes(request) - + voteable.update_attributes(request) + def unFlagAbuse(self, user, voteable, removeAll): if voteable.type == 'thread': url = _url_for_unflag_abuse_thread(voteable.id) @@ -104,31 +103,34 @@ class Thread(models.Model): #if you're an admin, when you unflag, remove ALL flags if removeAll: params['all'] = True - + request = perform_request('put', url, params) - voteable.update_attributes(request) - + voteable.update_attributes(request) + def pin(self, user, thread_id): url = _url_for_pin_thread(thread_id) params = {'user_id': user.id} request = perform_request('put', url, params) - self.update_attributes(request) + self.update_attributes(request) def un_pin(self, user, thread_id): url = _url_for_un_pin_thread(thread_id) params = {'user_id': user.id} request = perform_request('put', url, params) - self.update_attributes(request) - + self.update_attributes(request) + + def _url_for_flag_abuse_thread(thread_id): - return "{prefix}/threads/{thread_id}/abuse_flags".format(prefix=settings.PREFIX, thread_id=thread_id) - + return "{prefix}/threads/{thread_id}/abuse_flags".format(prefix=settings.PREFIX, thread_id=thread_id) + + def _url_for_unflag_abuse_thread(thread_id): - return "{prefix}/threads/{thread_id}/abuse_unflags".format(prefix=settings.PREFIX, thread_id=thread_id) - + return "{prefix}/threads/{thread_id}/abuse_unflags".format(prefix=settings.PREFIX, thread_id=thread_id) + + def _url_for_pin_thread(thread_id): - return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id) - + return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id) + + def _url_for_un_pin_thread(thread_id): - return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id) - + return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id) From 385026172e0b15d4b2564c98b9b5921f540afdb8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 15:28:27 -0400 Subject: [PATCH 194/280] remove double Comment from merge conflict --- .../static/coffee/src/discussion/content.coffee | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index e040c16c45..c44c321a3c 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -150,22 +150,6 @@ if Backbone? else @get("title") - class @Comments extends Backbone: -> - if @has("highlighted_title") - String(@get("highlighted_title")).replace(//g, '').replace(/<\/highlight>/g, '') - else - @get("title") - - toJSON: -> - json_attributes = _.clone(@attributes) - _.extend(json_attributes, { title: @display_title(), body: @display_body() }) - - created_at_date: -> - new Date(@get("created_at")) - - created_at_time: -> - new Date(@get("created_at")).getTime() - class @Comment extends @Content urlMappers: 'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id) From 8a02001a4fc42832dfca4383540be4de16dc059c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 15:40:19 -0400 Subject: [PATCH 195/280] minimize diff by restoring function locations --- .../coffee/src/discussion/content.coffee | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index c44c321a3c..8197985a1c 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -78,6 +78,16 @@ if Backbone? if @getContent(id) @getContent(id).updateInfo(info) $.extend @contentInfos, infos + + pinThread: -> + pinned = @get("pinned") + @set("pinned",pinned) + @trigger "change", @ + + unPinThread: -> + pinned = @get("pinned") + @set("pinned",pinned) + @trigger "change", @ flagAbuse: -> temp_array = @get("abuse_flaggers") @@ -128,16 +138,6 @@ if Backbone? @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 @trigger "change", @ - pinThread: -> - pinned = @get("pinned") - @set("pinned",pinned) - @trigger "change", @ - - unPinThread: -> - pinned = @get("pinned") - @set("pinned",pinned) - @trigger "change", @ - display_body: -> if @has("highlighted_body") String(@get("highlighted_body")).replace(//g, '').replace(/<\/highlight>/g, '') From b8114b9eb2df17f57440885c9d6037dcbbbc641a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 16:17:54 -0400 Subject: [PATCH 196/280] restore accidentally deleted chunk --- common/static/coffee/src/discussion/content.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index 8197985a1c..6361a4b76e 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -150,6 +150,16 @@ if Backbone? else @get("title") + toJSON: -> + json_attributes = _.clone(@attributes) + _.extend(json_attributes, { title: @display_title(), body: @display_body() }) + + created_at_date: -> + new Date(@get("created_at")) + + created_at_time: -> + new Date(@get("created_at")).getTime() + class @Comment extends @Content urlMappers: 'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id) From af1c411ddfc90b892f50c31ae23aa1c63ad79bd9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 16:23:21 -0400 Subject: [PATCH 197/280] gentler url diff --- lms/djangoapps/django_comment_client/base/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index 203b591761..fb687f4e47 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -10,9 +10,9 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'threads/(?P[\w\-]+)/reply$', 'create_comment', name='create_comment'), url(r'threads/(?P[\w\-]+)/delete', 'delete_thread', name='delete_thread'), url(r'threads/(?P[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), + url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', name='downvote_thread'), url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', name='flag_abuse_for_thread'), url(r'threads/(?P[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_thread', name='un_flag_abuse_for_thread'), - url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', name='downvote_thread'), url(r'threads/(?P[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), url(r'threads/(?P[\w\-]+)/pin$', 'pin_thread', name='pin_thread'), url(r'threads/(?P[\w\-]+)/unpin$', 'un_pin_thread', name='un_pin_thread'), From 30104979e707de2e506ee0cc15e846c76b59c98f Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 16:25:16 -0400 Subject: [PATCH 198/280] fix downvoting url --- lms/djangoapps/django_comment_client/base/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index fb687f4e47..18efbec502 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -10,7 +10,7 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'threads/(?P[\w\-]+)/reply$', 'create_comment', name='create_comment'), url(r'threads/(?P[\w\-]+)/delete', 'delete_thread', name='delete_thread'), url(r'threads/(?P[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), - url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', name='downvote_thread'), + url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'), url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', name='flag_abuse_for_thread'), url(r'threads/(?P[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_thread', name='un_flag_abuse_for_thread'), url(r'threads/(?P[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), From 32881ed265c771d04b89186f96415a220b786b65 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 22 Apr 2013 16:38:41 -0400 Subject: [PATCH 199/280] Change the name of the button so that it is clearer. --- common/lib/capa/capa/templates/matlabinput.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 7ca9a1961f..69e412f43e 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -35,7 +35,7 @@ % if button_enabled:
    - +
    %endif From 068e02efd539a450fbf5e768930a8cee1ffcdbe4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Apr 2013 17:51:48 -0400 Subject: [PATCH 200/280] make unflag all permissions match javascript --- lms/djangoapps/django_comment_client/base/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 1f20fcbc79..d2fa25e979 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _ from django.contrib.auth.models import User from mitxmako.shortcuts import render_to_response, render_to_string -from courseware.courses import get_course_with_access +from courseware.courses import get_course_with_access, get_course_by_id from course_groups.cohorts import get_cohort_id, is_commentable_cohorted from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context @@ -299,9 +299,9 @@ def flag_abuse_for_thread(request, course_id, thread_id): @permitted def un_flag_abuse_for_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) - + course = get_course_by_id(course_id) thread = cc.Thread.find(thread_id) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) + removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') thread.unFlagAbuse(user, thread, removeAll) return JsonResponse(utils.safe_content(thread.to_dict())) @@ -321,7 +321,8 @@ def flag_abuse_for_comment(request, course_id, comment_id): @permitted def un_flag_abuse_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) + course = get_course_by_id(course_id) + removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') comment = cc.Comment.find(comment_id) comment.unFlagAbuse(user, comment, removeAll) return JsonResponse(utils.safe_content(comment.to_dict())) From 6f103488360dc2e7134849817770f1971efe38d2 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Tue, 23 Apr 2013 10:07:51 -0400 Subject: [PATCH 201/280] addressed comments from pull request --- .../contentstore/tests/test_i18n.py | 28 +++- cms/envs/common.py | 2 + cms/static/js/base.js | 11 +- i18n/converter.py | 79 +++++----- i18n/dummy.py | 136 +++++------------ i18n/make_dummy.py | 23 +-- i18n/pofile.py | 143 ------------------ i18n/update.py | 13 +- 8 files changed, 131 insertions(+), 304 deletions(-) delete mode 100644 i18n/pofile.py diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index fba2da10dd..c3c0b25fc3 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -1,12 +1,12 @@ -# -*- coding: iso-8859-1 -*- +from unittest import skip -from django.test import TestCase from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.test.client import Client -from nose.tools import nottest -class InternationalizationTest(TestCase): +from .utils import ModuleStoreTestCase + +class InternationalizationTest(ModuleStoreTestCase): """ Tests to validate Internationalization. """ @@ -52,6 +52,22 @@ class InternationalizationTest(TestCase): status_code=200, html=True) + def test_course_explicit_english(self): + """Test viewing the index page with no courses""" + # Create a course so there is something to view + self.client = Client() + self.client.login(username=self.uname, password=self.password) + + resp = self.client.get(reverse('index'), + {}, + HTTP_ACCEPT_LANGUAGE='en' + ) + + self.assertContains(resp, + '

    My Courses

    ', + status_code=200, + html=True) + # **** # NOTE: @@ -62,7 +78,7 @@ class InternationalizationTest(TestCase): # actual French at that time. # Test temporarily disable since it depends on creation of dummy strings - @nottest + @skip def test_course_with_accents (self): """Test viewing the index page with no courses""" # Create a course so there is something to view @@ -75,7 +91,7 @@ class InternationalizationTest(TestCase): ) TEST_STRING = u'

    ' \ - + u'My Çöürsés L#' \ + + u'My \xc7\xf6\xfcrs\xe9s L#' \ + u'

    ' self.assertContains(resp, diff --git a/cms/envs/common.py b/cms/envs/common.py index 3cf5fe15b3..614491f50d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -128,6 +128,8 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'track.middleware.TrackMiddleware', 'mitxmako.middleware.MakoMiddleware', + + # Detects user-requested locale from 'accept-language' header in http request 'django.middleware.locale.LocaleMiddleware', 'django.middleware.transaction.TransactionMiddleware' diff --git a/cms/static/js/base.js b/cms/static/js/base.js index fa48b1699e..4112d2bb8e 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -826,11 +826,14 @@ function saveSetSectionScheduleDate(e) { data: JSON.stringify({ 'id': id, 'metadata': {'start': start}}) }).success(function () { var $thisSection = $('.courseware-section[data-id="' + id + '"]'); + var format = gettext('Will Release: %(date)s at $(time)s UTC'); + var willReleaseAt = interpolate(format, [input_date, input_time], true); $thisSection.find('.section-published-date').html( - '' + gettext('Will Release:') + - ' ' + input_date + ' at ' + input_time + - ' UTC' + + '' + willReleaseAt + '' + + '' + gettext('Edit') + ''); $thisSection.find('.section-published-date').animate({ 'background-color': 'rgb(182,37,104)' diff --git a/i18n/converter.py b/i18n/converter.py index fe66ff3e74..63d8f83e00 100644 --- a/i18n/converter.py +++ b/i18n/converter.py @@ -1,53 +1,45 @@ -import re, itertools - -# Converter is an abstract class that transforms strings. -# It hides embedded tags (HTML or Python sequences) from transformation -# -# To implement Converter, provide implementation for inner_convert_string() - +import re +import itertools class Converter: + """Converter is an abstract class that transforms strings. + It hides embedded tags (HTML or Python sequences) from transformation + + To implement Converter, provide implementation for inner_convert_string() + Strategy: + 1. extract tags embedded in the string + a. use the index of each extracted tag to re-insert it later + b. replace tags in string with numbers (<0>, <1>, etc.) + c. save extracted tags in a separate list + 2. convert string + 3. re-insert the extracted tags + + """ + # matches tags like these: - # HTML: , ,
    , - # Python: %(date)s, %(name)s - # - tag_pattern = re.compile(r'(<[-\w" .:?=/]*>)|({[^}]*})|(%\(.*\)\w)', re.I) + # HTML: , ,
    , + # Python: %(date)s, %(name)s + tag_pattern = re.compile(r'(<[-\w" .:?=/]*>)|({[^}]*})|(%\([^)]*\)\w)', re.I) - - def convert (self, string): - if self.tag_pattern.search(string): - result = self.convert_tagged_string(string) - else: - result = self.inner_convert_string(string) - return result - - # convert_tagged_string(string): - # returns: a converted tagged string - # param: string (contains html tags) - # - # Don't replace characters inside tags - # - # Strategy: - # 1. extract tags embedded in the string - # a. use the index of each extracted tag to re-insert it later - # b. replace tags in string with numbers (<0>, <1>, etc.) - # c. save extracted tags in a separate list - # 2. convert string - # 3. re-insert the extracted tags - # - def convert_tagged_string (self, string): + def convert(self, string): + """Returns: a converted tagged string + param: string (contains html tags) + + Don't replace characters inside tags + """ (string, tags) = self.detag_string(string) string = self.inner_convert_string(string) string = self.retag_string(string, tags) return string - # extracts tags from string. - # - # returns (string, list) where - # string: string has tags replaced by indices (
    ... => <0>, <1>, <2>, etc.) - # list: list of the removed tags ("
    ", "", "") - def detag_string (self, string): + def detag_string(self, string): + """Extracts tags from string. + + returns (string, list) where + string: string has tags replaced by indices (
    ... => <0>, <1>, <2>, etc.) + list: list of the removed tags ('
    ', '', '') + """ counter = itertools.count(0) count = lambda m: '<%s>' % counter.next() tags = self.tag_pattern.findall(string) @@ -57,9 +49,8 @@ class Converter: raise Exception('tags dont match:'+string) return (new, tags) - # substitutes each tag back into string, into occurrences of <0>, <1> etc - # - def retag_string (self, string, tags): + def retag_string(self, string, tags): + """substitutes each tag back into string, into occurrences of <0>, <1> etc""" for (i, tag) in enumerate(tags): p = '<%s>' % i string = re.sub(p, tag, string, 1) @@ -69,6 +60,6 @@ class Converter: # ------------------------------ # Customize this in subclasses of Converter - def inner_convert_string (self, string): + def inner_convert_string(self, string): return string # do nothing by default diff --git a/i18n/dummy.py b/i18n/dummy.py index a94d400ba0..798ee525b5 100644 --- a/i18n/dummy.py +++ b/i18n/dummy.py @@ -1,12 +1,6 @@ -# -*- coding: iso-8859-15 -*- - from converter import Converter -# This file converts string resource files. -# Java: file has name like messages_en.properties -# Flex: file has name like locales/en_US/Labels.properties - -# Creates new localization properties files in a dummy language (saved as 'vr', Vardebedian) +# Creates new localization properties files in a dummy language # Each property file is derived from the equivalent en_US file, except # 1. Every vowel is replaced with an equivalent with extra accent marks # 2. Every string is padded out to +30% length to simulate verbose languages (e.g. German) @@ -18,19 +12,18 @@ from converter import Converter # Example use: # >>> from dummy import Dummy # >>> c = Dummy() -# >>> print c.convert("hello my name is Bond, James Bond") -# héllö my nämé ïs Bönd, Jämés Bönd Lorem i# +# >>> c.convert("hello my name is Bond, James Bond") +# u'h\xe9ll\xf6 my n\xe4m\xe9 \xefs B\xf6nd, J\xe4m\xe9s B\xf6nd Lorem i#' # -# >>> print c.convert('don\'t convert tag ids') -# dön't çönvért täg ïds Lorem ipsu# +# >>> c.convert('don\'t convert tag ids') +# u'd\xf6n\'t \xe7\xf6nv\xe9rt t\xe4g \xefds Lorem ipsu#' # -# >>> print c.convert('don\'t convert %(name)s tags on %(date)s') -# dön't çönvért %(name)s tags on %(date)s Lorem ips# +# >>> c.convert('don\'t convert %(name)s tags on %(date)s') +# u"d\xf6n't \xe7\xf6nv\xe9rt %(name)s t\xe4gs \xf6n %(date)s Lorem ips#" # Substitute plain characters with accented lookalikes. # http://tlt.its.psu.edu/suggestions/international/web/codehtml.html#accent -# print "print u'\\x%x'" % 207 TABLE = {'A': u'\xC0', 'a': u'\xE4', 'b': u'\xDF', @@ -62,23 +55,23 @@ PAD_FACTOR = 1.3 class Dummy (Converter): - ''' + """ A string converter that generates dummy strings with fake accents and lorem ipsum padding. - ''' + """ - def convert (self, string): + def convert(self, string): result = Converter.convert(self, string) return self.pad(result) - def inner_convert_string (self, string): + def inner_convert_string(self, string): for (k,v) in TABLE.items(): string = string.replace(k, v) return string - def pad (self, string): - '''add some lorem ipsum text to the end of string''' + def pad(self, string): + """add some lorem ipsum text to the end of string""" size = len(string) if size < 7: target = size*3 @@ -86,15 +79,15 @@ class Dummy (Converter): target = int(size*PAD_FACTOR) return string + self.terminate(LOREM[:(target-size)]) - def terminate (self, string): - '''replaces the final char of string with #''' + def terminate(self, string): + """replaces the final char of string with #""" return string[:-1]+'#' - def init_msgs (self, msgs): - ''' + def init_msgs(self, msgs): + """ Make sure the first msg in msgs has a plural property. msgs is list of instances of pofile.Msg - ''' + """ if len(msgs)==0: return headers = msgs[0].get_property('msgstr') @@ -105,82 +98,35 @@ class Dummy (Converter): headers.append(plural) - def convert_msg (self, msg): - ''' + def convert_msg(self, msg): + """ Takes one Msg object and converts it (adds a dummy translation to it) msg is an instance of pofile.Msg - ''' - source = msg.get_property('msgid') - if len(source)==1 and len(source[0])==0: + """ + source = msg.msgid + if len(source)==0: # don't translate empty string return - plural = msg.get_property('msgid_plural') + + plural = msg.msgid_plural if len(plural)>0: # translate singular and plural - foreign_single = self.convert(merge(source)) - foreign_plural = self.convert(merge(plural)) - msg.set_property('msgstr[0]', split(foreign_single)) - msg.set_property('msgstr[1]', split(foreign_plural)) + foreign_single = self.convert(source) + foreign_plural = self.convert(plural) + plural = {'0': self.final_newline(source, foreign_single), + '1': self.final_newline(plural, foreign_plural)} + msg.msgstr_plural = plural return else: - src_merged = merge(source) - foreign = self.convert(src_merged) - if len(source)>1: - # If last char is a newline, make sure translation - # has a newline too. - if src_merged[-2:]=='\\n': - foreign += '\\n' - msg.set_property('msgstr', split(foreign)) - - -# ---------------------------------- -# String splitting utility functions - -SPLIT_SIZE = 70 - -def merge (string_list): - '''returns a single string: concatenates string_list''' - return ''.join(string_list) - -# .po file format requires long strings to be broken -# up into several shorter (<80 char) strings. -# The first string is empty (""), which indicates -# that more are to be read on following lines. - -def split (string): - ''' - Returns string split into fragments of a given size. - If there are multiple fragments, insert "" as the first fragment. - ''' - result = [chunk for chunk in chunks(string, SPLIT_SIZE)] - if len(result)>1: - result = [''] + result - return result - -def chunks(string, size): - ''' - Generate fragments of a given size from string. Avoid breaking - the string in the middle of an escape sequence (e.g. "\n") - ''' - strlen=len(string)-1 - esc = False - last = 0 - for i,char in enumerate(string): - if not esc and char == '\\': - esc = True - continue - if esc: - esc = False - if i>=last+size-1 or i==strlen: - chunk = string[last:i+1] - last = i+1 - yield chunk - -# testing -# >>> a = "abcd\\efghijklmnopqrstuvwxyz" -# >>> SPLIT_SIZE = 5 -# >>> split(a) -# ['abcd\\e', 'fghij', 'klmno', 'pqrst', 'uvwxy', 'z'] -# >>> merge(split(a)) -# 'abcd\\efghijklmnopqrstuvwxyz' + foreign = self.convert(source) + msg.msgstr = self.final_newline(source, foreign) + def final_newline(self, original, translated): + """ Returns a new translated string. + If last char of original is a newline, make sure translation + has a newline too. + """ + if len(original)>1: + if original[-1]=='\n' and translated[-1]!='\n': + return translated + '\n' + return translated diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py index 8bf9711c57..4ccfb0d5f1 100755 --- a/i18n/make_dummy.py +++ b/i18n/make_dummy.py @@ -16,7 +16,7 @@ # mitx/conf/locale/vr/LC_MESSAGES/django.po import os, sys -from pofile import PoFile +import polib from dummy import Dummy # Dummy language @@ -28,23 +28,26 @@ from dummy import Dummy OUT_LANG = 'fr' -def main (file): - ''' +def main(file): + """ Takes a source po file, reads it, and writes out a new po file containing a dummy translation. - ''' - pofile = PoFile(file) + """ + if not os.path.exists(file): + raise IOError('File does not exist: %s' % file) + pofile = polib.pofile(file) converter = Dummy() - converter.init_msgs(pofile.msgs) - for msg in pofile.msgs: + converter.init_msgs(pofile.translated_entries()) + for msg in pofile: converter.convert_msg(msg) new_file = new_filename(file, OUT_LANG) create_dir_if_necessary(new_file) - pofile.write(new_file) + pofile.save(new_file) + -def new_filename (original_filename, new_lang): - '''Returns a filename derived from original_filename, using new_lang as the locale''' +def new_filename(original_filename, new_lang): + """Returns a filename derived from original_filename, using new_lang as the locale""" orig_dir = os.path.dirname(original_filename) msgs_dir = os.path.basename(orig_dir) orig_file = os.path.basename(original_filename) diff --git a/i18n/pofile.py b/i18n/pofile.py deleted file mode 100644 index d91f76a925..0000000000 --- a/i18n/pofile.py +++ /dev/null @@ -1,143 +0,0 @@ -import re, codecs -from operator import itemgetter - -# Django stores externalized strings in .po and .mo files. -# po files are human readable and contain metadata about the strings. -# mo files are machine readable and optimized for runtime performance. - -# See https://docs.djangoproject.com/en/1.3/topics/i18n/internationalization/ -# See http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html - -# Usage: -# >>> pofile = PoFile('/path/to/file') - - -class PoFile: - - # Django requires po files to be in UTF8 with no BOM (byte order marker) - # see "Mind your charset" on this page: - # https://docs.djangoproject.com/en/1.3/topics/i18n/localization/ - - ENCODING = 'utf_8' - - def __init__ (self, pathname): - self.pathname = pathname - self.parse() - - def parse (self): - with codecs.open(self.pathname, 'r', self.ENCODING) as stream: - text = stream.read() - msgs = text.split('\n\n') - self.msgs = [Msg.parse(m) for m in msgs] - return msgs - - def write (self, out_pathname=None): - if out_pathname == None: - out_pathname = self.pathname - with codecs.open(out_pathname, 'w', self.ENCODING) as stream: - for msg in self.msgs: - msg.write(stream) - -class Msg: - - # A PoFile is parsed into a list of Msg objects, each of which corresponds - # to an externalized string entry. - - # Each Msg object may contain multiple comment lines, capturing metadata - - # Each Msg has a property list (self.props) with a dict of key-values. - # Each value is a list of strings - kwords = ['msgid', 'msgstr', 'msgctxt', 'msgid_plural'] - - # Line might begin with "msgid ..." or "msgid[2] ..." - pattern = re.compile('^(\w+)(\[(\d+)\])?') - - @classmethod - def parse (cls, string): - ''' - String is a fragment of a pofile (.po) source file. - This returns a Msg object created by parsing string. - ''' - lines = string.strip().split('\n') - msg = Msg() - msg.comments = [] - msg.props = {} - last_kword = None - for line in lines: - if line[0]=='#': - msg.comments.append(line) - elif line[0]=='"' and last_kword != None: - msg.add_string(last_kword, line) - else: - match = cls.pattern.search(line) - if match: - kword = match.group(1) - last_kword = kword - if kword in cls.kwords: - if match.group(3): - key = '%s[%s]' % (kword, match.group(3)) - msg.add_string(key, line[len(key):]) - else: - msg.add_string(kword, line[len(kword):]) - return msg - - def get_property (self, kword): - '''returns value for kword. Typically returns a list of strings''' - return self.props.get(kword, []) - - def set_property (self, kword, value): - '''sets value for kword. Typically returns a list of strings''' - self.props[kword] = value - - def add_string (self, kword, line): - '''Append line to the list of values stored for the property kword''' - props = self.props - value = self.get_property(kword) - value.append(self.cleanup_string(line)) - self.set_property(kword, value) - - def cleanup_string(self, string): - string = string.strip() - if len(string)>1 and string[0]=='"' and string[-1]=='"': - return string[1:-1] - else: - return string - - def write (self, stream): - '''Write a Msg to stream''' - for comment in self.comments: - stream.write(comment) - stream.write('\n') - for (key, values) in self.sort(self.props.items()): - stream.write(key + ' ') - for value in values: - stream.write('"'+value+'"') - stream.write('\n') - stream.write('\n') - - # Preferred ordering of key output - # Always print 'msgctxt' first, then 'msgid', etc. - KEY_ORDER = ('msgctxt', 'msgid', 'msgid_plural', 'msgstr', 'msgstr[0]', 'msgstr[1]') - - def keyword_compare (self, k1, k2): - for key in self.KEY_ORDER: - if key == k1: - return -1 - if key == k2: - return 1 - return 0 - - def sort (self, plist): - '''sorts a propertylist to bring the high-priority keys to the beginning of the list''' - return sorted(plist, key=itemgetter(0), cmp=self.keyword_compare) - - - -# Testing -# -# >>> file = 'mitx/conf/locale/en/LC_MESSAGES/django.po' -# >>> file1 = 'mitx/conf/locale/en/LC_MESSAGES/django1.po' -# >>> po = PoFile(file) -# >>> po.write(file1) -# $ diff file file1 - diff --git a/i18n/update.py b/i18n/update.py index 8a865c2528..447dcf71d5 100755 --- a/i18n/update.py +++ b/i18n/update.py @@ -42,8 +42,8 @@ BABEL_OUT = MSGS_DIR + '/mako.po' # These are the shell commands invoked by main() COMMANDS = { 'babel_mako': 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT), - 'make_django': 'django-admin.py makemessages --all --extension html -l en', - 'make_djangojs': 'django-admin.py makemessages --all -d djangojs --extension js -l en', + 'make_django': 'django-admin.py makemessages --all --ignore=src/* --extension html -l en', + 'make_djangojs': 'django-admin.py makemessages --all -d djangojs --ignore=src/* --extension js -l en', 'msgcat' : 'msgcat -o merged.po django.po %s' % BABEL_OUT, 'rename_django' : 'mv django.po django_old.po', 'rename_merged' : 'mv merged.po django.po', @@ -81,6 +81,15 @@ def main (): create_dir_if_necessary(LOCALE_DIR) log.info('Executing all commands from %s' % BASE_DIR) + remove_files = ['django.po', 'djangojs.po', 'nonesuch'] + for filename in remove_files: + path = MSGS_DIR + '/' + filename + log.info('Deleting file %s' % path) + if not os.path.exists(path): + log.warn("File does not exist: %s" % path) + else: + os.remove(path) + # Generate or update human-readable .po files from all source code. execute('babel_mako', log=log) execute('make_django', log=log) From 67f57a71454207b2cf6213bd582fcb97af0ed83c Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 23 Apr 2013 13:28:23 -0400 Subject: [PATCH 202/280] use new API verbs --- lms/lib/comment_client/comment.py | 4 ++-- lms/lib/comment_client/thread.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index 324de7923f..fb5a4ad0c3 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -78,8 +78,8 @@ def _url_for_comment(comment_id): def _url_for_flag_abuse_comment(comment_id): - return "{prefix}/comments/{comment_id}/abuse_flags".format(prefix=settings.PREFIX, comment_id=comment_id) + return "{prefix}/comments/{comment_id}/abuse_flag".format(prefix=settings.PREFIX, comment_id=comment_id) def _url_for_unflag_abuse_comment(comment_id): - return "{prefix}/comments/{comment_id}/abuse_unflags".format(prefix=settings.PREFIX, comment_id=comment_id) + return "{prefix}/comments/{comment_id}/abuse_unflag".format(prefix=settings.PREFIX, comment_id=comment_id) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 60a68dc3ae..0b0be576b8 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -121,11 +121,11 @@ class Thread(models.Model): def _url_for_flag_abuse_thread(thread_id): - return "{prefix}/threads/{thread_id}/abuse_flags".format(prefix=settings.PREFIX, thread_id=thread_id) + return "{prefix}/threads/{thread_id}/abuse_flag".format(prefix=settings.PREFIX, thread_id=thread_id) def _url_for_unflag_abuse_thread(thread_id): - return "{prefix}/threads/{thread_id}/abuse_unflags".format(prefix=settings.PREFIX, thread_id=thread_id) + return "{prefix}/threads/{thread_id}/abuse_unflag".format(prefix=settings.PREFIX, thread_id=thread_id) def _url_for_pin_thread(thread_id): From bcce41078b4399ba00d5c18e97445072021a6faa Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 23 Apr 2013 16:12:49 -0400 Subject: [PATCH 203/280] add more documentation about `showanswer` policy key --- doc/public/course_data_formats/course_xml.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/public/course_data_formats/course_xml.rst b/doc/public/course_data_formats/course_xml.rst index 22d96d1432..f3e52bf32d 100644 --- a/doc/public/course_data_formats/course_xml.rst +++ b/doc/public/course_data_formats/course_xml.rst @@ -387,7 +387,14 @@ Inherited When this content should be shown to students. Note that anyone with staff access to the course will always see everything. `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. + When to show answer. Values: never, attempted, answered, closed. Default: closed. Optional. + - `never`: never show answer + - `attempted`: show answer after first attempt + - `answered` : this is slightly different from `attempted` -- resetting the problems makes "done" False, but leaves attempts unchanged. + - `closed` : show answer after problem is closed, ie due date is past, or maximum attempts exceeded. + - `finished` : show answer after problem closed, or is correctly answered. + - `past_due` : show answer after problem due date is past. + - `always` : always allow answer to be shown. `graded` Whether this section will count towards the students grade. "true" or "false". Defaults to "false". From a307696d679c2c1c88417441d622e09bc82d066e Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 23 Apr 2013 16:15:46 -0400 Subject: [PATCH 204/280] more showanswer possibilities -> doc --- doc/public/course_data_formats/course_xml.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/public/course_data_formats/course_xml.rst b/doc/public/course_data_formats/course_xml.rst index f3e52bf32d..c17175bafa 100644 --- a/doc/public/course_data_formats/course_xml.rst +++ b/doc/public/course_data_formats/course_xml.rst @@ -387,7 +387,7 @@ Inherited When this content should be shown to students. Note that anyone with staff access to the course will always see everything. `showanswer` - When to show answer. Values: never, attempted, answered, closed. Default: closed. Optional. + When to show answer. Values: never, attempted, answered, closed, finished, past_due, always. Default: closed. Optional. - `never`: never show answer - `attempted`: show answer after first attempt - `answered` : this is slightly different from `attempted` -- resetting the problems makes "done" False, but leaves attempts unchanged. From 111ec62bb62f17cd3f2548ceb89926505d3c3c27 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Wed, 24 Apr 2013 10:42:35 -0400 Subject: [PATCH 205/280] merged from master --- cms/djangoapps/contentstore/tests/test_i18n.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index c3c0b25fc3..cbfa1c6bef 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -42,7 +42,6 @@ class InternationalizationTest(ModuleStoreTestCase): def test_course_plain_english(self): """Test viewing the index page with no courses""" - # Create a course so there is something to view self.client = Client() self.client.login(username=self.uname, password=self.password) @@ -54,7 +53,6 @@ class InternationalizationTest(ModuleStoreTestCase): def test_course_explicit_english(self): """Test viewing the index page with no courses""" - # Create a course so there is something to view self.client = Client() self.client.login(username=self.uname, password=self.password) @@ -81,7 +79,6 @@ class InternationalizationTest(ModuleStoreTestCase): @skip def test_course_with_accents (self): """Test viewing the index page with no courses""" - # Create a course so there is something to view self.client = Client() self.client.login(username=self.uname, password=self.password) From cd2fa7104a24d9b65578234c38886cf4ac69766b Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 11:04:45 -0400 Subject: [PATCH 206/280] Refer to inherited field. --- common/lib/xmodule/xmodule/capa_module.py | 1 + common/lib/xmodule/xmodule/html_module.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index c1606aeeb3..680305269d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -96,6 +96,7 @@ class CapaFields(object): input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) + # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) display_name = XModule.display_name seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index d901fc5fbe..71d8ee4765 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -19,6 +19,8 @@ log = logging.getLogger("mitx.courseware") class HtmlFields(object): data = String(help="Html contents to display for this module", scope=Scope.content) + # Used by the LMS on the sequential ribbon (displayed as a tooltip) + display_name = XModule.display_name class HtmlModule(HtmlFields, XModule): From 8ab467a9fc1098385d08b04274759263e7f802eb Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Wed, 24 Apr 2013 11:17:02 -0400 Subject: [PATCH 207/280] add config files for PyBabel and update.py --- .gitignore | 10 ++++++++++ conf/locale/babel.cfg | 19 +++++++++++++++++++ conf/locale/config | 1 + 3 files changed, 30 insertions(+) create mode 100644 conf/locale/babel.cfg create mode 100644 conf/locale/config diff --git a/.gitignore b/.gitignore index 8fb170c30f..e7b0b16be8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,13 @@ cover_html/ chromedriver.log /nbproject ghostdriver.log +/cms/doc/en/getting_started/ +/conf/locale/en +/conf/locale/fr +create-dev-env.hack.sh +distribute-0.6.36.tar.gz +i18n/googleTranslate.hack.py +i18n/mitx/conf/locale/fr/LC_MESSAGES/django.po +i18n/split.py +.gitignore + diff --git a/conf/locale/babel.cfg b/conf/locale/babel.cfg new file mode 100644 index 0000000000..5b8333cf1e --- /dev/null +++ b/conf/locale/babel.cfg @@ -0,0 +1,19 @@ +# Extraction from Python source files +#[python: cms/**.py] +#[python: lms/**.py] +#[python: common/**.py] + +# Extraction from Javscript source files +#[javascript: cms/**.js] +#[javascript: lms/**.js] +#[javascript: common/static/js/capa/**.js] +#[javascript: common/static/js/course_groups/**.js] +# do not extract from common/static/js/vendor/** + +# Extraction from Mako templates +[mako: cms/templates/**.html] +input_encoding = utf-8 +[mako: lms/templates/**.html] +input_encoding = utf-8 +[mako: common/templates/**.html] +input_encoding = utf-8 diff --git a/conf/locale/config b/conf/locale/config new file mode 100644 index 0000000000..fe811ee02e --- /dev/null +++ b/conf/locale/config @@ -0,0 +1 @@ +{"locales" : ["en", "fr", "de"]} From dd71ae818a6bbc83992b94cccb7ed9adf9d1632f Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Wed, 24 Apr 2013 11:33:47 -0400 Subject: [PATCH 208/280] ModuleStoreTestCase moved to new location --- cms/djangoapps/contentstore/tests/test_i18n.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index cbfa1c6bef..e6d68ba004 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.test.client import Client -from .utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class InternationalizationTest(ModuleStoreTestCase): """ From 4d55f87ab137975dc7cdf09291eebb56fe46b3e2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 24 Apr 2013 12:33:44 -0400 Subject: [PATCH 209/280] Add comments on duplicate calls to compile_assets --- rakefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rakefile b/rakefile index ae5dab2bb8..dc123db3f8 100644 --- a/rakefile +++ b/rakefile @@ -245,8 +245,13 @@ end desc task system, [:env, :options] => [:predjango] do |t, args| args.with_defaults(:env => 'dev', :options => default_options[system]) + + # Compile all assets first compile_assets(watch=false, debug=true) + + # Listen for any changes to assets compile_assets(watch=true, debug=true) + sh(django_admin(system, args.env, 'runserver', args.options)) end From 9d04908ad177b905199017245e9987bef0bbc629 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 14:40:55 -0400 Subject: [PATCH 210/280] Remove duplicated StringyX definitions. --- cms/xmodule_namespace.py | 18 +----- common/lib/xmodule/xmodule/capa_module.py | 27 +-------- .../xmodule/combined_open_ended_module.py | 3 +- common/lib/xmodule/xmodule/fields.py | 41 ++++++++++++++ common/lib/xmodule/xmodule/mako_module.py | 28 ++-------- .../xmodule/xmodule/peer_grading_module.py | 3 +- .../lib/xmodule/xmodule/tests/test_fields.py | 56 ++++++++++++++++++- common/lib/xmodule/xmodule/video_module.py | 3 +- lms/xmodule_namespace.py | 29 +--------- 9 files changed, 110 insertions(+), 98 deletions(-) diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index c9bb8f4c6e..1b509a14f4 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -4,22 +4,8 @@ Namespace defining common fields used by Studio for all blocks import datetime -from xblock.core import Namespace, Boolean, Scope, ModelType, String - - -class StringyBoolean(Boolean): - """ - Reads strings from JSON as booleans. - - If the string is 'true' (case insensitive), then return True, - otherwise False. - - JSON values that aren't strings are returned as is - """ - def from_json(self, value): - if isinstance(value, basestring): - return value.lower() == 'true' - return value +from xblock.core import Namespace, Scope, ModelType, String +from xmodule.fields import StringyBoolean class DateTuple(ModelType): diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 680305269d..dc9be6ce5f 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -16,36 +16,13 @@ from .progress import Progress from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.exceptions import NotFoundError, ProcessingError -from xblock.core import Integer, Scope, String, Boolean, Object, Float -from .fields import Timedelta, Date +from xblock.core import Scope, String, Boolean, Object +from .fields import Timedelta, Date, StringyInteger, StringyFloat from xmodule.util.date_utils import time_to_datetime log = logging.getLogger("mitx.courseware") -class StringyInteger(Integer): - """ - A model type that converts from strings to integers when reading from json - """ - def from_json(self, value): - try: - return int(value) - except: - return None - - -# TODO: move to fields.py and remove duplicated code. -class StringyFloat(Float): - """ - A model type that converts from string to floats when reading from json - """ - def from_json(self, value): - try: - return float(value) - except: - return None - - # Generated this many different variants of problems with rerandomize=per_student NUM_RANDOMIZATION_BINS = 20 diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 120e4f743a..239adcaa41 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,8 +8,7 @@ from .x_module import XModule from xblock.core import Integer, Scope, String, Boolean, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from collections import namedtuple -from .fields import Date -from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat +from .fields import Date, StringyFloat log = logging.getLogger("mitx.courseware") diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index bb85714252..3d56b7941e 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -7,6 +7,8 @@ from xblock.core import ModelType import datetime import dateutil.parser +from xblock.core import Integer, Float, Boolean + log = logging.getLogger(__name__) @@ -81,3 +83,42 @@ class Timedelta(ModelType): if cur_value > 0: values.append("%d %s" % (cur_value, attr)) return ' '.join(values) + + +class StringyInteger(Integer): + """ + A model type that converts from strings to integers when reading from json. + If value does not parse as an int, returns None. + """ + def from_json(self, value): + try: + return int(value) + except: + return None + + +class StringyFloat(Float): + """ + A model type that converts from string to floats when reading from json. + If value does not parse as a float, returns None. + """ + def from_json(self, value): + try: + return float(value) + except: + return None + + +class StringyBoolean(Boolean): + """ + Reads strings from JSON as booleans. + + If the string is 'true' (case insensitive), then return True, + otherwise False. + + JSON values that aren't strings are returned as-is. + """ + def from_json(self, value): + if isinstance(value, basestring): + return value.lower() == 'true' + return value diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py index 75dd655d41..84db6ad779 100644 --- a/common/lib/xmodule/xmodule/mako_module.py +++ b/common/lib/xmodule/xmodule/mako_module.py @@ -1,7 +1,5 @@ from .x_module import XModuleDescriptor, DescriptorSystem from .modulestore.inheritance import own_metadata -from xblock.core import Scope - class MakoDescriptorSystem(DescriptorSystem): @@ -46,26 +44,10 @@ class MakoModuleDescriptor(XModuleDescriptor): # cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata) @property def editable_metadata_fields(self): -# fields = {} -# for field, value in own_metadata(self).items(): -# if field in self.system_metadata_fields: -# continue -# -# fields[field] = value -# return fields - inherited_metadata = getattr(self, '_inherited_metadata', {}) - metadata = {} - for field in self.fields: - # Only save metadata that wasn't inherited - if field.scope != Scope.settings or field.name in self.system_metadata_fields: + fields = {} + for field, value in own_metadata(self).items(): + if field in self.system_metadata_fields: continue - if field.name in self._model_data: - metadata[field.name] = self._model_data[field.name] - if field.name in inherited_metadata and self._model_data.get(field.name) == inherited_metadata.get( - field.name): - metadata[field.name] = str(metadata[field.name]) + ' INHERITED' - else: - metadata[field.name] = str(getattr(self, field.name)) + ' DEFAULT' - - return metadata + fields[field] = value + return fields diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index db4514d0e0..35f2fa2d76 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -11,8 +11,7 @@ from xmodule.raw_module import RawDescriptor from xmodule.modulestore.django import modulestore from .timeinfo import TimeInfo from xblock.core import Object, Integer, Boolean, String, Scope -from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat -from xmodule.fields import Date +from xmodule.fields import Date, StringyFloat from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from open_ended_grading_classes import combined_open_ended_rubric diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index 7c8872efc1..9642f7c595 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -1,8 +1,8 @@ -"""Tests for Date class defined in fields.py.""" +"""Tests for classes defined in fields.py.""" import datetime import unittest from django.utils.timezone import UTC -from xmodule.fields import Date +from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean import time class DateTest(unittest.TestCase): @@ -78,3 +78,55 @@ class DateTest(unittest.TestCase): DateTest.date.from_json("2012-12-31T23:00:01-01:00")), "2013-01-01T00:00:01Z") + +class StringyIntegerTest(unittest.TestCase): + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyInteger().from_json(arg)) + + def test_integer(self): + self.assertEquals(5, '5') + self.assertEquals(0, '0') + self.assertEquals(-1023, '-1023') + + def test_none(self): + self.assertEquals(None, None) + self.assertEquals(None, 'abc') + self.assertEquals(None, '[1]') + self.assertEquals(None, '1.023') + + +class StringyFloatTest(unittest.TestCase): + + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyFloat().from_json(arg)) + + def test_float(self): + self.assertEquals(.23, '.23') + self.assertEquals(5, '5') + self.assertEquals(0, '0.0') + self.assertEquals(-1023.22, '-1023.22') + + def test_none(self): + self.assertEquals(None, None) + self.assertEquals(None, 'abc') + self.assertEquals(None, '[1]') + + +class StringyBooleanTest(unittest.TestCase): + + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyBoolean().from_json(arg)) + + def test_false(self): + self.assertEquals(False, "false") + self.assertEquals(False, "False") + self.assertEquals(False, "") + self.assertEquals(False, "hahahahah") + + def test_true(self): + self.assertEquals(True, "true") + self.assertEquals(True, "TruE") + + def test_pass_through(self): + self.assertEquals(123, 123) + diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 2343d24a57..fabc75037c 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -20,7 +20,8 @@ log = logging.getLogger(__name__) class VideoFields(object): data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) - display_name = String(help="Display name for this module", scope=Scope.settings) + # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) + display_name = XModule.display_name class VideoModule(VideoFields, XModule): diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index 14a6049186..6b78d18db0 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -1,33 +1,8 @@ """ Namespace that defines fields common to all blocks used in the LMS """ -from xblock.core import Namespace, Boolean, Scope, String, Float -from xmodule.fields import Date, Timedelta - - -class StringyBoolean(Boolean): - """ - Reads strings from JSON as booleans. - - 'true' (case insensitive) return True, other strings return False - Other types are returned unchanged - """ - def from_json(self, value): - if isinstance(value, basestring): - return value.lower() == 'true' - return value - - -class StringyFloat(Float): - """ - Reads values as floats. If the value parses as a float, returns - that, otherwise returns None - """ - def from_json(self, value): - try: - return float(value) - except: - return None +from xblock.core import Namespace, Boolean, Scope, String +from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean class LmsNamespace(Namespace): From 378b2ba7f7505ec4bb791003ad89cbd562d1d23f Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 14:46:35 -0400 Subject: [PATCH 211/280] Delete unused file. --- .../xblock_field_types.py | 14 -------------- common/lib/xmodule/xmodule/xml_module.py | 1 - 2 files changed, 15 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py deleted file mode 100644 index 2dcb7a4cda..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py +++ /dev/null @@ -1,14 +0,0 @@ -from xblock.core import Integer, Float - - -class StringyFloat(Float): - """ - A model type that converts from string to floats when reading from json - """ - - def from_json(self, value): - try: - return float(value) - except: - return None - diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 5846b4c18a..f9de929c05 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -155,7 +155,6 @@ class XmlDescriptor(XModuleDescriptor): Remove any attribute named in cls.metadata_attributes from the supplied xml_object """ - # TODO: change to use Fields definitions for attr in cls.metadata_attributes: if xml_object.get(attr) is not None: del xml_object.attrib[attr] From 5681f94a96346a27a87d0e3f0172c3401d0b730e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Apr 2013 14:55:04 -0400 Subject: [PATCH 212/280] studio - commented in the static HTML boilerplate for the course overview field - needs wiring to CodeMirror with a setVal() method --- cms/templates/settings.html | 55 ++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 3923c0f905..73ef1066db 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -179,7 +179,60 @@ from contentstore import utils
  • - Introductions, prerequisites, FAQs that are used on your course summary page + Introductions, prerequisites, FAQs that are used on your course summary page (formatted in HTML) + +
  • From e874de12498d7ba6a772c9a4d746f5a02513a174 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 18 Apr 2013 17:08:58 -0400 Subject: [PATCH 213/280] Some cleanup TODOs. --- common/lib/xmodule/xmodule/capa_module.py | 3 ++- common/lib/xmodule/xmodule/mako_module.py | 28 +++++++++++++++++++---- common/lib/xmodule/xmodule/xml_module.py | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9b0cc44ab4..c1606aeeb3 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -34,6 +34,7 @@ class StringyInteger(Integer): return None +# TODO: move to fields.py and remove duplicated code. class StringyFloat(Float): """ A model type that converts from string to floats when reading from json @@ -95,7 +96,7 @@ class CapaFields(object): input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) - display_name = String(help="Display name for this module", scope=Scope.settings) + display_name = XModule.display_name seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) markdown = String(help="Markdown source of this module", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py index 84db6ad779..75dd655d41 100644 --- a/common/lib/xmodule/xmodule/mako_module.py +++ b/common/lib/xmodule/xmodule/mako_module.py @@ -1,5 +1,7 @@ from .x_module import XModuleDescriptor, DescriptorSystem from .modulestore.inheritance import own_metadata +from xblock.core import Scope + class MakoDescriptorSystem(DescriptorSystem): @@ -44,10 +46,26 @@ class MakoModuleDescriptor(XModuleDescriptor): # cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata) @property def editable_metadata_fields(self): - fields = {} - for field, value in own_metadata(self).items(): - if field in self.system_metadata_fields: +# fields = {} +# for field, value in own_metadata(self).items(): +# if field in self.system_metadata_fields: +# continue +# +# fields[field] = value +# return fields + inherited_metadata = getattr(self, '_inherited_metadata', {}) + metadata = {} + for field in self.fields: + # Only save metadata that wasn't inherited + if field.scope != Scope.settings or field.name in self.system_metadata_fields: continue - fields[field] = value - return fields + if field.name in self._model_data: + metadata[field.name] = self._model_data[field.name] + if field.name in inherited_metadata and self._model_data.get(field.name) == inherited_metadata.get( + field.name): + metadata[field.name] = str(metadata[field.name]) + ' INHERITED' + else: + metadata[field.name] = str(getattr(self, field.name)) + ' DEFAULT' + + return metadata diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index f9de929c05..5846b4c18a 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -155,6 +155,7 @@ class XmlDescriptor(XModuleDescriptor): Remove any attribute named in cls.metadata_attributes from the supplied xml_object """ + # TODO: change to use Fields definitions for attr in cls.metadata_attributes: if xml_object.get(attr) is not None: del xml_object.attrib[attr] From 9e9886339a5da9990713f7857445aa0f463aeddf Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 11:04:45 -0400 Subject: [PATCH 214/280] Refer to inherited field. --- common/lib/xmodule/xmodule/capa_module.py | 1 + common/lib/xmodule/xmodule/html_module.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index c1606aeeb3..680305269d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -96,6 +96,7 @@ class CapaFields(object): input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) + # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) display_name = XModule.display_name seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index d901fc5fbe..71d8ee4765 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -19,6 +19,8 @@ log = logging.getLogger("mitx.courseware") class HtmlFields(object): data = String(help="Html contents to display for this module", scope=Scope.content) + # Used by the LMS on the sequential ribbon (displayed as a tooltip) + display_name = XModule.display_name class HtmlModule(HtmlFields, XModule): From cc2d06975e24f473304f3bda0ec18a4615c1da85 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 14:40:55 -0400 Subject: [PATCH 215/280] Remove duplicated StringyX definitions. --- cms/xmodule_namespace.py | 18 +----- common/lib/xmodule/xmodule/capa_module.py | 27 +-------- .../xmodule/combined_open_ended_module.py | 3 +- common/lib/xmodule/xmodule/fields.py | 41 ++++++++++++++ common/lib/xmodule/xmodule/mako_module.py | 28 ++-------- .../xmodule/xmodule/peer_grading_module.py | 3 +- .../lib/xmodule/xmodule/tests/test_fields.py | 56 ++++++++++++++++++- common/lib/xmodule/xmodule/video_module.py | 3 +- lms/xmodule_namespace.py | 29 +--------- 9 files changed, 110 insertions(+), 98 deletions(-) diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index c9bb8f4c6e..1b509a14f4 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -4,22 +4,8 @@ Namespace defining common fields used by Studio for all blocks import datetime -from xblock.core import Namespace, Boolean, Scope, ModelType, String - - -class StringyBoolean(Boolean): - """ - Reads strings from JSON as booleans. - - If the string is 'true' (case insensitive), then return True, - otherwise False. - - JSON values that aren't strings are returned as is - """ - def from_json(self, value): - if isinstance(value, basestring): - return value.lower() == 'true' - return value +from xblock.core import Namespace, Scope, ModelType, String +from xmodule.fields import StringyBoolean class DateTuple(ModelType): diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 680305269d..dc9be6ce5f 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -16,36 +16,13 @@ from .progress import Progress from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.exceptions import NotFoundError, ProcessingError -from xblock.core import Integer, Scope, String, Boolean, Object, Float -from .fields import Timedelta, Date +from xblock.core import Scope, String, Boolean, Object +from .fields import Timedelta, Date, StringyInteger, StringyFloat from xmodule.util.date_utils import time_to_datetime log = logging.getLogger("mitx.courseware") -class StringyInteger(Integer): - """ - A model type that converts from strings to integers when reading from json - """ - def from_json(self, value): - try: - return int(value) - except: - return None - - -# TODO: move to fields.py and remove duplicated code. -class StringyFloat(Float): - """ - A model type that converts from string to floats when reading from json - """ - def from_json(self, value): - try: - return float(value) - except: - return None - - # Generated this many different variants of problems with rerandomize=per_student NUM_RANDOMIZATION_BINS = 20 diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 120e4f743a..239adcaa41 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,8 +8,7 @@ from .x_module import XModule from xblock.core import Integer, Scope, String, Boolean, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from collections import namedtuple -from .fields import Date -from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat +from .fields import Date, StringyFloat log = logging.getLogger("mitx.courseware") diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index bb85714252..3d56b7941e 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -7,6 +7,8 @@ from xblock.core import ModelType import datetime import dateutil.parser +from xblock.core import Integer, Float, Boolean + log = logging.getLogger(__name__) @@ -81,3 +83,42 @@ class Timedelta(ModelType): if cur_value > 0: values.append("%d %s" % (cur_value, attr)) return ' '.join(values) + + +class StringyInteger(Integer): + """ + A model type that converts from strings to integers when reading from json. + If value does not parse as an int, returns None. + """ + def from_json(self, value): + try: + return int(value) + except: + return None + + +class StringyFloat(Float): + """ + A model type that converts from string to floats when reading from json. + If value does not parse as a float, returns None. + """ + def from_json(self, value): + try: + return float(value) + except: + return None + + +class StringyBoolean(Boolean): + """ + Reads strings from JSON as booleans. + + If the string is 'true' (case insensitive), then return True, + otherwise False. + + JSON values that aren't strings are returned as-is. + """ + def from_json(self, value): + if isinstance(value, basestring): + return value.lower() == 'true' + return value diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py index 75dd655d41..84db6ad779 100644 --- a/common/lib/xmodule/xmodule/mako_module.py +++ b/common/lib/xmodule/xmodule/mako_module.py @@ -1,7 +1,5 @@ from .x_module import XModuleDescriptor, DescriptorSystem from .modulestore.inheritance import own_metadata -from xblock.core import Scope - class MakoDescriptorSystem(DescriptorSystem): @@ -46,26 +44,10 @@ class MakoModuleDescriptor(XModuleDescriptor): # cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata) @property def editable_metadata_fields(self): -# fields = {} -# for field, value in own_metadata(self).items(): -# if field in self.system_metadata_fields: -# continue -# -# fields[field] = value -# return fields - inherited_metadata = getattr(self, '_inherited_metadata', {}) - metadata = {} - for field in self.fields: - # Only save metadata that wasn't inherited - if field.scope != Scope.settings or field.name in self.system_metadata_fields: + fields = {} + for field, value in own_metadata(self).items(): + if field in self.system_metadata_fields: continue - if field.name in self._model_data: - metadata[field.name] = self._model_data[field.name] - if field.name in inherited_metadata and self._model_data.get(field.name) == inherited_metadata.get( - field.name): - metadata[field.name] = str(metadata[field.name]) + ' INHERITED' - else: - metadata[field.name] = str(getattr(self, field.name)) + ' DEFAULT' - - return metadata + fields[field] = value + return fields diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index db4514d0e0..35f2fa2d76 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -11,8 +11,7 @@ from xmodule.raw_module import RawDescriptor from xmodule.modulestore.django import modulestore from .timeinfo import TimeInfo from xblock.core import Object, Integer, Boolean, String, Scope -from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat -from xmodule.fields import Date +from xmodule.fields import Date, StringyFloat from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from open_ended_grading_classes import combined_open_ended_rubric diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index 7c8872efc1..9642f7c595 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -1,8 +1,8 @@ -"""Tests for Date class defined in fields.py.""" +"""Tests for classes defined in fields.py.""" import datetime import unittest from django.utils.timezone import UTC -from xmodule.fields import Date +from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean import time class DateTest(unittest.TestCase): @@ -78,3 +78,55 @@ class DateTest(unittest.TestCase): DateTest.date.from_json("2012-12-31T23:00:01-01:00")), "2013-01-01T00:00:01Z") + +class StringyIntegerTest(unittest.TestCase): + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyInteger().from_json(arg)) + + def test_integer(self): + self.assertEquals(5, '5') + self.assertEquals(0, '0') + self.assertEquals(-1023, '-1023') + + def test_none(self): + self.assertEquals(None, None) + self.assertEquals(None, 'abc') + self.assertEquals(None, '[1]') + self.assertEquals(None, '1.023') + + +class StringyFloatTest(unittest.TestCase): + + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyFloat().from_json(arg)) + + def test_float(self): + self.assertEquals(.23, '.23') + self.assertEquals(5, '5') + self.assertEquals(0, '0.0') + self.assertEquals(-1023.22, '-1023.22') + + def test_none(self): + self.assertEquals(None, None) + self.assertEquals(None, 'abc') + self.assertEquals(None, '[1]') + + +class StringyBooleanTest(unittest.TestCase): + + def assertEquals(self, expected, arg): + self.assertEqual(expected, StringyBoolean().from_json(arg)) + + def test_false(self): + self.assertEquals(False, "false") + self.assertEquals(False, "False") + self.assertEquals(False, "") + self.assertEquals(False, "hahahahah") + + def test_true(self): + self.assertEquals(True, "true") + self.assertEquals(True, "TruE") + + def test_pass_through(self): + self.assertEquals(123, 123) + diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 2343d24a57..fabc75037c 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -20,7 +20,8 @@ log = logging.getLogger(__name__) class VideoFields(object): data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) - display_name = String(help="Display name for this module", scope=Scope.settings) + # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) + display_name = XModule.display_name class VideoModule(VideoFields, XModule): diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index 14a6049186..6b78d18db0 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -1,33 +1,8 @@ """ Namespace that defines fields common to all blocks used in the LMS """ -from xblock.core import Namespace, Boolean, Scope, String, Float -from xmodule.fields import Date, Timedelta - - -class StringyBoolean(Boolean): - """ - Reads strings from JSON as booleans. - - 'true' (case insensitive) return True, other strings return False - Other types are returned unchanged - """ - def from_json(self, value): - if isinstance(value, basestring): - return value.lower() == 'true' - return value - - -class StringyFloat(Float): - """ - Reads values as floats. If the value parses as a float, returns - that, otherwise returns None - """ - def from_json(self, value): - try: - return float(value) - except: - return None +from xblock.core import Namespace, Boolean, Scope, String +from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean class LmsNamespace(Namespace): From 45f86b662a6d2be7ba8f7a07449847eb95fd2835 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 14:46:35 -0400 Subject: [PATCH 216/280] Delete unused file. --- .../xblock_field_types.py | 14 -------------- common/lib/xmodule/xmodule/xml_module.py | 1 - 2 files changed, 15 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py deleted file mode 100644 index 2dcb7a4cda..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py +++ /dev/null @@ -1,14 +0,0 @@ -from xblock.core import Integer, Float - - -class StringyFloat(Float): - """ - A model type that converts from string to floats when reading from json - """ - - def from_json(self, value): - try: - return float(value) - except: - return None - diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 5846b4c18a..f9de929c05 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -155,7 +155,6 @@ class XmlDescriptor(XModuleDescriptor): Remove any attribute named in cls.metadata_attributes from the supplied xml_object """ - # TODO: change to use Fields definitions for attr in cls.metadata_attributes: if xml_object.get(attr) is not None: del xml_object.attrib[attr] From 1371fe030897b938e6a975753421f38cccdcc2fb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 24 Apr 2013 15:30:16 -0400 Subject: [PATCH 217/280] Make sure to run :predjango before gather_assets so that the xmodule_assets command is set up --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index 0e12f2e3db..1f60a18486 100644 --- a/rakefile +++ b/rakefile @@ -230,7 +230,7 @@ end # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" - task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:gather_assets:test", "fasttest_#{system}"] + task "test_#{system}", [:stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. From c7962cf07ba894d139d7aecaa9dc4006af117b30 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 24 Apr 2013 16:41:58 -0400 Subject: [PATCH 218/280] Don't explicitly declare display_name. --- common/lib/xmodule/xmodule/capa_module.py | 2 -- common/lib/xmodule/xmodule/html_module.py | 2 -- common/lib/xmodule/xmodule/video_module.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index dc9be6ce5f..4143345196 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -73,8 +73,6 @@ class CapaFields(object): input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) - # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) - display_name = XModule.display_name seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) markdown = String(help="Markdown source of this module", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 71d8ee4765..d901fc5fbe 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -19,8 +19,6 @@ log = logging.getLogger("mitx.courseware") class HtmlFields(object): data = String(help="Html contents to display for this module", scope=Scope.content) - # Used by the LMS on the sequential ribbon (displayed as a tooltip) - display_name = XModule.display_name class HtmlModule(HtmlFields, XModule): diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index fabc75037c..f902a9665b 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -20,8 +20,6 @@ log = logging.getLogger(__name__) class VideoFields(object): data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) - # display_name is used by the LMS on the sequential ribbon (displayed as a tooltip) - display_name = XModule.display_name class VideoModule(VideoFields, XModule): From 886e74342dbe2226227a6265daf2b384c075f75a Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 25 Apr 2013 10:39:20 -0400 Subject: [PATCH 219/280] Move the testcenter login to external_auth * hide the login behind a setting that is turned off by default * clean out some of the outdated settings and hardcoded test strings --- common/djangoapps/external_auth/views.py | 147 ++++++++++++++++++++++- common/djangoapps/student/views.py | 126 ------------------- lms/envs/dev.py | 4 +- lms/urls.py | 18 +-- 4 files changed, 151 insertions(+), 144 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index effae923b3..23b46aa803 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -12,7 +12,7 @@ from external_auth.djangostore import DjangoOpenIDStore from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login from django.contrib.auth.models import User -from student.models import UserProfile +from student.models import UserProfile, TestCenterUser, TestCenterRegistration from django.http import HttpResponse, HttpResponseRedirect from django.utils.http import urlquote @@ -34,6 +34,12 @@ from openid.server.trustroot import TrustRoot from openid.extensions import ax, sreg import student.views as student_views +# Required for Pearson +from courseware.views import get_module_for_descriptor, jump_to +from courseware.model_data import ModelDataCache +from xmodule.modulestore.django import modulestore +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore import Location log = logging.getLogger("mitx.external_auth") @@ -551,7 +557,7 @@ def provider_login(request): 'nickname': user.username, 'email': user.email, 'fullname': user.username - } + } # the request succeeded: return provider_respond(server, openid_request, response, results) @@ -606,3 +612,140 @@ def provider_xrds(request): # custom XRDS header necessary for discovery process response['X-XRDS-Location'] = get_xrds_url('xrds', request) return response + + +#------------------- +# Pearson +#------------------- +def course_from_id(course_id): + """Return the CourseDescriptor corresponding to this course_id""" + course_loc = CourseDescriptor.id_to_location(course_id) + return modulestore().get_instance(course_id, course_loc) + + +@csrf_exempt +def test_center_login(request): + ''' Log in students taking exams via Pearson + + Takes a POST request that contains the following keys: + - code - a security code provided by Pearson + - clientCandidateID + - registrationID + - exitURL - the url that we redirect to once we're done + - vueExamSeriesCode - a code that indicates the exam that we're using + ''' + # errors are returned by navigating to the error_url, adding a query parameter named "code" + # which contains the error code describing the exceptional condition. + def makeErrorURL(error_url, error_code): + log.error("generating error URL with error code {}".format(error_code)) + return "{}?code={}".format(error_url, error_code) + + # get provided error URL, which will be used as a known prefix for returning error messages to the + # Pearson shell. + error_url = request.POST.get("errorURL") + + # TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson + # with the code we calculate for the same parameters. + if 'code' not in request.POST: + return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")) + code = request.POST.get("code") + + # calculate SHA for query string + # TODO: figure out how to get the original query string, so we can hash it and compare. + + if 'clientCandidateID' not in request.POST: + return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")) + client_candidate_id = request.POST.get("clientCandidateID") + + # TODO: check remaining parameters, and maybe at least log if they're not matching + # expected values.... + # registration_id = request.POST.get("registrationID") + # exit_url = request.POST.get("exitURL") + + # find testcenter_user that matches the provided ID: + try: + testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id) + except TestCenterUser.DoesNotExist: + log.error("not able to find demographics for cand ID {}".format(client_candidate_id)) + return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")) + + # find testcenter_registration that matches the provided exam code: + # Note that we could rely in future on either the registrationId or the exam code, + # or possibly both. But for now we know what to do with an ExamSeriesCode, + # while we currently have no record of RegistrationID values at all. + if 'vueExamSeriesCode' not in request.POST: + # we are not allowed to make up a new error code, according to Pearson, + # so instead of "missingExamSeriesCode", we use a valid one that is + # inaccurate but at least distinct. (Sigh.) + log.error("missing exam series code for cand ID {}".format(client_candidate_id)) + return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")) + exam_series_code = request.POST.get('vueExamSeriesCode') + + registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code) + if not registrations: + log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id)) + return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned")) + + # TODO: figure out what to do if there are more than one registrations.... + # for now, just take the first... + registration = registrations[0] + + course_id = registration.course_id + course = course_from_id(course_id) # assume it will be found.... + if not course: + log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id)) + return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) + exam = course.get_test_center_exam(exam_series_code) + if not exam: + log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id)) + return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) + location = exam.exam_url + log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location)) + + # check if the test has already been taken + timelimit_descriptor = modulestore().get_instance(course_id, Location(location)) + if not timelimit_descriptor: + log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location)) + return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) + + timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, + timelimit_descriptor, depth=None) + timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, + timelimit_module_cache, course_id, position=None) + if not timelimit_module.category == 'timelimit': + log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location)) + return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) + + if timelimit_module and timelimit_module.has_ended: + log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at)) + return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken")) + + # check if we need to provide an accommodation: + time_accommodation_mapping = {'ET12ET': 'ADDHALFTIME', + 'ET30MN': 'ADD30MIN', + 'ETDBTM': 'ADDDOUBLE', } + + time_accommodation_code = None + for code in registration.get_accommodation_codes(): + if code in time_accommodation_mapping: + time_accommodation_code = time_accommodation_mapping[code] + + if time_accommodation_code: + timelimit_module.accommodation_code = time_accommodation_code + log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code)) + + # UGLY HACK!!! + # Login assumes that authentication has occurred, and that there is a + # backend annotation on the user object, indicating which backend + # against which the user was authenticated. We're authenticating here + # against the registration entry, and assuming that the request given + # this information is correct, we allow the user to be logged in + # without a password. This could all be formalized in a backend object + # that does the above checking. + # TODO: (brian) create a backend class to do this. + # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) + testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass") + login(request, testcenteruser.user) + + # And start the test: + return jump_to(request, course_id, location) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 98b5265d5f..abcb9d988b 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1073,132 +1073,6 @@ def accept_name_change(request): return accept_name_change_by_id(int(request.POST['id'])) -@csrf_exempt -def test_center_login(request): - # errors are returned by navigating to the error_url, adding a query parameter named "code" - # which contains the error code describing the exceptional condition. - def makeErrorURL(error_url, error_code): - log.error("generating error URL with error code {}".format(error_code)) - return "{}?code={}".format(error_url, error_code) - - # get provided error URL, which will be used as a known prefix for returning error messages to the - # Pearson shell. - error_url = request.POST.get("errorURL") - - # TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson - # with the code we calculate for the same parameters. - if 'code' not in request.POST: - return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")) - code = request.POST.get("code") - - # calculate SHA for query string - # TODO: figure out how to get the original query string, so we can hash it and compare. - - - if 'clientCandidateID' not in request.POST: - return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")) - client_candidate_id = request.POST.get("clientCandidateID") - - # TODO: check remaining parameters, and maybe at least log if they're not matching - # expected values.... - # registration_id = request.POST.get("registrationID") - # exit_url = request.POST.get("exitURL") - - # find testcenter_user that matches the provided ID: - try: - testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id) - except TestCenterUser.DoesNotExist: - log.error("not able to find demographics for cand ID {}".format(client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")) - - # find testcenter_registration that matches the provided exam code: - # Note that we could rely in future on either the registrationId or the exam code, - # or possibly both. But for now we know what to do with an ExamSeriesCode, - # while we currently have no record of RegistrationID values at all. - if 'vueExamSeriesCode' not in request.POST: - # we are not allowed to make up a new error code, according to Pearson, - # so instead of "missingExamSeriesCode", we use a valid one that is - # inaccurate but at least distinct. (Sigh.) - log.error("missing exam series code for cand ID {}".format(client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")) - exam_series_code = request.POST.get('vueExamSeriesCode') - # special case for supporting test user: - if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001': - log.warning("test user {} using unexpected exam code {}, coercing to 6002x001".format(client_candidate_id, exam_series_code)) - exam_series_code = '6002x001' - - registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code) - if not registrations: - log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned")) - - # TODO: figure out what to do if there are more than one registrations.... - # for now, just take the first... - registration = registrations[0] - - course_id = registration.course_id - course = course_from_id(course_id) # assume it will be found.... - if not course: - log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) - exam = course.get_test_center_exam(exam_series_code) - if not exam: - log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) - location = exam.exam_url - log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location)) - - # check if the test has already been taken - timelimit_descriptor = modulestore().get_instance(course_id, Location(location)) - if not timelimit_descriptor: - log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location)) - return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) - - timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, - timelimit_descriptor, depth=None) - timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, - timelimit_module_cache, course_id, position=None) - if not timelimit_module.category == 'timelimit': - log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location)) - return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) - - if timelimit_module and timelimit_module.has_ended: - log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at)) - return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken")) - - # check if we need to provide an accommodation: - time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME', - 'ET30MN' : 'ADD30MIN', - 'ETDBTM' : 'ADDDOUBLE', } - - time_accommodation_code = None - for code in registration.get_accommodation_codes(): - if code in time_accommodation_mapping: - time_accommodation_code = time_accommodation_mapping[code] - # special, hard-coded client ID used by Pearson shell for testing: - if client_candidate_id == "edX003671291147": - time_accommodation_code = 'TESTING' - - if time_accommodation_code: - timelimit_module.accommodation_code = time_accommodation_code - log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code)) - - # UGLY HACK!!! - # Login assumes that authentication has occurred, and that there is a - # backend annotation on the user object, indicating which backend - # against which the user was authenticated. We're authenticating here - # against the registration entry, and assuming that the request given - # this information is correct, we allow the user to be logged in - # without a password. This could all be formalized in a backend object - # that does the above checking. - # TODO: (brian) create a backend class to do this. - # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) - testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass") - login(request, testcenteruser.user) - - # And start the test: - return jump_to(request, course_id, location) - def _get_news(top=None): "Return the n top news items on settings.RSS_URL" diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 8363f744a0..0b03089774 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -224,9 +224,7 @@ FILE_UPLOAD_HANDLERS = ( PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) ########################## PEARSON TESTING ########################### -MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True -PEARSON_TEST_USER = "pearsontest" -PEARSON_TEST_PASSWORD = "12345" +MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False ########################## ANALYTICS TESTING ######################## diff --git a/lms/urls.py b/lms/urls.py index aac77a408a..082004c1be 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -32,12 +32,6 @@ urlpatterns = ('', url(r'^accept_name_change$', 'student.views.accept_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'), - - url(r'^testcenter/login$', 'student.views.test_center_login'), - - # url(r'^testcenter/login$', 'student.test_center_views.login'), - # url(r'^testcenter/logout$', 'student.test_center_views.logout'), - url(r'^event$', 'track.views.user_track'), url(r'^t/(?P