From fb6a07c812748916061109ac1aa4f807a1069652 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Nov 2012 18:39:54 -0500 Subject: [PATCH 001/665] 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/665] 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/665] 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/665] 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 @@ - - - - - - + + + + + + + + - + $(".gradable-status").each(function(index, ele) { + var gradeView = new CMS.Views.OverviewAssignmentGrader({ + el : ele, + graders : window.graderTypes + }); + }); +}); + + <%block name="header_extras"> - - - - - +

+ + + + + + <%block name="content"> - -
-
- + +
+
+ -
+

You are editing a draft

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - +

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+ +
+
- -
-
- + +
+
+ -
+

A Newer Version of This Exists

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
- + +
+
+ + +
+
+ + +
+

Your changes have been saved

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
- -
-
- + +
+
+ -
-

Your changes have been saved

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

-
+
+

X Has been removed

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
- -
-
- + +
+
+ -
-

X Has been removed

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
+
+

We're sorry, there was a error with Studio

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
- -
-
- + +
+
+ -
-

We're sorry, there was a error with Studio

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
+
+

There was an error in your submission

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
- -
-
- + +
+
+ -
-

There was an error in your submission

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

-
- - +
+

Your Studio account has been created, but needs to be activated

+

Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

+ +
+
- -
-
- 📢 - -
-

Studio will be unavailable this weekend

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - -
-
- - -
-
- 📢 - -
-

Studio will be unavailable this weekend

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+

Section Release Date

+
+ + +
+

On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

+ SaveCancel
+
- -
-
- +
+ +
-
-

Your Studio account has been created, but needs to be activated

-

Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

-
- - +
+
+
+ Course Content +

Course Outline

-
-
-
-

Section Release Date

-
- - -
-

On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

-
-
- SaveCancel -
-
+ +
+
-
-
-
- Course Content -

Course Outline

-
+
+
+
- -
-
-
- % for section in sections: -
-
- - -
-

- ${section.display_name} - -

- -
- -
- - -
-
-
- -
    - % for subsection in section.get_children(): - - % endfor -
+
+

+ ${section.display_name} + +

+
-
- % endfor -
+ +
+ + +
+
+
+ +
    + % for subsection in section.get_children(): + + % endfor +
+
+ + % endfor + +
+ +
+ + +
+
+ 📝 + +
+

You've Made Some Changes

+

Note: Your changes will not take effect until you save your progress.

+
+ + +
+
+ + +
+
+ + +
+

A Newer Version of This Exists

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + + + + + close notification + +
+
+ + +
+
+ + +
+

Are You Sure You Want to Edit That?

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

Saving

+

Hamster wheels are turning pretty fast right now. Hang on! Saving will be done soon.

-
+
+ + +
+
+ + +
+

Fun Fact:

+

Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade

+
+
+
\ No newline at end of file From d809df913daf3f5d83753922d8f157c94ff217d8 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 25 Feb 2013 14:45:27 -0500 Subject: [PATCH 037/665] studio - alerts: moved all states and documentation into new /alerts view --- cms/djangoapps/contentstore/views.py | 3 + cms/static/sass/_alerts.scss | 18 +- cms/static/sass/_base.scss | 27 +++ cms/templates/alerts.html | 326 +++++++++++++++++++++++++++ cms/templates/overview.html | 273 ---------------------- cms/urls.py | 1 + 6 files changed, 374 insertions(+), 274 deletions(-) create mode 100644 cms/templates/alerts.html diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6d5905afe7..2e30751d30 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -106,6 +106,9 @@ def howitworks(request): else: return render_to_response('howitworks.html', {}) +def alerts(request): + return render_to_response('alerts.html', {}) + # ==== Views for any logged-in user ================================== @login_required diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index de025e1be2..d211d0d2ed 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -292,7 +292,7 @@ @include box-sizing(border-box); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1); position: relative; - top: -($baseline*20); + top: -($baseline*1.5); z-index: 100; overflow: hidden; width: 100%; @@ -536,6 +536,22 @@ } } +// temporary +body.uxdesign.alerts { + + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + @extend .window; + width: flex-grid(12, 12); + margin-right: flex-gutter(); + padding: $baseline ($baseline*1.5); + } +} + // artifact styles // .alert { // padding: 15px 20px; diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index d2cdda443f..0d01a0b24b 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -339,11 +339,38 @@ h1 { .title-5 { } + + > section { + margin: 0 0 $baseline 0; + + header { + @include clearfix(); + + .title-2 { + width: flex-grid(5, 12); + margin: 0 flex-gutter() 0 0; + float: left; + } + + .tip { + @include font-size(13); + width: flex-grid(7, 12); + float: right; + margin-top: ($baseline/2); + text-align: right; + color: $gray-l2; + } + } + } } // layout - supplemental content .content-supplementary { + > section { + margin: 0 0 $baseline 0; + } + .bit { @include font-size(13); margin: 0 0 $baseline 0; diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html new file mode 100644 index 0000000000..efdac8e761 --- /dev/null +++ b/cms/templates/alerts.html @@ -0,0 +1,326 @@ +<%inherit file="base.html" /> +<%block name="title">Studio Alerts +<%block name="bodyclass">is-signedin course uxdesign alerts + +<%block name="content"> + +
+
+ + +
+

You are editing a draft

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

A Newer Version of This Exists

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

Your changes have been saved

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+
+
+ + +
+
+ + +
+

X Has been removed

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

We're sorry, there was a error with Studio

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

There was an error in your submission

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

Your Studio account has been created, but needs to be activated

+

Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

+
+ + +
+
+ +
+
+

Section Release Date

+
+ + +
+

On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

+
+
+ SaveCancel +
+
+ +
+ +
+ +
+
+
+ UX Design +

Alerts & Notifications

+
+
+
+ +
+
+
+
+
+

Alerts

+ persistant, static messages to the user +
+ +

In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

+
+ +
+
+

Notifications

+ contextual, feedback-based, and temporal messages to the user +
+ +

In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.

+
+
+
+
+ + +
+
+ 📝 + +
+

You've Made Some Changes

+

Note: Your changes will not take effect until you save your progress.

+
+ + +
+
+ + +
+
+ + +
+

A Newer Version of This Exists

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + + + + + close notification + +
+
+ + +
+
+ + +
+

Are You Sure You Want to Edit That?

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

Saving

+

Hamster wheels are turning pretty fast right now. Hang on! Saving will be done soon.

+
+
+
+ + +
+
+ + +
+

Fun Fact:

+

Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade

+
+
+
+ \ No newline at end of file diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 2950280702..1792cbc844 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -106,171 +106,6 @@ $(document).ready(function(){ <%block name="content"> - -
-
- - -
-

You are editing a draft

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - -
-
- - -
-
- - -
-

A Newer Version of This Exists

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - -
-
- - -
-
- - -
-

Your changes have been saved

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

-
-
-
- - -
-
- - -
-

X Has been removed

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
-
-
- - -
-
- - -
-

We're sorry, there was a error with Studio

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
-
-
- - -
-
- - -
-

There was an error in your submission

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

-
- - -
-
- - -
-
- 📢 - -
-

Studio will be unavailable this weekend

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - -
-
- - -
-
- 📢 - -
-

Studio will be unavailable this weekend

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
-
-
- - -
-
- - -
-

Your Studio account has been created, but needs to be activated

-

Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

-
- - -
-
-

Section Release Date

@@ -285,14 +120,6 @@ $(document).ready(function(){
-
- -
-
@@ -393,104 +220,4 @@ $(document).ready(function(){
- -
-
- 📝 - -
-

You've Made Some Changes

-

Note: Your changes will not take effect until you save your progress.

-
- - -
-
- - -
-
- - -
-

A Newer Version of This Exists

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - - - - - close notification - -
-
- - -
-
- - -
-

Are You Sure You Want to Edit That?

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

-
- - -
-
- - -
-
- - -
-

Saving

-

Hamster wheels are turning pretty fast right now. Hang on! Saving will be done soon.

-
-
-
- - -
-
- - -
-

Fun Fact:

-

Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade

-
-
-
\ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index 35b2707241..5c8bd42b10 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -78,6 +78,7 @@ urlpatterns = ('', # User creation and updating views urlpatterns += ( + url(r'^alerts$', 'contentstore.views.alerts', name='alerts'), url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'), url(r'^signup$', 'contentstore.views.signup', name='signup'), From 95ea01689f2a0ed955a173192191da8f8040f973 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 25 Feb 2013 16:30:36 -0500 Subject: [PATCH 038/665] studio - alerts: firmed up status-based notifications --- cms/static/sass/_alerts.scss | 62 +++++++++++++++++++++++---------- cms/static/sass/_base.scss | 40 ++++++++------------- cms/static/sass/_keyframes.scss | 8 ----- cms/templates/alerts.html | 57 ++++++++++++++++++------------ 4 files changed, 93 insertions(+), 74 deletions(-) diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index d211d0d2ed..8e5401112b 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -10,20 +10,12 @@ -webkit-transition: bottom 1.5s ease-in-out 0.25s; @include box-shadow(0 -1px 3px $shadow); position: fixed; - bottom: -1000px; z-index: 1000; width: 100%; - overflow: hidden; - opacity: 0; border-top: 4px solid $gray-l1; padding: ($baseline*0.75) ($baseline*2); background: $gray-d3; - &.is-shown { - bottom: 0; - opacity: 1.0; - } - &.wrapper-notification-warning { border-top-color: $orange; @@ -74,12 +66,11 @@ // shorter/status notifications &.wrapper-notification-status { - width: ($baseline*10); + width: ($baseline*12.5); right: ($baseline); padding: ($baseline/2) $baseline; .notification { - background: red; @include box-sizing(border-box); @include clearfix(); width: 100%; @@ -87,11 +78,19 @@ min-width: none; .icon { - width: auto; + width: $baseline; + margin-right: ($baseline*0.75); } .copy { - width: auto; + width: ($baseline*9); + + p { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + } } } } @@ -115,7 +114,7 @@ .icon { @include transition (color 0.5s ease-in-out); - @include font-size(28); + @include font-size(22); width: flex-grid(1, 12); margin-right: flex-gutter(); text-align: right; @@ -283,6 +282,16 @@ border-color: $blue-d2; } } + + &.saving { + + .icon-saving { + @include animation(rotateClockwise 3.0s forwards linear infinite); + width: 22px; + height: 25px; + line-height: 3rem !important; + } + } } // ==================== @@ -296,16 +305,11 @@ z-index: 100; overflow: hidden; width: 100%; - opacity: 0; border-bottom: 4px solid $gray-l1; border-top: 1px solid $black; padding: $baseline ($baseline*2); background: $gray-d3; - &.is-shown { - opacity: 1.0; - } - &.wrapper-alert-warning { border-bottom-color: $orange; @@ -536,6 +540,28 @@ } } +// js enabled +.js { + + .wrapper-alert { + display: none; + + &.is-shown { + display: block; + } + } + + .wrapper-notification { + bottom: -1000px; + opacity: 0; + + &.is-shown { + bottom: 0; + opacity: 1.0; + } + } +} + // temporary body.uxdesign.alerts { diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 0d01a0b24b..d5a8adc6cb 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -69,7 +69,7 @@ h1 { } .title-3 { - @include font-size(16); + @include font-size(18); margin-bottom: ($baseline/2); } @@ -93,6 +93,14 @@ h1 { font-weight: 500 } +p, ul, ol, dl { + margin-bottom: ($baseline/2); + + &:last-child { + margin-bottom: 0; + } +} + // ==================== // layout - basic page header @@ -316,32 +324,12 @@ h1 { color: $gray-d3; } - .title-1 { - - } - - .title-2 { - @include font-size(24); - margin: 0 0 ($baseline/2) 0; - font-weight: 600; - } - - .title-3 { - @include font-size(16); - margin: 0 0 ($baseline/4) 0; - font-weight: 500; - } - - .title-4 { - - } - - .title-5 { - - } - > section { - margin: 0 0 $baseline 0; + margin: 0 0 ($baseline*2) 0; + + &:last-child { + margin-bottom: 0; + } header { @include clearfix(); diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss index 394548a2e7..0ae1d78ffe 100644 --- a/cms/static/sass/_keyframes.scss +++ b/cms/static/sass/_keyframes.scss @@ -7,14 +7,6 @@ @include transform(rotate(0deg)); } - 25% { - @include transform(rotate(90deg)); - } - - 50% { - @include transform(rotate(180deg)); - } - 100% { @include transform(rotate(360deg)); } diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html index efdac8e761..869bad7260 100644 --- a/cms/templates/alerts.html +++ b/cms/templates/alerts.html @@ -4,7 +4,7 @@ <%block name="content"> -
+
@@ -28,7 +28,7 @@
-
+
@@ -52,7 +52,7 @@
-
+
@@ -64,7 +64,7 @@
-
+
@@ -76,7 +76,7 @@
-
+
@@ -88,7 +88,7 @@
-
+
@@ -109,7 +109,7 @@
-
+
📢 @@ -133,7 +133,7 @@
-
+
📢 @@ -145,7 +145,7 @@
-
+
@@ -182,14 +182,6 @@
-
- -
-
@@ -209,6 +201,17 @@

In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

+ +

Different Static Examples of Alerts

+

Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

+ +
@@ -218,13 +221,23 @@

In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.

+ +

Different Static Examples of Notifications

+ +
-
+
📝 @@ -248,7 +261,7 @@
-
+
@@ -277,7 +290,7 @@
-
+
@@ -301,7 +314,7 @@
-
+
@@ -313,7 +326,7 @@
-
+
From 0926395445cf20e15edfd4b0f80e1359994e1ac8 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Feb 2013 12:32:20 -0500 Subject: [PATCH 039/665] studio - alerts: added in close buttons and basic JS to control them/moved demo JS to alerts.html page --- cms/static/js/base.js | 35 ++++----- cms/static/sass/_alerts.scss | 137 +++++++++++++++++++++++++---------- cms/templates/alerts.html | 79 ++++++++++++++++---- 3 files changed, 179 insertions(+), 72 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index c2080c87f9..21571a8f4a 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -43,31 +43,28 @@ $(document).ready(function () { $('body').addClass('js'); - // notifications - $('.testing .test-notification').click(function(e) { - (e).preventDefault(); - manageNotification(e); - }); - - function manageNotification(e) { - var $notificationRibbon = $('.wrapper-notification'); - - // showing - $notificationRibbon.toggleClass('is-shown'); - - // controls for closing notification - $notificationRibbon.find('.action-notification-close').click(function(e) { - (e).preventDefault(); - $notificationRibbon.toggleClass('is-shown'); - }); - } - // lean/simple modal $('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' }); $('a.action-modal-close').click(function(e){ (e).preventDefault(); }); + // alert and notifications - manual close + $('.action-alert-close').click(function(e) { + (e).preventDefault(); + console.log('closing alert'); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + }); + + // alert and notifications - manual close + $('.action-notification-close').click(function(e) { + (e).preventDefault(); + $(this).closest('.wrapper-notification').removeClass('is-shown'); + }); + + + + // nav - dropdown related $body.click(function (e) { $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index 8e5401112b..5b9a08b0ea 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -5,9 +5,8 @@ .wrapper-notification { @include clearfix(); @include box-sizing(border-box); - // @include transition (bottom 1.5s ease-in-out 0.25s); - transition: bottom 1.5s ease-in-out 0.25s; - -webkit-transition: bottom 1.5s ease-in-out 0.25s; + transition: bottom 1.0s ease-in-out 0.125s; + -webkit-transition: bottom 1.0s ease-in-out 0.125s; @include box-shadow(0 -1px 3px $shadow); position: fixed; z-index: 1000; @@ -66,8 +65,9 @@ // shorter/status notifications &.wrapper-notification-status { - width: ($baseline*12.5); + width: ($baseline*6); right: ($baseline); + border-top-color: $pink; padding: ($baseline/2) $baseline; .notification { @@ -94,6 +94,30 @@ } } } + + // shorter/status notifications + &.wrapper-notification-help { + width: ($baseline*14); + right: ($baseline); + padding: ($baseline/2) $baseline; + + .notification { + @include box-sizing(border-box); + @include clearfix(); + width: 100%; + max-width: none; + min-width: none; + + .icon-help { + width: $baseline; + margin-right: ($baseline*0.75); + } + + .copy { + width: ($baseline*11); + } + } + } } .notification { @@ -135,27 +159,24 @@ // with cancel .action-notification-close { - @include transition(top .25s ease-in-out); - @include border-bottom-radius(3px); + @include border-top-radius(3px); position: absolute; - top: -($baseline/4); - left: ($baseline/2); + top: -31px; + right: $baseline; padding: ($baseline/4) ($baseline/2) 0 ($baseline/2); - background: $gray-d2; + background: $gray-l1; text-align: center; .label { @include text-sr(); } - .ss-icon { + .icon { @include font-size(14); color: $white; - } - - &:hover { - background: $blue; - top: 0; + width: auto; + margin: 0; + padding: 2px; } } @@ -185,31 +206,35 @@ &:last-child { margin-right: 0; } - - .action-primary { - @include font-size(13); - font-weight: 600; - } - - .action-secondary { - @include font-size(13); - } } } + + .action-primary { + @include blue-button(); + @include font-size(13); + border-color: $blue-d2; + font-weight: 600; + } + + .action-secondary { + @include font-size(13); + } } // notification types &.warning { .action-notification-close { + background: $orange; &:hover { - background: $orange; + background: $orange-s2; } } .action-primary { - @include orange-button; + @include orange-button(); + @include font-size(13); border-color: $orange-d2; } @@ -225,14 +250,16 @@ &.error { .action-notification-close { + background: $red-l1; &:hover { - background: $red-l1; + background: $red; } } .action-primary { - @include red-button; + @include red-button(); + @include font-size(13); border-color: $red-d2; } @@ -248,14 +275,16 @@ &.confirmation { .action-notification-close { + background: $green; &:hover { - background: $green; + background: $green-s2; } } .action-primary { - @include green-button; + @include green-button(); + @include font-size(13); border-color: $green-d2; } @@ -271,14 +300,16 @@ &.announcement { .action-notification-close { + background: $blue; &:hover { - background: $blue; + background: $blue-s1; } } .action-primary { - @include blue-button; + @include blue-button(); + @include font-size(13); border-color: $blue-d2; } } @@ -291,6 +322,10 @@ height: 25px; line-height: 3rem !important; } + + .copy p { + @include text-sr(); + } } } @@ -428,6 +463,7 @@ } } + // with actions &.has-actions { .icon { @@ -466,11 +502,38 @@ } } + // with cancel + .action-alert-close { + @include border-bottom-radius(3px); + position: absolute; + top: -($baseline/10); + right: $baseline; + padding: ($baseline/4) ($baseline/2) 0 ($baseline/2); + background: $gray-d1; + text-align: center; + + .label { + @include text-sr(); + } + + .icon { + @include font-size(14); + color: $white; + width: auto; + margin: 0; + padding: 2px; + } + + &:hover { + background: $gray-l1; + } + } + // alert types &.warning { .action-primary { - @include orange-button; + @include orange-button(); border-color: $orange-d2; } @@ -486,7 +549,7 @@ &.error { .action-primary { - @include red-button; + @include red-button(); border-color: $red-d2; } @@ -502,7 +565,7 @@ &.confirmation { .action-primary { - @include green-button; + @include green-button(); border-color: $green-d2; } @@ -518,7 +581,7 @@ &.announcement { .action-primary { - @include blue-button; + @include blue-button(); border-color: $blue-d2; } } @@ -527,7 +590,7 @@ .action-primary { border-color: $pink-d2; - @include pink-button; + @include pink-button(); } a { diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html index 869bad7260..a783a6f2fe 100644 --- a/cms/templates/alerts.html +++ b/cms/templates/alerts.html @@ -2,6 +2,26 @@ <%block name="title">Studio Alerts <%block name="bodyclass">is-signedin course uxdesign alerts +<%block name="jsextra"> + + + <%block name="content">
@@ -63,6 +83,23 @@
+ +
+
+ + +
+

Your changes have been saved

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+ + + + close alert + +
+
+
@@ -206,11 +243,16 @@

Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

@@ -225,11 +267,11 @@

Different Static Examples of Notifications

@@ -243,7 +285,7 @@

You've Made Some Changes

-

Note: Your changes will not take effect until you save your progress.

+

Your changes will not take effect until you save your progress.

-
+
@@ -325,15 +367,20 @@
- -
-
- + +
+
+

Fun Fact:

Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade

+ + + + close notification +
\ No newline at end of file From d09927e8ddf9be8640fcae31541f0e761e737db7 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Feb 2013 12:38:46 -0500 Subject: [PATCH 040/665] studio - alerts: small styling tweaks for the close button on help notifications --- cms/static/sass/_alerts.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index 5b9a08b0ea..5364e637d7 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -113,8 +113,12 @@ margin-right: ($baseline*0.75); } + .action-notification-close { + right: 0; + } + .copy { - width: ($baseline*11); + width: ($baseline*10); } } } From cb456ad22875b8eac9e9b7e552158e40e702ad97 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Feb 2013 12:45:44 -0500 Subject: [PATCH 041/665] studio - alerts: converted transition-based animation to keyframe/css animation and added in a demo for a fleeting notification --- cms/static/js/base.js | 9 ++--- cms/static/sass/_alerts.scss | 62 ++++++++++++++++++++------------- cms/static/sass/_keyframes.scss | 44 +++++++++++++++++++++++ cms/static/sass/_variables.scss | 4 +++ cms/templates/alerts.html | 21 ++++++++--- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 21571a8f4a..0bb0ae10f1 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -50,21 +50,18 @@ $(document).ready(function () { }); // alert and notifications - manual close - $('.action-alert-close').click(function(e) { + $('.action-alert-close, .alert.has-actions .nav-actions a').click(function(e) { (e).preventDefault(); console.log('closing alert'); $(this).closest('.wrapper-alert').removeClass('is-shown'); }); - // alert and notifications - manual close - $('.action-notification-close').click(function(e) { + // alert and notifications - manual & action-based close + $('.action-notification-close, .notification.has-actions .nav-actions a').click(function(e) { (e).preventDefault(); $(this).closest('.wrapper-notification').removeClass('is-shown'); }); - - - // nav - dropdown related $body.click(function (e) { $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index 5364e637d7..a582c5cfcf 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -5,10 +5,9 @@ .wrapper-notification { @include clearfix(); @include box-sizing(border-box); - transition: bottom 1.0s ease-in-out 0.125s; - -webkit-transition: bottom 1.0s ease-in-out 0.125s; @include box-shadow(0 -1px 3px $shadow); position: fixed; + bottom: 0; z-index: 1000; width: 100%; border-top: 4px solid $gray-l1; @@ -31,7 +30,7 @@ } } - &.wrapper-notification-error { + &.wrapper-cation-error { border-top-color: $red-l1; .icon-error { @@ -50,24 +49,31 @@ &.wrapper-notification-confirmation { border-top-color: $green; - .icon-error { + .icon-confirmation { color: $green; } &:hover { border-top-color: $green-s1; - .icon-error { + .icon-confirmation { color: $green-s1; } } } + &.wrapper-notification-saving { + border-top-color: $pink; + + &:hover { + border-top-color: $pink-s1; + } + } + // shorter/status notifications &.wrapper-notification-status { - width: ($baseline*6); + width: ($baseline*8); right: ($baseline); - border-top-color: $pink; padding: ($baseline/2) $baseline; .notification { @@ -77,25 +83,26 @@ max-width: none; min-width: none; + .icon, .copy { + float: none; + display: inline-block; + vertical-align: middle; + } + .icon { width: $baseline; + height: ($baseline*1.25); margin-right: ($baseline*0.75); + line-height: 3rem; } .copy { - width: ($baseline*9); - p { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - width: 100%; - } } - } + } } - // shorter/status notifications + // help notifications &.wrapper-notification-help { width: ($baseline*14); right: ($baseline); @@ -138,12 +145,15 @@ .icon, .copy { float: left; + display: inline-block; + vertical-align: middle; } .icon { @include transition (color 0.5s ease-in-out); @include font-size(22); width: flex-grid(1, 12); + height: ($baseline*1.25); margin-right: flex-gutter(); text-align: right; color: $white; @@ -165,9 +175,9 @@ .action-notification-close { @include border-top-radius(3px); position: absolute; - top: -31px; + top: -34px; right: $baseline; - padding: ($baseline/4) ($baseline/2) 0 ($baseline/2); + padding: ($baseline/5) ($baseline/2.5) 0 ($baseline/2.5); background: $gray-l1; text-align: center; @@ -278,6 +288,10 @@ &.confirmation { + .copy { + margin-top: ($baseline/5); + } + .action-notification-close { background: $green; @@ -323,8 +337,6 @@ .icon-saving { @include animation(rotateClockwise 3.0s forwards linear infinite); width: 22px; - height: 25px; - line-height: 3rem !important; } .copy p { @@ -619,12 +631,14 @@ } .wrapper-notification { - bottom: -1000px; - opacity: 0; + bottom: -($notification-height); &.is-shown { - bottom: 0; - opacity: 1.0; + @include animation(notificationsSlideUp 2s forwards ease-in-out 1); + } + + &.is-fleeting.is-shown { + @include animation(notificationsSlideUpDown 6s forwards ease-in-out 1); } } } diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss index 0ae1d78ffe..a13891160a 100644 --- a/cms/static/sass/_keyframes.scss +++ b/cms/static/sass/_keyframes.scss @@ -19,6 +19,50 @@ // ==================== +// notifications slide up +@mixin notificationsSlideUp { + 0% { + @include transform(translateY(0)); + } + + 90% { + @include transform(translateY(-($notification-height))); + } + + 100% { + @include transform(translateY(-($notification-height*0.99))); + } +} + +@-moz-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@-webkit-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@keyframes notificationsSlideUp { @include notificationsSlideUp();} + +// ==================== + +// notifications slide up +@mixin notificationsSlideUpDown { + 0%, 100% { + @include transform(translateY(0)); + } + + 15%, 85% { + @include transform(translateY(-($notification-height))); + } + + 20%, 80% { + @include transform(translateY(-($notification-height*0.99))); + } +} + +@-moz-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@-webkit-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();} + +// ==================== + // bounce in @mixin bounce-in { 0% { diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 907d5cbdb2..b89fa7cc18 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -136,6 +136,10 @@ $shadow-l1: rgba(0,0,0,0.1); $shadow-l2: rgba(0,0,0,0.05); $shadow-d1: rgba(0,0,0,0.4); + +// specific UI +$notification-height: ($baseline*10); + // colors - inherited $baseFontColor: #3c3c3c; $offBlack: #3c3c3c; diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html index a783a6f2fe..3682cb3200 100644 --- a/cms/templates/alerts.html +++ b/cms/templates/alerts.html @@ -250,8 +250,8 @@
  • Toggle Previous View/Action Removed Alert
  • Toggle System Error Alert
  • Toggle User Error Alert
  • -
  • Toggle Announcement Alert
  • -
  • Toggle Announcement with Actions Alert
  • +
  • Toggle Announcement Alert
  • +
  • Toggle Announcement with Actions Alert
  • Toggle Activiation Alert
  • @@ -270,6 +270,7 @@
  • Toggle Change Warning Notification
  • Toggle New Version Warning Notification
  • Toggle Editing Warning Notification
  • +
  • Toggle Confirmation Notification
  • Toggle Saving Notification
  • Toggle Help Notification
  • @@ -356,13 +357,23 @@
    -
    +
    -

    Saving

    -

    Hamster wheels are turning pretty fast right now. Hang on! Saving will be done soon.

    +

    Saving …

    +
    +
    +
    + + +
    +
    + + +
    +

    Your Section Has Been Created

    From ad70581b80635999ebb610c26abbb2371c738888 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 1 Mar 2013 15:43:07 -0500 Subject: [PATCH 042/665] studio - alerts: more progress including prompts --- cms/static/js/base.js | 8 +- cms/static/sass/_alerts.scss | 82 +++++++++++++++++++- cms/static/sass/_keyframes.scss | 24 +++++- cms/static/sass/_variables.scss | 6 ++ cms/templates/alerts.html | 129 ++++++++++++++++++++++++++------ 5 files changed, 222 insertions(+), 27 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 0bb0ae10f1..9af785045f 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -59,7 +59,13 @@ $(document).ready(function () { // alert and notifications - manual & action-based close $('.action-notification-close, .notification.has-actions .nav-actions a').click(function(e) { (e).preventDefault(); - $(this).closest('.wrapper-notification').removeClass('is-shown'); + $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); + }); + + // prompt close + $('.prompt .action-cancel').click(function(e) { + (e).preventDefault(); + $(this).closest('.wrapper-prompt').removeClass('is-shown').addClass('is-hiding'); }); // nav - dropdown related diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index a582c5cfcf..fc8b25ea2c 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -1,6 +1,43 @@ // studio alerts and notifications // ==================== +// prompts +.wrapper-prompt { + position: fixed; + top: 0; + background: $black-t1; + width: 100%; + height: 100%; + text-align: center; + z-index: 100; + + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -0.25em; /* Adjusts for spacing */ + } + + .prompt { + @include border-radius(3px); + @include box-sizing(border-box); + @include box-shadow(0 0 4px $shadow-d1); + display: inline-block; + vertical-align: middle; + width: $baseline*15; + padding: $baseline; + background: $white; + + .nav-actions { + + .nav-item { + + } + } + } +} + // notifications .wrapper-notification { @include clearfix(); @@ -622,6 +659,17 @@ // js enabled .js { + .wrapper-prompt { + @include transition (opacity 0.25s ease-in-out); + display: none; + opacity: 0; + + &.is-shown { + display: block; + opacity: 1.0; + } + } + .wrapper-alert { display: none; @@ -633,12 +681,17 @@ .wrapper-notification { bottom: -($notification-height); + // varying animations &.is-shown { - @include animation(notificationsSlideUp 2s forwards ease-in-out 1); + @include animation(notificationsSlideUp 1s forwards ease-in-out 1); } - &.is-fleeting.is-shown { - @include animation(notificationsSlideUpDown 6s forwards ease-in-out 1); + &.is-hiding { + @include animation(notificationsSlideDown 1s forwards ease-in-out 1); + } + + &.is-fleeting { + @include animation(notificationsSlideUpDown 2s forwards ease-in-out 1); } } } @@ -656,6 +709,29 @@ body.uxdesign.alerts { width: flex-grid(12, 12); margin-right: flex-gutter(); padding: $baseline ($baseline*1.5); + + ul { + + li { + @include clearfix(); + width: flex-grid(12, 12); + margin-bottom: ($baseline/4); + border-bottom: 1px solid $gray-l4; + padding-bottom: ($baseline/4); + + &:last-child { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; + } + + a { + float: left; + width: flex-grid(5, 12); + margin-right: flex-gutter(); + } + } + } } } diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss index a13891160a..372fb9e0ca 100644 --- a/cms/static/sass/_keyframes.scss +++ b/cms/static/sass/_keyframes.scss @@ -41,7 +41,29 @@ // ==================== -// notifications slide up +// notifications slide down +@mixin notificationsSlideDown { + 0% { + @include transform(translateY(-($notification-height*0.99))); + } + + 10% { + @include transform(translateY(-($notification-height))); + } + + 100% { + @include transform(translateY(0)); + } +} + +@-moz-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@-webkit-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@keyframes notificationsSlideDown { @include notificationsSlideDown();} + +// ==================== + +// notifications slide up then down @mixin notificationsSlideUpDown { 0%, 100% { @include transform(translateY(0)); diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index b89fa7cc18..35f39fcd3a 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -16,7 +16,13 @@ $error-red: rgb(253, 87, 87); // colors - new for re-org $black: rgb(0,0,0); +$black-t1: rgba(0,0,0,0.25); +$black-t2: rgba(0,0,0,0.50); +$black-t3: rgba(0,0,0,0.75); $white: rgb(255,255,255); +$white-t1: rgba(255,255,255,0.25); +$white-t2: rgba(255,255,255,0.50); +$white-t3: rgba(255,255,255,0.75); $gray: rgb(127,127,127); $gray-l1: tint($gray,20%); diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html index 3682cb3200..9b5ba70411 100644 --- a/cms/templates/alerts.html +++ b/cms/templates/alerts.html @@ -7,16 +7,46 @@ // notifications - demo $(document).ready(function() { - $('.test-notification').click(function(e) { + $('.hide-notification').click(function(e) { (e).preventDefault(); - $('.wrapper-notification').removeClass('is-shown'); - $(this.hash).toggleClass('is-shown'); + $('.wrapper-notification').removeClass('is-hiding is-shown'); + $(this.hash).addClass('is-hiding'); }); - $('.test-alert').click(function(e) { + $('.show-notification').click(function(e) { + (e).preventDefault(); + $('.wrapper-notification').removeClass('is-shown is-hiding'); + $(this.hash).addClass('is-shown'); + }); + + $('.show-notification-fleeting').click(function(e) { + (e).preventDefault(); + $('.wrapper-notification').removeClass('is-fleeting'); + $(this.hash).addClass('is-fleeting'); + }); + + $('.hide-alert').click(function(e) { + (e).preventDefault(); + $('.wrapper-alert').removeClass('is-hiding'); + $(this.hash).addClass('is-hiding'); + }); + + $('.show-alert').click(function(e) { (e).preventDefault(); $('.wrapper-alert').removeClass('is-shown'); - $(this.hash).toggleClass('is-shown'); + $(this.hash).addClass('is-shown'); + }); + + $('.hide-prompt').click(function(e) { + (e).preventDefault(); + $('.wrapper-prompt').removeClass('is-hiding is-shown'); + $(this.hash).addClass('is-hiding'); + }); + + $('.show-prompt').click(function(e) { + (e).preventDefault(); + $('.wrapper-prompt').removeClass('is-shown is-hiding'); + $(this.hash).addClass('is-shown'); }); }); @@ -243,16 +273,16 @@

    Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

    @@ -267,12 +297,46 @@

    Different Static Examples of Notifications

    + + +
    +
    +

    Prompts

    + presents a user with a choice, based on their previous interaction, that must be decided before they can proceed +
    + +

    In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).

    + +

    Different Static Examples of Notifications

    + +
    @@ -368,7 +432,7 @@
    -
    +
    @@ -394,4 +458,25 @@
    + + +
    +
    +
    +

    Are You Sure You Want to Do That?

    +
    + + +
    +
    \ No newline at end of file From 2ce4ecbc8481964e18d4cba475ba37b84f05a9db Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 1 Mar 2013 15:48:15 -0500 Subject: [PATCH 043/665] local merge with master - finished --- cms/templates/overview.html | 288 ++++++++++++++++-------------------- 1 file changed, 128 insertions(+), 160 deletions(-) diff --git a/cms/templates/overview.html b/cms/templates/overview.html index ec35e4864f..27fd208e0d 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -13,144 +13,145 @@ <%namespace name="units" file="widgets/units.html" /> <%block name="jsextra"> - - - - - - - - + + + + + + + + - + + <%block name="header_extras"> - + + + + - - - - +
      +
    1. + + New Unit + +
    2. +
    + + <%block name="content"> -
    -
    -

    Section Release Date

    -
    - - -
    -

    On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

    +
    +
    +

    Section Release Date

    +
    + + +
    +

    On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

    +
    + SaveCancel
    - SaveCancel
    -
    -
    -
    -
    - Course Content -

    Course Outline

    -
    +
    +
    +
    + Course Content +

    Course Outline

    +
    - -
    -
    + +
    +
    + +
    +
    +
    + % for section in sections: +
    +
    + -
    -
    -
    - % for section in sections: -
    -
    -

    ${section.display_name} @@ -211,44 +212,11 @@ $(document).ready(function(){ % endfor -

    -
    -
    - -
      - % for subsection in section.get_children(): - - % endfor -
    -
    -
    - % endfor -
    +
    + % endfor +
    +
    -
    -
    - +
    From bc0b1b09580a6b154ecc48ae1ba852cf0b41ff7e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Sat, 2 Mar 2013 22:06:55 -0500 Subject: [PATCH 044/665] studio - alerts: cleaned up animation standards and made mixins to reference as well as got prompt UI pattern behavior working --- cms/static/sass/_alerts.scss | 30 ++++++++--- cms/static/sass/_keyframes.scss | 95 ++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index 9a2ebf1854..a152db636a 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -9,7 +9,7 @@ width: 100%; height: 100%; text-align: center; - z-index: 100; + z-index: 10000; &:before { content: ''; @@ -372,7 +372,8 @@ &.saving { .icon-saving { - @include animation(rotateClockwise 3.0s forwards linear infinite); + @include anim-rotateClockwise(3s, linear, infinite); + width: 22px; } @@ -661,13 +662,26 @@ .js { .wrapper-prompt { - @include transition (opacity 0.25s ease-in-out); display: none; - opacity: 0; + + .prompt { + @include anim-bounceIn(0.5s); + opacity: 0.1; + } &.is-shown { display: block; - opacity: 1.0; + + .prompt { + opacity: 1.0; + } + } + + &.is-hiding { + + .prompt { + @include anim-bounceOut(0.5s); + } } } @@ -684,15 +698,15 @@ // varying animations &.is-shown { - @include animation(notificationsSlideUp 1s forwards ease-in-out 1); + @include anim-notificationsSlideUp(1s); } &.is-hiding { - @include animation(notificationsSlideDown 1s forwards ease-in-out 1); + @include anim-notificationsSlideDown(1s); } &.is-fleeting { - @include animation(notificationsSlideUpDown 2s forwards ease-in-out 1); + @include anim-notificationsSlideUpDown(2s); } } } diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss index 372fb9e0ca..817dc27132 100644 --- a/cms/static/sass/_keyframes.scss +++ b/cms/static/sass/_keyframes.scss @@ -17,6 +17,14 @@ @-o-keyframes rotateClockwise { @include rotateClockwise(); } @keyframes rotateClockwise { @include rotateClockwise();} +@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(rotateClockwise); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); + @include animation-iteration-count($count); +} + // ==================== // notifications slide up @@ -39,6 +47,14 @@ @-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); } @keyframes notificationsSlideUp { @include notificationsSlideUp();} +@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(notificationsSlideUp); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); + @include animation-iteration-count($count); +} + // ==================== // notifications slide down @@ -61,6 +77,14 @@ @-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); } @keyframes notificationsSlideDown { @include notificationsSlideDown();} +@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(notificationsSlideDown); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); + @include animation-iteration-count($count); +} + // ==================== // notifications slide up then down @@ -83,13 +107,21 @@ @-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } @keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();} +@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(notificationsSlideUpDown); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); + @include animation-iteration-count($count); +} + // ==================== // bounce in -@mixin bounce-in { +@mixin bounceIn { 0% { opacity: 0; - @include transform(scale(.3)); + @include transform(scale(0.3)); } 50% { @@ -102,14 +134,61 @@ } } -@-moz-keyframes bounce-in { @include bounce-in(); } -@-webkit-keyframes bounce-in { @include bounce-in(); } -@-o-keyframes bounce-in { @include bounce-in(); } -@keyframes bounce-in { @include bounce-in();} +@-moz-keyframes bounceIn { @include bounceIn(); } +@-webkit-keyframes bounceIn { @include bounceIn(); } +@-o-keyframes bounceIn { @include bounceIn(); } +@keyframes bounceIn { @include bounceIn();} -@mixin bounce-in-animation($duration, $timing: ease-in-out) { - @include animation-name(bounce-in); +@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(bounceIn); @include animation-duration($duration); @include animation-timing-function($timing); @include animation-fill-mode(both); + @include animation-iteration-count($count); +} + +// ==================== + +// bounce in +@mixin bounceOut { + 0% { + opacity: 0; + @include transform(scale(0.3)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + @include transform(scale(1)); + } + + 0% { + @include transform(scale(1)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + opacity: 0; + @include transform(scale(0.3)); + } +} + +@-moz-keyframes bounceOut { @include bounceOut(); } +@-webkit-keyframes bounceOut { @include bounceOut(); } +@-o-keyframes bounceOut { @include bounceOut(); } +@keyframes bounceOut { @include bounceOut();} + +@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1) { + @include animation-name(bounceOut); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); + @include animation-iteration-count($count); } \ No newline at end of file From 72c58cd454e65f282d0b7d93f0b454f73fdafdc8 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Sun, 3 Mar 2013 20:44:48 -0500 Subject: [PATCH 045/665] studio - alerts: refactored styles for common content types across UI, got prompts set up and styled, and refactored base template for future prompt/notification/alert use --- cms/static/js/base.js | 12 +- cms/static/sass/_alerts.scss | 296 +++++++++++++++----------------- cms/static/sass/_base.scss | 7 + cms/static/sass/_keyframes.scss | 32 ++-- cms/static/sass/_variables.scss | 2 + cms/templates/alerts.html | 295 ++++++++++++++++++------------- cms/templates/base.html | 39 +++-- 7 files changed, 375 insertions(+), 308 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 9af785045f..a6e8190e17 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -62,10 +62,16 @@ $(document).ready(function () { $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); }); - // prompt close - $('.prompt .action-cancel').click(function(e) { + // prompt pop + $('.action-prompt').click(function(e){ (e).preventDefault(); - $(this).closest('.wrapper-prompt').removeClass('is-shown').addClass('is-hiding'); + $body.toggleClass('prompt-is-shown'); + }); + + // prompt close + $('.prompt .action-cancel, .prompt .action-proceed').click(function(e) { + (e).preventDefault(); + $body.removeClass('prompt-is-shown'); }); // nav - dropdown related diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index a152db636a..c507034284 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -1,11 +1,35 @@ // studio alerts and notifications // ==================== +// shared +.wrapper-notification, .wrapper-alert, .prompt { + @include box-sizing(border-box); + background: $gray-d3; + border-top: 4px solid $gray-d4; + + .copy { + @include font-size(13); + color: $gray-l2; + + .title { + color: $white; + } + + .nav-actions { + + .action-primary { + color: $gray-d4; + } + } + } +} + // prompts .wrapper-prompt { + @include transition(all 0.05s ease-in-out); position: fixed; top: 0; - background: $black-t1; + background: $black-t0; width: 100%; height: 100%; text-align: center; @@ -20,20 +44,80 @@ } .prompt { - @include border-radius(3px); - @include box-sizing(border-box); - @include box-shadow(0 0 4px $shadow-d1); + @include border-radius(($baseline/5)); + @include box-shadow(0 0 3px $shadow-d1); display: inline-block; vertical-align: middle; - width: $baseline*15; - padding: $baseline; - background: $white; + width: $baseline*17.5; + border: 4px solid $black; + text-align: left; + + .copy { + border-top: 4px solid $blue; + padding: $baseline; + } .nav-actions { + @include box-shadow(inset 0 1px 2px $shadow-d1); + border-top: 1px solid $black-t1; + padding: ($baseline*0.75) $baseline; + background: $gray-d4; .nav-item { + display: inline-block; + margin-right: ($baseline*0.75); + &:last-child { + margin-right: 0; + } } + + .action-primary { + @include blue-button(); + @include font-size(13); + border-color: $blue-d2; + font-weight: 600; + } + + .action-secondary { + @include font-size(13); + } + } + } + + // types of prompts - error + .prompt.error { + + .icon-error { + color: $red-l1; + } + + .copy { + border-top-color: $red-l1; + } + } + + // types of prompts - confirmation + .prompt.confirmation { + + .icon-error { + color: $green; + } + + .copy { + border-top-color: $green; + } + } + + // types of prompts - error + .prompt.warning { + + .icon-warning { + color: $orange; + } + + .copy { + border-top-color: $orange; } } } @@ -41,15 +125,12 @@ // notifications .wrapper-notification { @include clearfix(); - @include box-sizing(border-box); @include box-shadow(0 -1px 3px $shadow); position: fixed; bottom: 0; z-index: 1000; width: 100%; - border-top: 4px solid $gray-l1; padding: ($baseline*0.75) ($baseline*2); - background: $gray-d3; &.wrapper-notification-warning { border-top-color: $orange; @@ -67,7 +148,7 @@ } } - &.wrapper-cation-error { + &.wrapper-notification-error { border-top-color: $red-l1; .icon-error { @@ -208,29 +289,6 @@ } } - // with cancel - .action-notification-close { - @include border-top-radius(3px); - position: absolute; - top: -34px; - right: $baseline; - padding: ($baseline/5) ($baseline/2.5) 0 ($baseline/2.5); - background: $gray-l1; - text-align: center; - - .label { - @include text-sr(); - } - - .icon { - @include font-size(14); - color: $white; - width: auto; - margin: 0; - padding: 2px; - } - } - // with actions &.has-actions { @@ -272,108 +330,17 @@ } } - // notification types - &.warning { - - .action-notification-close { - background: $orange; - - &:hover { - background: $orange-s2; - } - } - - .action-primary { - @include orange-button(); - @include font-size(13); - border-color: $orange-d2; - } - - a { - color: $orange; - - &:hover { - color: $orange-s2; - } - } - } - - &.error { - - .action-notification-close { - background: $red-l1; - - &:hover { - background: $red; - } - } - - .action-primary { - @include red-button(); - @include font-size(13); - border-color: $red-d2; - } - - a { - color: $red-l1; - - &:hover { - color: $red; - } - } - } - &.confirmation { .copy { margin-top: ($baseline/5); } - - .action-notification-close { - background: $green; - - &:hover { - background: $green-s2; - } - } - - .action-primary { - @include green-button(); - @include font-size(13); - border-color: $green-d2; - } - - a { - color: $green; - - &:hover { - color: $green-s2; - } - } - } - - &.announcement { - - .action-notification-close { - background: $blue; - - &:hover { - background: $blue-s1; - } - } - - .action-primary { - @include blue-button(); - @include font-size(13); - border-color: $blue-d2; - } } &.saving { .icon-saving { @include anim-rotateClockwise(3s, linear, infinite); - width: 22px; } @@ -559,12 +526,12 @@ // with cancel .action-alert-close { - @include border-bottom-radius(3px); + @include border-bottom-radius(($baseline/5)); position: absolute; top: -($baseline/10); right: $baseline; padding: ($baseline/4) ($baseline/2) 0 ($baseline/2); - background: $gray-d1; + background: $gray-d4; text-align: center; .label { @@ -580,16 +547,20 @@ } &:hover { - background: $gray-l1; + background: $gray-d1; } } +} - // alert types +.alert, .notification, .prompt { + + // types - warning &.warning { - - .action-primary { + + .nav-actions .action-primary { @include orange-button(); border-color: $orange-d2; + color: $gray-d4; } a { @@ -601,9 +572,10 @@ } } + // types - error &.error { - .action-primary { + .nav-actions .action-primary { @include red-button(); border-color: $red-d2; } @@ -617,9 +589,27 @@ } } + // types - announcement + &.announcement { + + .nav-actions .action-primary { + @include blue-button(); + border-color: $blue-d2; + } + + a { + color: $blue; + + &:hover { + color: $blue-s2; + } + } + } + + // types - confirmation &.confirmation { - .action-primary { + .nav-actions .action-primary { @include green-button(); border-color: $green-d2; } @@ -633,17 +623,10 @@ } } - &.announcement { - - .action-primary { - @include blue-button(); - border-color: $blue-d2; - } - } - + // types - step required &.step-required { - .action-primary { + .nav-actions .action-primary { border-color: $pink-d2; @include pink-button(); } @@ -661,28 +644,33 @@ // js enabled .js { + // prompt set-up .wrapper-prompt { - display: none; + visibility: hidden; + pointer-events: none; .prompt { - @include anim-bounceIn(0.5s); - opacity: 0.1; + opacity: 0; + } + } + + // prompt showing + &.prompt-is-shown { + + .wrapper-view { + -webkit-filter: blur(2px) grayscale(25%); + filter: blur(2px) grayscale(25%); } - &.is-shown { - display: block; + .wrapper-prompt.is-shown { + visibility: visible; + pointer-events: auto; .prompt { + @include anim-bounceIn(0.5s); opacity: 1.0; } } - - &.is-hiding { - - .prompt { - @include anim-bounceOut(0.5s); - } - } } .wrapper-alert { diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index d5a8adc6cb..5fd05d27a6 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -103,6 +103,13 @@ p, ul, ol, dl { // ==================== +// layout - basic +.wrapper-view { + +} + +// ==================== + // layout - basic page header .wrapper-mast { margin: 0; diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss index 817dc27132..a756f66b2e 100644 --- a/cms/static/sass/_keyframes.scss +++ b/cms/static/sass/_keyframes.scss @@ -17,12 +17,14 @@ @-o-keyframes rotateClockwise { @include rotateClockwise(); } @keyframes rotateClockwise { @include rotateClockwise();} -@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(rotateClockwise); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); + } // ==================== @@ -47,12 +49,14 @@ @-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); } @keyframes notificationsSlideUp { @include notificationsSlideUp();} -@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(notificationsSlideUp); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); + } // ==================== @@ -77,12 +81,13 @@ @-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); } @keyframes notificationsSlideDown { @include notificationsSlideDown();} -@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(notificationsSlideDown); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); } // ==================== @@ -107,12 +112,13 @@ @-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } @keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();} -@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(notificationsSlideUpDown); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); } // ==================== @@ -139,12 +145,13 @@ @-o-keyframes bounceIn { @include bounceIn(); } @keyframes bounceIn { @include bounceIn();} -@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(bounceIn); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); } // ==================== @@ -185,10 +192,11 @@ @-o-keyframes bounceOut { @include bounceOut(); } @keyframes bounceOut { @include bounceOut();} -@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1) { +@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1, $delay: 0) { @include animation-name(bounceOut); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); - @include animation-fill-mode(both); @include animation-iteration-count($count); + @include animation-fill-mode(both); } \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 35f39fcd3a..7998d0b199 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -16,10 +16,12 @@ $error-red: rgb(253, 87, 87); // colors - new for re-org $black: rgb(0,0,0); +$black-t0: rgba(0,0,0,0.125); $black-t1: rgba(0,0,0,0.25); $black-t2: rgba(0,0,0,0.50); $black-t3: rgba(0,0,0,0.75); $white: rgb(255,255,255); +$white-t0: rgba(255,255,255,0.125); $white-t1: rgba(255,255,255,0.25); $white-t2: rgba(255,255,255,0.50); $white-t3: rgba(255,255,255,0.75); diff --git a/cms/templates/alerts.html b/cms/templates/alerts.html index 9b5ba70411..12c605d863 100644 --- a/cms/templates/alerts.html +++ b/cms/templates/alerts.html @@ -37,15 +37,15 @@ $(this.hash).addClass('is-shown'); }); - $('.hide-prompt').click(function(e) { - (e).preventDefault(); - $('.wrapper-prompt').removeClass('is-hiding is-shown'); - $(this.hash).addClass('is-hiding'); + $('.hide-prompt').click(function(e){ + (e).preventDefault(); + $body.removeClass('prompt-is-shown'); }); $('.show-prompt').click(function(e) { - (e).preventDefault(); - $('.wrapper-prompt').removeClass('is-shown is-hiding'); + (e).preventDefault(); + $body.toggleClass('prompt-is-shown'); + $('.wrapper-prompt').removeClass('is-shown'); $(this.hash).addClass('is-shown'); }); }); @@ -53,6 +53,122 @@ <%block name="content"> +
    +
    +

    Section Release Date

    +
    + + +
    +

    On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

    +
    +
    + SaveCancel +
    +
    + +
    +
    +
    + UX Design +

    Alerts & Notifications

    +
    +
    +
    + +
    +
    +
    +
    +
    +

    Alerts

    + persistant, static messages to the user +
    + +

    In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

    + +

    Different Static Examples of Alerts

    +

    Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

    + + +
    + +
    +
    +

    Notifications

    + contextual, feedback-based, and temporal messages to the user +
    + +

    In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.

    + +

    Different Static Examples of Notifications

    + + +
    + +
    +
    +

    Prompts

    + presents a user with a choice, based on their previous interaction, that must be decided before they can proceed +
    + +

    In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).

    + +

    Different Static Examples of Prompts

    + + +
    +
    +
    +
    + + +<%block name="view_alerts">
    @@ -234,115 +350,9 @@
    + -
    -
    -

    Section Release Date

    -
    - - -
    -

    On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

    -
    -
    - SaveCancel -
    -
    - -
    -
    -
    - UX Design -

    Alerts & Notifications

    -
    -
    -
    - -
    -
    -
    -
    -
    -

    Alerts

    - persistant, static messages to the user -
    - -

    In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

    - -

    Different Static Examples of Alerts

    -

    Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

    - - -
    - -
    -
    -

    Notifications

    - contextual, feedback-based, and temporal messages to the user -
    - -

    In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.

    - -

    Different Static Examples of Notifications

    - - -
    - -
    -
    -

    Prompts

    - presents a user with a choice, based on their previous interaction, that must be decided before they can proceed -
    - -

    In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).

    - -

    Different Static Examples of Notifications

    - - -
    -
    -
    -
    - +<%block name="view_notifications">
    @@ -388,11 +398,6 @@ - - - - close notification -
    @@ -458,22 +463,66 @@
    + - +<%block name="view_prompts"> +
    -

    Are You Sure You Want to Do That?

    +

    Delete "Introduction & Overview"?

    +

    Deleting a section cannot be undone and its contents cannot be recovered.

    +
    +
    + + +
    +
    +
    +

    Use Advanced Problem Editor?

    +

    If you proceed, you cannot edit this problem using the simple problem editor.

    +
    + + +
    +
    + + +
    +
    +
    +

    There Were Errors in Your Submission

    +

    Please correct the errors noted on the page and try again.

    +
    + + diff --git a/cms/templates/base.html b/cms/templates/base.html index 498897bd11..387b45a0d1 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -6,12 +6,12 @@ - <%block name="title"></%block> | - % if context_course: - <% ctx_loc = context_course.location %> - ${context_course.display_name} | - % endif - edX Studio + <%block name="title"></%block> | + % if context_course: + <% ctx_loc = context_course.location %> + ${context_course.display_name} | + % endif + edX Studio @@ -22,14 +22,13 @@ - + <%block name="header_extras"> - <%include file="widgets/header.html" /> - <%include file="courseware_vendor_js.html"/> + <%include file="courseware_vendor_js.html"/> @@ -48,15 +47,23 @@ - <%block name="content"> - <%include file="widgets/footer.html" /> + +
    + <%include file="widgets/header.html" /> + + <%block name="view_alerts"> + + <%block name="content"> + <%include file="widgets/footer.html" /> + <%block name="view_notifications"> +
    + + <%block name="view_prompts"> <%block name="jsextra"> - - - + \ No newline at end of file From 66657db0bee2e26a3966e6ca04b2483f88918754 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 4 Mar 2013 09:49:41 -0700 Subject: [PATCH 046/665] Added support for superscripts in variables and fixed bug with normal subscripted variables raised to powers --- lms/lib/symmath/formula.py | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index c34156da52..db74d5b271 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -247,6 +247,65 @@ class formula(object): fix_hat(k) fix_hat(xml) + def flatten_pmathml(xml): + ''' + Give the text version of PMathML elements + ''' + tag = gettag(xml) + if tag == 'mn': return xml.text + elif tag == 'mi': return xml.text + # elif tag == 'msub': return '_'.join([flatten_pmathml(y) for y in xml]) + # elif tag == 'msup': return '^'.join([flatten_pmathml(y) for y in xml]) + elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml]) + raise Exception, '[flatten_pmathml] unknown tag %s' % tag + + # find "tagged" superscripts + # they have the character \u200b in the superscript + # replace them with a__b so snuggle doesn't get confused + def fix_superscripts(xml): + for k in xml: + tag = gettag(k) + + # match node to a superscript + if (tag == 'msup' and + len(k) == 2 and gettag(k[1]) == 'mrow' and + gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew + + k[1].remove(k[1][0]) + newk = etree.Element('mi') + newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1])) + xml.replace(k, newk) + + if (tag == 'msubsup' and + len(k) == 3 and gettag(k[2]) == 'mrow' and + gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew + + k[2].remove(k[2][0]) + newk = etree.Element('mi') + newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2])) + xml.replace(k, newk) + + fix_superscripts(k) + fix_superscripts(xml) + + # Snuggle returns an error when it sees an + # replace such elements with an , except the first element is of + # the form a_b. I.e. map a_b^c => (a_b)^c + def fix_msubsup(parent): + for child in parent: + # fix msubsup + if (gettag(child) == 'msubsup' and len(child) == 3): + newchild = etree.Element('msup') + newbase = etree.Element('mi') + newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1])) + newexp = child[2] + newchild.append(newbase) + newchild.append(newexp) + parent.replace(child, newchild) + + fix_msubsup(child) + fix_msubsup(xml) + self.xml = xml return self.xml @@ -257,6 +316,7 @@ class formula(object): try: xml = self.preprocess_pmathml(self.expr) except Exception, err: + # print 'Err %s while preprocessing; expr=%s' % (err, self.expr) return "Error! Cannot process pmathml" pmathml = etree.tostring(xml, pretty_print=True) self.the_pmathml = pmathml From eb1658730a6d258286e3d2134941015de91d8e2f Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 4 Mar 2013 12:30:04 -0500 Subject: [PATCH 047/665] studio - alerts: revised border styling on all elements and button/color inheritance --- cms/static/sass/_alerts.scss | 300 +++++++++++++++-------------------- 1 file changed, 126 insertions(+), 174 deletions(-) diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index c507034284..a6ce1f9830 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -24,6 +24,112 @@ } } +.alert, .notification, .prompt { + + // types - confirm + &.confirm { + + .nav-actions .action-primary { + @include blue-button(); + border-color: $blue-d2; + } + + a { + color: $blue; + + &:hover { + color: $blue-s2; + } + } + } + + // types - warning + &.warning { + + .nav-actions .action-primary { + @include orange-button(); + border-color: $orange-d2; + color: $gray-d4; + } + + a { + color: $orange; + + &:hover { + color: $orange-s2; + } + } + } + + // types - error + &.error { + + .nav-actions .action-primary { + @include red-button(); + border-color: $red-d2; + } + + a { + color: $red-l1; + + &:hover { + color: $red; + } + } + } + + // types - announcement + &.announcement { + + .nav-actions .action-primary { + @include blue-button(); + border-color: $blue-d2; + } + + a { + color: $blue; + + &:hover { + color: $blue-s2; + } + } + } + + // types - confirmation + &.confirmation { + + .nav-actions .action-primary { + @include green-button(); + border-color: $green-d2; + } + + a { + color: $green; + + &:hover { + color: $green-s2; + } + } + } + + // types - step required + &.step-required { + + .nav-actions .action-primary { + border-color: $pink-d2; + @include pink-button(); + } + + a { + color: $pink; + + &:hover { + color: $pink-s1; + } + } + } +} + // prompts .wrapper-prompt { @include transition(all 0.05s ease-in-out); @@ -73,9 +179,7 @@ } .action-primary { - @include blue-button(); @include font-size(13); - border-color: $blue-d2; font-weight: 600; } @@ -125,73 +229,48 @@ // notifications .wrapper-notification { @include clearfix(); - @include box-shadow(0 -1px 3px $shadow); + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue); position: fixed; bottom: 0; z-index: 1000; width: 100%; - padding: ($baseline*0.75) ($baseline*2); + border-top: 4px solid $black; + padding: $baseline ($baseline*2); &.wrapper-notification-warning { - border-top-color: $orange; + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $orange); .icon-warning { color: $orange; } - - &:hover { - border-top-color: $orange-s2; - - .icon-warning { - color: $orange-s2; - } - } } &.wrapper-notification-error { - border-top-color: $red-l1; + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $red-l1); .icon-error { color: $red-l1; } - - &:hover { - border-top-color: $red; - - .icon-error { - color: $red; - } - } } &.wrapper-notification-confirmation { - border-top-color: $green; + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $green); .icon-confirmation { color: $green; } - - &:hover { - border-top-color: $green-s1; - - .icon-confirmation { - color: $green-s1; - } - } } &.wrapper-notification-saving { - border-top-color: $pink; - - &:hover { - border-top-color: $pink-s1; - } + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $pink); } // shorter/status notifications &.wrapper-notification-status { + @include border-top-radius(3px); width: ($baseline*8); right: ($baseline); + border: 4px solid $black; padding: ($baseline/2) $baseline; .notification { @@ -222,8 +301,10 @@ // help notifications &.wrapper-notification-help { + @include border-top-radius(3px); width: ($baseline*14); right: ($baseline); + border: 4px solid $black; padding: ($baseline/2) $baseline; .notification { @@ -304,7 +385,7 @@ .nav-actions { width: flex-grid(4, 12); float: right; - margin-top: ($baseline/2); + margin-top: ($baseline/4); text-align: right; .nav-item { @@ -355,95 +436,55 @@ // alerts .wrapper-alert { @include box-sizing(border-box); - @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1); + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue); position: relative; top: -($baseline*1.5); z-index: 100; overflow: hidden; width: 100%; - border-bottom: 4px solid $gray-l1; + border-bottom: 2px solid $black; border-top: 1px solid $black; - padding: $baseline ($baseline*2); + padding: $baseline ($baseline*2) ($baseline*1.5) ($baseline*2); background: $gray-d3; &.wrapper-alert-warning { - border-bottom-color: $orange; + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange); .icon-warning { color: $orange; } - - &:hover { - border-bottom-color: $orange-s2; - - .icon-warning { - color: $orange-s2; - } - } } &.wrapper-alert-error { - border-bottom-color: $red-l1; + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1); .icon-error { color: $red-l1; } - - &:hover { - border-bottom-color: $red; - - .icon-error { - color: $red; - } - } } &.wrapper-alert-confirmation { - border-bottom-color: $green; + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green); .icon-confirmation { color: $green; } - - &:hover { - border-bottom-color: $green-s2; - - .icon-confirmation { - color: $green-s2; - } - } } &.wrapper-alert-announcement { - border-bottom-color: $blue; + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue); .icon-announcement { color: $blue; } - - &:hover { - border-bottom-color: $blue-s2; - - .icon-announcement { - color: $blue-s2; - } - } } &.wrapper-alert-step-required { - border-bottom-color: $pink; + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink); .icon-step-required { color: $pink; } - - &:hover { - border-bottom-color: $pink-s2; - - .icon-announcement { - color: $pink-s2; - } - } } } @@ -552,95 +593,6 @@ } } -.alert, .notification, .prompt { - - // types - warning - &.warning { - - .nav-actions .action-primary { - @include orange-button(); - border-color: $orange-d2; - color: $gray-d4; - } - - a { - color: $orange; - - &:hover { - color: $orange-s2; - } - } - } - - // types - error - &.error { - - .nav-actions .action-primary { - @include red-button(); - border-color: $red-d2; - } - - a { - color: $red-l1; - - &:hover { - color: $red; - } - } - } - - // types - announcement - &.announcement { - - .nav-actions .action-primary { - @include blue-button(); - border-color: $blue-d2; - } - - a { - color: $blue; - - &:hover { - color: $blue-s2; - } - } - } - - // types - confirmation - &.confirmation { - - .nav-actions .action-primary { - @include green-button(); - border-color: $green-d2; - } - - a { - color: $green; - - &:hover { - color: $green-s2; - } - } - } - - // types - step required - &.step-required { - - .nav-actions .action-primary { - border-color: $pink-d2; - @include pink-button(); - } - - a { - color: $pink; - - &:hover { - color: $pink-s1; - } - } - } -} - // js enabled .js { From ec90d349e20ed76b2b53e4281aeb5e7ad70727fc Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 4 Mar 2013 12:47:07 -0500 Subject: [PATCH 048/665] studio - alerts: revisited advanced editor notification UI to marry new styles/behavior - WIP --- cms/static/js/base.js | 2 +- cms/static/js/views/settings/advanced_view.js | 4 ++-- cms/templates/settings_advanced.html | 23 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index a6e8190e17..3cb1829ffb 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -57,7 +57,7 @@ $(document).ready(function () { }); // alert and notifications - manual & action-based close - $('.action-notification-close, .notification.has-actions .nav-actions a').click(function(e) { + $('.action-notification-close').click(function(e) { (e).preventDefault(); $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); }); diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index d20a21f7e7..dc9adb30ed 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -141,13 +141,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ event.keyCode === 8 || event.keyCode === 46)) return; } this.$el.find(".message-status").removeClass("is-shown"); - $('.wrapper-notification').addClass('is-shown'); + $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown'); this.buttonsVisible = true; } }, hideSaveCancelButtons: function() { - $('.wrapper-notification').removeClass('is-shown'); + $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); this.buttonsVisible = false; }, diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index ceee406398..f04015a4a9 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -105,20 +105,25 @@ editor.render();
    -
    +
    + +
    - - -

    Note: Your changes will not take effect until you save your - progress. Take care with key and value formatting, as validation is not implemented.

    +

    You've Made Some Changes

    +

    Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.

    -
    +
    +
    \ No newline at end of file From cbdf9ea25bc9ffd43ffd6213a0e20cdfb6670364 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 5 Mar 2013 09:57:14 -0500 Subject: [PATCH 049/665] studio - alerts: proofing older, but still needed in page alerts (unit drafts) styling --- cms/static/sass/_alerts.scss | 117 +++++++++++++++++++---------------- cms/templates/unit.html | 4 +- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index a6ce1f9830..395519f867 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -271,6 +271,7 @@ width: ($baseline*8); right: ($baseline); border: 4px solid $black; + border-bottom: none; padding: ($baseline/2) $baseline; .notification { @@ -305,6 +306,7 @@ width: ($baseline*14); right: ($baseline); border: 4px solid $black; + border-bottom: none; padding: ($baseline/2) $baseline; .notification { @@ -690,69 +692,74 @@ body.uxdesign.alerts { } } +// ==================== + // artifact styles -// .alert { -// padding: 15px 20px; -// margin-bottom: 30px; -// border-radius: 3px; -// border: 1px solid #edbd3c; -// border-radius: 3px; -// background: #fbf6e1; -// // background: #edbd3c; -// font-size: 14px; -// @include clearfix; +.main-wrapper { + .alert { + padding: 15px 20px; + margin-bottom: 30px; + border-radius: 3px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + // background: #edbd3c; + font-size: 14px; + @include clearfix; -// .alert-message { -// float: left; -// margin-top: 4px; -// } + .alert-message { + float: left; + margin: 4px 0 0 0; + color: $gray-d3; + } -// strong { -// font-weight: 700; -// } + strong { + font-weight: 700; + } -// .alert-action { -// float: right; + .alert-action { + float: right; -// &.secondary { -// @include orange-button; -// } -// } -// } + &.secondary { + @include orange-button; + } + } + } +} -// body.error { -// background: $darkGrey; -// color: #3c3c3c; +body.error { + background: $gray-d4; + color: $gray-d3; -// .primary-header { -// display: none; -// } + .primary-header { + display: none; + } -// .error-prompt { -// width: 700px; -// margin: 150px auto; -// padding: 60px 50px 90px; -// border-radius: 3px; -// background: #fff; -// text-align: center; -// } + .error-prompt { + width: 700px; + margin: 150px auto; + padding: 60px 50px 90px; + border-radius: 3px; + background: $white; + text-align: center; + } -// h1 { -// float: none; -// margin: 0; -// font-size: 60px; -// font-weight: 300; -// color: #3c3c3c; -// } + h1 { + float: none; + margin: 0; + font-size: 60px; + font-weight: 300; + color: $gray-d3; + } -// .description { -// margin-bottom: 50px; -// font-size: 21px; -// } + .description { + margin-bottom: 50px; + font-size: 21px; + } -// .back-button { -// @include blue-button; -// padding: 14px 40px 18px; -// font-size: 18px; -// } -// } + .back-button { + @include blue-button; + padding: 14px 40px 18px; + font-size: 18px; + } +} diff --git a/cms/templates/unit.html b/cms/templates/unit.html index fa4b5dc20b..63d04a837f 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -33,7 +33,7 @@ <%block name="content">
    -
    +

    You are editing a draft. % if published_date: This unit was originally published on ${published_date}. @@ -143,7 +143,7 @@

    -
    +

    This unit has been published. To make changes, you must edit a draft.

    This is a draft of the published unit. To update the live version, you must replace it with this draft.

    From c6545eb092d7bcbe2d934ac2753d6fb8113f0468 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 6 Mar 2013 06:21:08 -0700 Subject: [PATCH 050/665] Begin to document symmath as we go --- .../js/capa/symbolic_mathjax_preprocessor.js | 22 +++++++ .../course_data_formats/symbolic_response.rst | 26 ++++++++ lms/lib/symmath/formula.py | 59 +++++++++++++++++-- 3 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 common/static/js/capa/symbolic_mathjax_preprocessor.js create mode 100644 doc/public/course_data_formats/symbolic_response.rst diff --git a/common/static/js/capa/symbolic_mathjax_preprocessor.js b/common/static/js/capa/symbolic_mathjax_preprocessor.js new file mode 100644 index 0000000000..19104553dc --- /dev/null +++ b/common/static/js/capa/symbolic_mathjax_preprocessor.js @@ -0,0 +1,22 @@ +window.SymbolicMathjaxPreprocessor = function () { + this.fn = function (eqn) { + // flags and config + var superscriptsOn = true; + + if (superscriptsOn) { + // find instances of "__" and make them superscripts ("^") and tag them + // as such. Specifcally replace instances of "__X" or "__{XYZ}" with + // "^{CHAR$1}", marking superscripts as different from powers + + // a zero width space--this is an invisible character that no one would + // use, that gets passed through MathJax and to the server + var c = "\u200b"; + eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); + + // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath + // input, which is too bad. This would be preferable to the char tag + } + + return eqn; + }; +}; diff --git a/doc/public/course_data_formats/symbolic_response.rst b/doc/public/course_data_formats/symbolic_response.rst new file mode 100644 index 0000000000..773821766e --- /dev/null +++ b/doc/public/course_data_formats/symbolic_response.rst @@ -0,0 +1,26 @@ +################# +Symbolic Response +################# + +This document plans to document features that the current symbolic response +supports. In general it allows the input and validation of math expressions, +up to commutativity and some identities. + + +******** +Features +******** + +This is a partial list of features, to be revised as we go along: + * sub and superscripts: an expression following the ``^`` character + indicates exponentiation. To use superscripts in variables, the syntax + is ``b_x__d`` for the variable ``b`` with subscript ``x`` and super + ``d``. + + An example of a problem:: + + + + + + It's a bit of a pain to enter that. diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index 7c4ea084d6..914a65d1b0 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -248,14 +248,21 @@ class formula(object): fix_hat(xml) def flatten_pmathml(xml): - ''' - Give the text version of PMathML elements + ''' Give the text version of certain PMathML elements + + Sometimes MathML will be given with each letter separated (it + doesn't know if its implicit multiplication or what). From an xml + node, find the (text only) variable name it represents. So it takes + + m + a + x + + and returns 'max', for easier use later on. ''' tag = gettag(xml) if tag == 'mn': return xml.text elif tag == 'mi': return xml.text - # elif tag == 'msub': return '_'.join([flatten_pmathml(y) for y in xml]) - # elif tag == 'msup': return '^'.join([flatten_pmathml(y) for y in xml]) elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml]) raise Exception, '[flatten_pmathml] unknown tag %s' % tag @@ -263,23 +270,63 @@ class formula(object): # they have the character \u200b in the superscript # replace them with a__b so snuggle doesn't get confused def fix_superscripts(xml): + ''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z' + + In the javascript, variables with '__X' in them had an invisible + character inserted into the sup (to distinguish from powers) + E.g. normal: + + a + b + c + + to be interpreted '(a_b)^c' (nothing done by this method) + + And modified: + + b + x + + + d + + + to be interpreted 'a_b__c' + + also: + + x + + + B + + + to be 'x__B' + ''' for k in xml: tag = gettag(k) - # match node to a superscript + # match things like the last example-- + # the second item in msub is an mrow with the first + # character equal to \u200b if (tag == 'msup' and len(k) == 2 and gettag(k[1]) == 'mrow' and gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew + # replace the msup with 'X__Y' k[1].remove(k[1][0]) newk = etree.Element('mi') newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1])) xml.replace(k, newk) + # match things like the middle example- + # the third item in msubsup is an mrow with the first + # character equal to \u200b if (tag == 'msubsup' and len(k) == 3 and gettag(k[2]) == 'mrow' and gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew + # replace the msubsup with 'X_Y__Z' k[2].remove(k[2][0]) newk = etree.Element('mi') newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2])) @@ -316,7 +363,7 @@ class formula(object): try: xml = self.preprocess_pmathml(self.expr) except Exception, err: - # print 'Err %s while preprocessing; expr=%s' % (err, self.expr) + log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr)) return "Error! Cannot process pmathml" pmathml = etree.tostring(xml, pretty_print=True) self.the_pmathml = pmathml From 62514d85a37fdfa5e60fa676902554e9c8eb1b19 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 6 Mar 2013 15:36:38 -0500 Subject: [PATCH 051/665] studio - alerts: loosely wired advanced settings with new notification and alert messages --- cms/static/js/views/settings/advanced_view.js | 6 ++-- cms/static/sass/_alerts.scss | 2 +- cms/templates/settings_advanced.html | 33 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index dc9adb30ed..1aaa7d0c1e 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -114,13 +114,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ }, showMessage: function (type) { - this.$el.find(".message-status").removeClass("is-shown"); + $(".wrapper-alert").removeClass("is-shown"); if (type) { if (type === this.error_saving) { - this.$el.find(".message-status.error").addClass("is-shown"); + $(".wrapper-alert-error").addClass("is-shown"); } else if (type === this.successful_changes) { - this.$el.find(".message-status.confirm").addClass("is-shown"); + $(".wrapper-alert-confirmation").addClass("is-shown"); this.hideSaveCancelButtons(); } } diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss index 395519f867..07ac1e7913 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/_alerts.scss @@ -234,7 +234,7 @@ bottom: 0; z-index: 1000; width: 100%; - border-top: 4px solid $black; + border-width: 2px; padding: $baseline ($baseline*2); &.wrapper-notification-warning { diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index f04015a4a9..00325f01b4 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -102,7 +102,9 @@ editor.render();
    + +<%block name="view_notifications">
    @@ -126,4 +128,35 @@ editor.render();
    + + +<%block name="view_alerts"> + +
    +
    + + +
    +

    Your policy changes have been saved.

    +

    Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

    +
    + + + + close alert + +
    +
    + + +
    +
    + + +
    +

    There was an error saving your information

    +

    Please see the error below and correct it to ensure there are no problems in rendering your course.

    +
    +
    +
    \ No newline at end of file From 559a311acbc093ded0a7da9d0d57e90a2b0687d3 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 7 Mar 2013 11:37:15 -0500 Subject: [PATCH 052/665] studio - alerts: refactored static alerts demo view to have a different template/view/url name to help with user facing views vs. documentation views --- cms/djangoapps/contentstore/views.py | 4 ++-- cms/templates/{alerts.html => ux-alerts.html} | 0 cms/urls.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename cms/templates/{alerts.html => ux-alerts.html} (100%) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index df40a1972d..464342f3e1 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -110,8 +110,8 @@ def howitworks(request): else: return render_to_response('howitworks.html', {}) -def alerts(request): - return render_to_response('alerts.html', {}) +def ux_alerts(request): + return render_to_response('ux-alerts.html', {}) # ==== Views for any logged-in user ================================== diff --git a/cms/templates/alerts.html b/cms/templates/ux-alerts.html similarity index 100% rename from cms/templates/alerts.html rename to cms/templates/ux-alerts.html diff --git a/cms/urls.py b/cms/urls.py index b2c5670913..7a384b3f20 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -82,7 +82,7 @@ urlpatterns = ('', # User creation and updating views urlpatterns += ( - url(r'^alerts$', 'contentstore.views.alerts', name='alerts'), + url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts'), url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'), url(r'^signup$', 'contentstore.views.signup', name='signup'), From 4d136f8d3bc1d247941b68f0af8b2caac4e947b0 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 11:51:45 -0500 Subject: [PATCH 053/665] fixed annotation tooltip styling issue in studio --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index 308b379ec1..c462d4806e 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -127,6 +127,7 @@ $body-font-size: em(14); font-weight: 400; padding: 0 10px 10px 10px; background-color: transparent; + border-color: transparent; } p { color: inherit; From f5c3775b5dcbb8b16e6a0fcd27fd8b835516a56e Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 12:21:47 -0500 Subject: [PATCH 054/665] fixed the annotation tooltip line height so it is the same in studio and the lms. --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index c462d4806e..b5739b28fc 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -144,6 +144,7 @@ $body-font-size: em(14); margin: 0px 0px 10px 0; max-height: 225px; overflow: auto; + line-height: normal; } .annotatable-reply { display: block; From 60b060263c15bb90fc658349224c50158609e31d Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 16:02:22 -0500 Subject: [PATCH 055/665] refactor highlight css to prevent issues with cascade --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index b5739b28fc..f8ae779b8c 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -55,6 +55,7 @@ $body-font-size: em(14); display: inline; cursor: pointer; + $highlight_index: 0; @each $highlight in ( (yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)), (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)), @@ -62,12 +63,13 @@ $body-font-size: em(14); (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)), (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)), (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) { - + + $highlight_index: $highlight_index + 1; $marker: nth($highlight,1); $color: nth($highlight,2); $selected_color: nth($highlight,3); - @if $marker == yellow { + @if $highlight_index == 1 { &.highlight { background-color: $color; &.selected { background-color: $selected_color; } @@ -167,5 +169,3 @@ $body-font-size: em(14); border-top-color: rgba(0, 0, 0, .85); } } - - From 49f85211fa5c5550897d25aceb786ac82d1259ee Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Fri, 8 Mar 2013 03:39:34 -0700 Subject: [PATCH 056/665] More documentation for the javascript --- .../js/capa/symbolic_mathjax_preprocessor.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/common/static/js/capa/symbolic_mathjax_preprocessor.js b/common/static/js/capa/symbolic_mathjax_preprocessor.js index 19104553dc..766e5efc03 100644 --- a/common/static/js/capa/symbolic_mathjax_preprocessor.js +++ b/common/static/js/capa/symbolic_mathjax_preprocessor.js @@ -1,22 +1,35 @@ +/* This file defines a processor in between the student's math input + (AsciiMath) and what is read by MathJax. It allows for our own + customizations, such as use of the syntax "a_b__x" in superscripts, or + possibly coloring certain variables, etc&. + + It is used in the definition like the following: + + + + +*/ window.SymbolicMathjaxPreprocessor = function () { - this.fn = function (eqn) { - // flags and config - var superscriptsOn = true; + this.fn = function (eqn) { + // flags and config + var superscriptsOn = true; - if (superscriptsOn) { - // find instances of "__" and make them superscripts ("^") and tag them - // as such. Specifcally replace instances of "__X" or "__{XYZ}" with - // "^{CHAR$1}", marking superscripts as different from powers + if (superscriptsOn) { + // find instances of "__" and make them superscripts ("^") and tag them + // as such. Specifcally replace instances of "__X" or "__{XYZ}" with + // "^{CHAR$1}", marking superscripts as different from powers - // a zero width space--this is an invisible character that no one would - // use, that gets passed through MathJax and to the server - var c = "\u200b"; - eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); + // a zero width space--this is an invisible character that no one would + // use, that gets passed through MathJax and to the server + var c = "\u200b"; + eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); - // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath - // input, which is too bad. This would be preferable to the char tag - } + // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath + // input, which is too bad. This would be preferable to this char tag + } - return eqn; - }; + return eqn; + }; }; From 094458dd6f0e4437a71dcbcd990d31286725dc16 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Mon, 11 Mar 2013 16:19:36 -0400 Subject: [PATCH 057/665] Modified tooltip positioning on non-overlapping annotation spans. --- .../xmodule/js/src/annotatable/display.coffee | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 2ad49ae6d7..523b0e99cf 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -75,6 +75,7 @@ class @Annotatable classes: 'ui-tooltip-annotatable' events: show: @onShowTip + move: @onMoveTip onClickToggleAnnotations: (e) => @toggleAnnotations() @@ -87,6 +88,40 @@ class @Annotatable onShowTip: (event, api) => event.preventDefault() if @annotationsHidden + onMoveTip: (event, api, position) => + ### + This method handles an edge case in which a tooltip is displayed above + a non-overlapping span like this: + + (( TOOLTIP )) + \/ + text text text ... text text text ...... + + + The problem is that the tooltip looks disconnected from both spans, so + we should re-position the tooltip to appear above the span. + ### + + tip = api.elements.tooltip + adjust_y = api.options.position?.adjust?.y || 0 + target = api.elements.target + rects = $(target).get(0).getClientRects() + is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right) + + if is_non_overlapping + focus_rect = rects[0] + rect_center = focus_rect.left + (focus_rect.width / 2) + rect_top = focus_rect.top + tip_width = $(tip).width() + tip_height = $(tip).height() + tip_left = rect_center - (tip_width / 2) + tip_top = window.pageYOffset + rect_top - tip_height + adjust_y + win_width = $(window).width() + if tip_left + tip_width > win_width + tip_left = win_width - tip_width + position.left = tip_left + position.top = tip_top + getSpanForProblemReturn: (el) -> problem_id = $(@problemReturnSelector).index(el) @$(@spanSelector).filter("[data-problem-id='#{problem_id}']") From fcf82ba2bc44cb701c020d0494e2537139635f27 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Mon, 11 Mar 2013 18:02:22 -0400 Subject: [PATCH 058/665] fixed pep8 violations for annotation module --- common/lib/xmodule/xmodule/annotatable_module.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py index f093b76f52..1385296ddf 100644 --- a/common/lib/xmodule/xmodule/annotatable_module.py +++ b/common/lib/xmodule/xmodule/annotatable_module.py @@ -11,13 +11,13 @@ from xmodule.contentstore.content import StaticContent log = logging.getLogger(__name__) + class AnnotatableModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/annotatable/display.coffee')], - 'js': [] - } + 'js': []} js_module_name = "Annotatable" css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} icon_class = 'annotatable' @@ -34,11 +34,11 @@ class AnnotatableModule(XModule): if color is not None: if color in self.highlight_colors: - cls.append('highlight-'+color) + cls.append('highlight-' + color) attr['_delete'] = highlight_key attr['value'] = ' '.join(cls) - return { 'class' : attr } + return {'class': attr} def _get_annotation_data_attr(self, index, el): """ Returns a dict in which the keys are the HTML data attributes @@ -58,7 +58,7 @@ class AnnotatableModule(XModule): if xml_key in el.attrib: value = el.get(xml_key, '') html_key = attrs_map[xml_key] - data_attrs[html_key] = { 'value': value, '_delete': xml_key } + data_attrs[html_key] = {'value': value, '_delete': xml_key} return data_attrs @@ -76,7 +76,6 @@ class AnnotatableModule(XModule): delete_key = attr[key]['_delete'] del el.attrib[delete_key] - def _render_content(self): """ Renders annotatable content with annotation spans and returns HTML. """ xmltree = etree.fromstring(self.content) @@ -123,9 +122,9 @@ class AnnotatableModule(XModule): self.element_id = self.location.html_id() self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + class AnnotatableDescriptor(RawDescriptor): module_class = AnnotatableModule stores_state = True template_dir_name = "annotatable" mako_template = "widgets/raw-edit.html" - From d860b167d6838443d05b5b67805047a7e032f6a3 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Tue, 12 Mar 2013 14:09:56 -0400 Subject: [PATCH 059/665] fixed tooltip positioning for non-overlapping spans in studio --- .../xmodule/css/annotatable/display.scss | 4 +++ .../xmodule/js/src/annotatable/display.coffee | 34 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index f8ae779b8c..6e1a38ee31 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -1,6 +1,10 @@ $border-color: #C8C8C8; $body-font-size: em(14); +.annotatable-wrapper { + position: relative; +} + .annotatable-header { margin-bottom: .5em; .annotatable-title { diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 523b0e99cf..e38e48eeda 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -1,7 +1,8 @@ class @Annotatable _debug: false - # selectors for the annotatable xmodule + # selectors for the annotatable xmodule + wrapperSelector: '.annotatable-wrapper' toggleAnnotationsSelector: '.annotatable-toggle-annotations' toggleInstructionsSelector: '.annotatable-toggle-instructions' instructionsSelector: '.annotatable-instructions' @@ -61,7 +62,7 @@ class @Annotatable my: 'bottom center' # of tooltip at: 'top center' # of target target: $(el) # where the tooltip was triggered (i.e. the annotation span) - container: @$el + container: @$(@wrapperSelector) adjust: y: -5 show: @@ -104,23 +105,38 @@ class @Annotatable tip = api.elements.tooltip adjust_y = api.options.position?.adjust?.y || 0 + container = api.options.position?.container || $('body') target = api.elements.target + rects = $(target).get(0).getClientRects() is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right) if is_non_overlapping - focus_rect = rects[0] + # we want to choose the largest of the two non-overlapping spans and display + # the tooltip above the center of it (see api.options.position settings) + focus_rect = (if rects[0].width > rects[1].width then rects[0] else rects[1]) rect_center = focus_rect.left + (focus_rect.width / 2) rect_top = focus_rect.top tip_width = $(tip).width() tip_height = $(tip).height() - tip_left = rect_center - (tip_width / 2) - tip_top = window.pageYOffset + rect_top - tip_height + adjust_y + + # tooltip is positioned relative to its container, so we need to factor in offsets + container_offset = $(container).offset() + offset_left = -container_offset.left + offset_top = $('body').scrollTop() - container_offset.top + + tip_left = offset_left + rect_center - (tip_width / 2) + tip_top = offset_top + rect_top - tip_height + adjust_y + + # make sure the new tip position doesn't clip the edges of the screen win_width = $(window).width() - if tip_left + tip_width > win_width - tip_left = win_width - tip_width - position.left = tip_left - position.top = tip_top + if tip_left < offset_left + tip_left = offset_left + else if tip_left + tip_width > win_width + offset_left + tip_left = win_width + offset_left - tip_width + + # final step: update the position object (used by qtip2 to show the tip after the move event) + $.extend position, 'left': tip_left, 'top': tip_top getSpanForProblemReturn: (el) -> problem_id = $(@problemReturnSelector).index(el) From bf6ca1b0e759252795ca89ad905828d30ceada28 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Tue, 12 Mar 2013 17:32:00 -0400 Subject: [PATCH 060/665] use document to get scrollTop for firefox --- common/lib/xmodule/xmodule/js/src/annotatable/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index e38e48eeda..8a32c8f51e 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -123,7 +123,7 @@ class @Annotatable # tooltip is positioned relative to its container, so we need to factor in offsets container_offset = $(container).offset() offset_left = -container_offset.left - offset_top = $('body').scrollTop() - container_offset.top + offset_top = $(document).scrollTop() - container_offset.top tip_left = offset_left + rect_center - (tip_width / 2) tip_top = offset_top + rect_top - tip_height + adjust_y From 75b561267c0f8162f9e69b8c085da0544c30bc6b Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 14 Mar 2013 05:09:15 -0600 Subject: [PATCH 061/665] Script feature fix --- .../course_data_formats/symbolic_response.rst | 20 ++- lms/lib/symmath/formula.py | 25 ++++ lms/lib/symmath/test_formula.py | 115 ++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 lms/lib/symmath/test_formula.py diff --git a/doc/public/course_data_formats/symbolic_response.rst b/doc/public/course_data_formats/symbolic_response.rst index 773821766e..8463faab3c 100644 --- a/doc/public/course_data_formats/symbolic_response.rst +++ b/doc/public/course_data_formats/symbolic_response.rst @@ -19,8 +19,22 @@ This is a partial list of features, to be revised as we go along: An example of a problem:: - - - + + + It's a bit of a pain to enter that. + + * The script-style math variant. What would be outputted in latex if you + entered ``\mathcal{N}``. This is used in some variables. + + An example:: + + + + + + There is no fancy preprocessing needed, but if you had superscripts or + something, you would need to include that part. diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index 914a65d1b0..604941ffdd 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -74,6 +74,15 @@ def to_latex(x): # LatexPrinter._print_dot = _print_dot xs = latex(x) xs = xs.replace(r'\XI', 'XI') # workaround for strange greek + + # substitute back into latex form for scripts + # literally something of the form + # 'scriptN' becomes '\\mathcal{N}' + # note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms + xs = re.sub(r'script([a-zA-Z0-9]+)', + '\\mathcal{\\1}', + xs) + #return '%s{}{}' % (xs[1:-1]) if xs[0] == '$': return '[mathjax]%s[/mathjax]
    ' % (xs[1:-1]) # for sympy v6 @@ -106,6 +115,7 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False 'i': sympy.I, # lowercase i is also sqrt(-1) 'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key" 'I': sympy.Symbol('I'), # otherwise it is sqrt(-1) + 'N': sympy.Symbol('N'), # or it is some kind of sympy function #'X':sympy.sympify('Matrix([[0,1],[1,0]])'), #'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'), #'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'), @@ -266,6 +276,21 @@ class formula(object): elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml]) raise Exception, '[flatten_pmathml] unknown tag %s' % tag + def fix_mathvariant(parent): + '''Fix certain kinds of math variants + + Literally replace N + with 'scriptN'. There have been problems using script_N or script(N) + ''' + for child in parent: + if (gettag(child) == 'mstyle' and child.get('mathvariant') == 'script'): + newchild = etree.Element('mi') + newchild.text = 'script%s' % flatten_pmathml(child[0]) + parent.replace(child, newchild) + fix_mathvariant(child) + fix_mathvariant(xml) + + # find "tagged" superscripts # they have the character \u200b in the superscript # replace them with a__b so snuggle doesn't get confused diff --git a/lms/lib/symmath/test_formula.py b/lms/lib/symmath/test_formula.py new file mode 100644 index 0000000000..d3f16ed6b3 --- /dev/null +++ b/lms/lib/symmath/test_formula.py @@ -0,0 +1,115 @@ +""" +Tests of symbolic math +""" + + +import unittest +import formula +import re +from lxml import etree + +def stripXML(xml): + xml = xml.replace('\n', '') + xml = re.sub(r'\> +\<', '><', xml) + return xml + +class FormulaTest(unittest.TestCase): + # for readability later + mathml_start = '' + mathml_end = '' + + def setUp(self): + self.formulaInstance = formula.formula('') + + def test_replace_mathvariants(self): + expr = ''' + + N +''' + + expected = 'scriptN' + + # wrap + expr = stripXML(self.mathml_start + expr + self.mathml_end) + expected = stripXML(self.mathml_start + expected + self.mathml_end) + + # process the expression + xml = etree.fromstring(expr) + xml = self.formulaInstance.preprocess_pmathml(xml) + test = etree.tostring(xml) + + # success? + self.assertEqual(test, expected) + + + def test_fix_simple_superscripts(self): + expr = ''' + + a + + + b + +''' + + expected = 'a__b' + + # wrap + expr = stripXML(self.mathml_start + expr + self.mathml_end) + expected = stripXML(self.mathml_start + expected + self.mathml_end) + + # process the expression + xml = etree.fromstring(expr) + xml = self.formulaInstance.preprocess_pmathml(xml) + test = etree.tostring(xml) + + # success? + self.assertEqual(test, expected) + + def test_fix_complex_superscripts(self): + expr = ''' + + a + b + + + c + +''' + + expected = 'a_b__c' + + # wrap + expr = stripXML(self.mathml_start + expr + self.mathml_end) + expected = stripXML(self.mathml_start + expected + self.mathml_end) + + # process the expression + xml = etree.fromstring(expr) + xml = self.formulaInstance.preprocess_pmathml(xml) + test = etree.tostring(xml) + + # success? + self.assertEqual(test, expected) + + + def test_fix_msubsup(self): + expr = ''' + + a + b + c +''' + + expected = 'a_bc' # which is (a_b)^c + + # wrap + expr = stripXML(self.mathml_start + expr + self.mathml_end) + expected = stripXML(self.mathml_start + expected + self.mathml_end) + + # process the expression + xml = etree.fromstring(expr) + xml = self.formulaInstance.preprocess_pmathml(xml) + test = etree.tostring(xml) + + # success? + self.assertEqual(test, expected) From 4c8a45f85ecfb6422bd10de3b79f3a5ef51c70f9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:29:26 -0400 Subject: [PATCH 062/665] Code to add in an open ended tab automatically --- cms/djangoapps/contentstore/utils.py | 12 +++++++- cms/djangoapps/contentstore/views.py | 28 +++++++++++++++++-- .../models/settings/course_metadata.py | 9 ++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index cba30131b5..4113361445 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -2,9 +2,10 @@ from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError +import copy DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] - +OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"} def get_modulestore(location): """ @@ -158,3 +159,12 @@ def update_item(location, value): get_modulestore(location).delete_item(location) else: get_modulestore(location).update_item(location, value) + +def add_open_ended_panel_tab(course): + course_tabs = copy.copy(course.tabs) + changed = False + if OPEN_ENDED_PANEL not in course_tabs: + course_tabs.append(OPEN_ENDED_PANEL) + changed = True + return changed, course_tabs + diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6566350f8d..b066f476a3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -47,6 +47,7 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item +from .utils import add_open_ended_panel_tab from xmodule.modulestore.xml_importer import import_from_xml from contentstore.course_info_model import get_course_updates,\ @@ -68,7 +69,8 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] -ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading'] +OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] +ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @@ -295,6 +297,9 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) + log.debug(course.tabs) + log.debug(type(course.tabs)) + log.debug("LOOK HERE NOW!!!!!") # Set component types according to course policy file component_types = list(COMPONENT_TYPES) @@ -1329,7 +1334,26 @@ def course_advanced_updates(request, org, course, name): return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json") elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key - return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json") + request_body = json.loads(request.body) + filter_tabs = True + if ADVANCED_COMPONENT_POLICY_KEY in request_body: + log.debug("Advanced component in.") + for oe_type in OPEN_ENDED_COMPONENT_TYPES: + log.debug(request_body[ADVANCED_COMPONENT_POLICY_KEY]) + if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + log.debug("OE type in.") + course_module = modulestore().get_item(location) + changed, new_tabs = add_open_ended_panel_tab(course_module) + log.debug(new_tabs) + if changed: + request_body.update({'tabs' : new_tabs}) + filter_tabs = False + break + log.debug(request_body) + log.debug(filter_tabs) + log.debug("LOOK HERE FOR TAB SAVING!!!!") + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + return HttpResponse(response_json, mimetype="application/json") @login_required diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 24245a39d5..af0923213b 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -1,6 +1,7 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore from xmodule.x_module import XModuleDescriptor +import copy class CourseMetadata(object): @@ -30,7 +31,7 @@ class CourseMetadata(object): return course @classmethod - def update_from_json(cls, course_location, jsondict): + def update_from_json(cls, course_location, jsondict, filter_tabs=True): """ Decode the json into CourseMetadata and save any changed attrs to the db. @@ -40,9 +41,13 @@ class CourseMetadata(object): dirty = False + filtered_list = copy.copy(cls.FILTERED_LIST) + if not filter_tabs: + filtered_list.remove("tabs") + for k, v in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? - if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v): + if k not in filtered_list and (k not in descriptor.metadata or descriptor.metadata[k] != v): dirty = True descriptor.metadata[k] = v From a717dffd4886a185ae2d4414f060e295871dbd82 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:31:30 -0400 Subject: [PATCH 063/665] Remove debug statements --- cms/djangoapps/contentstore/views.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b066f476a3..591ec7d7cf 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -297,10 +297,7 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) - log.debug(course.tabs) - log.debug(type(course.tabs)) - log.debug("LOOK HERE NOW!!!!!") - + # Set component types according to course policy file component_types = list(COMPONENT_TYPES) if isinstance(course_advanced_keys, list): @@ -1337,21 +1334,14 @@ def course_advanced_updates(request, org, course, name): request_body = json.loads(request.body) filter_tabs = True if ADVANCED_COMPONENT_POLICY_KEY in request_body: - log.debug("Advanced component in.") for oe_type in OPEN_ENDED_COMPONENT_TYPES: - log.debug(request_body[ADVANCED_COMPONENT_POLICY_KEY]) if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - log.debug("OE type in.") course_module = modulestore().get_item(location) changed, new_tabs = add_open_ended_panel_tab(course_module) - log.debug(new_tabs) if changed: request_body.update({'tabs' : new_tabs}) filter_tabs = False break - log.debug(request_body) - log.debug(filter_tabs) - log.debug("LOOK HERE FOR TAB SAVING!!!!") response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") From 10eb7e45ea58a776113087515c1a00748f954320 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:42:41 -0400 Subject: [PATCH 064/665] Add in some docs --- cms/djangoapps/contentstore/utils.py | 8 ++++++++ cms/djangoapps/contentstore/views.py | 14 +++++++++++--- cms/djangoapps/models/settings/course_metadata.py | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4113361445..7e034d8da8 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -161,9 +161,17 @@ def update_item(location, value): get_modulestore(location).update_item(location, value) def add_open_ended_panel_tab(course): + """ + Used to add the open ended panel tab to a course if it does not exist. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs course_tabs = copy.copy(course.tabs) changed = False + #Check to see if open ended panel is defined in the course if OPEN_ENDED_PANEL not in course_tabs: + #Add panel to the tabs if it is not defined course_tabs.append(OPEN_ENDED_PANEL) changed = True return changed, course_tabs diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 591ec7d7cf..b7fcc9988e 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -297,7 +297,7 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) - + # Set component types according to course policy file component_types = list(COMPONENT_TYPES) if isinstance(course_advanced_keys, list): @@ -310,7 +310,6 @@ def edit_unit(request, location): templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) for template in templates: category = template.location.category - if category in course_advanced_keys: category = ADVANCED_COMPONENT_CATEGORY @@ -1332,15 +1331,24 @@ def course_advanced_updates(request, org, course, name): elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key request_body = json.loads(request.body) + #Whether or not to filter the tabs key out of the settings metadata filter_tabs = True + #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab + #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading + #module. if ADVANCED_COMPONENT_POLICY_KEY in request_body: + #Check to see if the user instantiated any open ended components for oe_type in OPEN_ENDED_COMPONENT_TYPES: if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Get the course so that we can scrape current tabs course_module = modulestore().get_item(location) + #Add an open ended tab to the course if needed changed, new_tabs = add_open_ended_panel_tab(course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json if changed: request_body.update({'tabs' : new_tabs}) - filter_tabs = False + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False break response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index af0923213b..2747cc0751 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -41,7 +41,9 @@ class CourseMetadata(object): dirty = False + #Copy the filtered list to avoid permanently changing the class attribute filtered_list = copy.copy(cls.FILTERED_LIST) + #Don't filter on the tab attribute if filter_tabs is False if not filter_tabs: filtered_list.remove("tabs") From 03caf94c9842ddc89224cedd917d72cc83f76fbd Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 15 Mar 2013 16:26:39 -0400 Subject: [PATCH 065/665] remove redundent XML attribtues which are in metadata --- common/lib/xmodule/xmodule/templates/discussion/default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml index d34e6378e6..49c0ce9c48 100644 --- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml +++ b/common/lib/xmodule/xmodule/templates/discussion/default.yaml @@ -5,5 +5,5 @@ metadata: id: 6002x_group_discussion_by_this discussion_category: Week 1 data: | - + children: [] From b54ebb346027dbe46e17d606a9865f95cbf062a5 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 15 Mar 2013 22:43:20 -0400 Subject: [PATCH 066/665] make discussion module use MetadataOnlyEditingDescriptor which will not present a code edit region and only display the metadata editor --- cms/templates/widgets/metadata-only-edit.html | 1 + common/lib/xmodule/xmodule/discussion_module.py | 3 ++- common/lib/xmodule/xmodule/editing_module.py | 10 ++++++++++ .../xmodule/js/src/raw/edit/metadata-only.coffee | 5 +++++ common/lib/xmodule/xmodule/raw_module.py | 2 +- 5 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 cms/templates/widgets/metadata-only-edit.html create mode 100644 common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee diff --git a/cms/templates/widgets/metadata-only-edit.html b/cms/templates/widgets/metadata-only-edit.html new file mode 100644 index 0000000000..a784f3798c --- /dev/null +++ b/cms/templates/widgets/metadata-only-edit.html @@ -0,0 +1 @@ +<%include file="metadata-edit.html" /> diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py index 7725a88e77..a0a5207e16 100644 --- a/common/lib/xmodule/xmodule/discussion_module.py +++ b/common/lib/xmodule/xmodule/discussion_module.py @@ -3,6 +3,7 @@ from pkg_resources import resource_string, resource_listdir from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor +from xmodule.editing_module import MetadataOnlyEditingDescriptor from xblock.core import String, Scope @@ -28,7 +29,7 @@ class DiscussionModule(DiscussionFields, XModule): return self.system.render_template('discussion/_discussion_module.html', context) -class DiscussionDescriptor(DiscussionFields, RawDescriptor): +class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor): module_class = DiscussionModule template_dir_name = "discussion" diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py index b93727a96b..1f07861ae8 100644 --- a/common/lib/xmodule/xmodule/editing_module.py +++ b/common/lib/xmodule/xmodule/editing_module.py @@ -40,6 +40,16 @@ class XMLEditingDescriptor(EditingDescriptor): js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/xml.coffee')]} js_module_name = "XMLEditingDescriptor" +class MetadataOnlyEditingDescriptor(EditingDescriptor): + """ + Module that provides a raw editing view of its data as XML. It does not perform + any validation of its definition + """ + + js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/metadata-only.coffee')]} + js_module_name = "MetadataOnlyEditingDescriptor" + + mako_template = "widgets/metadata-only-edit.html" class JSONEditingDescriptor(EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee b/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee new file mode 100644 index 0000000000..8c9afe86aa --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee @@ -0,0 +1,5 @@ +class @MetadataOnlyEditingDescriptor extends XModule.Descriptor + constructor: (@element) -> + + save: -> + data: null diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py index 2c6e157018..6b5c2441be 100644 --- a/common/lib/xmodule/xmodule/raw_module.py +++ b/common/lib/xmodule/xmodule/raw_module.py @@ -1,5 +1,5 @@ from lxml import etree -from xmodule.editing_module import XMLEditingDescriptor +from xmodule.editing_module import XMLEditingDescriptor, MetadataOnlyEditingDescriptor from xmodule.xml_module import XmlDescriptor import logging import sys From 3e65a688288695c8b1042de9ce56f3cc175d47c4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 15 Mar 2013 23:01:03 -0400 Subject: [PATCH 067/665] auto generate the discussion id on create. Also add 'discussion_id' to the list of 'system-metadata' so that end users cannot edit it --- common/lib/xmodule/xmodule/modulestore/mongo.py | 7 +++++++ .../lib/xmodule/xmodule/templates/discussion/default.yaml | 2 +- common/lib/xmodule/xmodule/x_module.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index c5e5bbfdf8..d115a92852 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -9,6 +9,7 @@ from fs.osfs import OSFS from itertools import repeat from path import path from datetime import datetime, timedelta +from uuid import uuid4 from importlib import import_module from xmodule.errortracker import null_error_tracker, exc_info_to_str @@ -496,6 +497,12 @@ class MongoModuleStore(ModuleStoreBase): """ try: source_item = self.collection.find_one(location_to_query(source)) + + # allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated + for key in source_item['metadata'].keys(): + if source_item['metadata'][key] == '$$GUID$$': + source_item['metadata'][key] = uuid4().hex + source_item['_id'] = Location(location).dict() self.collection.insert(source_item) item = self._load_items([source_item])[0] diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml index 49c0ce9c48..049e34b3e7 100644 --- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml +++ b/common/lib/xmodule/xmodule/templates/discussion/default.yaml @@ -2,7 +2,7 @@ metadata: display_name: Discussion Tag for: Topic-Level Student-Visible Label - id: 6002x_group_discussion_by_this + id: $$GUID$$ discussion_category: Week 1 data: | diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 02835e0d5d..5e1ba826b8 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -340,7 +340,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'] # A list of descriptor attributes that must be equal for the descriptors to # be equal From 9a18685c1c674668500d2382e223d932f6c5177d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 18 Mar 2013 09:01:54 -0400 Subject: [PATCH 068/665] add a discussion module write through cache to speed up Forum rendering --- cms/djangoapps/contentstore/utils.py | 1 + cms/one_time_startup.py | 8 +++ .../cache_toolbox/discussion_cache.py | 52 +++++++++++++++++++ .../xmodule/xmodule/modulestore/__init__.py | 3 +- .../lib/xmodule/xmodule/modulestore/mongo.py | 20 ++++++- lms/djangoapps/django_comment_client/utils.py | 16 +----- 6 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 common/djangoapps/cache_toolbox/discussion_cache.py diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 0a99441fe9..7d9d539604 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -6,6 +6,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] + def get_modulestore(location): """ Returns the correct modulestore to use for modifying the specified location diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py index 38a2fef847..961ffd15fb 100644 --- a/cms/one_time_startup.py +++ b/cms/one_time_startup.py @@ -1,6 +1,8 @@ from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore +from cache_toolbox.discussion_cache import discussion_cache_register_for_updates +from django.dispatch import Signal from django.core.cache import get_cache, InvalidCacheBackendError @@ -9,6 +11,12 @@ for store_name in settings.MODULESTORE: store = modulestore(store_name) store.metadata_inheritance_cache = cache + modulestore_update_signal = Signal( + providing_args=['modulestore', 'course_id', 'location'] + ) + store.modulestore_update_signal = modulestore_update_signal + discussion_cache_register_for_updates(store) + if hasattr(settings, 'DATADOG_API'): dog_http_api.api_key = settings.DATADOG_API dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True) diff --git a/common/djangoapps/cache_toolbox/discussion_cache.py b/common/djangoapps/cache_toolbox/discussion_cache.py new file mode 100644 index 0000000000..57390528bf --- /dev/null +++ b/common/djangoapps/cache_toolbox/discussion_cache.py @@ -0,0 +1,52 @@ +import logging +from django.core.cache import cache, get_cache +from datetime import datetime, timedelta + +def _get_discussion_cache(): + return cache + + +def get_discussion_cache_key(course_id): + return 'discussion_{0}'.format(course_id) + + +def get_discussion_cache_entry(modulestore, course_id): + cache_entry = None + cache = _get_discussion_cache() + + if cache is not None: + cache_entry = cache.get(get_discussion_cache_key(course_id), None) + if cache_entry is not None: + delta = datetime.now() - cache_entry.get('timestamp', datetime.min) + if delta > Timedelta(0,300): + cache_entry = None + + if cache_entry is None: + cache_entry = generate_discussion_cache_entry(modulestore, course_id) + + return cache_entry.get('modules',[]) + + +def generate_discussion_cache_entry(modulestore, course_id): + components = course_id.split('/') + all_discussion_modules = modulestore.get_items(['i4x', components[0], components[1], 'discussion', None], + course_id=course_id) + + cache = _get_discussion_cache() + if cache is not None: + cache.set(get_discussion_cache_key(course_id), {'modules': all_discussion_modules, 'timestamp': datetime.now()}) + return all_discussion_modules + + +def modulestore_update_signal_handler(modulestore = None, course_id = None, location = None, **kwargs): + """called when there is an write event in our modulestore + """ + if location.category == 'discussion': + logging.debug('******* got modulestore update signal. Regenerating discussion cache for {0}'.format(course_id)) + # refresh the cache entry if we've changed a discussion module + generate_discussion_cache_entry(modulestore, course_id) + + +def discussion_cache_register_for_updates(modulestore): + if modulestore.modulestore_update_signal is not None: + modulestore.modulestore_update_signal.connect(modulestore_update_signal_handler) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 022e016a58..a1a159b700 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -251,7 +251,6 @@ class Location(_LocationBase): def __repr__(self): return "Location%s" % repr(tuple(self)) - @property def course_id(self): """Return the ID of the Course that this item belongs to by looking @@ -413,7 +412,6 @@ class ModuleStore(object): return courses - class ModuleStoreBase(ModuleStore): ''' Implement interface functionality that can be shared. @@ -424,6 +422,7 @@ class ModuleStoreBase(ModuleStore): ''' self._location_errors = {} # location -> ErrorLog self.metadata_inheritance_cache = None + self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes def _get_errorlog(self, location): """ diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index d115a92852..b0b8b11aef 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -31,6 +31,10 @@ log = logging.getLogger(__name__) # there is only one revision for each item. Once we start versioning inside the CMS, # that assumption will have to change +def get_course_id_no_run(location): + ''' + ''' + return "/".join([location.org, location.course]) class MongoKeyValueStore(KeyValueStore): """ @@ -527,6 +531,15 @@ class MongoModuleStore(ModuleStoreBase): # recompute (and update) the metadata inheritance tree which is cached self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) + self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location)) + + def fire_updated_modulestore_signal(self, course_id, location): + if self.modulestore_update_signal is not None: + self.modulestore_update_signal.send(None, + modulestore = self, + course_id = course_id, + location = location + ) def get_course_for_item(self, location, depth=0): ''' @@ -594,6 +607,8 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'definition.children': children}) # recompute (and update) the metadata inheritance tree which is cached self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) + # fire signal that we've written to DB + self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location)) def update_metadata(self, location, metadata): """ @@ -619,7 +634,8 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'metadata': metadata}) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) + self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) + self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location)) def delete_item(self, location): """ @@ -640,7 +656,7 @@ class MongoModuleStore(ModuleStoreBase): self.collection.remove({'_id': Location(location).dict()}) # recompute (and update) the metadata inheritance tree which is cached self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) - + self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location)) def get_parent_locations(self, location, course_id): '''Find all locations that are the parents of this location in this diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 42233b84da..fbd0b29eca 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -16,6 +16,7 @@ from django.utils import simplejson from django_comment_client.models import Role from django_comment_client.permissions import check_permissions_by_view from xmodule.modulestore.exceptions import NoPathToItem +from cache_toolbox.discussion_cache import get_discussion_cache_entry from mitxmako import middleware import pystache_custom as pystache @@ -146,28 +147,15 @@ def sort_map_entries(category_map): def initialize_discussion_info(course): - global _DISCUSSIONINFO - # only cache in-memory discussion information for 10 minutes - # this is because we need a short-term hack fix for - # mongo-backed courseware whereby new discussion modules can be added - # without LMS service restart - - if _DISCUSSIONINFO[course.id]: - timestamp = _DISCUSSIONINFO[course.id].get('timestamp', datetime.now()) - age = datetime.now() - timestamp - # expire every 5 minutes - if age.seconds < 300: - return - course_id = course.id discussion_id_map = {} unexpanded_category_map = defaultdict(list) # get all discussion models within this course_id - all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) + all_modules = get_discussion_cache_entry(modulestore(), course_id) for module in all_modules: skip_module = False From 16f9f6ef4f0293aab1e5ff41e8d4d25eb3b24f92 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 18 Mar 2013 13:15:01 -0400 Subject: [PATCH 069/665] studio - tender widget: added initial reference and behavior --- cms/static/sass/_tender-widget.scss | 20 ++++++++++++++++++++ cms/static/sass/base-style.scss | 1 + cms/templates/base.html | 1 + cms/templates/widgets/footer.html | 7 +++---- cms/templates/widgets/tender.html | 13 +++++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 cms/static/sass/_tender-widget.scss create mode 100644 cms/templates/widgets/tender.html diff --git a/cms/static/sass/_tender-widget.scss b/cms/static/sass/_tender-widget.scss new file mode 100644 index 0000000000..fa406009ee --- /dev/null +++ b/cms/static/sass/_tender-widget.scss @@ -0,0 +1,20 @@ +// tender help/support widget +// ==================== + +// tender help link + + +// ==================== + +// tender modal UI +#tender_window { + +} + +#tender_frame { + +} + +.widget-layout { + font-family: 'Open Sans', sans-serif; +} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index dceac4233d..bdfdab2cb3 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -32,6 +32,7 @@ @import "account"; @import "index"; @import 'jquery-ui-calendar'; +@import 'tender-widget'; @import 'content-types'; diff --git a/cms/templates/base.html b/cms/templates/base.html index f7b2c46f61..5e6f4348b0 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -56,6 +56,7 @@ <%include file="widgets/footer.html" /> <%block name="jsextra"> + <%include file="widgets/tender.html" /> diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 0f265dfc2c..c38d5dec7f 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -17,13 +17,12 @@ + - - % if user.is_authenticated(): - - % endif diff --git a/cms/templates/widgets/tender.html b/cms/templates/widgets/tender.html new file mode 100644 index 0000000000..e2bba3d2ef --- /dev/null +++ b/cms/templates/widgets/tender.html @@ -0,0 +1,13 @@ +% if user.is_authenticated(): + + +% endif \ No newline at end of file From 7507f91d2274a64c2993717bcbecb078346461a3 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Mon, 18 Mar 2013 15:43:43 -0400 Subject: [PATCH 070/665] Added TODO and explanation for the top-level scss variables. --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index 6e1a38ee31..e2c095de2d 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -1,3 +1,9 @@ +/* TODO: move top-level variables to a common _variables.scss. + * NOTE: These variables were only added here because when this was integrated with the CMS, + * SASS compilation errors were triggered because the CMS didn't have the same variables defined + * that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS. + * -Abarrett and Vshnayder + */ $border-color: #C8C8C8; $body-font-size: em(14); From c901cecdea15249a63e41e4468fe5467b4b83999 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 18 Mar 2013 16:23:30 -0400 Subject: [PATCH 071/665] studio - tender widget: added compromise of style given iframe and clunky DOM elements that tender is using. meh. --- cms/static/sass/_tender-widget.scss | 87 ++++++++++++++++++++++++++--- cms/static/sass/_variables.scss | 1 + cms/templates/base.html | 2 +- cms/templates/widgets/footer.html | 2 +- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/cms/static/sass/_tender-widget.scss b/cms/static/sass/_tender-widget.scss index fa406009ee..422be36908 100644 --- a/cms/static/sass/_tender-widget.scss +++ b/cms/static/sass/_tender-widget.scss @@ -1,20 +1,91 @@ // tender help/support widget // ==================== -// tender help link - - -// ==================== - -// tender modal UI -#tender_window { - +#tender_frame, #tender_window { + background-image: none !important; } #tender_frame { + @include border-radius(3px); + @include box-shadow(0 2px 3px $shadow); + border: 1px solid $gray; + background: $white; +} +#tender_closer { + color: $blue-l2 !important; + margin-top: 15px; + margin-right: 5px; +} + +// ==================== + +// tender style overrides - not rendered through here, but an archive is needed +#tender_frame iframe html { + font-size: 62.5%; } .widget-layout { font-family: 'Open Sans', sans-serif; +} + +.widget-layout .search, +.widget-layout .tabs, +.widget-layout .header h1 a { + display: none; +} + +.widget-layout .header { + background: rgb(85, 151, 221); + padding: 20px; +} + +.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label { + font-weight: 600; +} + +.widget-layout .header h1 { + font-weight: 700; + font-size: 24px; + font-size: 2.4rem; +} + +.widget-layout .content { + padding: 20px; +} + +.widget-layout label { + font-size: 14px; + font-size: 1.4rem; + margin-bottom: 5px; + color: rgb(127,127,127) !important; +} + +.widget-layout input[type="text"], .widget-layout textarea { + padding: 10px; + font-size: 16px; + font-size: 1.6rem; + color: rgb(0,0,0) !important; +} + +.widget-layout textarea { + width: 97%; +} + +.widget-layout .form-actions { + border-top: 1px solid #ccc; + margin-top: 10px; + padding-top: 10px; +} + +.widget-layout dl.form { + margin-bottom: 15px; +} + +.widget-layout #brain_buster_captcha { + display: block; + width: 100%; + border-bottom: 1px solid #ccc; + margin-bottom: 10px; + padding-bottom: 10px; } \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 4d8e26b2f9..8e588b6234 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -16,6 +16,7 @@ $error-red: rgb(253, 87, 87); // colors - new for re-org $black: rgb(0,0,0); +$black-t0: rgba(0,0,0,0.125); $white: rgb(255,255,255); $gray: rgb(127,127,127); diff --git a/cms/templates/base.html b/cms/templates/base.html index 5e6f4348b0..77323be4d1 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -54,9 +54,9 @@ <%block name="content"> <%include file="widgets/footer.html" /> + <%include file="widgets/tender.html" /> <%block name="jsextra"> - <%include file="widgets/tender.html" /> diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index c38d5dec7f..e3063dafa7 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -18,7 +18,7 @@ edX Studio Help - diff --git a/cms/templates/widgets/tender.html b/cms/templates/widgets/tender.html index e2bba3d2ef..300b71701c 100644 --- a/cms/templates/widgets/tender.html +++ b/cms/templates/widgets/tender.html @@ -1,12 +1,12 @@ % if user.is_authenticated(): +Provide Feedback From 69c95ca785b60da37ea5e3aadadf177bef1f4c01 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 26 Mar 2013 09:51:24 -0400 Subject: [PATCH 089/665] Newline cleanup. --- cms/static/js/base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 0521371b6a..f623607d14 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -303,6 +303,7 @@ function saveSubsection() { data: JSON.stringify({ 'id': id, 'metadata': metadata}), success: function () { $spinner.delay(500).fadeOut(150); + $changedInput = null; }, error: function () { showToastMessage('There has been an error while saving your changes.'); From df935d422d31fcf34489f8b0fa501a4ac627212a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 09:52:26 -0400 Subject: [PATCH 090/665] Fix issues with open ended image grading and peer grading centralized module finder. --- .../open_ended_grading_classes/openendedchild.py | 4 ---- lms/djangoapps/courseware/module_render.py | 10 +++------- lms/djangoapps/open_ended_grading/views.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 2e49565bec..b9341f0cbe 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -357,10 +357,6 @@ class OpenEndedChild(object): if get_data['can_upload_files'] in ['true', '1']: has_file_to_upload = True file = get_data['student_file'][0] - if self.system.track_fuction: - self.system.track_function('open_ended_image_upload', {'filename': file.name}) - else: - log.info("No tracking function found when uploading image.") uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file) if uploaded_to_s3: image_tag = self.generate_image_tag_from_url(s3_public_url, file.name) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..a1c09d3d83 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -208,9 +208,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } - def get_or_default(key, default): - getattr(settings, key, default) - #This is a hacky way to pass settings to the combined open ended xmodule #It needs an S3 interface to upload images to S3 #It needs the open ended grading interface in order to get peer grading to be done @@ -226,12 +223,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if is_descriptor_combined_open_ended: s3_interface = { - 'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''), - 'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''), - 'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','') + 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''), + 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''), + 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','') } - def inner_get_module(descriptor): """ Delegate to get_module. It does an access check, so may return None diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 65cfe22ed0..78da00bf2b 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -111,7 +111,7 @@ def peer_grading(request, course_id): #Get the peer grading modules currently in the course items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None]) #See if any of the modules are centralized modules (ie display info from multiple problems) - items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict] + items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict] #Get the first one item_location = items[0].location #Generate a url for the first module and redirect the user to it From d4615da555f77a15ba7c4f70d380f813f195a6f7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 09:57:52 -0400 Subject: [PATCH 091/665] Adjust max image dim, add in safety for rewriting links --- .../combined_open_ended_modulev1.py | 6 +++++- .../open_ended_image_submission.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 98a54601de..c7df87fd45 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -363,7 +363,11 @@ class CombinedOpenEndedV1Module(): """ self.update_task_states() html = self.current_task.get_html(self.system) - return_html = rewrite_links(html, self.rewrite_content_links) + return_html = html + try: + return_html = rewrite_links(html, self.rewrite_content_links) + except: + pass return return_html def get_current_attributes(self, task_number): diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 6956f336a5..759645840f 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [ ] #Maximum allowed dimensions (x and y) for an uploaded image -MAX_ALLOWED_IMAGE_DIM = 1500 +MAX_ALLOWED_IMAGE_DIM = 2000 #Dimensions to which image is resized before it is evaluated for color count, etc MAX_IMAGE_DIM = 150 From 8afe2eb001a925bd49e9e5fb9678c3572e47ad0e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 10:35:47 -0400 Subject: [PATCH 092/665] Increase max score allowed --- .../open_ended_grading_classes/combined_open_ended_modulev1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index c7df87fd45..f88fd9ab82 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -24,7 +24,7 @@ MAX_ATTEMPTS = 1 MAX_SCORE = 1 #The highest score allowed for the overall xmodule and for each rubric point -MAX_SCORE_ALLOWED = 3 +MAX_SCORE_ALLOWED = 50 #If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress #Metadata overrides this. From f681d4300d7c9226eed57ee117169126598f9d42 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 26 Mar 2013 10:42:44 -0400 Subject: [PATCH 093/665] More cleanup in base.js. --- cms/static/js/base.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index f623607d14..bd8dc0bae8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -236,7 +236,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) { time_val = '00:00'; // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing - date = Date.parse(date_val + " " + time_val); + var date = Date.parse(date_val + " " + time_val); if (format == null) format = 'yyyy-MM-ddTHH:mm'; @@ -254,6 +254,7 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { } function autosaveInput(e) { + var self = this; if (this.saveTimer) { clearTimeout(this.saveTimer); } @@ -261,7 +262,7 @@ function autosaveInput(e) { this.saveTimer = setTimeout(function () { $changedInput = $(e.target); saveSubsection(); - this.saveTimer = null; + self.saveTimer = null; }, 500); } From 97cb4910a7b8d36123941538776a1d53ec4be034 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 11:04:14 -0400 Subject: [PATCH 094/665] Add in default bucket, edit image url checks --- .../open_ended_grading_classes/open_ended_image_submission.py | 2 +- lms/djangoapps/courseware/module_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 759645840f..2eb9502269 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -178,7 +178,7 @@ class URLProperties(object): Runs all available url tests @return: True if URL passes tests, false if not. """ - url_is_okay = self.check_suffix() and self.check_if_parses() and self.check_domain() + url_is_okay = self.check_suffix() and self.check_if_parses() return url_is_okay def check_domain(self): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index a1c09d3d83..15f95f1beb 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -225,7 +225,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours s3_interface = { 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''), 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''), - 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','') + 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended') } def inner_get_module(descriptor): From 87f545329a6f9f75fed6cdc16502a23e124a9ebe Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Mar 2013 11:05:33 -0400 Subject: [PATCH 095/665] studio - adding in tender-widget sass file --- cms/static/sass/elements/_tender-widget.scss | 142 +++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 cms/static/sass/elements/_tender-widget.scss diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss new file mode 100644 index 0000000000..fce62b8675 --- /dev/null +++ b/cms/static/sass/elements/_tender-widget.scss @@ -0,0 +1,142 @@ +// tender help/support widget +// ==================== + +#tender_frame, #tender_window { + background-image: none !important; + background: none; +} + +#tender_window { + @include border-radius(3px); + @include box-shadow(0 2px 3px $shadow); + background: $white !important; + border: 1px solid $gray; +} + +#tender_window { + padding: 0 !important; +} + +#tender_frame { + background: $white; +} + +#tender_closer { + color: $blue-l2 !important; + margin-top: 15px; + margin-right: 5px; + text-transform: uppercase; + + &:hover { + color: $blue-l4 !important; + } +} + +// ==================== + +// tender style overrides - not rendered through here, but an archive is needed +#tender_frame iframe html { + font-size: 62.5%; +} + +.widget-layout { + font-family: 'Open Sans', sans-serif; +} + +.widget-layout .search, +.widget-layout .tabs, +.widget-layout .footer, +.widget-layout .header h1 a { + display: none; +} + +.widget-layout .header { + background: rgb(85, 151, 221); + padding: 20px; +} + +.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label { + font-weight: 600; +} + +.widget-layout .header h1 { + font-weight: 500; + font-size: 24px; +} + +.widget-layout .content { + overflow: auto; + padding: 20px; +} + +.widget-layout label { + font-size: 14px; + margin-bottom: 5px; + color: #4c4c4c; + font-weight: 500; +} + +.widget-layout input[type="text"], .widget-layout textarea { + padding: 10px; + font-size: 16px; + color: rgb(0,0,0) !important; + border: 1px solid #b0b6c2; + border-radius: 2px; + background-color: #edf1f5; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #edf1f5),color-stop(100%, #fdfdfe)); + background-image: -webkit-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -moz-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -ms-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -o-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: linear-gradient(top, #edf1f5,#fdfdfe); + background-color: #edf1f5; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; + -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; + box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; +} + +.widget-layout input[type="text"]:focus, .widget-layout textarea:focus { + background-color: #fffcf1; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fffcf1),color-stop(100%, #fffefd)); + background-image: -webkit-linear-gradient(top, #fffcf1,#fffefd); + background-image: -moz-linear-gradient(top, #fffcf1,#fffefd); + background-image: -ms-linear-gradient(top, #fffcf1,#fffefd); + background-image: -o-linear-gradient(top, #fffcf1,#fffefd); + background-image: linear-gradient(top, #fffcf1,#fffefd); + outline: 0; +} + +.widget-layout textarea { + width: 97%; +} + +.widget-layout .form-actions { + border-top: 1px solid #ccc; + margin-top: 10px; + padding-top: 10px; +} + +.widget-layout dl.form { + float: none; + width: 100%; + border-bottom: 1px solid #f2f2f2; + margin-bottom: 10px; + padding-bottom: 10px; +} + +.widget-layout #brain_buster_captcha { + +} + +// specific elements +.widget-layout #discussion_body { + margin: 0 0 15px 0; +} + +.widget-layout .category dt, .widget-layout .category dd { + display: inline-block !important; +} + +.widget-layout .category dt { + margin-right: 15px !important; +} \ No newline at end of file From 24301d2a0761510143f7bc62bc9d7d0d01abd5ca Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:30:31 -0400 Subject: [PATCH 096/665] Moved helper functions from terrain/steps.py to terrain/helpers.py --- common/djangoapps/terrain/helpers.py | 152 +++++++++++++++++++++++++ common/djangoapps/terrain/steps.py | 164 ++------------------------- 2 files changed, 159 insertions(+), 157 deletions(-) create mode 100644 common/djangoapps/terrain/helpers.py diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/helpers.py new file mode 100644 index 0000000000..55c8f3db5a --- /dev/null +++ b/common/djangoapps/terrain/helpers.py @@ -0,0 +1,152 @@ +from lettuce import world, step +from .factories import * +from django.conf import settings +from django.http import HttpRequest +from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login +from django.contrib.auth.middleware import AuthenticationMiddleware +from django.contrib.sessions.middleware import SessionMiddleware +from student.models import CourseEnrollment +from bs4 import BeautifulSoup +import os.path +from selenium.common.exceptions import WebDriverException +from urllib import quote_plus +from lettuce.django import django_url + +@world.absorb +def wait(seconds): + time.sleep(float(seconds)) + +@world.absorb +def scroll_to_bottom(): + # Maximize the browser + world.browser.execute_script("window.scrollTo(0, screen.height);") + + +@world.absorb +def create_user(uname): + + # If the user already exists, don't try to create it again + if len(User.objects.filter(username=uname)) > 0: + return + + portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') + portal_user.set_password('test') + portal_user.save() + + registration = world.RegistrationFactory(user=portal_user) + registration.register(portal_user) + registration.activate() + + user_profile = world.UserProfileFactory(user=portal_user) + + +@world.absorb +def log_in(username, password): + ''' + Log the user in programatically + ''' + + # Authenticate the user + user = authenticate(username=username, password=password) + assert(user is not None and user.is_active) + + # Send a fake HttpRequest to log the user in + # We need to process the request using + # Session middleware and Authentication middleware + # to ensure that session state can be stored + request = HttpRequest() + SessionMiddleware().process_request(request) + AuthenticationMiddleware().process_request(request) + login(request, user) + + # Save the session + request.session.save() + + # Retrieve the sessionid and add it to the browser's cookies + cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key} + try: + world.browser.cookies.add(cookie_dict) + + # WebDriver has an issue where we cannot set cookies + # before we make a GET request, so if we get an error, + # we load the '/' page and try again + except: + world.browser.visit(django_url('/')) + world.browser.cookies.add(cookie_dict) + + +@world.absorb +def register_by_course_id(course_id, is_staff=False): + create_user('robot') + u = User.objects.get(username='robot') + if is_staff: + u.is_staff = True + u.save() + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) + + +@world.absorb +def save_the_html(path='/tmp'): + u = world.browser.url + html = world.browser.html.encode('ascii', 'ignore') + filename = '%s.html' % quote_plus(u) + f = open('%s/%s' % (path, filename), 'w') + f.write(html) + f.close + + +@world.absorb +def save_the_course_content(path='/tmp'): + html = world.browser.html.encode('ascii', 'ignore') + soup = BeautifulSoup(html) + + # get rid of the header, we only want to compare the body + soup.head.decompose() + + # for now, remove the data-id attributes, because they are + # causing mismatches between cms-master and master + for item in soup.find_all(attrs={'data-id': re.compile('.*')}): + del item['data-id'] + + # we also need to remove them from unrendered problems, + # where they are contained in the text of divs instead of + # in attributes of tags + # Be careful of whether or not it was the last attribute + # and needs a trailing space + for item in soup.find_all(text=re.compile(' data-id=".*?" ')): + s = unicode(item.string) + item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) + + for item in soup.find_all(text=re.compile(' data-id=".*?"')): + s = unicode(item.string) + item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) + + # prettify the html so it will compare better, with + # each HTML tag on its own line + output = soup.prettify() + + # use string slicing to grab everything after 'courseware/' in the URL + u = world.browser.url + section_url = u[u.find('courseware/') + 11:] + + + if not os.path.exists(path): + os.makedirs(path) + + filename = '%s.html' % (quote_plus(section_url)) + f = open('%s/%s' % (path, filename), 'w') + f.write(output) + f.close + +@world.absorb +def css_click(css_selector): + try: + world.browser.find_by_css(css_selector).click() + + except WebDriverException: + # Occassionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + time.sleep(1) + world.browser.find_by_css(css_selector).click() diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 3bc838a6af..ae36227fee 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,20 +1,8 @@ from lettuce import world, step -from .factories import * +from .helpers import * from lettuce.django import django_url -from django.conf import settings -from django.http import HttpRequest -from django.contrib.auth.models import User -from django.contrib.auth import authenticate, login -from django.contrib.auth.middleware import AuthenticationMiddleware -from django.contrib.sessions.middleware import SessionMiddleware -from student.models import CourseEnrollment -from urllib import quote_plus from nose.tools import assert_equals -from bs4 import BeautifulSoup import time -import re -import os.path -from selenium.common.exceptions import WebDriverException from logging import getLogger logger = getLogger(__name__) @@ -22,8 +10,7 @@ logger = getLogger(__name__) @step(u'I wait (?:for )?"(\d+)" seconds?$') def wait(step, seconds): - time.sleep(float(seconds)) - + world.wait(seconds) @step('I reload the page$') def reload_the_page(step): @@ -87,8 +74,8 @@ def the_page_title_should_contain(step, title): @step('I am a logged in user$') def i_am_logged_in_user(step): - create_user('robot') - log_in('robot', 'test') + world.create_user('robot') + world.log_in('robot', 'test') @step('I am not logged in$') @@ -98,151 +85,14 @@ def i_am_not_logged_in(step): @step('I am staff for course "([^"]*)"$') def i_am_staff_for_course_by_id(step, course_id): - register_by_course_id(course_id, True) + world.register_by_course_id(course_id, True) @step('I log in$') def i_log_in(step): - log_in('robot', 'test') + world.log_in('robot', 'test') @step(u'I am an edX user$') def i_am_an_edx_user(step): - create_user('robot') - -#### helper functions - - -@world.absorb -def scroll_to_bottom(): - # Maximize the browser - world.browser.execute_script("window.scrollTo(0, screen.height);") - - -@world.absorb -def create_user(uname): - - # If the user already exists, don't try to create it again - if len(User.objects.filter(username=uname)) > 0: - return - - portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') - portal_user.set_password('test') - portal_user.save() - - registration = world.RegistrationFactory(user=portal_user) - registration.register(portal_user) - registration.activate() - - user_profile = world.UserProfileFactory(user=portal_user) - - -@world.absorb -def log_in(username, password): - ''' - Log the user in programatically - ''' - - # Authenticate the user - user = authenticate(username=username, password=password) - assert(user is not None and user.is_active) - - # Send a fake HttpRequest to log the user in - # We need to process the request using - # Session middleware and Authentication middleware - # to ensure that session state can be stored - request = HttpRequest() - SessionMiddleware().process_request(request) - AuthenticationMiddleware().process_request(request) - login(request, user) - - # Save the session - request.session.save() - - # Retrieve the sessionid and add it to the browser's cookies - cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key} - try: - world.browser.cookies.add(cookie_dict) - - # WebDriver has an issue where we cannot set cookies - # before we make a GET request, so if we get an error, - # we load the '/' page and try again - except: - world.browser.visit(django_url('/')) - world.browser.cookies.add(cookie_dict) - - -@world.absorb -def register_by_course_id(course_id, is_staff=False): - create_user('robot') - u = User.objects.get(username='robot') - if is_staff: - u.is_staff = True - u.save() - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) - - -@world.absorb -def save_the_html(path='/tmp'): - u = world.browser.url - html = world.browser.html.encode('ascii', 'ignore') - filename = '%s.html' % quote_plus(u) - f = open('%s/%s' % (path, filename), 'w') - f.write(html) - f.close - - -@world.absorb -def save_the_course_content(path='/tmp'): - html = world.browser.html.encode('ascii', 'ignore') - soup = BeautifulSoup(html) - - # get rid of the header, we only want to compare the body - soup.head.decompose() - - # for now, remove the data-id attributes, because they are - # causing mismatches between cms-master and master - for item in soup.find_all(attrs={'data-id': re.compile('.*')}): - del item['data-id'] - - # we also need to remove them from unrendered problems, - # where they are contained in the text of divs instead of - # in attributes of tags - # Be careful of whether or not it was the last attribute - # and needs a trailing space - for item in soup.find_all(text=re.compile(' data-id=".*?" ')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) - - for item in soup.find_all(text=re.compile(' data-id=".*?"')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) - - # prettify the html so it will compare better, with - # each HTML tag on its own line - output = soup.prettify() - - # use string slicing to grab everything after 'courseware/' in the URL - u = world.browser.url - section_url = u[u.find('courseware/') + 11:] - - - if not os.path.exists(path): - os.makedirs(path) - - filename = '%s.html' % (quote_plus(section_url)) - f = open('%s/%s' % (path, filename), 'w') - f.write(output) - f.close - -@world.absorb -def css_click(css_selector): - try: - world.browser.find_by_css(css_selector).click() - - except WebDriverException: - # Occassionally, MathJax or other JavaScript can cover up - # an element temporarily. - # If this happens, wait a second, then try again - time.sleep(1) - world.browser.find_by_css(css_selector).click() + world.create_user('robot') From 315b360e4cafeab3fec798272ed2e5ee22cb88d0 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:31:41 -0400 Subject: [PATCH 097/665] Fixed an import error in terrain/helpers.py --- common/djangoapps/terrain/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/helpers.py index 55c8f3db5a..12d6818659 100644 --- a/common/djangoapps/terrain/helpers.py +++ b/common/djangoapps/terrain/helpers.py @@ -12,6 +12,7 @@ import os.path from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url +import time @world.absorb def wait(seconds): From e494d529fc48f21c1bb01bdee7dc8515035b6219 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:38:30 -0400 Subject: [PATCH 098/665] Split terrain/helpers.py into ui_helpers.py and course_helpers.py --- .../terrain/{helpers.py => course_helpers.py} | 32 ------------------- common/djangoapps/terrain/steps.py | 3 +- common/djangoapps/terrain/ui_helpers.py | 30 +++++++++++++++++ 3 files changed, 32 insertions(+), 33 deletions(-) rename common/djangoapps/terrain/{helpers.py => course_helpers.py} (82%) create mode 100644 common/djangoapps/terrain/ui_helpers.py diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/course_helpers.py similarity index 82% rename from common/djangoapps/terrain/helpers.py rename to common/djangoapps/terrain/course_helpers.py index 12d6818659..dbdaa2a21c 100644 --- a/common/djangoapps/terrain/helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -12,17 +12,6 @@ import os.path from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url -import time - -@world.absorb -def wait(seconds): - time.sleep(float(seconds)) - -@world.absorb -def scroll_to_bottom(): - # Maximize the browser - world.browser.execute_script("window.scrollTo(0, screen.height);") - @world.absorb def create_user(uname): @@ -87,15 +76,6 @@ def register_by_course_id(course_id, is_staff=False): CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) -@world.absorb -def save_the_html(path='/tmp'): - u = world.browser.url - html = world.browser.html.encode('ascii', 'ignore') - filename = '%s.html' % quote_plus(u) - f = open('%s/%s' % (path, filename), 'w') - f.write(html) - f.close - @world.absorb def save_the_course_content(path='/tmp'): @@ -139,15 +119,3 @@ def save_the_course_content(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(output) f.close - -@world.absorb -def css_click(css_selector): - try: - world.browser.find_by_css(css_selector).click() - - except WebDriverException: - # Occassionally, MathJax or other JavaScript can cover up - # an element temporarily. - # If this happens, wait a second, then try again - time.sleep(1) - world.browser.find_by_css(css_selector).click() diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index ae36227fee..6e54b71aa6 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,5 +1,6 @@ from lettuce import world, step -from .helpers import * +from .course_helpers import * +from .ui_helpers import * from lettuce.django import django_url from nose.tools import assert_equals import time diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py new file mode 100644 index 0000000000..4667957e87 --- /dev/null +++ b/common/djangoapps/terrain/ui_helpers.py @@ -0,0 +1,30 @@ +from lettuce import world, step +import time +from urllib import quote_plus + +@world.absorb +def wait(seconds): + time.sleep(float(seconds)) + + +@world.absorb +def css_click(css_selector): + try: + world.browser.find_by_css(css_selector).click() + + except WebDriverException: + # Occassionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + time.sleep(1) + world.browser.find_by_css(css_selector).click() + +@world.absorb +def save_the_html(path='/tmp'): + u = world.browser.url + html = world.browser.html.encode('ascii', 'ignore') + filename = '%s.html' % quote_plus(u) + f = open('%s/%s' % (path, filename), 'w') + f.write(html) + f.close + From 0562f11c5622c94214162ac5c43fd69b8851601f Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:41:30 -0400 Subject: [PATCH 099/665] Fixed import issue with WebDriverException --- common/djangoapps/terrain/course_helpers.py | 1 - common/djangoapps/terrain/ui_helpers.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index dbdaa2a21c..8c949de1ad 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -9,7 +9,6 @@ from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment from bs4 import BeautifulSoup import os.path -from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 4667957e87..2ad7150740 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -1,6 +1,7 @@ from lettuce import world, step import time from urllib import quote_plus +from selenium.common.exceptions import WebDriverException @world.absorb def wait(seconds): From b0eb73302b9753acbc53f3ddc4fe86226f51292b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:50:50 -0400 Subject: [PATCH 100/665] Moved some courseware/features/common.py steps into terrain/steps.py --- common/djangoapps/terrain/steps.py | 38 ++++++++- lms/djangoapps/courseware/features/common.py | 83 -------------------- 2 files changed, 36 insertions(+), 85 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e54b71aa6..8356b5446d 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -72,6 +72,9 @@ def the_page_title_should_be(step, title): def the_page_title_should_contain(step, title): assert(title in world.browser.title) +@step('I log in$') +def i_log_in(step): + world.log_in('robot', 'test') @step('I am a logged in user$') def i_am_logged_in_user(step): @@ -89,11 +92,42 @@ def i_am_staff_for_course_by_id(step, course_id): world.register_by_course_id(course_id, True) -@step('I log in$') -def i_log_in(step): +@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') +def click_the_link_called(step, text): + world.browser.find_link_by_text(text).click() + + +@step(r'should see that the url is "([^"]*)"$') +def should_have_the_url(step, url): + assert_equals(world.browser.url, url) + +@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') +def should_see_a_link_called(step, text): + assert len(world.browser.find_link_by_text(text)) > 0 + +@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') +def should_see_in_the_page(step, text): + assert_in(text, world.browser.html) + + +@step('I am logged in$') +def i_am_logged_in(step): + world.create_user('robot') world.log_in('robot', 'test') + world.browser.visit(django_url('/')) + + +@step('I am not logged in$') +def i_am_not_logged_in(step): + world.browser.cookies.delete() @step(u'I am an edX user$') def i_am_an_edx_user(step): world.create_user('robot') + + +@step(u'User "([^"]*)" is an edX user$') +def registered_edx_user(step, uname): + world.create_user(uname) + diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 7d41637c8e..8477347580 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -6,83 +6,10 @@ from student.models import CourseEnrollment from xmodule.modulestore import Location from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates -import time from logging import getLogger logger = getLogger(__name__) - -@step(u'I wait (?:for )?"(\d+)" seconds?$') -def wait(step, seconds): - time.sleep(float(seconds)) - - -@step('I (?:visit|access|open) the homepage$') -def i_visit_the_homepage(step): - world.browser.visit(django_url('/')) - assert world.browser.is_element_present_by_css('header.global', 10) - - -@step(u'I (?:visit|access|open) the dashboard$') -def i_visit_the_dashboard(step): - world.browser.visit(django_url('/dashboard')) - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - - -@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') -def click_the_link_called(step, text): - world.browser.find_link_by_text(text).click() - - -@step('I should be on the dashboard page$') -def i_should_be_on_the_dashboard(step): - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - assert world.browser.title == 'Dashboard' - - -@step(u'I (?:visit|access|open) the courses page$') -def i_am_on_the_courses_page(step): - world.browser.visit(django_url('/courses')) - assert world.browser.is_element_present_by_css('section.courses') - - -@step('I should see that the path is "([^"]*)"$') -def i_should_see_that_the_path_is(step, path): - assert world.browser.url == django_url(path) - - -@step(u'the page title should be "([^"]*)"$') -def the_page_title_should_be(step, title): - assert world.browser.title == title - - -@step(r'should see that the url is "([^"]*)"$') -def should_have_the_url(step, url): - assert_equals(world.browser.url, url) - - -@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') -def should_see_a_link_called(step, text): - assert len(world.browser.find_link_by_text(text)) > 0 - - -@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') -def should_see_in_the_page(step, text): - assert_in(text, world.browser.html) - - -@step('I am logged in$') -def i_am_logged_in(step): - world.create_user('robot') - world.log_in('robot', 'test') - world.browser.visit(django_url('/')) - - -@step('I am not logged in$') -def i_am_not_logged_in(step): - world.browser.cookies.delete() - - TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = "Problem" @@ -135,16 +62,6 @@ def add_tab_to_course(step, course, extra_tab_name): display_name=str(extra_tab_name)) -@step(u'I am an edX user$') -def i_am_an_edx_user(step): - world.create_user('robot') - - -@step(u'User "([^"]*)" is an edX user$') -def registered_edx_user(step, uname): - world.create_user(uname) - - def flush_xmodule_store(): # Flush and initialize the module store # It needs the templates because it creates new records From c12e1fb1cec0fabd3d825dc7f270381146b1a2e7 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:54:17 -0400 Subject: [PATCH 101/665] Added missing import statement --- common/djangoapps/terrain/steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 8356b5446d..8dac372a64 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -2,7 +2,7 @@ from lettuce import world, step from .course_helpers import * from .ui_helpers import * from lettuce.django import django_url -from nose.tools import assert_equals +from nose.tools import assert_equals, assert_in import time from logging import getLogger From 5e69050a163fc19e6ce042b206e8a25f105ac509 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:01:55 -0400 Subject: [PATCH 102/665] Elminated unused functions from courseware/features/courses.py and moved the rest to common.py --- lms/djangoapps/courseware/features/common.py | 87 +++++++ lms/djangoapps/courseware/features/courses.py | 234 ------------------ .../courseware/features/smart-accordion.py | 2 +- 3 files changed, 88 insertions(+), 235 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/courses.py diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 8477347580..2d366d462d 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -6,6 +6,9 @@ from student.models import CourseEnrollment from xmodule.modulestore import Location from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates +from xmodule.course_module import CourseDescriptor +from courseware.courses import get_course_by_id +from xmodule import seq_module, vertical_module from logging import getLogger logger = getLogger(__name__) @@ -94,3 +97,87 @@ def section_location(course_num): course=course_num, category='sequential', name=TEST_SECTION_NAME.replace(" ", "_")) + + +def get_courses(): + ''' + Returns dict of lists of courses available, keyed by course.org (ie university). + Courses are sorted by course.number. + ''' + courses = [c for c in modulestore().get_courses() + if isinstance(c, CourseDescriptor)] + courses = sorted(courses, key=lambda course: course.number) + return courses + + +def get_courseware_with_tabs(course_id): + """ + Given a course_id (string), return a courseware array of dictionaries for the + top three levels of navigation. Same as get_courseware() except include + the tabs on the right hand main navigation page. + + This hides the appropriate courseware as defined by the hide_from_toc field: + chapter.lms.hide_from_toc + + Example: + + [{ + 'chapter_name': 'Overview', + 'sections': [{ + 'clickable_tab_count': 0, + 'section_name': 'Welcome', + 'tab_classes': [] + }, { + 'clickable_tab_count': 1, + 'section_name': 'System Usage Sequence', + 'tab_classes': ['VerticalDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Lab0: Using the tools', + 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Circuit Sandbox', + 'tab_classes': [] + }] + }, { + 'chapter_name': 'Week 1', + 'sections': [{ + 'clickable_tab_count': 4, + 'section_name': 'Administrivia and Circuit Elements', + 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Basic Circuit Analysis', + 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Resistor Divider', + 'tab_classes': [] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Week 1 Tutorials', + 'tab_classes': [] + }] + }, { + 'chapter_name': 'Midterm Exam', + 'sections': [{ + 'clickable_tab_count': 2, + 'section_name': 'Midterm Exam', + 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor'] + }] + }] + """ + + course = get_course_by_id(course_id) + chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc] + courseware = [{'chapter_name': c.display_name_with_default, + 'sections': [{'section_name': s.display_name_with_default, + 'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, + 'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0, + 'class': t.__class__.__name__} + for t in s.get_children()]} + for s in c.get_children() if not s.lms.hide_from_toc]} + for c in chapters] + + return courseware diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py deleted file mode 100644 index c99fb58b85..0000000000 --- a/lms/djangoapps/courseware/features/courses.py +++ /dev/null @@ -1,234 +0,0 @@ -from lettuce import world -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.django import modulestore -from courseware.courses import get_course_by_id -from xmodule import seq_module, vertical_module - -from logging import getLogger -logger = getLogger(__name__) - -## support functions - - -def get_courses(): - ''' - Returns dict of lists of courses available, keyed by course.org (ie university). - Courses are sorted by course.number. - ''' - courses = [c for c in modulestore().get_courses() - if isinstance(c, CourseDescriptor)] - courses = sorted(courses, key=lambda course: course.number) - return courses - - -def get_courseware_with_tabs(course_id): - """ - Given a course_id (string), return a courseware array of dictionaries for the - top three levels of navigation. Same as get_courseware() except include - the tabs on the right hand main navigation page. - - This hides the appropriate courseware as defined by the hide_from_toc field: - chapter.lms.hide_from_toc - - Example: - - [{ - 'chapter_name': 'Overview', - 'sections': [{ - 'clickable_tab_count': 0, - 'section_name': 'Welcome', - 'tab_classes': [] - }, { - 'clickable_tab_count': 1, - 'section_name': 'System Usage Sequence', - 'tab_classes': ['VerticalDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Lab0: Using the tools', - 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Circuit Sandbox', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Week 1', - 'sections': [{ - 'clickable_tab_count': 4, - 'section_name': 'Administrivia and Circuit Elements', - 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Basic Circuit Analysis', - 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Resistor Divider', - 'tab_classes': [] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Week 1 Tutorials', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Midterm Exam', - 'sections': [{ - 'clickable_tab_count': 2, - 'section_name': 'Midterm Exam', - 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor'] - }] - }] - """ - - course = get_course_by_id(course_id) - chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc] - courseware = [{'chapter_name': c.display_name_with_default, - 'sections': [{'section_name': s.display_name_with_default, - 'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, - 'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0, - 'class': t.__class__.__name__} - for t in s.get_children()]} - for s in c.get_children() if not s.lms.hide_from_toc]} - for c in chapters] - - return courseware - - -def process_section(element, num_tabs=0): - ''' - Process section reads through whatever is in 'course-content' and classifies it according to sequence module type. - - This function is recursive - - There are 6 types, with 6 actions. - - Sequence Module - -contains one child module - - Vertical Module - -contains other modules - -process it and get its children, then process them - - Capa Module - -problem type, contains only one problem - -for this, the most complex type, we created a separate method, process_problem - - Video Module - -video type, contains only one video - -we only check to ensure that a section with class of video exists - - HTML Module - -html text - -we do not check anything about it - - Custom Tag Module - -a custom 'hack' module type - -there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type - - can be used like this: - e = world.browser.find_by_css('section.course-content section') - process_section(e) - - ''' - if element.has_class('xmodule_display xmodule_SequenceModule'): - logger.debug('####### Processing xmodule_SequenceModule') - child_modules = element.find_by_css("div>div>section[class^='xmodule']") - for mod in child_modules: - process_section(mod) - - elif element.has_class('xmodule_display xmodule_VerticalModule'): - logger.debug('####### Processing xmodule_VerticalModule') - vert_list = element.find_by_css("li section[class^='xmodule']") - for item in vert_list: - process_section(item) - - elif element.has_class('xmodule_display xmodule_CapaModule'): - logger.debug('####### Processing xmodule_CapaModule') - assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module" - p = element.find_by_css("section[id^='problem']").first - p_id = p['id'] - logger.debug('####################') - logger.debug('id is "%s"' % p_id) - logger.debug('####################') - process_problem(p, p_id) - - elif element.has_class('xmodule_display xmodule_VideoModule'): - logger.debug('####### Processing xmodule_VideoModule') - assert element.find_by_css("section[class^='video']"), "No video found in Video Module" - - elif element.has_class('xmodule_display xmodule_HtmlModule'): - logger.debug('####### Processing xmodule_HtmlModule') - pass - - elif element.has_class('xmodule_display xmodule_CustomTagModule'): - logger.debug('####### Processing xmodule_CustomTagModule') - pass - - else: - assert False, "Class for element not recognized!!" - - -def process_problem(element, problem_id): - ''' - Process problem attempts to - 1) scan all the input fields and reset them - 2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect') - 3) click the 'show answer' button IF it exists and IF the answer is not already displayed - 4) enter the correct answer in each input box - 5) click the 'check' button and verify that answers are correct - - Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM. - The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective. - ''' - - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - - ## clear out all input to ensure an incorrect result - for field in input_fields: - field.find_by_css("input").first.fill('') - - ## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect' - # This would need to be reworked because multiple choice problems don't have this status - # if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect': - prob_xmod.find_by_css("section.action input.check").first.click() - - ## all elements become disconnected after the click - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - # Wait for the ajax reload - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - for field in input_fields: - assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id) - - show_button = element.find_by_css("section.action input.show").first - ## this logic is to ensure we do not accidentally hide the answers - if show_button.value.lower() == 'show answer': - show_button.click() - else: - pass - - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - - ## in each field, find the answer, and send it to the field. - ## Note that this does not work if the answer type is a strange format, e.g. "either a or b" - for field in input_fields: - field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text) - - prob_xmod.find_by_css("section.action input.check").first.click() - - ## assert that we entered the correct answers - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - for field in input_fields: - ## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space) - assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index a7eb782722..539bce96ce 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -2,7 +2,7 @@ from lettuce import world, step from re import sub from nose.tools import assert_equals from xmodule.modulestore.django import modulestore -from courses import * +from common import * from logging import getLogger logger = getLogger(__name__) From 6dd86f7a97826ec7af6fcb608928d6f0a7c07660 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:19:46 -0400 Subject: [PATCH 103/665] Refactored courseware_common and open_ended to use ui helpers --- common/djangoapps/terrain/ui_helpers.py | 16 +++++++++ .../courseware/features/courseware_common.py | 15 +++----- .../courseware/features/openended.py | 36 +++++++------------ 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 2ad7150740..d56ce3649b 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -20,6 +20,22 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() +@world.absorb +def css_fill(css_selector, text): + world.browser.find_by_css(css_selector).first.fill(text) + +@world.absorb +def click_link(partial_text): + world.browser.find_link_by_partial_text(partial_text).first.click() + +@world.absorb +def css_text(css_selector): + return world.browser.find_by_css(css_selector).first.text + +@world.absorb +def css_visible(css_selector): + return world.browser.find_by_css(css_selector).visible + @world.absorb def save_the_html(path='/tmp'): u = world.browser.url diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 96304e016f..567254c334 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -9,11 +9,10 @@ def i_click_on_view_courseware(step): @step('I click on the "([^"]*)" tab$') -def i_click_on_the_tab(step, tab): - world.browser.find_link_by_partial_text(tab).first.click() +def i_click_on_the_tab(step, tab_text): + world.click_link(tab_text) world.save_the_html() - @step('I visit the courseware URL$') def i_visit_the_course_info_url(step): url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') @@ -32,13 +31,9 @@ def i_am_on_the_dashboard_page(step): @step('the "([^"]*)" tab is active$') -def the_tab_is_active(step, tab): - css = '.course-tabs a.active' - active_tab = world.browser.find_by_css(css) - assert (active_tab.text == tab) - +def the_tab_is_active(step, tab_text): + assert world.css_text('.course-tabs a.active') == tab_text @step('the login dialog is visible$') def login_dialog_visible(step): - css = 'form#login_form.login_form' - assert world.browser.find_by_css(css).visible + assert world.css_visible('form#login_form.login_form') diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 0725a051ff..7601bfcc53 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -12,7 +12,7 @@ def navigate_to_an_openended_question(step): problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' world.browser.visit(django_url(problem)) tab_css = 'ol#sequence-list > li > a[data-element="5"]' - world.browser.find_by_css(tab_css).click() + world.css_click(tab_css) @step('I navigate to an openended question as staff$') @@ -22,50 +22,41 @@ def navigate_to_an_openended_question_as_staff(step): problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' world.browser.visit(django_url(problem)) tab_css = 'ol#sequence-list > li > a[data-element="5"]' - world.browser.find_by_css(tab_css).click() + world.css_click(tab_css) @step(u'I enter the answer "([^"]*)"$') def enter_the_answer_text(step, text): - textarea_css = 'textarea' - world.browser.find_by_css(textarea_css).first.fill(text) + world.css_fill('textarea', text) @step(u'I submit the answer "([^"]*)"$') def i_submit_the_answer_text(step, text): - textarea_css = 'textarea' - world.browser.find_by_css(textarea_css).first.fill(text) - check_css = 'input.check' - world.browser.find_by_css(check_css).click() + world.css_fill('textarea', text) + world.css_click('input.check') @step('I click the link for full output$') def click_full_output_link(step): - link_css = 'a.full' - world.browser.find_by_css(link_css).first.click() + world.css_click('a.full') @step(u'I visit the staff grading page$') def i_visit_the_staff_grading_page(step): - # course_u = '/courses/MITx/3.091x/2012_Fall' - # sg_url = '%s/staff_grading' % course_u - world.browser.click_link_by_text('Instructor') - world.browser.click_link_by_text('Staff grading') - # world.browser.visit(django_url(sg_url)) + world.click_link('Instructor') + world.click_link('Staff grading') @step(u'I see the grader message "([^"]*)"$') def see_grader_message(step, msg): message_css = 'div.external-grader-message' - grader_msg = world.browser.find_by_css(message_css).text - assert_in(msg, grader_msg) + assert_in(msg, world.css_text(message_css)) @step(u'I see the grader status "([^"]*)"$') def see_the_grader_status(step, status): status_css = 'div.grader-status' - grader_status = world.browser.find_by_css(status_css).text - assert_equals(status, grader_status) + assert_equals(status, world.css_text(status_css)) @step('I see the red X$') @@ -77,7 +68,7 @@ def see_the_red_x(step): @step(u'I see the grader score "([^"]*)"$') def see_the_grader_score(step, score): score_css = 'div.result-output > p' - score_text = world.browser.find_by_css(score_css).text + score_text = world.css_text(score_css) assert_equals(score_text, 'Score: %s' % score) @@ -89,14 +80,13 @@ def see_full_output_link(step): @step('I see the spelling grading message "([^"]*)"$') def see_spelling_msg(step, msg): - spelling_css = 'div.spelling' - spelling_msg = world.browser.find_by_css(spelling_css).text + spelling_msg = world.css_text('div.spelling') assert_equals('Spelling: %s' % msg, spelling_msg) @step(u'my answer is queued for instructor grading$') def answer_is_queued_for_instructor_grading(step): list_css = 'ul.problem-list > li > a' - actual_msg = world.browser.find_by_css(list_css).text + actual_msg = world.css_text(list_css) expected_msg = "(0 graded, 1 pending)" assert_in(expected_msg, actual_msg) From 4528490fac9050881eba0ff98df07782e71bbabc Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:40:33 -0400 Subject: [PATCH 104/665] Refactored lms/coureware lettuce tests to use terrain helpers for common ui manipulations --- common/djangoapps/terrain/course_helpers.py | 1 + common/djangoapps/terrain/steps.py | 6 ++++- common/djangoapps/terrain/ui_helpers.py | 23 ++++++++++++++++++- .../courseware/features/courseware_common.py | 13 +++++------ lms/djangoapps/courseware/features/login.py | 4 +--- .../courseware/features/openended.py | 6 ++--- .../courseware/features/problems.py | 4 ++-- .../courseware/features/registration.py | 8 +++---- lms/djangoapps/courseware/features/signup.py | 2 +- .../courseware/features/smart-accordion.py | 10 ++++---- .../courseware/features/xqueue_setup.py | 1 + 11 files changed, 50 insertions(+), 28 deletions(-) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 8c949de1ad..ebf5745f11 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -12,6 +12,7 @@ import os.path from urllib import quote_plus from lettuce.django import django_url + @world.absorb def create_user(uname): diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 8dac372a64..e99dec44b3 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -13,6 +13,7 @@ logger = getLogger(__name__) def wait(step, seconds): world.wait(seconds) + @step('I reload the page$') def reload_the_page(step): world.browser.reload() @@ -72,10 +73,12 @@ def the_page_title_should_be(step, title): def the_page_title_should_contain(step, title): assert(title in world.browser.title) + @step('I log in$') def i_log_in(step): world.log_in('robot', 'test') + @step('I am a logged in user$') def i_am_logged_in_user(step): world.create_user('robot') @@ -101,10 +104,12 @@ def click_the_link_called(step, text): def should_have_the_url(step, url): assert_equals(world.browser.url, url) + @step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') def should_see_a_link_called(step, text): assert len(world.browser.find_link_by_text(text)) > 0 + @step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') def should_see_in_the_page(step, text): assert_in(text, world.browser.html) @@ -130,4 +135,3 @@ def i_am_an_edx_user(step): @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): world.create_user(uname) - diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index d56ce3649b..1aac9cc72e 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -2,12 +2,29 @@ from lettuce import world, step import time from urllib import quote_plus from selenium.common.exceptions import WebDriverException +from lettuce.django import django_url + @world.absorb def wait(seconds): time.sleep(float(seconds)) +@world.absorb +def visit(url): + world.browser.visit(django_url(url)) + + +@world.absorb +def url_equals(url): + return world.browser.url == django_url(url) + + +@world.absorb +def is_css_present(css_selector): + return world.browser.is_element_present_by_css(css_selector, wait_time=4) + + @world.absorb def css_click(css_selector): try: @@ -20,22 +37,27 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() + @world.absorb def css_fill(css_selector, text): world.browser.find_by_css(css_selector).first.fill(text) + @world.absorb def click_link(partial_text): world.browser.find_link_by_partial_text(partial_text).first.click() + @world.absorb def css_text(css_selector): return world.browser.find_by_css(css_selector).first.text + @world.absorb def css_visible(css_selector): return world.browser.find_by_css(css_selector).visible + @world.absorb def save_the_html(path='/tmp'): u = world.browser.url @@ -44,4 +66,3 @@ def save_the_html(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(html) f.close - diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 567254c334..6aa9559e65 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -1,11 +1,9 @@ from lettuce import world, step -from lettuce.django import django_url @step('I click on View Courseware') def i_click_on_view_courseware(step): - css = 'a.enter-course' - world.browser.find_by_css(css).first.click() + world.css_click('a.enter-course') @step('I click on the "([^"]*)" tab$') @@ -13,10 +11,10 @@ def i_click_on_the_tab(step, tab_text): world.click_link(tab_text) world.save_the_html() + @step('I visit the courseware URL$') def i_visit_the_course_info_url(step): - url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') - world.browser.visit(url) + world.visit('/courses/MITx/6.002x/2012_Fall/courseware') @step(u'I do not see "([^"]*)" anywhere on the page') @@ -26,14 +24,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text): @step(u'I am on the dashboard page$') def i_am_on_the_dashboard_page(step): - assert world.browser.is_element_present_by_css('section.courses') - assert world.browser.url == django_url('/dashboard') + assert world.is_css_present('section.courses') + assert world.url_equals('/dashboard') @step('the "([^"]*)" tab is active$') def the_tab_is_active(step, tab_text): assert world.css_text('.course-tabs a.active') == tab_text + @step('the login dialog is visible$') def login_dialog_visible(step): assert world.css_visible('form#login_form.login_form') diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 094db078ca..3e3c0efbc4 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -28,9 +28,7 @@ def i_should_see_the_login_error_message(step, msg): @step(u'click the dropdown arrow$') def click_the_dropdown(step): - css = ".dropdown" - e = world.browser.find_by_css(css) - e.click() + world.css_click('.dropdown') #### helper functions diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 7601bfcc53..2f14b808a3 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -61,8 +61,7 @@ def see_the_grader_status(step, status): @step('I see the red X$') def see_the_red_x(step): - x_css = 'div.grader-status > span.incorrect' - assert world.browser.find_by_css(x_css) + assert world.is_css_present('div.grader-status > span.incorrect') @step(u'I see the grader score "([^"]*)"$') @@ -74,8 +73,7 @@ def see_the_grader_score(step, score): @step('I see the link for full output$') def see_full_output_link(step): - link_css = 'a.full' - assert world.browser.find_by_css(link_css) + assert world.is_css_present('a.full') @step('I see the spelling grading message "([^"]*)"$') diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index d2d379a212..bdd9062ef3 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -339,7 +339,7 @@ def assert_answer_mark(step, problem_type, correctness): # At least one of the correct selectors should be present for sel in selector_dict[problem_type]: - has_expected = world.browser.is_element_present_by_css(sel, wait_time=4) + has_expected = world.is_css_present(sel) # As soon as we find the selector, break out of the loop if has_expected: @@ -366,7 +366,7 @@ def inputfield(problem_type, choice=None, input_num=1): # If the input element doesn't exist, fail immediately - assert(world.browser.is_element_present_by_css(sel, wait_time=4)) + assert world.is_css_present(sel) # Retrieve the input element return world.browser.find_by_css(sel) diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 94b9b50f6c..63f044b16f 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -13,17 +13,17 @@ def i_register_for_the_course(step, course): register_link = intro_section.find_by_css('a.register') register_link.click() - assert world.browser.is_element_present_by_css('section.container.dashboard') + assert world.is_css_present('section.container.dashboard') @step(u'I should see the course numbered "([^"]*)" in my dashboard$') def i_should_see_that_course_in_my_dashboard(step, course): course_link_css = 'section.my-courses a[href*="%s"]' % course - assert world.browser.is_element_present_by_css(course_link_css) + assert world.is_css_present(course_link_css) @step(u'I press the "([^"]*)" button in the Unenroll dialog') def i_press_the_button_in_the_unenroll_dialog(step, value): button_css = 'section#unenroll-modal input[value="%s"]' % value - world.browser.find_by_css(button_css).click() - assert world.browser.is_element_present_by_css('section.container.dashboard') + world.css_click(button_css) + assert world.is_css_present('section.container.dashboard') diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index 3a697a6102..d9edcb215b 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -22,4 +22,4 @@ def i_check_checkbox(step, checkbox): @step('I should see "([^"]*)" in the dashboard banner$') def i_should_see_text_in_the_dashboard_banner_section(step, text): css_selector = "section.dashboard-banner h2" - assert (text in world.browser.find_by_css(css_selector).text) + assert (text in world.css_text(css_selector)) diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index 539bce96ce..8240a13905 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -32,20 +32,20 @@ def i_verify_all_the_content_of_each_course(step): pass for test_course in registered_courses: - test_course.find_by_css('a').click() + test_course.css_click('a') check_for_errors() # Get the course. E.g. 'MITx/6.002x/2012_Fall' current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url)) validate_course(current_course, ids) - world.browser.find_link_by_text('Courseware').click() - assert world.browser.is_element_present_by_id('accordion', wait_time=2) + world.click_link('Courseware') + assert world.is_css_present('accordion') check_for_errors() browse_course(current_course) # clicking the user link gets you back to the user's home page - world.browser.find_by_css('.user-link').click() + world.css_click('.user-link') check_for_errors() @@ -94,7 +94,7 @@ def browse_course(course_id): world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() ## sometimes the course-content takes a long time to load - assert world.browser.is_element_present_by_css('.course-content', wait_time=5) + assert world.is_css_present('.course-content') ## look for server error div check_for_errors() diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py index 23706941a9..d6d7a13a5c 100644 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ b/lms/djangoapps/courseware/features/xqueue_setup.py @@ -3,6 +3,7 @@ from lettuce import before, after, world from django.conf import settings import threading + @before.all def setup_mock_xqueue_server(): From dde0d1676b8176119d5f33bf234221836c781aac Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:02:40 -0400 Subject: [PATCH 105/665] Refactored terrain/steps.py to use ui_helpers Added a wait time before checking the page HTML, and changed it to check just in the HTML body --- common/djangoapps/terrain/steps.py | 27 +++++++++++-------------- common/djangoapps/terrain/ui_helpers.py | 7 ++++++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index e99dec44b3..dc8d2f8b87 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -26,42 +26,40 @@ def browser_back(step): @step('I (?:visit|access|open) the homepage$') def i_visit_the_homepage(step): - world.browser.visit(django_url('/')) - assert world.browser.is_element_present_by_css('header.global', 10) - + world.visit('/') + assert world.is_css_present('header.global') @step(u'I (?:visit|access|open) the dashboard$') def i_visit_the_dashboard(step): - world.browser.visit(django_url('/dashboard')) - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - + world.visit('/dashboard') + assert world.is_css_present('section.container.dashboard') @step('I should be on the dashboard page$') def i_should_be_on_the_dashboard(step): - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) + assert world.is_css_present('section.container.dashboard') assert world.browser.title == 'Dashboard' @step(u'I (?:visit|access|open) the courses page$') def i_am_on_the_courses_page(step): - world.browser.visit(django_url('/courses')) - assert world.browser.is_element_present_by_css('section.courses') + world.visit('/courses') + assert world.is_css_present('section.courses') @step(u'I press the "([^"]*)" button$') def and_i_press_the_button(step, value): button_css = 'input[value="%s"]' % value - world.browser.find_by_css(button_css).first.click() + world.css_click(button_css) @step(u'I click the link with the text "([^"]*)"$') def click_the_link_with_the_text_group1(step, linktext): - world.browser.find_link_by_text(linktext).first.click() + world.click_link(linktext) @step('I should see that the path is "([^"]*)"$') def i_should_see_that_the_path_is(step, path): - assert world.browser.url == django_url(path) + assert world.url_equals(path) @step(u'the page title should be "([^"]*)"$') @@ -97,8 +95,7 @@ def i_am_staff_for_course_by_id(step, course_id): @step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') def click_the_link_called(step, text): - world.browser.find_link_by_text(text).click() - + world.click_link(text) @step(r'should see that the url is "([^"]*)"$') def should_have_the_url(step, url): @@ -112,7 +109,7 @@ def should_see_a_link_called(step, text): @step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') def should_see_in_the_page(step, text): - assert_in(text, world.browser.html) + assert_in(text, world.css_text('body')) @step('I am logged in$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 1aac9cc72e..3009d1fa8d 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -50,7 +50,12 @@ def click_link(partial_text): @world.absorb def css_text(css_selector): - return world.browser.find_by_css(css_selector).first.text + + # Wait for the css selector to appear + if world.is_css_present(css_selector): + return world.browser.find_by_css(css_selector).first.text + else: + return "" @world.absorb From e69931ec5a06ecec9bc57b2875181e94b9b2f059 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:45:25 -0400 Subject: [PATCH 106/665] Refactored studio lettuce tests to use terrain/ui_helpers for ui manipulation --- .../features/advanced-settings.py | 40 ++----- .../contentstore/features/common.py | 102 +++++------------- .../contentstore/features/courses.py | 15 ++- .../contentstore/features/section.py | 26 ++--- .../contentstore/features/signup.py | 2 +- .../features/studio-overview-togglesection.py | 24 ++--- .../contentstore/features/subsection.py | 15 ++- common/djangoapps/terrain/ui_helpers.py | 34 ++++++ 8 files changed, 109 insertions(+), 149 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 7e86e94a31..0232c3b908 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -2,8 +2,6 @@ from lettuce import world, step from common import * import time from terrain.steps import reload_the_page -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.support import expected_conditions as EC from nose.tools import assert_true, assert_false, assert_equal @@ -22,9 +20,9 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"' def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' if world.browser.is_element_present_by_css(expand_icon_css): - css_click(expand_icon_css) + world.css_click(expand_icon_css) link_css = 'li.nav-course-settings-advanced a' - css_click(link_css) + world.css_click(link_css) @step('I am on the Advanced Course Settings page in Studio$') @@ -35,24 +33,8 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) - - # def is_invisible(driver): - # return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) - css = 'a.%s-button' % name.lower() - wait_for(is_visible) - time.sleep(float(1)) - css_click_at(css) - -# is_invisible is not returning a boolean, not working -# try: -# css_click_at(css) -# wait_for(is_invisible) -# except WebDriverException, e: -# css_click_at(css) -# wait_for(is_invisible) + world.css_click_at(css) @step(u'I edit the value of a policy key$') @@ -61,7 +43,7 @@ def edit_the_value_of_a_policy_key(step): It is hard to figure out how to get into the CodeMirror area, so cheat and do it from the policy key field :) """ - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') @@ -85,7 +67,7 @@ def i_see_default_advanced_settings(step): @step('the settings are alphabetized$') def they_are_alphabetized(step): - key_elements = css_find(KEY_CSS) + key_elements = world.css_find(KEY_CSS) all_keys = [] for key in key_elements: all_keys.append(key.value) @@ -118,13 +100,13 @@ def assert_policy_entries(expected_keys, expected_values): for counter in range(len(expected_keys)): index = get_index_of(expected_keys[counter]) assert_false(index == -1, "Could not find key: " + expected_keys[counter]) - assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect") + assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect") def get_index_of(expected_key): - for counter in range(len(css_find(KEY_CSS))): + for counter in range(len(world.css_find(KEY_CSS))): # Sometimes get stale reference if I hold on to the array of elements - key = css_find(KEY_CSS)[counter].value + key = world.css_find(KEY_CSS)[counter].value if key == expected_key: return counter @@ -133,14 +115,14 @@ def get_index_of(expected_key): def get_display_name_value(): index = get_index_of(DISPLAY_NAME_KEY) - return css_find(VALUE_CSS)[index].value + return world.css_find(VALUE_CSS)[index].value def change_display_name_value(step, new_value): - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] display_name = get_display_name_value() for count in range(len(display_name)): e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) # Must delete "" before typing the JSON value e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) - press_the_notification_button(step, "Save") \ No newline at end of file + press_the_notification_button(step, "Save") diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 820b60123b..4cc5759949 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,11 +1,6 @@ from lettuce import world, step -from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException, StaleElementReferenceException -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates @@ -20,9 +15,9 @@ def i_visit_the_studio_homepage(step): # To make this go to port 8001, put # LETTUCE_SERVER_PORT = 8001 # in your settings.py file. - world.browser.visit(django_url('/')) + world.visit('/') signin_css = 'a.action-signin' - assert world.browser.is_element_present_by_css(signin_css, 10) + assert world.is_css_present(signin_css) @step('I am logged into Studio$') @@ -43,7 +38,7 @@ def i_press_the_category_delete_icon(step, category): css = 'a.delete-button.delete-subsection-button span.delete-icon' else: assert False, 'Invalid category: %s' % category - css_click(css) + world.css_click(css) @step('I have opened a new course in Studio$') @@ -87,56 +82,6 @@ def flush_xmodule_store(): update_templates() -def assert_css_with_text(css, text): - assert_true(world.browser.is_element_present_by_css(css, 5)) - assert_equal(world.browser.find_by_css(css).text, text) - - -def css_click(css): - ''' - First try to use the regular click method, - but if clicking in the middle of an element - doesn't work it might be that it thinks some other - element is on top of it there so click in the upper left - ''' - try: - css_find(css).first.click() - except WebDriverException, e: - css_click_at(css) - - -def css_click_at(css, x=10, y=10): - ''' - A method to click at x,y coordinates of the element - rather than in the center of the element - ''' - e = css_find(css).first - e.action_chains.move_to_element_with_offset(e._element, x, y) - e.action_chains.click() - e.action_chains.perform() - - -def css_fill(css, value): - world.browser.find_by_css(css).first.fill(value) - - -def css_find(css): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) - - world.browser.is_element_present_by_css(css, 5) - wait_for(is_visible) - return world.browser.find_by_css(css) - - -def wait_for(func): - WebDriverWait(world.browser.driver, 5).until(func) - - -def id_find(id): - return world.browser.find_by_id(id) - - def clear_courses(): flush_xmodule_store() @@ -145,9 +90,9 @@ def fill_in_course_info( name='Robot Super Course', org='MITx', num='101'): - css_fill('.new-course-name', name) - css_fill('.new-course-org', org) - css_fill('.new-course-number', num) + world.css_fill('.new-course-name', name) + world.css_fill('.new-course-org', org) + world.css_fill('.new-course-number', num) def log_into_studio( @@ -155,21 +100,22 @@ def log_into_studio( email='robot+studio@edx.org', password='test', is_staff=False): - create_studio_user(uname=uname, email=email, is_staff=is_staff) - world.browser.cookies.delete() - world.browser.visit(django_url('/')) - signin_css = 'a.action-signin' - world.browser.is_element_present_by_css(signin_css, 10) - # click the signin button - css_click(signin_css) + create_studio_user(uname=uname, email=email, is_staff=is_staff) + + world.browser.cookies.delete() + world.visit('/') + + signin_css = 'a.action-signin' + world.is_css_present(signin_css) + world.css_click(signin_css) login_form = world.browser.find_by_css('form#login_form') login_form.find_by_name('email').fill(email) login_form.find_by_name('password').fill(password) login_form.find_by_name('submit').click() - assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + assert_true(world.is_css_present('.new-course-button')) def create_a_course(): @@ -184,26 +130,26 @@ def create_a_course(): world.browser.reload() course_link_css = 'span.class-name' - css_click(course_link_css) + world.css_click(course_link_css) course_title_css = 'span.course-title' - assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) + assert_true(world.is_css_present(course_title_css)) def add_section(name='My Section'): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) name_css = 'input.new-section-name' save_css = 'input.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) span_css = 'span.section-name-span' - assert_true(world.browser.is_element_present_by_css(span_css, 5)) + assert_true(world.is_css_present(span_css)) def add_subsection(name='Subsection One'): css = 'a.new-subsection-item' - css_click(css) + world.css_click(css) name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index e394165f08..8301e6708f 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -11,7 +11,7 @@ def no_courses(step): @step('I click the New Course button$') def i_click_new_course(step): - css_click('.new-course-button') + world.css_click('.new-course-button') @step('I fill in the new course information$') @@ -27,7 +27,7 @@ def i_create_a_course(step): @step('I click the course link in My Courses$') def i_click_the_course_link_in_my_courses(step): course_css = 'span.class-name' - css_click(course_css) + world.css_click(course_css) ############ ASSERTIONS ################### @@ -35,28 +35,27 @@ def i_click_the_course_link_in_my_courses(step): @step('the Courseware page has loaded in Studio$') def courseware_page_has_loaded_in_studio(step): course_title_css = 'span.course-title' - assert world.browser.is_element_present_by_css(course_title_css) + assert world.is_css_present(course_title_css) @step('I see the course listed in My Courses$') def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' - assert_css_with_text(course_css, 'Robot Super Course') - + assert world.css_has_text(course_css, 'Robot Super Course') @step('the course is loaded$') def course_is_loaded(step): class_css = 'a.class-name' - assert_css_with_text(class_css, 'Robot Super Course') + assert world.css_has_text(course_css, 'Robot Super Cousre') @step('I am on the "([^"]*)" tab$') def i_am_on_tab(step, tab_name): header_css = 'div.inner-wrapper h1' - assert_css_with_text(header_css, tab_name) + assert world.css_has_text(header_css, tab_name) @step('I see a link for adding a new section$') def i_see_new_section_link(step): link_css = 'a.new-courseware-section-button' - assert_css_with_text(link_css, '+ New Section') + assert world.css_has_text(link_css, '+ New Section') diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index b5ddb48a09..e57d50bbfe 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -10,7 +10,7 @@ import time @step('I click the new section link$') def i_click_new_section_link(step): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) @step('I enter the section name and click save$') @@ -31,19 +31,19 @@ def i_have_added_new_section(step): @step('I click the Edit link for the release date$') def i_click_the_edit_link_for_the_release_date(step): button_css = 'div.section-published-date a.edit-button' - css_click(button_css) + world.css_click(button_css) @step('I save a new section release date$') def i_save_a_new_section_release_date(step): date_css = 'input.start-date.date.hasDatepicker' time_css = 'input.start-time.time.ui-timepicker-input' - css_fill(date_css, '12/25/2013') + world.css_fill(date_css, '12/25/2013') # hit TAB to get to the time field - e = css_find(date_css).first + e = world.css_find(date_css).first e._element.send_keys(Keys.TAB) - css_fill(time_css, '12:00am') - e = css_find(time_css).first + world.css_fill(time_css, '12:00am') + e = world.css_find(time_css).first e._element.send_keys(Keys.TAB) time.sleep(float(1)) world.browser.click_link_by_text('Save') @@ -64,13 +64,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step): @step('I click to edit the section name$') def i_click_to_edit_section_name(step): - css_click('span.section-name-span') + world.css_click('span.section-name-span') @step('I see the complete section name with a quote in the editor$') def i_see_complete_section_name_with_quote_in_editor(step): css = '.edit-section-name' - assert world.browser.is_element_present_by_css(css, 5) + assert world.is_css_present(css) assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') @@ -85,7 +85,7 @@ def i_see_a_release_date_for_my_section(step): import re css = 'span.published-status' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) status_text = world.browser.find_by_css(css).text # e.g. 11/06/2012 at 16:25 @@ -99,7 +99,7 @@ def i_see_a_release_date_for_my_section(step): @step('I see a link to create a new subsection$') def i_see_a_link_to_create_a_new_subsection(step): css = 'a.new-subsection-item' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) @step('the section release date picker is not visible$') @@ -120,10 +120,10 @@ def the_section_release_date_is_updated(step): def save_section_name(name): name_css = '.new-section-name' save_css = '.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) def see_my_section_on_the_courseware_page(name): section_css = 'span.section-name-span' - assert_css_with_text(section_css, name) + assert world.css_has_text(section_css, name) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index e8d0dd8229..cd4adb79fb 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -17,7 +17,7 @@ def i_press_the_button_on_the_registration_form(step): submit_css = 'form#register_form button#submit' # Workaround for click not working on ubuntu # for some unknown reason. - e = css_find(submit_css) + e = world.css_find(submit_css) e.type(' ') @step('I should see be on the studio home page$') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 060d592cfd..85a25a55ac 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -49,7 +49,7 @@ def have_a_course_with_two_sections(step): def navigate_to_the_course_overview_page(step): log_into_studio(is_staff=True) course_locator = '.class-name' - css_click(course_locator) + world.css_click(course_locator) @step(u'I navigate to the courseware page of a course with multiple sections') @@ -66,44 +66,44 @@ def i_add_a_section(step): @step(u'I click the "([^"]*)" link$') def i_click_the_text_span(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) + assert_true(world.browser.is_element_present_by_css(span_locator)) # first make sure that the expand/collapse text is the one you expected assert_equal(world.browser.find_by_css(span_locator).value, text) - css_click(span_locator) + world.css_click(span_locator) @step(u'I collapse the first section$') def i_collapse_a_section(step): collapse_locator = 'section.courseware-section a.collapse' - css_click(collapse_locator) + world.css_click(collapse_locator) @step(u'I expand the first section$') def i_expand_a_section(step): expand_locator = 'section.courseware-section a.expand' - css_click(expand_locator) + world.css_click(expand_locator) @step(u'I see the "([^"]*)" link$') def i_see_the_span_with_text(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) - assert_equal(world.browser.find_by_css(span_locator).value, text) - assert_true(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_equal(world.css_find(span_locator).value, text) + assert_true(world.css_visible(span_locator)) @step(u'I do not see the "([^"]*)" link$') def i_do_not_see_the_span_with_text(step, text): # Note that the span will exist on the page but not be visible span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator)) - assert_false(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_false(world.css_visible(span_locator)) @step(u'all sections are expanded$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_true(s.visible) @@ -111,6 +111,6 @@ def all_sections_are_expanded(step): @step(u'all sections are collapsed$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_false(s.visible) diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 88e1424898..f5863be27b 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -15,8 +15,7 @@ def i_have_opened_a_new_course_section(step): @step('I click the New Subsection link') def i_click_the_new_subsection_link(step): - css = 'a.new-subsection-item' - css_click(css) + world.css_click('a.new-subsection-item') @step('I enter the subsection name and click save$') @@ -31,13 +30,13 @@ def i_save_subsection_name_with_quote(step): @step('I click to edit the subsection name$') def i_click_to_edit_subsection_name(step): - css_click('span.subsection-name-value') + world.css_click('span.subsection-name-value') @step('I see the complete subsection name with a quote in the editor$') def i_see_complete_subsection_name_with_quote_in_editor(step): css = '.subsection-display-name-input' - assert world.browser.is_element_present_by_css(css, 5) + assert world.is_css_present(css) assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') @@ -70,11 +69,11 @@ def the_subsection_does_not_exist(step): def save_subsection_name(name): name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) def see_subsection_name(name): css = 'span.subsection-name' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) css = 'span.subsection-name-value' - assert_css_with_text(css, name) + assert world.css_has_text(css, name) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 3009d1fa8d..e2f701d089 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -2,6 +2,9 @@ from lettuce import world, step import time from urllib import quote_plus from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait from lettuce.django import django_url @@ -9,6 +12,9 @@ from lettuce.django import django_url def wait(seconds): time.sleep(float(seconds)) +@world.absorb +def wait_for(func): + WebDriverWait(world.browser.driver, 5).until(func) @world.absorb def visit(url): @@ -24,9 +30,27 @@ def url_equals(url): def is_css_present(css_selector): return world.browser.is_element_present_by_css(css_selector, wait_time=4) +@world.absorb +def css_has_text(css_selector, text): + return world.css_text(css_selector) == text + +@world.absorb +def css_find(css): + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + + world.browser.is_element_present_by_css(css, 5) + wait_for(is_visible) + return world.browser.find_by_css(css) @world.absorb def css_click(css_selector): + ''' + First try to use the regular click method, + but if clicking in the middle of an element + doesn't work it might be that it thinks some other + element is on top of it there so click in the upper left + ''' try: world.browser.find_by_css(css_selector).click() @@ -37,6 +61,16 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() +@world.absorb +def css_click_at(css, x=10, y=10): + ''' + A method to click at x,y coordinates of the element + rather than in the center of the element + ''' + e = css_find(css).first + e.action_chains.move_to_element_with_offset(e._element, x, y) + e.action_chains.click() + e.action_chains.perform() @world.absorb def css_fill(css_selector, text): From a58ae9b62d60450b7bf18a49531487e2150cf094 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:49:50 -0400 Subject: [PATCH 107/665] Refactored studio lettuce test section.py to use more of ui helpers --- cms/djangoapps/contentstore/features/section.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index e57d50bbfe..41236f6dfd 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -105,13 +105,13 @@ def i_see_a_link_to_create_a_new_subsection(step): @step('the section release date picker is not visible$') def the_section_release_date_picker_not_visible(step): css = 'div.edit-subsection-publish-settings' - assert False, world.browser.find_by_css(css).visible + assert not world.css_visible(css) @step('the section release date is updated$') def the_section_release_date_is_updated(step): css = 'span.published-status' - status_text = world.browser.find_by_css(css).text + status_text = world.css_text(css) assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') From 00d25b684cf10bd2c8dd39a5077e365b3259bfde Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 14:04:04 -0400 Subject: [PATCH 108/665] Moved modulestore flush code into terrain/course_helpers --- .../contentstore/features/common.py | 19 +------------------ .../contentstore/features/courses.py | 2 +- .../features/studio-overview-togglesection.py | 6 +++--- .../contentstore/features/subsection.py | 2 +- common/djangoapps/terrain/course_helpers.py | 15 +++++++++++++++ lms/djangoapps/courseware/features/common.py | 15 +-------------- 6 files changed, 22 insertions(+), 37 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 4cc5759949..0b5c9acbed 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -43,7 +43,7 @@ def i_press_the_category_delete_icon(step, category): @step('I have opened a new course in Studio$') def i_have_opened_a_new_course(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() @@ -69,23 +69,6 @@ def create_studio_user( user_profile = world.UserProfileFactory(user=studio_user) -def flush_xmodule_store(): - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" - _MODULESTORES = {} - modulestore().collection.drop() - update_templates() - - -def clear_courses(): - flush_xmodule_store() - - def fill_in_course_info( name='Robot Super Course', org='MITx', diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 8301e6708f..348cc25e97 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -6,7 +6,7 @@ from common import * @step('There are no courses$') def no_courses(step): - clear_courses() + world.clear_courses() @step('I click the New Course button$') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 85a25a55ac..dc22d3ad1a 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -8,13 +8,13 @@ logger = getLogger(__name__) @step(u'I have a course with no sections$') def have_a_course(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() @step(u'I have a course with 1 section$') def have_a_course_with_1_section(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( @@ -25,7 +25,7 @@ def have_a_course_with_1_section(step): @step(u'I have a course with multiple sections$') def have_a_course_with_two_sections(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index f5863be27b..2094e65ccb 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -7,7 +7,7 @@ from nose.tools import assert_equal @step('I have opened a new course section in Studio$') def i_have_opened_a_new_course_section(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() add_section() diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index ebf5745f11..2ac3befd82 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -7,6 +7,8 @@ from django.contrib.auth import authenticate, login from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates from bs4 import BeautifulSoup import os.path from urllib import quote_plus @@ -119,3 +121,16 @@ def save_the_course_content(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(output) f.close + +@world.absorb +def clear_courses(): + # Flush and initialize the module store + # It needs the templates because it creates new records + # by cloning from the template. + # Note that if your test module gets in some weird state + # (though it shouldn't), do this manually + # from the bash shell to drop it: + # $ mongo test_xmodule --eval "db.dropDatabase()" + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 2d366d462d..f015725ae9 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -24,7 +24,7 @@ def create_course(step, course): # First clear the modulestore so we don't try to recreate # the same course twice # This also ensures that the necessary templates are loaded - flush_xmodule_store() + world.clear_courses() # Create the course # We always use the same org and display name, @@ -65,19 +65,6 @@ def add_tab_to_course(step, course, extra_tab_name): display_name=str(extra_tab_name)) -def flush_xmodule_store(): - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" - _MODULESTORES = {} - modulestore().collection.drop() - update_templates() - - def course_id(course_num): return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, TEST_COURSE_NAME.replace(" ", "_")) From 27d5ebf027224239c5109820794d6e5c0098930d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 14:27:10 -0400 Subject: [PATCH 109/665] pep8 fixes --- .../features/advanced-settings.feature | 8 +++--- .../features/advanced-settings.py | 1 + .../contentstore/features/common.py | 1 + .../contentstore/features/courses.feature | 2 +- .../contentstore/features/courses.py | 1 + .../contentstore/features/section.py | 2 +- .../contentstore/features/signup.py | 1 + .../studio-overview-togglesection.feature | 28 +++++++++---------- .../contentstore/features/subsection.py | 1 + common/djangoapps/terrain/course_helpers.py | 1 + common/djangoapps/terrain/steps.py | 3 ++ common/djangoapps/terrain/ui_helpers.py | 11 ++++++-- .../features/high-level-tabs.feature | 2 +- 13 files changed, 39 insertions(+), 23 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index af97709ad0..66039e19b1 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -1,6 +1,6 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists - I want to be able to manually enter JSON key/value pairs + I want to be able to manually enter JSON key /value pairs Scenario: A course author sees default advanced settings Given I have opened a new course in Studio @@ -27,16 +27,16 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - Scenario: Test how multi-line input appears + 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 Then it is displayed as formatted And I reload the page Then it is displayed as formatted - Scenario: Test automatic quoting of non-JSON values + 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 + When I create a non -JSON value not in quotes Then it is displayed as a string And I reload the page Then it is displayed as a string diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 0232c3b908..a2708d8c96 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -16,6 +16,7 @@ DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_VALUE = '"Robot Super Course"' ############### ACTIONS #################### + @step('I select the Advanced Settings$') def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 0b5c9acbed..870ab89694 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -10,6 +10,7 @@ from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature index 39d39b50aa..455313b0e2 100644 --- a/cms/djangoapps/contentstore/features/courses.feature +++ b/cms/djangoapps/contentstore/features/courses.feature @@ -10,4 +10,4 @@ Feature: Create Course And I fill in the new course information And I press the "Save" button Then the Courseware page has loaded in Studio - And I see a link for adding a new section \ No newline at end of file + And I see a link for adding a new section diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 348cc25e97..b3b6f91bdb 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -43,6 +43,7 @@ def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' assert world.css_has_text(course_css, 'Robot Super Course') + @step('the course is loaded$') def course_is_loaded(step): class_css = 'a.class-name' diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 41236f6dfd..65f3bd4897 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -112,7 +112,7 @@ def the_section_release_date_picker_not_visible(step): def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.css_text(css) - assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') + assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am') ############ HELPER METHODS ################### diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index cd4adb79fb..2dcf0d63fe 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -20,6 +20,7 @@ def i_press_the_button_on_the_registration_form(step): e = world.css_find(submit_css) e.type(' ') + @step('I should see be on the studio home page$') def i_should_see_be_on_the_studio_home_page(step): assert world.browser.find_by_css('div.inner-wrapper') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 52c10e41a8..88492d55e3 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -1,30 +1,30 @@ 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 + 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 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 + @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 + 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 diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 2094e65ccb..8695ea1c4f 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -72,6 +72,7 @@ def save_subsection_name(name): world.css_fill(name_css, name) world.css_click(save_css) + def see_subsection_name(name): css = 'span.subsection-name' assert world.is_css_present(css) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 2ac3befd82..85dfa85b37 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -122,6 +122,7 @@ def save_the_course_content(path='/tmp'): f.write(output) f.close + @world.absorb def clear_courses(): # Flush and initialize the module store diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index dc8d2f8b87..bf78a1d2b7 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -29,11 +29,13 @@ def i_visit_the_homepage(step): world.visit('/') assert world.is_css_present('header.global') + @step(u'I (?:visit|access|open) the dashboard$') def i_visit_the_dashboard(step): world.visit('/dashboard') assert world.is_css_present('section.container.dashboard') + @step('I should be on the dashboard page$') def i_should_be_on_the_dashboard(step): assert world.is_css_present('section.container.dashboard') @@ -97,6 +99,7 @@ def i_am_staff_for_course_by_id(step, course_id): def click_the_link_called(step, text): world.click_link(text) + @step(r'should see that the url is "([^"]*)"$') def should_have_the_url(step, url): assert_equals(world.browser.url, url) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index e2f701d089..6dadb976a7 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -12,10 +12,12 @@ from lettuce.django import django_url def wait(seconds): time.sleep(float(seconds)) + @world.absorb def wait_for(func): WebDriverWait(world.browser.driver, 5).until(func) + @world.absorb def visit(url): world.browser.visit(django_url(url)) @@ -30,23 +32,26 @@ def url_equals(url): def is_css_present(css_selector): return world.browser.is_element_present_by_css(css_selector, wait_time=4) + @world.absorb def css_has_text(css_selector, text): return world.css_text(css_selector) == text + @world.absorb def css_find(css): def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) world.browser.is_element_present_by_css(css, 5) wait_for(is_visible) return world.browser.find_by_css(css) + @world.absorb def css_click(css_selector): ''' - First try to use the regular click method, + First try to use the regular click method, but if clicking in the middle of an element doesn't work it might be that it thinks some other element is on top of it there so click in the upper left @@ -61,6 +66,7 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() + @world.absorb def css_click_at(css, x=10, y=10): ''' @@ -72,6 +78,7 @@ def css_click_at(css, x=10, y=10): e.action_chains.click() e.action_chains.perform() + @world.absorb def css_fill(css_selector, text): world.browser.find_by_css(css_selector).first.fill(text) diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 473f3f1572..c60ec7b374 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,7 +3,7 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -Scenario: I can navigate to all high -level tabs in a course +Scenario: I can navigate to all high - level tabs in a course Given: I am registered for the course "6.002x" And The course "6.002x" has extra tab "Custom Tab" And I am logged in From 0500ba4dd5e4a8563a31c6557f8ca331cdba8cfa Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 26 Mar 2013 11:17:56 -0400 Subject: [PATCH 110/665] Disabled pylint warnings for lettuce steps: * Missing docstring * Redefining name from outer scope --- cms/djangoapps/contentstore/features/advanced-settings.py | 3 +++ cms/djangoapps/contentstore/features/common.py | 3 +++ cms/djangoapps/contentstore/features/courses.py | 3 +++ cms/djangoapps/contentstore/features/section.py | 3 +++ cms/djangoapps/contentstore/features/signup.py | 3 +++ .../contentstore/features/studio-overview-togglesection.py | 3 +++ cms/djangoapps/contentstore/features/subsection.py | 3 +++ common/djangoapps/terrain/course_helpers.py | 3 +++ common/djangoapps/terrain/steps.py | 3 +++ common/djangoapps/terrain/ui_helpers.py | 3 +++ lms/djangoapps/courseware/features/common.py | 3 +++ lms/djangoapps/courseware/features/courseware.py | 3 +++ lms/djangoapps/courseware/features/courseware_common.py | 3 +++ lms/djangoapps/courseware/features/homepage.py | 3 +++ lms/djangoapps/courseware/features/login.py | 3 +++ lms/djangoapps/courseware/features/openended.py | 3 +++ lms/djangoapps/courseware/features/problems.py | 2 ++ lms/djangoapps/courseware/features/registration.py | 3 +++ lms/djangoapps/courseware/features/signup.py | 4 +++- lms/djangoapps/courseware/features/smart-accordion.py | 3 +++ lms/djangoapps/courseware/features/xqueue_setup.py | 4 +++- 21 files changed, 62 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index a2708d8c96..16562b6b15 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * import time diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 870ab89694..3878340af3 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_true from nose.tools import assert_equal diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index b3b6f91bdb..5da7720945 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 65f3bd4897..0c0f5536a0 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index 2dcf0d63fe..6ca358183b 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index dc22d3ad1a..7f717b731c 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_true, assert_false, assert_equal diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 8695ea1c4f..54f49f2fa6 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 85dfa85b37..f0df456c80 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from .factories import * from django.conf import settings diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index bf78a1d2b7..a8a32db173 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from .course_helpers import * from .ui_helpers import * diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 6dadb976a7..d4d99e17b5 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step import time from urllib import quote_plus diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index f015725ae9..f6256adfa1 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_equals, assert_in from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/courseware.py b/lms/djangoapps/courseware/features/courseware.py index 7e99cc9f55..234f3a84d2 100644 --- a/lms/djangoapps/courseware/features/courseware.py +++ b/lms/djangoapps/courseware/features/courseware.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 6aa9559e65..4e9aa3fb7b 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step diff --git a/lms/djangoapps/courseware/features/homepage.py b/lms/djangoapps/courseware/features/homepage.py index 442098c161..62e9096e70 100644 --- a/lms/djangoapps/courseware/features/homepage.py +++ b/lms/djangoapps/courseware/features/homepage.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_in diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 3e3c0efbc4..bc90ea301c 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import step, world from django.contrib.auth.models import User diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 2f14b808a3..d848eb55d7 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url from nose.tools import assert_equals, assert_in diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index bdd9062ef3..b25d606c4e 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,6 +2,8 @@ Steps for problem.feature lettuce tests ''' +#pylint: disable=C0111 +#pylint: disable=W0621 from lettuce import world, step from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 63f044b16f..72bde65f99 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url from common import TEST_COURSE_ORG, TEST_COURSE_NAME diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index d9edcb215b..5ba385ef54 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -1,5 +1,7 @@ -from lettuce import world, step +#pylint: disable=C0111 +#pylint: disable=W0621 +from lettuce import world, step @step('I fill in "([^"]*)" on the registration form with "([^"]*)"$') def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value): diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index 8240a13905..63408d7683 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from re import sub from nose.tools import assert_equals diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py index d6d7a13a5c..90a68961ee 100644 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ b/lms/djangoapps/courseware/features/xqueue_setup.py @@ -1,9 +1,11 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer from lettuce import before, after, world from django.conf import settings import threading - @before.all def setup_mock_xqueue_server(): From 586f566b4276b74756a0ce3bfe258ba979a45401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 26 Mar 2013 11:54:06 -0400 Subject: [PATCH 111/665] Use advertised_start as a simple string LMS Lighthouse [#297] --- common/lib/xmodule/xmodule/course_module.py | 12 ++++++++---- common/lib/xmodule/xmodule/fields.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index b1e5fa02c8..7999f8d6da 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -7,6 +7,8 @@ import requests import time from datetime import datetime +import dateutil.parser + from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.timeparse import parse_time @@ -150,7 +152,7 @@ class CourseFields(object): enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) start = Date(help="Start time when this module is visible", scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings) - advertised_start = StringOrDate(help="Date that this course is advertised to start", scope=Scope.settings) + advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): announcement = self.announcement if announcement is not None: announcement = to_datetime(announcement) - if self.advertised_start is None or isinstance(self.advertised_start, basestring): + + try: + start = dateutil.parser.parse(self.advertised_start) + except (ValueError, AttributeError): start = to_datetime(self.start) - else: - start = to_datetime(self.advertised_start) + now = to_datetime(time.gmtime()) return announcement, start, now diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 99ead854ad..0abe850d68 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -23,6 +23,8 @@ class Date(ModelType): """ if field is None: return field + elif field is "": + return None elif isinstance(field, basestring): d = dateutil.parser.parse(field) return d.utctimetuple() From 7c68508b85c0a31b0c4745172558d3075cde0a23 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Mar 2013 12:42:30 -0400 Subject: [PATCH 112/665] studio - finalized tender widget styling --- cms/static/sass/elements/_tender-widget.scss | 133 +++++++++++++++++-- cms/templates/widgets/tender.html | 1 + 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss index fce62b8675..4d2cdea373 100644 --- a/cms/static/sass/elements/_tender-widget.scss +++ b/cms/static/sass/elements/_tender-widget.scss @@ -23,7 +23,7 @@ #tender_closer { color: $blue-l2 !important; - margin-top: 15px; + margin-top: 10px; margin-right: 5px; text-transform: uppercase; @@ -66,6 +66,7 @@ .widget-layout .content { overflow: auto; + height: auto !important; padding: 20px; } @@ -110,10 +111,20 @@ width: 97%; } +.widget-layout p.note { + text-align: right !important; + display: inline-block !important; + position: absolute !important; + right: -130px !important; + top: -5px !important; + font-size: 13px !important; + opacity: 0.80; +} + .widget-layout .form-actions { - border-top: 1px solid #ccc; - margin-top: 10px; - padding-top: 10px; + margin: 15px 0; + border: none; + padding: 0; } .widget-layout dl.form { @@ -124,19 +135,119 @@ padding-bottom: 10px; } -.widget-layout #brain_buster_captcha { +.widget-layout dl.form:last-child { + border: none; + padding-bottom: 0; + margin-bottom: 20px; +} +.widget-layout dl.form dt, .widget-layout dl.form dd { + display: inline-block; + vertical-align: middle; +} + +.widget-layout dl.form dt { + margin-right: 15px; + width: 70px; +} + +.widget-layout dl.form dd { + width: 65%; + position: relative; } // specific elements .widget-layout #discussion_body { + +} + +.widget-layout #discussion_body:before { + content: "What Question or Feedback Would You Like to Share?"; + display: block; + font-size: 14px; + margin-bottom: 5px; + color: #4c4c4c; + font-weight: 500; +} + + +.widget-layout dl#brain_buster_captcha { + float: none; + width: 100%; + border-top: 1px solid #f2f2f2; + margin-top: 10px; + padding-top: 10px; +} + +.widget-layout dl#brain_buster_captcha dd { + display: block !important; +} + +.widget-layout dl#brain_buster_captcha dd label { + display: block; + margin: 0 15px 0 0 !important; +} + +.widget-layout dl#brain_buster_captcha dd #captcha_answer { + display: block; + width: 97%%; +} + +.widget-layout .form-actions .btn-post_topic { + display: block; + width: 100%; + height: auto !important; + font-size: 16px; + font-weight: 700; + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + -webkit-transition-property: background-color,0.15s; + -moz-transition-property: background-color,0.15s; + -ms-transition-property: background-color,0.15s; + -o-transition-property: background-color,0.15s; + transition-property: background-color,0.15s; + -webkit-transition-duration: box-shadow,0.15s; + -moz-transition-duration: box-shadow,0.15s; + -ms-transition-duration: box-shadow,0.15s; + -o-transition-duration: box-shadow,0.15s; + transition-duration: box-shadow,0.15s; + -webkit-transition-timing-function: ease-out; + -moz-transition-timing-function: ease-out; + -ms-transition-timing-function: ease-out; + -o-transition-timing-function: ease-out; + transition-timing-function: ease-out; + -webkit-transition-delay: 0; + -moz-transition-delay: 0; + -ms-transition-delay: 0; + -o-transition-delay: 0; + transition-delay: 0; + border: 1px solid #34854c; + border-radius: 3px; + background-color: rgba(255,255,255,0.3); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.3)),color-stop(100%, rgba(255,255,255,0))); + background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -moz-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -ms-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -o-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-color: #25b85a; + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + color: #fff; + text-align: center; + margin-top: 20px; + padding: 10px 20px; +} + +.widget-layout .form-actions #private-discussion-opt { + float: none; + text-align: left; margin: 0 0 15px 0; } -.widget-layout .category dt, .widget-layout .category dd { - display: inline-block !important; -} - -.widget-layout .category dt { - margin-right: 15px !important; +.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active { + background-color: #16ca57; + color: #fff; } \ No newline at end of file diff --git a/cms/templates/widgets/tender.html b/cms/templates/widgets/tender.html index 300b71701c..27cc574490 100644 --- a/cms/templates/widgets/tender.html +++ b/cms/templates/widgets/tender.html @@ -1,5 +1,6 @@ % if user.is_authenticated(): Provide Feedback + @@ -53,11 +51,14 @@ document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); - <%block name="content"> - <%include file="widgets/footer.html" /> +
    + <%include file="widgets/header.html" /> + <%block name="content"> + <%include file="widgets/sock.html" /> + <%include file="widgets/footer.html" /> +
    + <%include file="widgets/tender.html" /> <%block name="jsextra"> - - - + \ No newline at end of file diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index f9166bf166..cbf436ecc5 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -80,5 +80,4 @@
    -
    \ No newline at end of file diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 1cf9b17710..7a819fceba 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -151,7 +151,7 @@
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    - + close modal @@ -164,7 +164,7 @@
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    - + close modal @@ -177,7 +177,7 @@
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    - + close modal diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index a3674cfe20..9ff98fa26b 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -16,13 +16,13 @@ --> diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html new file mode 100644 index 0000000000..ff5f9c9ad4 --- /dev/null +++ b/cms/templates/widgets/sock.html @@ -0,0 +1,8 @@ +<%! from django.core.urlresolvers import reverse %> +% if user.is_authenticated(): +
    +
    +

    Sock!

    +
    +
    +% endif \ No newline at end of file From 25acab497e05ab6d9883726d0cba2ec5146fa6ae Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:33:04 -0400 Subject: [PATCH 131/665] studio - corrected JQ selector for smoothscrolling in-page links --- cms/static/js/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7135e2780c..211981b05a 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -81,7 +81,7 @@ $(document).ready(function () { }); // general link management - smooth scrolling page links - $('a[rel*="view"][href|="#"]').bind('click', smoothScrollLink); + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); // tender feedback window scrolling $('a.show-tender').bind('click', smoothScrollTop); From 2120481738489d872db41916b75b0336a77a3a9e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:34:25 -0400 Subject: [PATCH 132/665] studio - corrected JQ selector for smoothscrolling in-page links --- cms/static/js/base.js | 4 ++-- cms/templates/howitworks.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bd8dc0bae8..7466233331 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -81,7 +81,7 @@ $(document).ready(function () { }); // general link management - smooth scrolling page links - $('a[rel*="view"]').bind('click', linkSmoothScroll); + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); // toggling overview section details @@ -148,7 +148,7 @@ $(document).ready(function () { }); }); -function linkSmoothScroll(e) { +function smoothScrollLink(e) { (e).preventDefault(); $.smoothScroll({ diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 1cf9b17710..7a819fceba 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -151,7 +151,7 @@
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    - + close modal @@ -164,7 +164,7 @@
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    - + close modal @@ -177,7 +177,7 @@
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    - + close modal From 74439746cc804fc2dfe9bc6b679f0a714093aa74 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:38:36 -0400 Subject: [PATCH 133/665] studio - made provide feedback conditional for logged in users --- cms/templates/widgets/footer.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 9ff98fa26b..18ecf2bc39 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -18,9 +18,11 @@ + % if user.is_authenticated(): + + % endif From e3c646492c1147f74fec0e9df45f58f1c9fe892e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:39:24 -0400 Subject: [PATCH 134/665] studio - made provide feedback conditional for logged in users --- cms/templates/widgets/footer.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index a3674cfe20..c0cf8f73a6 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -18,9 +18,11 @@ + % if user.is_authenticated(): + + % endif From 2c0e5b82ff2535770a5ca605aa1b1bd521c756d4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 07:29:22 -0400 Subject: [PATCH 135/665] Return a 403 when an anonymous user attempts to hit modx_dispatch. Fixes https://www.pivotaltracker.com/story/show/46916015 and https://www.pivotaltracker.com/story/show/46916029 --- lms/djangoapps/courseware/module_render.py | 4 +++ .../courseware/tests/test_module_render.py | 31 +++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..4747f7b341 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -8,6 +8,7 @@ from functools import partial from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.http import Http404 from django.http import HttpResponse @@ -412,6 +413,9 @@ def modx_dispatch(request, dispatch, location, course_id): if not Location.is_valid(location): raise Http404("Invalid location") + if not request.user.is_authenticated(): + raise PermissionDenied + # Check for submitted files and basic file size checks p = request.POST.copy() if request.FILES: diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 3a3a7ac5ea..90ca796a2f 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1,14 +1,7 @@ -import logging -from mock import MagicMock, patch +from mock import MagicMock import json -import factory -import unittest -from nose.tools import set_trace -from django.http import Http404, HttpResponse, HttpRequest -from django.conf import settings -from django.contrib.auth.models import User -from django.test.client import Client +from django.http import Http404, HttpResponse from django.conf import settings from django.test import TestCase from django.test.client import RequestFactory @@ -16,13 +9,9 @@ from django.core.urlresolvers import reverse from django.test.utils import override_settings from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.exceptions import NotFoundError -from xmodule.modulestore import Location import courseware.module_render as render -from xmodule.modulestore.django import modulestore, _MODULESTORES -from xmodule.seq_module import SequenceModule +from xmodule.modulestore.django import modulestore from courseware.tests.tests import PageLoader -from student.models import Registration from courseware.model_data import ModelDataCache from .factories import UserFactory @@ -52,7 +41,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) class ModuleRenderTestCase(PageLoader): def setUp(self): self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview'] - self._MODULESTORES = {} self.course_id = 'edX/toy/2012_Fall' self.toy_course = modulestore().get_course(self.course_id) @@ -104,12 +92,23 @@ class ModuleRenderTestCase(PageLoader): self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect') + def test_anonymous_modx_dispatch(self): + dispatch_url = reverse( + 'modx_dispatch', + args=[ + 'edX/toy/2012_Fall', + 'i4x://edX/toy/videosequence/Toy_Videos', + 'goto_position' + ] + ) + response = self.client.post(dispatch_url, {'position': 2}) + self.assertEquals(403, response.status_code) + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestTOC(TestCase): """Check the Table of Contents for a course""" def setUp(self): - self._MODULESTORES = {} # Toy courses should be loaded self.course_name = 'edX/toy/2012_Fall' From 521843876efb005303e8ff7423442eb9830ab99e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 08:10:25 -0400 Subject: [PATCH 136/665] Make the django_comment_client return errors that can't be parsed as JSON just as simple strings when in an ajax context --- .../django_comment_client/middleware.py | 16 +++++++++- .../tests/test_middleware.py | 32 +++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/django_comment_client/middleware.py b/lms/djangoapps/django_comment_client/middleware.py index abf2d40cab..b9efc1589e 100644 --- a/lms/djangoapps/django_comment_client/middleware.py +++ b/lms/djangoapps/django_comment_client/middleware.py @@ -1,10 +1,24 @@ from comment_client import CommentClientError from django_comment_client.utils import JsonError import json +import logging + +log = logging.getLogger(__name__) class AjaxExceptionMiddleware(object): + """ + Middleware that captures CommentClientErrors during ajax requests + and tranforms them into json responses + """ def process_exception(self, request, exception): + """ + Processes CommentClientErrors in ajax requests. If the request is an ajax request, + returns a http response that encodes the error as json + """ if isinstance(exception, CommentClientError) and request.is_ajax(): - return JsonError(json.loads(exception.message)) + try: + return JsonError(json.loads(exception.message)) + except ValueError: + return JsonError(exception.message) return None diff --git a/lms/djangoapps/django_comment_client/tests/test_middleware.py b/lms/djangoapps/django_comment_client/tests/test_middleware.py index 55e4c72c75..ab9517c160 100644 --- a/lms/djangoapps/django_comment_client/tests/test_middleware.py +++ b/lms/djangoapps/django_comment_client/tests/test_middleware.py @@ -1,7 +1,3 @@ -import string -import random -import collections - from django.test import TestCase import comment_client @@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase): # TODO: check whether the correct error message is produced. # The error message should be the same as the argument to CommentClientError - def setUp(self): - self.a = middleware.AjaxExceptionMiddleware() - self.request1 = django.http.HttpRequest() - self.request0 = django.http.HttpRequest() - self.exception1 = comment_client.CommentClientError('{}') - self.exception0 = ValueError() - self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" - self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX" + def setUp(self): + self.a = middleware.AjaxExceptionMiddleware() + self.request1 = django.http.HttpRequest() + self.request0 = django.http.HttpRequest() + self.exception1 = comment_client.CommentClientError('{}') + self.exception2 = comment_client.CommentClientError('Foo!') + self.exception0 = ValueError() + self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" + self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX" - def test_process_exception(self): - self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError) - self.assertIsNone(self.a.process_exception(self.request1, self.exception0)) - self.assertIsNone(self.a.process_exception(self.request0, self.exception1)) - self.assertIsNone(self.a.process_exception(self.request0, self.exception0)) + def test_process_exception(self): + self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError) + self.assertIsInstance(self.a.process_exception(self.request1, self.exception2), middleware.JsonError) + self.assertIsNone(self.a.process_exception(self.request1, self.exception0)) + self.assertIsNone(self.a.process_exception(self.request0, self.exception1)) + self.assertIsNone(self.a.process_exception(self.request0, self.exception0)) From 285e3ee1edfbb5cbb22f821b05b7a752aab25c73 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 10:49:47 -0400 Subject: [PATCH 137/665] Capa response now displays full stack trace on student input error if the user is a staff member. Otherwise, it displays just the exception message. --- common/lib/capa/capa/responsetypes.py | 7 +++--- common/lib/xmodule/xmodule/capa_module.py | 16 +++++++++++- .../xmodule/xmodule/tests/test_capa_module.py | 25 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 2c556211f8..465c212b30 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -17,6 +17,7 @@ import logging import numbers import numpy import os +import sys import random import re import requests @@ -1233,9 +1234,9 @@ def sympy_check2(): log.debug(msg, exc_info=True) log.debug(traceback.format_exc()) - # Notify student - raise StudentInputError( - "Error: Problem could not be evaluated with your input") + # Notify student with a student input error + _, _, traceback_obj = sys.exc_info() + raise StudentInputError, StudentInputError(err.message), traceback_obj #----------------------------------------------------------------------------- diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index da8b5b4f96..203e14fdc1 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -725,9 +725,23 @@ class CapaModule(CapaFields, XModule): try: correct_map = self.lcp.grade_answers(answers) self.set_state_from_lcp() + except StudentInputError as inst: log.exception("StudentInputError in capa_module:problem_check") - return {'success': inst.message} + + # If the user is a staff member, include + # the full exception, including traceback, + # in the response + if self.system.user_is_staff: + msg = traceback.format_exc() + + # Otherwise, display just the error message, + # without a stack trace + else: + msg = inst.message + + return {'success': msg } + except Exception, err: if self.system.DEBUG: msg = "Error checking problem: " + str(err) diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index d2458cb3d0..d769b65914 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -505,6 +505,9 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_student_input_error(self): module = CapaFactory.create(attempts=1) + # Ensure that the user is NOT staff + module.system.user_is_staff = False + # Simulate a student input exception with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') @@ -515,10 +518,32 @@ class CapaModuleTest(unittest.TestCase): # Expect an AJAX alert message in 'success' self.assertTrue('test error' in result['success']) + # We do NOT include traceback information for + # a non-staff user + self.assertFalse('Traceback' in result['success']) + # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_student_input_error_with_staff_user(self): + module = CapaFactory.create(attempts=1) + # Ensure that the user IS staff + module.system.user_is_staff = True + + # Simulate a student input exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + + get_request_dict = { CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + self.assertTrue('test error' in result['success']) + + # We DO include traceback information for staff users + self.assertTrue('Traceback' in result['success']) + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) From 754e30240d7c62c7efd80c38d552c4d168d23262 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 11:01:25 -0400 Subject: [PATCH 138/665] studio - adjusting tender widget window height based on field removal --- cms/static/sass/elements/_tender-widget.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss index 146d5b4111..478489d0e8 100644 --- a/cms/static/sass/elements/_tender-widget.scss +++ b/cms/static/sass/elements/_tender-widget.scss @@ -9,6 +9,7 @@ #tender_window { @include border-radius(3px); @include box-shadow(0 2px 3px $shadow); + height: 650px !important; background: $white !important; border: 1px solid $gray; } @@ -72,7 +73,7 @@ .widget-layout .flash { margin: -10px 0 15px 0; - padding: 5px 10px !important; + padding: 10px 20px !important; background-image: none !important; } From 8252ba15df79f3f2b213d8afed58c3a152f0bb2b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:02:30 -0400 Subject: [PATCH 139/665] Changed error message for StudentInputError for non-staff to a generic message. Otherwise, the default exception messages are cryptic for students (e.g. "cannot convert string to float") --- common/lib/xmodule/xmodule/capa_module.py | 4 ++-- common/lib/xmodule/xmodule/tests/test_capa_module.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 203e14fdc1..c3159bb3ee 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -735,10 +735,10 @@ class CapaModule(CapaFields, XModule): if self.system.user_is_staff: msg = traceback.format_exc() - # Otherwise, display just the error message, + # Otherwise, display just an error message, # without a stack trace else: - msg = inst.message + msg = "Error: Problem could not be evaluated with your input" return {'success': msg } diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index d769b65914..b5e1ff311c 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -516,11 +516,8 @@ class CapaModuleTest(unittest.TestCase): result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' - self.assertTrue('test error' in result['success']) - - # We do NOT include traceback information for - # a non-staff user - self.assertFalse('Traceback' in result['success']) + expected_msg = 'Error: Problem could not be evaluated with your input' + self.assertEqual(expected_msg, result['success']) # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) From 5bc44e50da28ca31f10bbf447fd112c948717f86 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:13:31 -0400 Subject: [PATCH 140/665] Changed error messages to account for NumericalResponse formatting, which is the only other response type to use StudentInputError. --- common/lib/capa/capa/responsetypes.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 2 +- common/lib/xmodule/xmodule/tests/test_capa_module.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 465c212b30..08cfa8b9d9 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -834,7 +834,7 @@ class NumericalResponse(LoncapaResponse): import sys type, value, traceback = sys.exc_info() - raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" % + raise StudentInputError, ("Could not interpret '%s' as a number" % cgi.escape(student_answer)), traceback if correct: diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index c3159bb3ee..773ae73d59 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -738,7 +738,7 @@ class CapaModule(CapaFields, XModule): # Otherwise, display just an error message, # without a stack trace else: - msg = "Error: Problem could not be evaluated with your input" + msg = "Error: %s" % str(inst.message) return {'success': msg } diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index b5e1ff311c..3617086f85 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -516,7 +516,7 @@ class CapaModuleTest(unittest.TestCase): result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' - expected_msg = 'Error: Problem could not be evaluated with your input' + expected_msg = 'Error: test error' self.assertEqual(expected_msg, result['success']) # Expect that the number of attempts is NOT incremented From 0f5e8c5f3bb8acbe8b4396ab172ca1740b7b89fd Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:17:21 -0400 Subject: [PATCH 141/665] pep8 fixes --- common/lib/capa/capa/responsetypes.py | 8 ++--- common/lib/xmodule/xmodule/capa_module.py | 10 +++--- .../xmodule/xmodule/tests/test_capa_module.py | 32 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 08cfa8b9d9..e79399c5fc 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1141,9 +1141,9 @@ def sympy_check2(): correct = [] messages = [] for input_dict in input_list: - correct.append('correct' + correct.append('correct' if input_dict['ok'] else 'incorrect') - msg = (self.clean_message_html(input_dict['msg']) + msg = (self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None) messages.append(msg) @@ -1168,7 +1168,7 @@ def sympy_check2(): correct_map.set_overall_message(overall_message) for k in range(len(idset)): - npoints = (self.maxpoints[idset[k]] + npoints = (self.maxpoints[idset[k]] if correct[k] == 'correct' else 0) correct_map.set(idset[k], correct[k], msg=messages[k], npoints=npoints) @@ -2085,7 +2085,7 @@ class AnnotationResponse(LoncapaResponse): option_scoring = dict([(option['id'], { 'correctness': choices.get(option['choice']), 'points': scoring.get(option['choice']) - }) for option in self._find_options(inputfield) ]) + }) for option in self._find_options(inputfield)]) scoring_map[inputfield.get('id')] = option_scoring diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 773ae73d59..af29c4c2fe 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -576,7 +576,7 @@ class CapaModule(CapaFields, XModule): # save any state changes that may occur self.set_state_from_lcp() return response - + def get_answer(self, get): ''' @@ -731,7 +731,7 @@ class CapaModule(CapaFields, XModule): # If the user is a staff member, include # the full exception, including traceback, - # in the response + # in the response if self.system.user_is_staff: msg = traceback.format_exc() @@ -740,7 +740,7 @@ class CapaModule(CapaFields, XModule): else: msg = "Error: %s" % str(inst.message) - return {'success': msg } + return {'success': msg} except Exception, err: if self.system.DEBUG: @@ -792,7 +792,7 @@ class CapaModule(CapaFields, XModule): event_info['answers'] = answers # Too late. Cannot submit - if self.closed() and not self.max_attempts ==0: + if self.closed() and not self.max_attempts == 0: event_info['failure'] = 'closed' self.system.track_function('save_problem_fail', event_info) return {'success': False, @@ -812,7 +812,7 @@ class CapaModule(CapaFields, XModule): self.system.track_function('save_problem_success', event_info) msg = "Your answers have been saved" - if not self.max_attempts ==0: + if not self.max_attempts == 0: msg += " but not graded. Hit 'Check' to grade them." return {'success': True, 'msg': msg} diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 3617086f85..18d20a2756 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -407,7 +407,7 @@ class CapaModuleTest(unittest.TestCase): mock_html.return_value = "Test HTML" # Check the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -428,7 +428,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_correct.return_value = False # Check the problem - get_request_dict = { CapaFactory.input_key(): '0'} + get_request_dict = {CapaFactory.input_key(): '0'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -446,7 +446,7 @@ class CapaModuleTest(unittest.TestCase): with patch('xmodule.capa_module.CapaModule.closed') as mock_closed: mock_closed.return_value = True with self.assertRaises(xmodule.exceptions.NotFoundError): - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} module.check_problem(get_request_dict) # Expect that number of attempts NOT incremented @@ -492,7 +492,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_queued.return_value = True mock_get_queuetime.return_value = datetime.datetime.now() - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -512,7 +512,7 @@ class CapaModuleTest(unittest.TestCase): with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -532,7 +532,7 @@ class CapaModuleTest(unittest.TestCase): with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -540,7 +540,7 @@ class CapaModuleTest(unittest.TestCase): # We DO include traceback information for staff users self.assertTrue('Traceback' in result['success']) - + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) @@ -595,11 +595,11 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(done=False) # Save the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that answers are saved to the problem - expected_answers = { CapaFactory.answer_key(): '3.14'} + expected_answers = {CapaFactory.answer_key(): '3.14'} self.assertEqual(module.lcp.student_answers, expected_answers) # Expect that the result is success @@ -614,7 +614,7 @@ class CapaModuleTest(unittest.TestCase): mock_closed.return_value = True # Try to save the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that the result is failure @@ -625,7 +625,7 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize='always', done=True) # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we cannot save @@ -636,7 +636,7 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize='never', done=True) # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we succeed @@ -648,7 +648,7 @@ class CapaModuleTest(unittest.TestCase): # Just in case, we also check what happens if we have # more attempts than allowed. attempts = random.randint(1, 10) - module = CapaFactory.create(attempts=attempts -1, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 1, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Final Check") module = CapaFactory.create(attempts=attempts, max_attempts=attempts) @@ -658,14 +658,14 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.check_button_name(), "Final Check") # Otherwise, button name is "Check" - module = CapaFactory.create(attempts=attempts -2, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 2, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") - module = CapaFactory.create(attempts=attempts -3, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 3, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") # If no limit on attempts, then always show "Check" - module = CapaFactory.create(attempts=attempts -3) + module = CapaFactory.create(attempts=attempts - 3) self.assertEqual(module.check_button_name(), "Check") module = CapaFactory.create(attempts=0) From 6edee96caf528b73f2d9c097800ba8b867942de2 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:24:16 -0400 Subject: [PATCH 142/665] Added "Staff Debug Info" prefix to traceback message. --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index af29c4c2fe..d7346faa67 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -733,7 +733,7 @@ class CapaModule(CapaFields, XModule): # the full exception, including traceback, # in the response if self.system.user_is_staff: - msg = traceback.format_exc() + msg = "Staff debug info: %s" % traceback.format_exc() # Otherwise, display just an error message, # without a stack trace From 7101c76016e4c42b18ba5858556b836da6fde66b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 27 Mar 2013 12:02:32 -0400 Subject: [PATCH 143/665] comment on rewrite links change --- .../combined_open_ended_modulev1.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index f88fd9ab82..6fe37b9525 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -365,6 +365,10 @@ class CombinedOpenEndedV1Module(): html = self.current_task.get_html(self.system) return_html = html try: + #Without try except block, get this error: + # File "/home/vik/mitx_all/mitx/common/lib/xmodule/xmodule/x_module.py", line 263, in rewrite_content_links + # if link.startswith(XASSET_SRCREF_PREFIX): + # Placing try except so that if the error is fixed, this code will start working again. return_html = rewrite_links(html, self.rewrite_content_links) except: pass @@ -786,7 +790,7 @@ class CombinedOpenEndedV1Descriptor(): template_dir_name = "combinedopenended" def __init__(self, system): - self.system =system + self.system = system @classmethod def definition_from_xml(cls, xml_object, system): From 3a4bdf19fb6733dae57b557223481601dbbe8efb Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 12:26:49 -0400 Subject: [PATCH 144/665] studio - tweaking footer navigation for tender widget label --- cms/templates/widgets/footer.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index c0cf8f73a6..c5fc81957f 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,12 +20,9 @@ % if user.is_authenticated(): - % endif - + % endif From 15ea32b095abe4d033075640121ad418ced0179d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 12:53:58 -0400 Subject: [PATCH 145/665] Fixed bug 294, caused by unicode encoding error when creating logging strings. Added unit tests that verify the fix. --- common/djangoapps/student/tests/test_login.py | 107 ++++++++++++++++++ common/djangoapps/student/views.py | 8 +- 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 common/djangoapps/student/tests/test_login.py diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py new file mode 100644 index 0000000000..dda58a4462 --- /dev/null +++ b/common/djangoapps/student/tests/test_login.py @@ -0,0 +1,107 @@ +from django.test import TestCase +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from student.models import Registration, UserProfile +import json + +class LoginTest(TestCase): + ''' + Test student.views.login_user() view + ''' + + def setUp(self): + + # Create one user and save it to the database + self.user = User.objects.create_user('test', 'test@edx.org', 'test_password') + self.user.is_active = True + self.user.save() + + # Create a registration for the user + Registration().register(self.user) + + # Create a profile for the user + UserProfile(user=self.user).save() + + # Create the test client + self.client = Client() + + # Store the login url + self.url = reverse('login') + + def test_login_success(self): + response = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=True) + + def test_login_success_unicode_email(self): + unicode_email = u'test@edx.org' + unichr(40960) + + self.user.email = unicode_email + self.user.save() + + response = self._login_response(unicode_email, 'test_password') + self._assert_response(response, success=True) + + + def test_login_fail_no_user_exists(self): + response = self._login_response('not_a_user@edx.org', 'test_password') + self._assert_response(response, success=False, + value='Email or password is incorrect') + + def test_login_fail_wrong_password(self): + response = self._login_response('test@edx.org', 'wrong_password') + self._assert_response(response, success=False, + value='Email or password is incorrect') + + def test_login_not_activated(self): + + # De-activate the user + self.user.is_active = False + self.user.save() + + # Should now be unable to login + response = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=False, + value="This account has not been activated") + + + def test_login_unicode_email(self): + unicode_email = u'test@edx.org' + unichr(40960) + response = self._login_response(unicode_email, 'test_password') + self._assert_response(response, success=False) + + def test_login_unicode_password(self): + unicode_password = u'test_password' + unichr(1972) + response = self._login_response('test@edx.org', unicode_password) + self._assert_response(response, success=False) + + def _login_response(self, email, password): + post_params = {'email': email, 'password': password} + return self.client.post(self.url, post_params) + + def _assert_response(self, response, success=None, value=None): + ''' + Assert that the response had status 200 and returned a valid + JSON-parseable dict. + + If success is provided, assert that the response had that + value for 'success' in the JSON dict. + + If value is provided, assert that the response contained that + value for 'value' in the JSON dict. + ''' + self.assertEqual(response.status_code, 200) + + try: + response_dict = json.loads(response.content) + except ValueError: + self.fail("Could not parse response content as JSON: %s" + % str(response.content)) + + if success is not None: + self.assertEqual(response_dict['success'], success) + + if value is not None: + msg = ("'%s' did not contain '%s'" % + (str(response_dict['value']), str(value))) + self.assertTrue(value in response_dict['value'], msg) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 5dbaf5d2c2..84730421e8 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -369,14 +369,14 @@ def login_user(request, error=""): try: user = User.objects.get(email=email) except User.DoesNotExist: - log.warning("Login failed - Unknown user email: {0}".format(email)) + log.warning(u"Login failed - Unknown user email: {0}".format(email)) return HttpResponse(json.dumps({'success': False, 'value': 'Email or password is incorrect.'})) # TODO: User error message username = user.username user = authenticate(username=username, password=password) if user is None: - log.warning("Login failed - password for {0} is invalid".format(email)) + log.warning(u"Login failed - password for {0} is invalid".format(email)) return HttpResponse(json.dumps({'success': False, 'value': 'Email or password is incorrect.'})) @@ -392,7 +392,7 @@ def login_user(request, error=""): log.critical("Login failed - Could not create session. Is memcached running?") log.exception(e) - log.info("Login success - {0} ({1})".format(username, email)) + log.info(u"Login success - {0} ({1})".format(username, email)) try_change_enrollment(request) @@ -400,7 +400,7 @@ def login_user(request, error=""): return HttpResponse(json.dumps({'success': True})) - log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) + log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username)) reactivation_email_for_user(user) not_activated_msg = "This account has not been activated. We have " + \ From 227a5e8266ddc72e9719eb2b6035a12ee0788c56 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 12:56:06 -0400 Subject: [PATCH 146/665] Delete converters, move unit tests to test_fields, add new additional test cases. --- .../tests/test_course_settings.py | 75 +++-------------- .../models/settings/course_details.py | 28 +++++-- .../models/settings/course_grading.py | 2 - common/djangoapps/util/converters.py | 37 --------- common/lib/xmodule/xmodule/fields.py | 1 - .../lib/xmodule/xmodule/tests/test_fields.py | 80 +++++++++++++++++++ 6 files changed, 113 insertions(+), 110 deletions(-) delete mode 100644 common/djangoapps/util/converters.py create mode 100644 common/lib/xmodule/xmodule/tests/test_fields.py diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2e7bc5db83..fe90ad18aa 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,8 +1,6 @@ import datetime import json import copy -from util import converters -from util.converters import jsdate_to_time from django.contrib.auth.models import User from django.test.client import Client @@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails, from models.settings.course_grading import CourseGradingModel from contentstore.utils import get_modulestore -from django.test import TestCase from .utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore -import time - - -# YYYY-MM-DDThh:mm:ss.s+/-HH:MM -class ConvertersTestCase(TestCase): - @staticmethod - def struct_to_datetime(struct_time): - return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, - struct_time.tm_mday, struct_time.tm_hour, - struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) - - def compare_dates(self, date1, date2, expected_delta): - dt1 = ConvertersTestCase.struct_to_datetime(date1) - dt2 = ConvertersTestCase.struct_to_datetime(date2) - self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" - + str(date2) + "!=" + str(expected_delta)) - - def test_iso_to_struct(self): - '''Test conversion from iso compatible date strings to struct_time''' - self.compare_dates(converters.jsdate_to_time("2013-01-01"), - converters.jsdate_to_time("2012-12-31"), - datetime.timedelta(days=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), - converters.jsdate_to_time("2012-12-31T23"), - datetime.timedelta(hours=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), - converters.jsdate_to_time("2012-12-31T23:59"), - datetime.timedelta(minutes=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), - converters.jsdate_to_time("2012-12-31T23:59:59"), - datetime.timedelta(seconds=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"), - converters.jsdate_to_time("2012-12-31T23:59:59Z"), - datetime.timedelta(seconds=1)) - self.compare_dates( - converters.jsdate_to_time("2012-12-31T23:00:01-01:00"), - converters.jsdate_to_time("2013-01-01T00:00:00+01:00"), - datetime.timedelta(hours=1, seconds=1)) - - def test_struct_to_iso(self): - ''' - Test converting time reprs to iso dates - ''' - self.assertEqual( - converters.time_to_isodate( - time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:59:59Z")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:00:01-01:00")), - "2013-01-01T00:00:01Z") - +from xmodule.fields import Date class CourseTestCase(ModuleStoreTestCase): def setUp(self): @@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") + @staticmethod + def struct_to_datetime(struct_time): + return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, + struct_time.tm_mday, struct_time.tm_hour, + struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) + def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: + date = Date() if field in encoded and encoded[field] is not None: - encoded_encoded = jsdate_to_time(encoded[field]) - dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) + encoded_encoded = date.from_json(encoded[field]) + dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) if isinstance(details[field], datetime.datetime): dt2 = details[field] else: - details_encoded = jsdate_to_time(details[field]) - dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) + details_encoded = date.from_json(details[field]) + dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded) expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index d3cd5fe164..09d57774ab 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -1,14 +1,14 @@ -from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata import json from json.encoder import JSONEncoder import time +import calendar from contentstore.utils import get_modulestore -from util.converters import jsdate_to_time, time_to_date from models.settings import course_grading from contentstore.utils import update_item +from xmodule.fields import Date import re import logging @@ -81,8 +81,14 @@ class CourseDetails(object): dirty = False + # In the descriptor's setter, the date is converted to JSON using Date's to_json method. + # Calling to_json on something that is already JSON doesn't work. Since reaching directly + # into the model is nasty, convert the JSON Date to a Python date, which is what the + # setter expects as input. + date = Date() + if 'start_date' in jsondict: - converted = jsdate_to_time(jsondict['start_date']) + converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: @@ -90,7 +96,7 @@ class CourseDetails(object): descriptor.start = converted if 'end_date' in jsondict: - converted = jsdate_to_time(jsondict['end_date']) + converted = date.from_json(jsondict['end_date']) else: converted = None @@ -99,7 +105,7 @@ class CourseDetails(object): descriptor.end = converted if 'enrollment_start' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_start']) + converted = date.from_json(jsondict['enrollment_start']) else: converted = None @@ -108,7 +114,7 @@ class CourseDetails(object): descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_end']) + converted = date.from_json(jsondict['enrollment_end']) else: converted = None @@ -172,12 +178,20 @@ class CourseDetails(object): # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): + @staticmethod + def time_to_date(time_obj): + """ + Convert a time.time_struct to a true universal time (can pass to js Date + constructor) + """ + return calendar.timegm(time_obj) * 1000 + def default(self, obj): if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() elif isinstance(obj, time.struct_time): - return time_to_date(obj) + return CourseSettingsEncoder.time_to_date(obj) else: return JSONEncoder.default(self, obj) diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index b20fb71f66..ee9b4ac0eb 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -1,7 +1,5 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore -import re -from util import converters from datetime import timedelta diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py deleted file mode 100644 index 212cceb77d..0000000000 --- a/common/djangoapps/util/converters.py +++ /dev/null @@ -1,37 +0,0 @@ -import time -import datetime -import calendar -import dateutil.parser - - -def time_to_date(time_obj): - """ - Convert a time.time_struct to a true universal time (can pass to js Date - constructor) - """ - return calendar.timegm(time_obj) * 1000 - - -def time_to_isodate(source): - '''Convert to an iso date''' - if isinstance(source, time.struct_time): - return time.strftime('%Y-%m-%dT%H:%M:%SZ', source) - elif isinstance(source, datetime): - return source.isoformat() + 'Z' - - -def jsdate_to_time(field): - """ - Convert a universal time (iso format) or msec since epoch to a time obj - """ - if field is None: - return field - elif isinstance(field, basestring): - d = dateutil.parser.parse(field) - return d.utctimetuple() - elif isinstance(field, (int, long, float)): - return time.gmtime(field / 1000) - elif isinstance(field, time.struct_time): - return field - else: - raise ValueError("Couldn't convert %r to time" % field) diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 0abe850d68..ea857933fc 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -14,7 +14,6 @@ class Date(ModelType): ''' Date fields know how to parse and produce json (iso) compatible formats. ''' - # NB: these are copies of util.converters.* def from_json(self, field): """ Parse an optional metadata key containing a time: if present, complain diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py new file mode 100644 index 0000000000..7c8872efc1 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -0,0 +1,80 @@ +"""Tests for Date class defined in fields.py.""" +import datetime +import unittest +from django.utils.timezone import UTC +from xmodule.fields import Date +import time + +class DateTest(unittest.TestCase): + date = Date() + + @staticmethod + def struct_to_datetime(struct_time): + return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, + struct_time.tm_mday, struct_time.tm_hour, + struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) + + def compare_dates(self, date1, date2, expected_delta): + dt1 = DateTest.struct_to_datetime(date1) + dt2 = DateTest.struct_to_datetime(date2) + self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + + str(date2) + "!=" + str(expected_delta)) + + def test_from_json(self): + '''Test conversion from iso compatible date strings to struct_time''' + self.compare_dates( + DateTest.date.from_json("2013-01-01"), + DateTest.date.from_json("2012-12-31"), + datetime.timedelta(days=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00"), + DateTest.date.from_json("2012-12-31T23"), + datetime.timedelta(hours=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00"), + DateTest.date.from_json("2012-12-31T23:59"), + datetime.timedelta(minutes=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00:00"), + DateTest.date.from_json("2012-12-31T23:59:59"), + datetime.timedelta(seconds=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00:00Z"), + DateTest.date.from_json("2012-12-31T23:59:59Z"), + datetime.timedelta(seconds=1)) + self.compare_dates( + DateTest.date.from_json("2012-12-31T23:00:01-01:00"), + DateTest.date.from_json("2013-01-01T00:00:00+01:00"), + datetime.timedelta(hours=1, seconds=1)) + + def test_return_None(self): + self.assertIsNone(DateTest.date.from_json("")) + self.assertIsNone(DateTest.date.from_json(None)) + self.assertIsNone(DateTest.date.from_json(['unknown value'])) + + def test_old_due_date_format(self): + current = datetime.datetime.today() + self.assertEqual( + time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)), + DateTest.date.from_json("March 12 12:00")) + self.assertEqual( + time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)), + DateTest.date.from_json("December 4 16:30")) + + def test_to_json(self): + ''' + Test converting time reprs to iso dates + ''' + self.assertEqual( + DateTest.date.to_json( + time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), + "2012-12-31T23:59:59Z") + self.assertEqual( + DateTest.date.to_json( + DateTest.date.from_json("2012-12-31T23:59:59Z")), + "2012-12-31T23:59:59Z") + self.assertEqual( + DateTest.date.to_json( + DateTest.date.from_json("2012-12-31T23:00:01-01:00")), + "2013-01-01T00:00:01Z") + From cddc868656d784da1db5585879c9518918b6a512 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 13:01:10 -0400 Subject: [PATCH 147/665] Login URL resolves differently in LMS and CMS, which breaks login_test when loaded by rake test_cms I moved the test into lms/courseware/tests so they run correctly. --- .../student => lms/djangoapps/courseware}/tests/test_login.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {common/djangoapps/student => lms/djangoapps/courseware}/tests/test_login.py (100%) diff --git a/common/djangoapps/student/tests/test_login.py b/lms/djangoapps/courseware/tests/test_login.py similarity index 100% rename from common/djangoapps/student/tests/test_login.py rename to lms/djangoapps/courseware/tests/test_login.py From ac86687fa104d8c0c96ce3e73b7ad29f7baf5a91 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 14:33:59 -0400 Subject: [PATCH 148/665] Added exception handling that solves SchematicResponse exceptions causing a 500 error. When XModule raises a ProcessingError during an AJAX request, this module_render now returns a 404 to further reduce number of 500 responses. --- common/lib/capa/capa/responsetypes.py | 22 +++++-- common/lib/xmodule/xmodule/capa_module.py | 16 +++-- common/lib/xmodule/xmodule/exceptions.py | 8 ++- .../xmodule/xmodule/tests/test_capa_module.py | 60 ++++++++++++------- lms/djangoapps/courseware/module_render.py | 11 +++- 5 files changed, 86 insertions(+), 31 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index e79399c5fc..bc8e7ff541 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -53,12 +53,17 @@ class LoncapaProblemError(Exception): class ResponseError(Exception): ''' - Error for failure in processing a response + Error for failure in processing a response, including + exceptions that occur when executing a custom script. ''' pass class StudentInputError(Exception): + ''' + Error for an invalid student input. + For example, submitting a string when the problem expects a number + ''' pass #----------------------------------------------------------------------------- @@ -1151,7 +1156,7 @@ def sympy_check2(): # Raise an exception else: log.error(traceback.format_exc()) - raise LoncapaProblemError( + raise ResponseError( "CustomResponse: check function returned an invalid dict") # The check function can return a boolean value, @@ -1226,7 +1231,7 @@ def sympy_check2(): Handle an exception raised during the execution of custom Python code. - Raises a StudentInputError + Raises a ResponseError ''' # Log the error if we are debugging @@ -1236,7 +1241,7 @@ def sympy_check2(): # Notify student with a student input error _, _, traceback_obj = sys.exc_info() - raise StudentInputError, StudentInputError(err.message), traceback_obj + raise ResponseError, ResponseError(err.message), traceback_obj #----------------------------------------------------------------------------- @@ -1912,7 +1917,14 @@ class SchematicResponse(LoncapaResponse): submission = [json.loads(student_answers[ k]) for k in sorted(self.answer_ids)] self.context.update({'submission': submission}) - exec self.code in global_context, self.context + + try: + exec self.code in global_context, self.context + + except Exception as err: + _, _, traceback_obj = sys.exc_info() + raise ResponseError, ResponseError(err.message), traceback_obj + cmap = CorrectMap() cmap.set_dict(dict(zip(sorted( self.answer_ids), self.context['correct']))) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d7346faa67..fd25016ca5 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -12,12 +12,13 @@ from lxml import etree from pkg_resources import resource_string from capa.capa_problem import LoncapaProblem -from capa.responsetypes import StudentInputError +from capa.responsetypes import StudentInputError, \ + ResponseError, LoncapaProblemError from capa.util import convert_files_to_filenames from .progress import Progress from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float from .fields import Timedelta @@ -454,7 +455,14 @@ class CapaModule(CapaFields, XModule): return 'Error' before = self.get_progress() - d = handlers[dispatch](get) + + try: + d = handlers[dispatch](get) + + except Exception as err: + _, _, traceback_obj = sys.exc_info() + raise ProcessingError, ProcessingError(err.message), traceback_obj + after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -726,7 +734,7 @@ class CapaModule(CapaFields, XModule): correct_map = self.lcp.grade_answers(answers) self.set_state_from_lcp() - except StudentInputError as inst: + except (StudentInputError, ResponseError, LoncapaProblemError) as inst: log.exception("StudentInputError in capa_module:problem_check") # If the user is a staff member, include diff --git a/common/lib/xmodule/xmodule/exceptions.py b/common/lib/xmodule/xmodule/exceptions.py index 3db5ceccde..d38fbb12bb 100644 --- a/common/lib/xmodule/xmodule/exceptions.py +++ b/common/lib/xmodule/xmodule/exceptions.py @@ -1,6 +1,12 @@ class InvalidDefinitionError(Exception): pass - class NotFoundError(Exception): pass + +class ProcessingError(Exception): + ''' + An error occurred while processing a request to the XModule. + For example: if an exception occurs while checking a capa problem. + ''' + pass diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 18d20a2756..cb7d599413 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -7,6 +7,8 @@ import random import xmodule import capa +from capa.responsetypes import StudentInputError, \ + LoncapaProblemError, ResponseError from xmodule.capa_module import CapaModule from xmodule.modulestore import Location from lxml import etree @@ -502,38 +504,52 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.attempts, 1) - def test_check_problem_student_input_error(self): - module = CapaFactory.create(attempts=1) + def test_check_problem_error(self): - # Ensure that the user is NOT staff - module.system.user_is_staff = False + # Try each exception that capa_module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: - # Simulate a student input exception - with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: - mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + # Create the module + module = CapaFactory.create(attempts=1) - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.check_problem(get_request_dict) + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class('test error') + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' expected_msg = 'Error: test error' self.assertEqual(expected_msg, result['success']) - # Expect that the number of attempts is NOT incremented - self.assertEqual(module.attempts, 1) + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) - def test_check_problem_student_input_error_with_staff_user(self): - module = CapaFactory.create(attempts=1) + def test_check_problem_error_with_staff_user(self): + + # Try each exception that capa module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: - # Ensure that the user IS staff - module.system.user_is_staff = True + # Create the module + module = CapaFactory.create(attempts=1) - # Simulate a student input exception - with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: - mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + # Ensure that the user IS staff + module.system.user_is_staff = True - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.check_problem(get_request_dict) + # Simulate answering a problem that raises an exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class('test error') + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' self.assertTrue('test error' in result['success']) @@ -541,6 +557,10 @@ class CapaModuleTest(unittest.TestCase): # We DO include traceback information for staff users self.assertTrue('Traceback' in result['success']) + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) + + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..182c45775d 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -22,7 +22,7 @@ from .models import StudentModule from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from student.models import unique_id_for_user from xmodule.errortracker import exc_info_to_str -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.x_module import ModuleSystem @@ -443,9 +443,18 @@ def modx_dispatch(request, dispatch, location, course_id): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, p) + + # If we can't find the module, respond with a 404 except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + # For XModule-specific errors, we respond with 404 + except ProcessingError: + log.exception("Module encountered an error while prcessing AJAX call") + raise Http404 + + # If any other error occurred, re-raise it to trigger a 500 response except: log.exception("error processing ajax call") raise From 99cd3fafdb5f0d2cbe00ad541cb0a07ad83197e5 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 14:48:44 -0400 Subject: [PATCH 149/665] Added error handling of XModule processing errors to CMS Added tests for SchematicResponse error handling --- cms/djangoapps/contentstore/views.py | 8 ++++++- .../lib/capa/capa/tests/test_responsetypes.py | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 561708c833..6ff3e41510 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -42,7 +42,7 @@ from xmodule.modulestore.mongo import MongoUsage from mitxmako.shortcuts import render_to_response, render_to_string from xmodule.modulestore.django import modulestore from xmodule_modifiers import replace_static_urls, wrap_xmodule -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from functools import partial from xmodule.contentstore.django import contentstore @@ -448,9 +448,15 @@ def preview_dispatch(request, preview_id, location, dispatch=None): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, request.POST) + except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + except ProcessingError: + log.exception("Module raised an error while processing AJAX request") + raise Http404 + except: log.exception("error processing ajax call") raise diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index ac50e6defc..d42e9afcb8 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -13,7 +13,8 @@ import textwrap from . import test_system import capa.capa_problem as lcp -from capa.responsetypes import LoncapaProblemError, StudentInputError +from capa.responsetypes import LoncapaProblemError, \ + StudentInputError, ResponseError from capa.correctmap import CorrectMap from capa.util import convert_files_to_filenames from capa.xqueue_interface import dateformat @@ -865,7 +866,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(script=script, cfn="check_func") # Expect that an exception gets raised when we check the answer - with self.assertRaises(StudentInputError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) def test_script_exception_inline(self): @@ -875,7 +876,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(answer=script) # Expect that an exception gets raised when we check the answer - with self.assertRaises(StudentInputError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) def test_invalid_dict_exception(self): @@ -889,7 +890,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(script=script, cfn="check_func") # Expect that an exception gets raised when we check the answer - with self.assertRaises(LoncapaProblemError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) @@ -922,6 +923,18 @@ class SchematicResponseTest(ResponseTest): # is what we expect) self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') + def test_script_exception(self): + + # Construct a script that will raise an exception + script = "raise Exception('test')" + problem = self.build_problem(answer=script) + + # Expect that an exception gets raised when we check the answer + with self.assertRaises(ResponseError): + submission_dict = {'test': 'test'} + input_dict = {'1_2_1': json.dumps(submission_dict)} + problem.grade_answers(input_dict) + class AnnotationResponseTest(ResponseTest): from response_xml_factory import AnnotationResponseXMLFactory From 22537ffd3b05269b688972e7e2ad81e118cc1da7 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 14:51:39 -0400 Subject: [PATCH 150/665] Don't need to convert to milliseconds. --- cms/djangoapps/models/settings/course_details.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 09d57774ab..b45f5bd343 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -178,20 +178,12 @@ class CourseDetails(object): # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): - @staticmethod - def time_to_date(time_obj): - """ - Convert a time.time_struct to a true universal time (can pass to js Date - constructor) - """ - return calendar.timegm(time_obj) * 1000 - def default(self, obj): if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() elif isinstance(obj, time.struct_time): - return CourseSettingsEncoder.time_to_date(obj) + return Date().to_json(obj) else: return JSONEncoder.default(self, obj) From 5c78218b1360bf9e0eb6bcee41cbc44e1aeb1dac Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 14:52:27 -0400 Subject: [PATCH 151/665] Don't need to convert to milliseconds. --- cms/djangoapps/models/settings/course_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index b45f5bd343..876000c7fe 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -4,7 +4,6 @@ from xmodule.modulestore.inheritance import own_metadata import json from json.encoder import JSONEncoder import time -import calendar from contentstore.utils import get_modulestore from models.settings import course_grading from contentstore.utils import update_item From 122c8567c5d370a6e54e075d4e736c96bcfef646 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 15:00:08 -0400 Subject: [PATCH 152/665] An integrity error while creating an enrollment just means that our work has already been done. Fixes https://www.pivotaltracker.com/story/show/46915947 --- common/djangoapps/student/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 5dbaf5d2c2..d0deffd7b9 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -325,7 +325,12 @@ def change_enrollment(request): "course:{0}".format(course_num), "run:{0}".format(run)]) - enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) + try: + enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) + except IntegrityError: + # If we've already created this enrollment in a separate transaction, + # then just continue + pass return {'success': True} elif action == "unenroll": From df1be877390c6869b766870c7d5e40bbfe258913 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 15:20:40 -0400 Subject: [PATCH 153/665] * Changed 404 errors to 400 errors * Removed duplicate traceback log message * Now provide string, not Exception, as second tuple item to raise --- cms/djangoapps/contentstore/views.py | 2 +- common/lib/capa/capa/responsetypes.py | 3 +-- common/lib/xmodule/xmodule/capa_module.py | 2 +- lms/djangoapps/courseware/module_render.py | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6ff3e41510..24f3eae8a4 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -455,7 +455,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): except ProcessingError: log.exception("Module raised an error while processing AJAX request") - raise Http404 + return HttpResponseBadRequest() except: log.exception("error processing ajax call") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bc8e7ff541..3d19fb4cb1 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1237,11 +1237,10 @@ def sympy_check2(): # Log the error if we are debugging msg = 'Error occurred while evaluating CustomResponse' log.debug(msg, exc_info=True) - log.debug(traceback.format_exc()) # Notify student with a student input error _, _, traceback_obj = sys.exc_info() - raise ResponseError, ResponseError(err.message), traceback_obj + raise ResponseError, err.message, traceback_obj #----------------------------------------------------------------------------- diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index fd25016ca5..4975478421 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -461,7 +461,7 @@ class CapaModule(CapaFields, XModule): except Exception as err: _, _, traceback_obj = sys.exc_info() - raise ProcessingError, ProcessingError(err.message), traceback_obj + raise ProcessingError, err.message, traceback_obj after = self.get_progress() d.update({ diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 182c45775d..39d16dbb19 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -10,7 +10,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.http import Http404 -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import csrf_exempt from requests.auth import HTTPBasicAuth @@ -449,10 +449,10 @@ def modx_dispatch(request, dispatch, location, course_id): log.exception("Module indicating to user that request doesn't exist") raise Http404 - # For XModule-specific errors, we respond with 404 + # For XModule-specific errors, we respond with 400 except ProcessingError: log.exception("Module encountered an error while prcessing AJAX call") - raise Http404 + return HttpResponseBadRequest() # If any other error occurred, re-raise it to trigger a 500 response except: From 756f75951dca8311ff0d642af2ef3abddbf2c9b3 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 16:28:49 -0400 Subject: [PATCH 154/665] studio - in progress work on help UI --- cms/static/client_templates/checklist.html | 2 +- cms/static/sass/_base.scss | 3 +- cms/static/sass/_variables.scss | 2 +- cms/static/sass/base-style.scss | 2 + cms/static/sass/elements/_sock.scss | 95 +++++++++++++++++++++- cms/static/sass/views/_account.scss | 2 +- cms/templates/howitworks.html | 4 +- cms/templates/widgets/footer.html | 5 +- cms/templates/widgets/sock.html | 41 +++++++++- 9 files changed, 143 insertions(+), 13 deletions(-) diff --git a/cms/static/client_templates/checklist.html b/cms/static/client_templates/checklist.html index ec6ff4e892..6884b0e9c9 100644 --- a/cms/static/client_templates/checklist.html +++ b/cms/static/client_templates/checklist.html @@ -44,7 +44,7 @@ <% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
      -
    • +
    • rel="external" title="This link will open in a new browser window/tab" diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5901b19306..5ce131288e 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -357,7 +357,8 @@ h1 { // layout - grandfathered .main-wrapper { position: relative; - margin: 40px; + margin: ($baseline*2); + padding-bottom: $footer-primary-height; } .inner-wrapper { diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ccbd3ed7b0..ffa99e3fc6 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -155,7 +155,7 @@ $shadow-l1: rgba(0,0,0,0.1); $shadow-d1: rgba(0,0,0,0.4); // colors - inherited -$baseFontColor: #3c3c3c; +$baseFontColor: $gray-d2; $offBlack: #3c3c3c; $green: #108614; $lightGrey: #edf1f5; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index bc6638bf14..e1afa45804 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -21,6 +21,8 @@ @import 'base'; // elements +@import 'elements/typography'; +@import 'elements/icons'; @import 'elements/header'; @import 'elements/sock'; @import 'elements/footer'; diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss index e1b147b849..e8fbcc3ef2 100644 --- a/cms/static/sass/elements/_sock.scss +++ b/cms/static/sass/elements/_sock.scss @@ -9,10 +9,101 @@ .sock { @include clearfix(); - @include font-size(13); + @extend .t-copy-sub2; max-width: $fg-max-width; min-width: $fg-min-width; width: flex-grid(12); - margin: 0 auto ($baseline*2) auto; + margin: 0 auto $baseline auto; + + header { + + .title { + @extend .t-title-3; + } + + .ss-icon { + @extend .t-icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .ss-icon { + @include transition(color .25s ease-in-out); + @include font-size(15); + @extend .t-icon-inline; + @extend .icon-inline; + margin-right: ($baseline/4); + color: $blue-l2; + } + + &:hover, &:active { + + .ss-icon { + color: $white; + } + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @include blue-button; + @include transition(all .15s); + @include font-size(13); + font-weight: 500; + padding: ($baseline/4) ($baseline/2); + text-align: center; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } } } \ No newline at end of file diff --git a/cms/static/sass/views/_account.scss b/cms/static/sass/views/_account.scss index 1206db5e76..2be94a81ea 100644 --- a/cms/static/sass/views/_account.scss +++ b/cms/static/sass/views/_account.scss @@ -71,7 +71,7 @@ body.signup, body.signin { @include blue-button; @include transition(all .15s); @include font-size(15); - display:block; + display: block; width: 100%; padding: ($baseline*0.75) ($baseline/2); font-weight: 600; diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 7a819fceba..6c0029c425 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -134,10 +134,10 @@ diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 18ecf2bc39..c00bf0187a 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,12 +20,9 @@
    • % if user.is_authenticated(): % endif - diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html index ff5f9c9ad4..d8d191a773 100644 --- a/cms/templates/widgets/sock.html +++ b/cms/templates/widgets/sock.html @@ -2,7 +2,46 @@ % if user.is_authenticated():
      -

      Sock!

      +
      +

      Studio Support

      +
      + +
      +

      Studio Support

      + +
      +

      Need help getting started with Studio? Want to know how to manage a particular part of your course using Studio? Take advantage of our documentation, help forums, as well as our edX101 introduction course for course authors.

      +
      + + +
      + +
      % endif \ No newline at end of file From 6564cc57e6f7a2bddfab8a9dabbcc012687135a1 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 27 Mar 2013 16:29:55 -0400 Subject: [PATCH 155/665] Fix typo with hyphen in cms lettuce feature files --- .../contentstore/features/advanced-settings.feature | 6 +++--- .../features/studio-overview-togglesection.feature | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 66039e19b1..db7294c14c 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -27,16 +27,16 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - Scenario: Test how multi -line input appears + 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 Then it is displayed as formatted And I reload the page Then it is displayed as formatted - Scenario: Test automatic quoting of non -JSON values + 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 + When I create a non-JSON value not in quotes Then it is displayed as a string And I reload the page Then it is displayed as a string diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 88492d55e3..762dea6838 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -21,7 +21,7 @@ Feature: Overview Toggle Section Then I see the "Collapse All Sections" link And all sections are expanded - @skip -phantom + @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 From f038237ee9f2d7a5dae9c2ebdb6a2ba57db860c8 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 16:34:08 -0400 Subject: [PATCH 156/665] Changed log.exception to log.warning --- cms/djangoapps/contentstore/views.py | 2 +- common/lib/capa/capa/responsetypes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 24f3eae8a4..bfdfb1742b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -454,7 +454,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): raise Http404 except ProcessingError: - log.exception("Module raised an error while processing AJAX request") + log.warning("Module raised an error while processing AJAX request") return HttpResponseBadRequest() except: diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 3d19fb4cb1..bc62654bef 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1236,7 +1236,7 @@ def sympy_check2(): # Log the error if we are debugging msg = 'Error occurred while evaluating CustomResponse' - log.debug(msg, exc_info=True) + log.warning(msg, exc_info=True) # Notify student with a student input error _, _, traceback_obj = sys.exc_info() From 9c671163fdf1be224cf4d3f310380fa9caa75cf8 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 17:11:02 -0400 Subject: [PATCH 157/665] Added exc_info=True to log.warning Changed log.exception to log.warning --- cms/djangoapps/contentstore/views.py | 3 ++- common/lib/xmodule/xmodule/capa_module.py | 3 ++- lms/djangoapps/courseware/module_render.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index bfdfb1742b..0d58133763 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -454,7 +454,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None): raise Http404 except ProcessingError: - log.warning("Module raised an error while processing AJAX request") + log.warning("Module raised an error while processing AJAX request", + exc_info=True) return HttpResponseBadRequest() except: diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 4975478421..3e594f9d46 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -735,7 +735,8 @@ class CapaModule(CapaFields, XModule): self.set_state_from_lcp() except (StudentInputError, ResponseError, LoncapaProblemError) as inst: - log.exception("StudentInputError in capa_module:problem_check") + log.warning("StudentInputError in capa_module:problem_check", + exc_info=True) # If the user is a staff member, include # the full exception, including traceback, diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 39d16dbb19..48aab024df 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -451,7 +451,8 @@ def modx_dispatch(request, dispatch, location, course_id): # For XModule-specific errors, we respond with 400 except ProcessingError: - log.exception("Module encountered an error while prcessing AJAX call") + log.warning("Module encountered an error while prcessing AJAX call", + exc_info=True) return HttpResponseBadRequest() # If any other error occurred, re-raise it to trigger a 500 response From c8ab45cc579c675265b0d9223d61eb2c3310614c Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 17:40:24 -0400 Subject: [PATCH 158/665] studio - footer help ui revamp - animation, structure, styling - WIP --- cms/static/js/base.js | 11 +- cms/static/sass/_base.scss | 8 ++ cms/static/sass/_cms_mixins.scss | 15 +++ cms/static/sass/_variables.scss | 2 +- cms/static/sass/base-style.scss | 1 - cms/static/sass/elements/_footer.scss | 121 ++++++++++++++++++++++ cms/static/sass/elements/_icons.scss | 16 +++ cms/static/sass/elements/_sock.scss | 109 ------------------- cms/static/sass/elements/_typography.scss | 82 +++++++++++++++ cms/templates/base.html | 5 +- cms/templates/widgets/footer.html | 45 +++++++- cms/templates/widgets/sock.html | 47 --------- 12 files changed, 299 insertions(+), 163 deletions(-) create mode 100644 cms/static/sass/elements/_icons.scss delete mode 100644 cms/static/sass/elements/_sock.scss create mode 100644 cms/static/sass/elements/_typography.scss delete mode 100644 cms/templates/widgets/sock.html diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 211981b05a..d93d2fd8d4 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -34,11 +34,11 @@ $(document).ready(function () { $(this).select(); }); + $('body').addClass('js'); + $('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.new-unit-item').bind('click', createNewUnit); - $('body').addClass('js'); - // lean/simple modal $('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' }); $('a.action-modal-close').click(function(e){ @@ -86,6 +86,8 @@ $(document).ready(function () { // tender feedback window scrolling $('a.show-tender').bind('click', smoothScrollTop); + // toggling footer additional support + $('.show-support').bind('click', toggleSupport); // toggling overview section details $(function () { @@ -456,6 +458,11 @@ function onKeyUp(e) { } } +function toggleSupport(e) { + e.preventDefault(); + $body.toggleClass('footer-is-expanded'); +} + function toggleSubmodules(e) { e.preventDefault(); $(this).toggleClass('expand').toggleClass('collapse'); diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5ce131288e..328c7e99c3 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -56,6 +56,14 @@ h1 { min-height: 100%; } +.wrapper-main { + padding-bottom: $footer-primary-height; +} + +.js.footer-is-expanded .wrapper-main { + padding-bottom: ($footer-primary-height*4); +} + // layout - basic page header .wrapper-mast { margin: 0; diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index 015a94b762..d837000a24 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -110,6 +110,21 @@ } } +@mixin gray-button { + @include button; + border: 1px solid $gray-d1; + border-radius: 3px; + @include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0)); + background-color: $gray-d2; + @include box-shadow(0 1px 0 $white-t1 inset); + color: $gray-l3; + + &:hover { + background-color: $gray-d3; + color: $white; + } +} + @mixin green-button { @include button; border: 1px solid $darkGreen; diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ffa99e3fc6..806fbc8c57 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -13,7 +13,7 @@ $fg-max-width: 1280px; $fg-min-width: 900px; // elements -$footer-primary-height: ($baseline*3); +$footer-primary-height: (60px); // type $sans-serif: 'Open Sans', $verdana; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index e1afa45804..3015c3592d 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -24,7 +24,6 @@ @import 'elements/typography'; @import 'elements/icons'; @import 'elements/header'; -@import 'elements/sock'; @import 'elements/footer'; @import 'elements/navigation'; @import 'elements/forms'; diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index dfebc6d44c..f2941e11be 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -75,4 +75,125 @@ } } } + + // sock - additional help + .sock { + @include clearfix(); + @extend .t-copy-sub2; + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto $baseline auto; + border-bottom: 1px solid $gray-l2; + padding-bottom: $baseline; + + header { + + .title { + @extend .t-title-3; + } + + .ss-icon { + @extend .t-icon; + @extend .icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .ss-icon { + @include font-size(15); + @extend .t-icon; + @extend .icon-inline; + } + + &:hover, &:active { + + .ss-icon { + } + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @include gray-button; + @include transition(all .15s); + @include font-size(13); + font-weight: 500; + padding: ($baseline/4) ($baseline/2); + text-align: center; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } + } +} + + +// js-enabled styling +.js .wrapper-footer { + @include transition(height 2s ease-in-out); + height: $footer-primary-height; + overflow: hidden; + + .sock { + display: none; + } +} + + // expanded view +.js.footer-is-expanded .wrapper-footer { + height: ($footer-primary-height*4); + + .sock { + display: block; + } } \ No newline at end of file diff --git a/cms/static/sass/elements/_icons.scss b/cms/static/sass/elements/_icons.scss new file mode 100644 index 0000000000..2bc73d8b8d --- /dev/null +++ b/cms/static/sass/elements/_icons.scss @@ -0,0 +1,16 @@ +// studio - elements - icons +// ==================== + +.icon { + +} + +.ss-icon { + +} + +.icon-inline { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); +} \ No newline at end of file diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss deleted file mode 100644 index e8fbcc3ef2..0000000000 --- a/cms/static/sass/elements/_sock.scss +++ /dev/null @@ -1,109 +0,0 @@ -// studio - elements - sock -// ==================== - -.wrapper-sock { - margin: 0; - padding: $baseline $baseline $footer-primary-height $baseline; - position: relative; - width: 100%; - - .sock { - @include clearfix(); - @extend .t-copy-sub2; - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto $baseline auto; - - header { - - .title { - @extend .t-title-3; - } - - .ss-icon { - @extend .t-icon-inline; - } - } - - // shared elements - .support, .feedback { - @include box-sizing(border-box); - - .title { - - } - - .copy { - margin: 0 0 $baseline 0; - } - - .list-actions { - @include clearfix(); - - .action-item { - float: left; - margin-right: ($baseline/2); - - &:last-child { - margin-right: 0; - } - - .action { - display: block; - - .ss-icon { - @include transition(color .25s ease-in-out); - @include font-size(15); - @extend .t-icon-inline; - @extend .icon-inline; - margin-right: ($baseline/4); - color: $blue-l2; - } - - &:hover, &:active { - - .ss-icon { - color: $white; - } - } - } - - .tip { - @extend .sr; - } - } - - .action-primary { - @include blue-button; - @include transition(all .15s); - @include font-size(13); - font-weight: 500; - padding: ($baseline/4) ($baseline/2); - text-align: center; - } - } - } - - // studio support content - .support { - width: flex-grid(8,12); - float: left; - margin-right: flex-gutter(); - - .action-item { - width: flexgrid(4,8); - } - } - - // studio feedback content - .feedback { - width: flex-grid(4,12); - float: left; - - .action-item { - width: flexgrid(4,4); - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss new file mode 100644 index 0000000000..a9b3d362ee --- /dev/null +++ b/cms/static/sass/elements/_typography.scss @@ -0,0 +1,82 @@ +// studio - elements - typography +// ==================== + +// headings/titles +.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 { + color: $gray-d3; +} + +.t-title-1 { + @include font-size(32); +} + +.t-title-2 { + @include font-size(24); + margin: 0 0 ($baseline/2) 0; + font-weight: 600; +} + +.t-title-3 { + @include font-size(16); + margin: 0 0 ($baseline/2) 0; + font-weight: 600; +} + +.t-title-4 { + +} + +.t-title-5 { + +} + +// ==================== + +// copy +.t-copy-base { + @include font-size(16); +} + +.t-copy-lead1 { + @include font-size(18); +} + +.t-copy-lead2 { + @include font-size(20); +} + +.t-copy-sub1 { + @include font-size(14); +} + +.t-copy-sub2 { + @include font-size(13); +} + +.t-copy-sub3 { + @include font-size(12); +} + +// ==================== + +// actions/labels +.t-action { + @include font-size(14); + font-weight: 600; +} + +.t-action-primary { + @include font-size(14); + font-weight: 600; +} + +.t-action-primary-s { + @include font-size(13); +} + +// ==================== + +// misc +.t-icon { + line-height: 0; +} \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index 44847c1da4..e587619bae 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -53,8 +53,9 @@
      <%include file="widgets/header.html" /> - <%block name="content"> - <%include file="widgets/sock.html" /> +
      + <%block name="content"> +
      <%include file="widgets/footer.html" />
      diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index c00bf0187a..b6a3b84272 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,6 +1,49 @@ <%! from django.core.urlresolvers import reverse %>
    +
    %endif @@ -202,6 +203,8 @@ function goto( mode) %if instructor_access:

    You may also delete the entire state of a student for a problem:

    +

    To delete the state of other XBlocks specify modulename/urlname, eg + combinedopenended/Humanities_SA_Peer

    %endif %endif From 57e5eb683bbb745c911d6988a3f08ac3a7e08bc2 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 1 Apr 2013 16:54:56 -0400 Subject: [PATCH 253/665] studio - basic design, interaction, and content for static in-page help --- cms/static/js/base.js | 14 +- cms/static/sass/_base.scss | 27 ++-- cms/static/sass/_cms_mixins.scss | 103 +++++++++++++-- cms/static/sass/_variables.scss | 4 +- cms/static/sass/base-style.scss | 4 +- cms/static/sass/elements/_controls.scss | 143 ++++++++++++++++++++ cms/static/sass/elements/_footer.scss | 133 +------------------ cms/static/sass/elements/_header.scss | 2 +- cms/static/sass/elements/_sock.scss | 152 ++++++++++++++++++++++ cms/static/sass/elements/_typography.scss | 23 ++-- cms/static/sass/views/_index.scss | 3 +- cms/templates/base.html | 11 +- cms/templates/widgets/footer.html | 52 +------- cms/templates/widgets/sock.html | 52 ++++++++ 14 files changed, 499 insertions(+), 224 deletions(-) create mode 100644 cms/static/sass/elements/_controls.scss create mode 100644 cms/static/sass/elements/_sock.scss create mode 100644 cms/templates/widgets/sock.html diff --git a/cms/static/js/base.js b/cms/static/js/base.js index d93d2fd8d4..7bea860531 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -87,7 +87,7 @@ $(document).ready(function () { $('a.show-tender').bind('click', smoothScrollTop); // toggling footer additional support - $('.show-support').bind('click', toggleSupport); + $('.cta-show-sock').bind('click', toggleSock); // toggling overview section details $(function () { @@ -458,9 +458,17 @@ function onKeyUp(e) { } } -function toggleSupport(e) { +function toggleSock(e) { e.preventDefault(); - $body.toggleClass('footer-is-expanded'); + $body.toggleClass('sock-is-shown'); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $('.wrapper-sock') + }); } function toggleSubmodules(e) { diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 328c7e99c3..10c046d22a 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -22,7 +22,7 @@ body, input { a { text-decoration: none; color: $blue; - @include transition(color .15s); + @include transition(color 0.25s ease-in-out); &:hover { color: #cb9c40; @@ -50,18 +50,22 @@ h1 { // ==================== -// layout - view +// layout - overall view .wrapper-view { - position: relative; min-height: 100%; + position: relative; } .wrapper-main { - padding-bottom: $footer-primary-height; + background: $gray-l5; + margin: ($baseline*1.5) 0 0 0; + padding-bottom: $bottom-height; } -.js.footer-is-expanded .wrapper-main { - padding-bottom: ($footer-primary-height*4); +.wrapper-bottom { + position: absolute; + bottom: 0; + height: $bottom-height; } // layout - basic page header @@ -286,19 +290,17 @@ h1 { } .title-1 { - + @extend .t-title-1; } .title-2 { - @include font-size(24); + @extend .t-title-2; margin: 0 0 ($baseline/2) 0; - font-weight: 600; } .title-3 { - @include font-size(16); + @extend .t-title-3; margin: 0 0 ($baseline/2) 0; - font-weight: 600; } .title-4 { @@ -365,8 +367,7 @@ h1 { // layout - grandfathered .main-wrapper { position: relative; - margin: ($baseline*2); - padding-bottom: $footer-primary-height; + margin: 0 ($baseline*2); } .inner-wrapper { diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index d837000a24..a25a07cb73 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -1,6 +1,7 @@ // studio - utilities - mixins and extends // ==================== +// mixins - utility @mixin clearfix { &:after { content: ''; @@ -11,6 +12,7 @@ } } +// mixins - grandfathered @mixin button { display: inline-block; padding: 4px 20px 6px; @@ -294,20 +296,97 @@ } } -@mixin sr-text { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - @mixin active { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); background-color: rgba(255, 255, 255, .3); @include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} \ No newline at end of file +} + +// ==================== + +// extends - buttons +.btn { + @include box-sizing(border-box); + @include transition(color 0.25s ease-in-out, border-color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out); + display: inline-block; + cursor: pointer; + + &:hover, &:active { + + } + + &.disabled, &[disabled] { + cursor: default; + pointer-events: none; + opacity: 0.5; + } + + .icon-inline { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } +} + +// pill button +.btn-pill { + @include border-radius($baseline/5); +} + +.btn-rounded { + @include border-radius($baseline/2); +} + +// primary button +.btn-primary { + @extend .btn; + @extend .btn-pill; + padding:($baseline/2) $baseline; + border-width: 1px; + border-style: solid; + line-height: 1.5em; + text-align: center; + + &:hover, &:active { + @include box-shadow(0 2px 1px $shadow-l1); + } + + &.current, &.active { + @include box-shadow(inset 1px 1px 2px $shadow-d1); + + &:hover, &:active { + @include box-shadow(inset 1px 1px 1px $shadow-d1); + } + } +} + +// secondary button +.btn-secondary { + @extend .btn; + @extend .btn-pill; + border-width: 1px; + border-style: solid; + padding:($baseline/2) $baseline; + background: transparent; + line-height: 1.5em; + text-align: center; + + &:hover, &:active { + + } + + &.current, &.active { + + } +} + +// ==================== + +// extends - depth levels +.depth0 { z-index: 0; } +.depth1 { z-index: 10; } +.depth2 { z-index: 100; } +.depth3 { z-index: 1000; } +.depth4 { z-index: 10000; } +.depth5 { z-index: 100000; } \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 806fbc8c57..88143c2365 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -13,7 +13,7 @@ $fg-max-width: 1280px; $fg-min-width: 900px; // elements -$footer-primary-height: (60px); +$bottom-height: ($baseline*3); // type $sans-serif: 'Open Sans', $verdana; @@ -170,4 +170,4 @@ $disabledGreen: rgb(124, 206, 153); $darkGreen: rgb(52, 133, 76); $lightBluishGrey: rgb(197, 207, 223); $lightBluishGrey2: rgb(213, 220, 228); -$error-red: rgb(253, 87, 87); +$error-red: rgb(253, 87, 87); \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 3015c3592d..da17441c71 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -23,9 +23,11 @@ // elements @import 'elements/typography'; @import 'elements/icons'; +@import 'elements/controls'; +@import 'elements/navigation'; @import 'elements/header'; @import 'elements/footer'; -@import 'elements/navigation'; +@import 'elements/sock'; @import 'elements/forms'; @import 'elements/modal'; @import 'elements/alerts'; diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss new file mode 100644 index 0000000000..c4e96616a8 --- /dev/null +++ b/cms/static/sass/elements/_controls.scss @@ -0,0 +1,143 @@ +// studio - elements - UI controls +// ==================== + +// gray primary button +.btn-primary-gray { + @extend .btn-primary; + background: $gray-l1; + border-color: $gray-l2; + color: $white; + + &:hover, &:active { + border-color: $gray-l1; + background: $gray; + } + + &.current, &.active { + background: $gray-d1; + color: $gray-l1; + + &:hover, &:active { + background: $gray-d1; + } + } +} + +// blue primary button +.btn-primary-blue { + @extend .btn-primary; + background: $blue; + border-color: $blue-s1; + color: $white; + + &:hover, &:active { + background: $blue-s2; + border-color: $blue-s2; + } + + &.current, &.active { + background: $blue-d1; + color: $blue-l4; + border-color: $blue-d2; + + &:hover, &:active { + background: $blue-d1; + } + } +} + +// green primary button +.btn-primary-green { + @extend .btn-primary; + background: $green; + border-color: $green; + color: $white; + + &:hover, &:active { + background: $green-s1; + border-color: $green-s1; + } + + &.current, &.active { + background: $green-d1; + color: $green-l4; + border-color: $green-d2; + + &:hover, &:active { + background: $green-d1; + } + } +} + +// gray secondary button +.btn-secondary-gray { + @extend .btn-secondary; + border-color: $gray-l3; + color: $gray-l1; + + &:hover, &:active { + background: $gray-l3; + color: $gray-d2; + } + + &.current, &.active { + background: $gray-d2; + color: $gray-l5; + + &:hover, &:active { + background: $gray-d2; + } + } +} + +// blue secondary button +.btn-secondary-blue { + @extend .btn-secondary; + border-color: $blue-l3; + color: $blue; + + &:hover, &:active { + background: $blue-l3; + color: $blue-s2; + } + + &.current, &.active { + border-color: $blue-l3; + background: $blue-l3; + color: $blue-d1; + + &:hover, &:active { + + } + } +} + +// green secondary button +.btn-secondary-green { + @extend .btn-secondary; + border-color: $green-l4; + color: $green-l2; + + &:hover, &:active { + background: $green-l4; + color: $green-s1; + } + + &.current, &.active { + background: $green-s1; + color: $green-l4; + + &:hover, &:active { + background: $green-s1; + } + } +} + +// ==================== + +// layout-based buttons + +// ==================== + +// calls-to-action + diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index f2941e11be..d0bb9b9dee 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -3,13 +3,10 @@ .wrapper-footer { @include box-shadow(inset 0 1px 2px $shadow-d1); - margin: ($baseline*1.5) 0 0 0; - padding: $baseline; - position: absolute; - bottom: 0; + position: relative; width: 100%; - height: $footer-primary-height; - background: $gray-l3; + padding: $baseline; + background: $gray-l4; footer.primary { @include clearfix(); @@ -55,8 +52,6 @@ .ss-icon { @include transition(top .25s ease-in-out .25s); @include font-size(15); - position: relative; - top: 0; display: inline-block; vertical-align: middle; margin-right: ($baseline/4); @@ -67,133 +62,15 @@ color: $gray-d2; .ss-icon { - top: -($baseline/10); color: $gray-d2; } } - } - } - } - } - // sock - additional help - .sock { - @include clearfix(); - @extend .t-copy-sub2; - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto $baseline auto; - border-bottom: 1px solid $gray-l2; - padding-bottom: $baseline; - - header { - - .title { - @extend .t-title-3; - } - - .ss-icon { - @extend .t-icon; - @extend .icon-inline; - } - } - - // shared elements - .support, .feedback { - @include box-sizing(border-box); - - .title { - - } - - .copy { - margin: 0 0 $baseline 0; - } - - .list-actions { - @include clearfix(); - - .action-item { - float: left; - margin-right: ($baseline/2); - - &:last-child { - margin-right: 0; - } - - .action { - display: block; - - .ss-icon { - @include font-size(15); - @extend .t-icon; - @extend .icon-inline; - } - - &:hover, &:active { - - .ss-icon { - } - } - } - - .tip { - @extend .sr; + &.is-active { + color: $gray-d2; } } - - .action-primary { - @include gray-button; - @include transition(all .15s); - @include font-size(13); - font-weight: 500; - padding: ($baseline/4) ($baseline/2); - text-align: center; - } } } - - // studio support content - .support { - width: flex-grid(8,12); - float: left; - margin-right: flex-gutter(); - - .action-item { - width: flexgrid(4,8); - } - } - - // studio feedback content - .feedback { - width: flex-grid(4,12); - float: left; - - .action-item { - width: flexgrid(4,4); - } - } - } -} - - -// js-enabled styling -.js .wrapper-footer { - @include transition(height 2s ease-in-out); - height: $footer-primary-height; - overflow: hidden; - - .sock { - display: none; - } -} - - // expanded view -.js.footer-is-expanded .wrapper-footer { - height: ($footer-primary-height*4); - - .sock { - display: block; } } \ No newline at end of file diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index e8df37f57f..12c736de7d 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -2,7 +2,7 @@ // ==================== .wrapper-header { - margin: 0 0 ($baseline*1.5) 0; + margin: 0; padding: $baseline; border-bottom: 1px solid $gray; @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss new file mode 100644 index 0000000000..27f43935f6 --- /dev/null +++ b/cms/static/sass/elements/_sock.scss @@ -0,0 +1,152 @@ +// studio - elements - support sock +// ==================== + +.wrapper-sock { + @include transition(background 0.25s ease-in-out); + @include clearfix(); + position: relative; + width: 100%; + margin: ($baseline*2.5) 0; + padding: 0 $baseline; + border-top: 1px solid $gray-l4; + + // actions + .list-cta { + position: relative; + top: -($baseline); + margin: 0 auto; + text-align: center; + + .cta-show-sock { + @extend .btn-secondary-gray; + @extend .t-action3; + background: $gray-l5; + padding: ($baseline/2) $baseline; + + .icon { + @include font-size(17); + } + } + } + + // sock - additional help + .sock { + @include clearfix(); + @extend .t-copy-sub2; + display: none; + opacity: 0.0; + pointer-events: none; + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + color: $gray-l3; + + // support body + header { + + .title { + @extend .t-title-3; + margin-bottom: ($baseline/2); + } + + .ss-icon { + @extend .t-icon; + @extend .icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + @extend .t-title-3; + color: $white; + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .icon { + @include font-size(18); + } + + &:hover, &:active { + + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @extend .btn-primary-blue; + @extend .t-action3; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } + } +} + +// case: sock content is shown +.sock-is-shown { + + .wrapper-sock { + @include linear-gradient($gray-d4 0%, $gray-d3 2%, $gray-d3 98%, $gray-d4 100%); + border-bottom: 1px solid $white; + border-top: 1px solid $white; + padding-bottom: ($baseline*2); + padding-top: ($baseline*2); + + .cta-show-sock { + display: none; + opacity: 0.0; + pointer-events: none; + } + + .sock { + display: block; + opacity: 1.0; + pointer-events: auto; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss index a9b3d362ee..32c4b3928b 100644 --- a/cms/static/sass/elements/_typography.scss +++ b/cms/static/sass/elements/_typography.scss @@ -7,18 +7,16 @@ } .t-title-1 { - @include font-size(32); + @include font-size(36); } .t-title-2 { @include font-size(24); - margin: 0 0 ($baseline/2) 0; font-weight: 600; } .t-title-3 { @include font-size(16); - margin: 0 0 ($baseline/2) 0; font-weight: 600; } @@ -60,18 +58,23 @@ // ==================== // actions/labels -.t-action { +.t-action1 { @include font-size(14); font-weight: 600; } -.t-action-primary { - @include font-size(14); - font-weight: 600; -} - -.t-action-primary-s { +.t-action2 { @include font-size(13); + font-weight: 600; + text-transform: uppercase; +} + +.t-action3 { + @include font-size(13); +} + +.t-action4 { + @include font-size(12); } // ==================== diff --git a/cms/static/sass/views/_index.scss b/cms/static/sass/views/_index.scss index 88beccc82d..7c45530339 100644 --- a/cms/static/sass/views/_index.scss +++ b/cms/static/sass/views/_index.scss @@ -296,8 +296,7 @@ body.index { // call to action content .wrapper-content-cta { position: relative; - padding-bottom: ($footer-primary-height*2); - padding-top: ($baseline*2); + padding: ($baseline*2) 0; background: $white; } diff --git a/cms/templates/base.html b/cms/templates/base.html index e587619bae..46d566116e 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -53,10 +53,17 @@
    <%include file="widgets/header.html" /> +
    - <%block name="content"> + <%block name="content"> + % if user.is_authenticated(): + <%include file="widgets/sock.html" /> + % endif +
    + +
    + <%include file="widgets/footer.html" />
    - <%include file="widgets/footer.html" />
    <%include file="widgets/tender.html" /> diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index b6a3b84272..eea155ce19 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,49 +1,5 @@ <%! from django.core.urlresolvers import reverse %> - - @@ -251,7 +279,7 @@

    We're sorry, there was a error with Studio

    -

    Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

    +

    Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

    @@ -263,7 +291,7 @@
    @@ -320,7 +348,7 @@

    Your Studio account has been created, but needs to be activated

    -

    Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

    +

    Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

    + -
    +
    @@ -172,40 +185,39 @@ from contentstore import utils
  • -
    - +
    -
    +
    Enter your YouTube video's ID (along with any restriction parameters) -
    +
  • -
    +
    -
    +

    Requirements

    Expectations of the students taking this course -
    +
    1. - Time spent on all course work -
    2. -
    -
    + Time spent on all course work + + + @@ -215,7 +227,7 @@ from contentstore import utils

    Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.

    Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.

    -
    +
    % if context_course: @@ -234,4 +246,4 @@ from contentstore import utils
    - \ No newline at end of file + From e3a1279c8f2229bab759367976ffb6cfdf6d8a66 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 8 Apr 2013 14:10:38 -0400 Subject: [PATCH 388/665] studio - corrected a typo and adjusted copy on the help sock --- cms/templates/widgets/sock.html | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html index b47b88a0ee..823624c531 100644 --- a/cms/templates/widgets/sock.html +++ b/cms/templates/widgets/sock.html @@ -5,20 +5,20 @@ Looking for Help with Studio? - +

    edX Studio Help

    - +

    Studio Support

    - +
    -

    Need help with Studio? Creating a course is complex, so we're here to help? Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.

    +

    Need help with Studio? Creating a course is complex, so we're here to help. Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.

    - +
    - +
    -
    -
    \ No newline at end of file +
    +
    From 069f997595171a94457ada259ff5567617cd9efa Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 14:33:46 -0400 Subject: [PATCH 389/665] import was putting the xml data into definition.data.data rather than definition.data --- .../lib/xmodule/xmodule/modulestore/xml_importer.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 53eaebf850..2047f016ae 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -284,7 +284,7 @@ def import_from_xml(store, data_dir, course_dirs=None, except KeyError: # Ignore any missing keys in _model_data pass - + if 'data' in content: module_data = content['data'] @@ -301,16 +301,17 @@ def import_from_xml(store, data_dir, course_dirs=None, # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # no good, so we have to do this kludge if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code - lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, - static_content_store, link, remap_dict)) + lxml_rewrite_links(module_data, lambda link: + verify_content_links(module, course_data_path, + static_content_store, link, remap_dict)) for key in remap_dict.keys(): module_data = module_data.replace(key, remap_dict[key]) - except Exception, e: + except Exception: logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) - store.update_item(module.location, content) + store.update_item(module.location, module_data) if hasattr(module, 'children') and module.children != []: store.update_children(module.location, module.children) From 82882020080c23c82919d804963ac767bb79ba4f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 8 Apr 2013 14:42:15 -0400 Subject: [PATCH 390/665] Marketing request: Lighthouse [#338, fixed] --- .../images/university/delft/delft-cover.jpg | Bin 197366 -> 231238 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/lms/static/images/university/delft/delft-cover.jpg b/lms/static/images/university/delft/delft-cover.jpg index e69c836908809035fb829672efaf530b684cc26a..fe41928b41b44a7c951cf0e1ffc2ed9f7583f3e2 100644 GIT binary patch literal 231238 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 _^(R?!y74jqO^2vYrfvR~9f0D(|pB4gw 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 Oj$k{)I|s!4xA=bz+NW&* literal 197366 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

    tFgg^jU4LW$hrk_*T+gMs@%B+D32N(jHLA?MmP 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 z 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 Date: Mon, 8 Apr 2013 14:48:16 -0400 Subject: [PATCH 391/665] fix line breaks per Cale --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 2047f016ae..1a48bc647c 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -301,9 +301,8 @@ def import_from_xml(store, data_dir, course_dirs=None, # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # no good, so we have to do this kludge if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code - lxml_rewrite_links(module_data, lambda link: - verify_content_links(module, course_data_path, - static_content_store, link, remap_dict)) + lxml_rewrite_links(module_data, + lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict)) for key in remap_dict.keys(): module_data = module_data.replace(key, remap_dict[key]) From c12d265a21ba72d940a57370c5638d62264c6f39 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 15:00:54 -0400 Subject: [PATCH 392/665] calling update_item implicitly already puts the data in definition.data, so we want to make sure we don't end up nesting definition.data.data --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 3626bc819d..cd84d2199d 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -327,10 +327,10 @@ def import_module(module, store, course_data_path, static_content_store, allow_n except Exception, e: logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) - if allow_not_found: - store.update_item(module.location, content, allow_not_found=allow_not_found) - else: - store.update_item(module.location, content) + if allow_not_found: + store.update_item(module.location, module_data, allow_not_found=allow_not_found) + else: + store.update_item(module.location, module_data) if hasattr(module, 'children') and module.children != []: store.update_children(module.location, module.children) From 4cbb92533f8237018875e1f9e2f0e9486d6d4572 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 15:08:05 -0400 Subject: [PATCH 393/665] fix broken unit test --- .../lib/xmodule/xmodule/modulestore/xml_importer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index cd84d2199d..a29b56f5b6 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -302,6 +302,7 @@ def import_module(module, store, course_data_path, static_content_store, allow_n # Ignore any missing keys in _model_data pass + module_data = {} if 'data' in content: module_data = content['data'] @@ -324,13 +325,15 @@ def import_module(module, store, course_data_path, static_content_store, allow_n for key in remap_dict.keys(): module_data = module_data.replace(key, remap_dict[key]) - except Exception, e: + except Exception: logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) + else: + module_data = content - if allow_not_found: - store.update_item(module.location, module_data, allow_not_found=allow_not_found) - else: - store.update_item(module.location, module_data) + if allow_not_found: + store.update_item(module.location, module_data, allow_not_found=allow_not_found) + else: + store.update_item(module.location, module_data) if hasattr(module, 'children') and module.children != []: store.update_children(module.location, module.children) From 63b5153cde06c36f9b8011f66fe2cf16730198c1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 8 Apr 2013 15:08:23 -0400 Subject: [PATCH 394/665] Clean up violations from ichuang's PR --- .pylintrc | 8 +++- .../management/commands/dump_grades.py | 11 ++--- lms/djangoapps/instructor/views.py | 42 +++++++++---------- lms/djangoapps/open_ended_grading/views.py | 2 - 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.pylintrc b/.pylintrc index 2f2be69eb0..49fcf80eb9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -35,6 +35,7 @@ load-plugins= # it should appear only once). disable= # C0301: Line too long +# C0302: Too many lines in module # W0141: Used builtin function 'map' # W0142: Used * or ** magic # R0201: Method could be a function @@ -42,8 +43,11 @@ disable= # R0902: Too many instance attributes # R0903: Too few public methods (1/2) # R0904: Too many public methods +# R0911: Too many return statements +# R0912: Too many branches # R0913: Too many arguments - C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 +# R0914: Too many local variables + C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 [REPORTS] @@ -92,7 +96,7 @@ zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. -generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size +generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size,content [BASIC] diff --git a/lms/djangoapps/instructor/management/commands/dump_grades.py b/lms/djangoapps/instructor/management/commands/dump_grades.py index 13f86c0e0f..3707ad33ed 100644 --- a/lms/djangoapps/instructor/management/commands/dump_grades.py +++ b/lms/djangoapps/instructor/management/commands/dump_grades.py @@ -3,17 +3,12 @@ # django management command: dump grades to csv files # for use by batch processes -import os -import sys -import string -import datetime -import json +import csv -from instructor.views import * +from instructor.views import get_student_grade_summary_data from courseware.courses import get_course_by_id from xmodule.modulestore.django import modulestore -from django.conf import settings from django.core.management.base import BaseCommand @@ -45,7 +40,7 @@ class Command(BaseCommand): request = self.DummyRequest() try: course = get_course_by_id(course_id) - except Exception as err: + except Exception: if course_id in modulestore().courses: course = modulestore().courses[course_id] else: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 762b993504..a3b4f42bf7 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -11,7 +11,6 @@ import requests from requests.status_codes import codes import urllib from collections import OrderedDict -import json from StringIO import StringIO @@ -21,7 +20,6 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response -import requests from django.core.urlresolvers import reverse from courseware import grades @@ -36,11 +34,7 @@ from django_comment_client.models import (Role, from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem -from xmodule.modulestore.search import path_to_location import xmodule.graders as xmgraders import track.views @@ -48,14 +42,15 @@ from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) -template_imports = {'urllib': urllib} - # internal commands for managing forum roles: FORUM_ROLE_ADD = 'add' FORUM_ROLE_REMOVE = 'remove' def split_by_comma_and_whitespace(s): + """ + Return string s, split by , or whitespace + """ return re.split(r'[\s,]', s) @@ -141,7 +136,7 @@ def instructor_dashboard(request, course_id): # 'beta', so adding it to get_access_group_name doesn't really make # sense. name = course_beta_test_group_name(course.location) - (group, created) = Group.objects.get_or_create(name=name) + (group, _) = Group.objects.get_or_create(name=name) return group # process actions from form POST @@ -237,13 +232,13 @@ def instructor_dashboard(request, course_id): if '/' not in problem_to_reset: # allow state of modules other than problem to be reset problem_to_reset = "problem/" + problem_to_reset # but problem is the default try: - (org, course_name, run) = course_id.split("/") + (org, course_name, _) = course_id.split("/") module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id, course_id=course_id, module_state_key=module_state_key) msg += "Found module to reset. " - except Exception as e: + except Exception: msg += "Couldn't find module with that urlname. " if "Delete student state for problem" in action: @@ -352,7 +347,7 @@ def instructor_dashboard(request, course_id): return_csv('', datatable, fp=fp) fp.seek(0) files = {'datafile': fp} - msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files) + msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 @@ -423,7 +418,7 @@ def instructor_dashboard(request, course_id): datatable = {'header': ['username', 'email'] + profkeys} def getdat(u): p = u.profile - return [u.username, u.email] + [getattr(p,x,'') for x in profkeys] + return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] datatable['data'] = [getdat(u) for u in enrolled_students] datatable['title'] = 'Student profile data for course %s' % course_id @@ -433,17 +428,17 @@ def instructor_dashboard(request, course_id): elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump','') - if problem_to_dump[-4:]==".xml": - problem_to_dump=problem_to_dump[:-4] + if problem_to_dump[-4:] == ".xml": + problem_to_dump = problem_to_dump[:-4] try: - (org, course_name, run)=course_id.split("/") - module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump + (org, course_name, run) = course_id.split("/") + module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump smdat = StudentModule.objects.filter(course_id=course_id, module_state_key=module_state_key) smdat = smdat.order_by('student') msg += "Found %d records to dump " % len(smdat) except Exception as err: - msg+="Couldn't find module with that urlname. " + msg += "Couldn't find module with that urlname. " msg += "

    %s
    " % escape(err) smdat = [] @@ -741,7 +736,7 @@ def _list_course_forum_members(course_id, rolename, datatable): # make sure datatable is set up properly for display first, before checking for errors datatable['header'] = ['Username', 'Full name', 'Roles'] datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id) - datatable['data'] = []; + datatable['data'] = [] try: role = Role.objects.get(name=rolename, course_id=course_id) except Role.DoesNotExist: @@ -923,7 +918,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, datarow = [student.id, student.username, student.profile.name, student.email] try: datarow.append(student.externalauthmap.external_email) - except: # ExternalAuthMap.DoesNotExist + except: # ExternalAuthMap.DoesNotExist datarow.append('') if get_grades: @@ -1040,7 +1035,8 @@ def _do_enroll_students(course, course_id, students, overload=False): datatable['data'] = [[x, status[x]] for x in status] datatable['title'] = 'Enrollment of students' - def sf(stat): return [x for x in status if status[x] == stat] + def sf(stat): + return [x for x in status if status[x] == stat] data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'), deleted=sf('deleted'), datatable=datatable) @@ -1136,7 +1132,7 @@ def dump_grading_context(course): ''' msg = "-----------------------------------------------------------------------------\n" msg += "Course grader:\n" - + msg += '%s\n' % course.grader.__class__ graders = {} if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader): @@ -1151,7 +1147,7 @@ def dump_grading_context(course): gc = course.grading_context msg += "graded sections:\n" - + msg += '%s\n' % gc['graded_sections'].keys() for (gs, gsvals) in gc['graded_sections'].items(): msg += "--> Section %s:\n" % (gs) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 78da00bf2b..cb617d609d 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -27,8 +27,6 @@ from mitxmako.shortcuts import render_to_string log = logging.getLogger(__name__) -template_imports = {'urllib': urllib} - system = ModuleSystem( ajax_url=None, track_function=None, From a0f5a51135274241f2f469078b8f75222f973249 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 15:34:12 -0400 Subject: [PATCH 395/665] wip to fix broken test --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 1a48bc647c..97b3396baa 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -309,6 +309,8 @@ def import_from_xml(store, data_dir, course_dirs=None, except Exception: logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) + else: + module_data = content store.update_item(module.location, module_data) From 4e331a2c6a2999ad7847212a00135e5810121f45 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 8 Apr 2013 15:39:01 -0400 Subject: [PATCH 396/665] studio - restyling course basic details and adding in a course's URL more prominently WIP --- cms/static/sass/elements/_controls.scss | 2 +- cms/static/sass/elements/_forms.scss | 39 +++++++++++--- cms/static/sass/views/_settings.scss | 67 ++++++++++++++++++++++++- cms/templates/settings.html | 27 +++++----- 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index c4e96616a8..953b2d15e5 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -97,7 +97,7 @@ color: $blue; &:hover, &:active { - background: $blue-l3; + background: $blue-l4; color: $blue-s2; } diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 3bda079473..1faf4a883e 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -8,11 +8,11 @@ input[type="password"], textarea.text { padding: 6px 8px 8px; @include box-sizing(border-box); - border: 1px solid $mediumGrey; + border: 1px solid $gray-l2; border-radius: 2px; - @include linear-gradient($lightGrey, tint($lightGrey, 90%)); - background-color: $lightGrey; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); + @include linear-gradient($gray-l5, $white); + background-color: $gray-l5; + @include box-shadow(inset 0 1px 2px $shadow-l1); font-family: 'Open Sans', sans-serif; font-size: 11px; color: $baseFontColor; @@ -21,7 +21,7 @@ textarea.text { &::-webkit-input-placeholder, &:-moz-placeholder, &:-ms-input-placeholder { - color: #979faf; + color: $gray-l2; } &:focus { @@ -39,7 +39,7 @@ textarea.text { color: $gray-d2; } - input, textarea { + label, input, textarea { pointer-events: none; } } @@ -61,8 +61,31 @@ form { .note { @include box-sizing(border-box); - padding: $baseline; - background: $gray-l4; + + .title { + + } + + .copy { + + } + + // note with actions + &.has-actions { + @include clearfix(); + + .title { + + } + + .copy { + + } + + .list-actions { + + } + } } .note-promotion { diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index 6b8bec7912..87d076ab72 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -147,7 +147,7 @@ body.course.settings { } label { - @include font-size(14); + @extend .t-copy-sub1; @include transition(color, 0.15s, ease-in-out); margin: 0 0 ($baseline/4) 0; font-weight: 400; @@ -243,12 +243,31 @@ body.course.settings { .list-input { @include clearfix(); + padding: 0 ($baseline/2); .field { margin-bottom: 0; } } + // course details that should appear more like content than elements to change + .field.is-not-editable { + + label { + + } + + input, textarea { + @extend .t-copy-lead1; + @include box-shadow(none); + border: none; + background: none; + padding: 0; + margin: 0; + font-weight: 600; + } + } + #field-course-organization { float: left; width: flex-grid(2, 9); @@ -265,6 +284,52 @@ body.course.settings { float: left; width: flex-grid(5, 9); } + + // course link note + .note-promotion-courseURL { + @include box-shadow(0 1px 1px $shadow-l1); + @include border-radius(($baseline/5)); + margin-top: ($baseline*1.5); + border: 1px solid $gray-l3; + padding: ($baseline/2) 0 0 0; + + .title { + @extend .t-copy-sub1; + margin: 0 0 ($baseline/10) 0; + padding: 0 ($baseline/2); + } + + .copy { + padding: 0 ($baseline/2) ($baseline/2) ($baseline/2); + + .link-courseURL { + @extend .t-copy-lead1; + + &:hover { + + } + } + } + + .list-actions { + @include box-shadow(inset 0 1px 1px $shadow-l1); + border-top: 1px solid $gray-l4; + padding: ($baseline/2); + background: $gray-l5; + + .action-primary { + @include blue-button(); + @include font-size(13); + + .icon { + @extend .t-icon; + @include font-size(16); + display: inline-block; + vertical-align: middle; + } + } + } + } } // specific fields - schedule diff --git a/cms/templates/settings.html b/cms/templates/settings.html index afc958c608..9e16c15c0a 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -67,20 +67,6 @@ from contentstore import utils The nuts and bolts of your course - -
    1. @@ -97,6 +83,19 @@ from contentstore import utils
    + +
    +

    Course URL (for student enrollment and access)

    + + + +

    From 54afc5eb6331c700936a7cca6e9b15e60abbf4ca Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 16:09:41 -0400 Subject: [PATCH 397/665] seems like we need to purge 'filename' from both _model_data as well as xml_attributes --- common/lib/xmodule/xmodule/tests/test_export.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index 443014f9ef..170a89d783 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -24,6 +24,11 @@ def strip_filenames(descriptor): """ print "strip filename from {desc}".format(desc=descriptor.location.url()) descriptor._model_data.pop('filename', None) + + if hasattr(descriptor, 'xml_attributes'): + if 'filename' in descriptor.xml_attributes: + del descriptor.xml_attributes['filename'] + for d in descriptor.get_children(): strip_filenames(d) From 3f22826e26c85033daaebc3914a0946256d21605 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 8 Apr 2013 16:28:31 -0400 Subject: [PATCH 398/665] studio - alerts: adding some ARIA-centric accessibility information to example prompts/notifications and advanced settings save notification --- cms/static/js/base.js | 2 +- cms/static/js/views/settings/advanced_view.js | 8 +-- cms/templates/settings_advanced.html | 10 +-- cms/templates/ux-alerts.html | 62 ++++++++----------- 4 files changed, 36 insertions(+), 46 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bb3fe88510..6d047050be 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -545,7 +545,7 @@ function removeDateSetter(e) { function hideNotification(e) { (e).preventDefault(); - $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); + $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); } function hideAlert(e) { diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 536f23004e..9c62499773 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -104,10 +104,10 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ $(".wrapper-alert").removeClass("is-shown"); if (type) { if (type === this.error_saving) { - $(".wrapper-alert-error").addClass("is-shown"); + $(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false'); } else if (type === this.successful_changes) { - $(".wrapper-alert-confirmation").addClass("is-shown"); + $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); this.hideSaveCancelButtons(); } } @@ -119,13 +119,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ showSaveCancelButtons: function(event) { if (!this.notificationBarShowing) { this.$el.find(".message-status").removeClass("is-shown"); - $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown'); + $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false'); this.notificationBarShowing = true; } }, hideSaveCancelButtons: function() { if (this.notificationBarShowing) { - $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding'); + $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); this.notificationBarShowing = false; } }, diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 4cd1a4acd9..034f1a32e2 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -108,13 +108,13 @@ editor.render(); <%block name="view_notifications"> -
    + - \ 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 508/665] 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 509/665] 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 510/665] 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 511/665] 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 512/665] 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 513/665] 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 514/665] 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 515/665] 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 516/665] 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 517/665] 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 518/665] 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 519/665] 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 520/665] 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 521/665] 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 522/665] 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 523/665] 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 524/665] 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 525/665] 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 526/665] 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 527/665] 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 528/665] 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 529/665] 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 530/665] 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 531/665] 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 532/665] 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 533/665] 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 534/665] 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 535/665] 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 536/665] 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 537/665] 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 538/665] 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 539/665] 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 540/665] 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 566/665] 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 567/665] 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 568/665] 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 569/665] 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 570/665] 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 571/665] 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 572/665] 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 573/665] 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 574/665] 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 575/665] 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 576/665] 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 577/665] 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 578/665] 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 579/665] 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 580/665] 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 581/665] 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 582/665] 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 583/665] 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 584/665] 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 585/665] 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 586/665] 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 587/665] 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 588/665] 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 589/665] 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 590/665] 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 591/665] 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 592/665] 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 593/665] 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 594/665] 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