From 0cadc8af36d088a57eddb550d73cdaeed2762e10 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 12 Jul 2013 14:13:53 -0400 Subject: [PATCH 001/179] Change status bar UI --- .../css/combinedopenended/display.scss | 5 ++++- .../combined_open_ended_modulev1.py | 22 ++++++++++++++++--- .../combined_open_ended_status.html | 13 ++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 78f0213c8d..951242ac6c 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -56,13 +56,16 @@ section.legend-container { section.combined-open-ended-status { + padding: 20px 20px 20px 0px; + .statusitem { color: #2C2C2C; background-color : #d4d4d4; font-size: .9em; - padding: 2px; + padding: 20px; display: inline; width: 20%; + margin-right: 20px; .show-results { margin-top: .3em; text-align:right; 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 1fe62035e6..f5feb36ae6 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 @@ -33,6 +33,8 @@ TRUE_DICT = ["True", True, "TRUE", "true"] HUMAN_TASK_TYPE = { 'selfassessment': "Self Assessment", 'openended': "edX Assessment", + 'ml_grading.conf' : "AI Assessment", + 'peer_grading.conf' : "Peer Assessment", } # Default value that controls whether or not to skip basic spelling checks in the controller @@ -468,6 +470,19 @@ class CombinedOpenEndedV1Module(): } return last_response_dict + def extract_human_name_from_task(self, task_xml): + tree = etree.fromstring(task_xml) + log.info(etree.tostring(tree)) + payload = tree.xpath("/openended/openendedparam/grader_payload") + if len(payload)==0: + task_name = "selfassessment" + else: + inner_payload = json.loads(payload[0].text) + task_name = inner_payload['grader_settings'] + + human_task = HUMAN_TASK_TYPE[task_name] + return human_task + def update_task_states(self): """ Updates the task state of the combined open ended module with the task state of the current child module. @@ -689,9 +704,10 @@ class CombinedOpenEndedV1Module(): Output: The status html to be rendered """ status = [] - for i in xrange(0, self.current_task_number + 1): - task_data = self.get_last_response(i) - task_data.update({'task_number': i + 1}) + for i in xrange(0, len(self.task_xml)): + human_task_name = self.extract_human_name_from_task(self.task_xml[i]) + + task_data = {'task_number': i + 1, 'human_task' : human_task_name, 'current' : self.current_task_number==i} status.append(task_data) context = { diff --git a/lms/templates/combinedopenended/combined_open_ended_status.html b/lms/templates/combinedopenended/combined_open_ended_status.html index d13077737f..5495885044 100644 --- a/lms/templates/combinedopenended/combined_open_ended_status.html +++ b/lms/templates/combinedopenended/combined_open_ended_status.html @@ -1,22 +1,13 @@
-
- Status -
%for i in xrange(0,len(status_list)): <%status=status_list[i]%> - %if i==len(status_list)-1: + %if status['current']:
%else:
%endif - %if status['grader_type'] in grader_type_image_dict and render_via_ajax: - <% grader_image = grader_type_image_dict[status['grader_type']]%> - - %else: - ${status['human_task']} - %endif - (${status['human_state']}) + ${status['human_task']}
%endfor
From ffad965536e75c427d4e79a72e1e68f5ed52ff1f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 12 Jul 2013 15:06:11 -0400 Subject: [PATCH 002/179] fix some combinedopenended ui and data routing --- .../lib/xmodule/xmodule/css/combinedopenended/display.scss | 6 ++++++ .../open_ended_grading_classes/peer_grading_service.py | 6 ++++-- common/lib/xmodule/xmodule/peer_grading_module.py | 3 ++- .../combinedopenended/combined_open_ended_status.html | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 951242ac6c..86c028a8b0 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -108,6 +108,12 @@ section.combined-open-ended-status { float: right; } } + + .icon-caret-right { + display: inline-block; + vertical-align: baseline; + margin-right: ($baseline/4); + } } div.combined-rubric-container { diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py index 56bd1ec0a8..f80aff610a 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py @@ -41,7 +41,7 @@ class PeerGradingService(GradingService): return self.try_to_decode(self._render_rubric(response)) def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, - submission_flagged): + submission_flagged, answer_unknown): data = {'grader_id': grader_id, 'submission_id': submission_id, 'score': score, @@ -50,7 +50,9 @@ class PeerGradingService(GradingService): 'location': location, 'rubric_scores': rubric_scores, 'rubric_scores_complete': True, - 'submission_flagged': submission_flagged} + 'submission_flagged': submission_flagged, + 'answer_unknown' : answer_unknown, + } return self.try_to_decode(self.post(self.save_grade_url, data)) def is_student_calibrated(self, problem_location, grader_id): diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 7df444a892..0dbdde836e 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -310,10 +310,11 @@ class PeerGradingModule(PeerGradingFields, XModule): submission_key = data.get('submission_key') rubric_scores = data.getlist('rubric_scores[]') submission_flagged = data.get('submission_flagged') + answer_unknown = data.get('answer_unknown', False) try: response = self.peer_gs.save_grade(location, grader_id, submission_id, - score, feedback, submission_key, rubric_scores, submission_flagged) + score, feedback, submission_key, rubric_scores, submission_flagged, answer_unknown) return response except GradingServiceError: # This is a dev_facing_error diff --git a/lms/templates/combinedopenended/combined_open_ended_status.html b/lms/templates/combinedopenended/combined_open_ended_status.html index 5495885044..66afb79021 100644 --- a/lms/templates/combinedopenended/combined_open_ended_status.html +++ b/lms/templates/combinedopenended/combined_open_ended_status.html @@ -8,7 +8,11 @@
%endif ${status['human_task']} +
+ %if i + %endif %endfor
From 59f639d9d2e1891ff6979c97d51d4005ee5021c4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 18 Jul 2013 14:40:11 -0400 Subject: [PATCH 003/179] Change js around --- .../js/src/combinedopenended/display.coffee | 110 ++++++++++-------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 5939fbcdd8..7eb4f06805 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -1,9 +1,11 @@ class @Rubric constructor: () -> - @initialize: (location) -> - $('.rubric').data("location", location) - $('input[class="score-selection"]').change @tracking_callback + @initialize: (location,el) -> + @el = el + @$el = $(el) + @$('.rubric').data("location", location) + @$('input[class="score-selection"]').change @tracking_callback # set up the hotkeys $(window).unbind('keydown', @keypress_callback) $(window).keydown @keypress_callback @@ -12,8 +14,11 @@ class @Rubric @category = $(@categories.first()) @category.prepend('> ') @category_index = 0 - - + + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + @keypress_callback: (event) => # don't try to do this when user is typing in a text input if $(event.target).is('input, textarea') @@ -86,34 +91,48 @@ class @Rubric return true class @CombinedOpenEnded - constructor: (element) -> - @element=element - @reinitialize(element) + constructor: (el) -> + @el=el + @$el = $(el) + @reinitialize(el) $(window).keydown @keydown_handler $(window).keyup @keyup_handler + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + reinitialize: (element) -> - @wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule') - @el = $(element).find('section.combined-open-ended') - @combined_open_ended=$(element).find('section.combined-open-ended') - @id = @el.data('id') - @ajax_url = @el.data('ajax-url') - @state = @el.data('state') - @task_count = @el.data('task-count') - @task_number = @el.data('task-number') - @accept_file_upload = @el.data('accept-file-upload') - @location = @el.data('location') + @wrapper=@$('section.xmodule_CombinedOpenEndedModule') + @coe = @$('section.combined-open-ended') + + #Get data from combinedopenended + @allow_reset = @coe.data('allow_reset') + @id = @coe.data('id') + @ajax_url = @coe.data('ajax-url') + @state = @coe.data('state') + @task_count = @coe.data('task-count') + @task_number = @coe.data('task-number') + @accept_file_upload = @coe.data('accept-file-upload') + @location = @coe.data('location') + @child_state = @coe.data('state') + @child_type = @coe.data('child-type') + # set up handlers for click tracking Rubric.initialize(@location) @is_ctrl = false - @allow_reset = @el.data('allow_reset') + #Setup reset @reset_button = @$('.reset-button') @reset_button.click @reset + + #Setup next problem @next_problem_button = @$('.next-step-button') @next_problem_button.click @next_problem + @status_container = @$('.status-elements') + #setup show results @show_results_button=@$('.show-results-button') @show_results_button.click @show_results @@ -122,33 +141,32 @@ class @CombinedOpenEnded # valid states: 'initial', 'assessing', 'post_assessment', 'done' Collapsible.setCollapsibles(@el) - @submit_evaluation_button = $('.submit-evaluation-button') + @submit_evaluation_button = @$('.submit-evaluation-button') @submit_evaluation_button.click @message_post - @results_container = $('.result-container') - @combined_rubric_container = $('.combined-rubric-container') + @results_container = @$('.result-container') + @combined_rubric_container = @$('.combined-rubric-container') - @legend_container= $('.legend-container') + @legend_container= @$('.legend-container') @show_legend_current() # Where to put the rubric once we load it - @el = $(element).find('section.open-ended-child') - @errors_area = @$('.error') - @answer_area = @$('textarea.answer') - @prompt_container = @$('.prompt') - @rubric_wrapper = @$('.rubric-wrapper') - @hint_wrapper = @$('.hint-wrapper') - @message_wrapper = @$('.message-wrapper') - @submit_button = @$('.submit-button') - @child_state = @el.data('state') - @child_type = @el.data('child-type') + + @oe = @$('section.open-ended-child') + @errors_area = @$(@oe).find('.error') + @answer_area = @$(@oe).find('textarea.answer') + @prompt_container = @$(@oe).find('.prompt') + @rubric_wrapper = @$(@oe).find('.rubric-wrapper') + @hint_wrapper = @$(@oe).find('.hint-wrapper') + @message_wrapper = @$(@oe).find('.message-wrapper') + @submit_button = @$(@oe).find('.submit-button') if @child_type=="openended" - @skip_button = @$('.skip-button') + @skip_button = @$(@oe).find('.skip-button') @skip_button.click @skip_post_assessment - @file_upload_area = @$('.file-upload') + @file_upload_area = $(@oe).find('.file-upload') @can_upload_files = false - @open_ended_child= @$('.open-ended-child') + @open_ended_child= $(@oe).find('.open-ended-child') @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' @@ -166,10 +184,6 @@ class @CombinedOpenEnded @show_combined_rubric_current() @show_results_current() - # locally scoped jquery. - $: (selector) -> - $(selector, @el) - show_results_current: () => data = {'task_number' : @task_number-1} $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => @@ -223,10 +237,10 @@ class @CombinedOpenEnded evaluation_scoring = $(event.target).parent() fd = new FormData() - feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('input.submission_id')[0].value - grader_id = external_grader_message.find('input.grader_id')[0].value - score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() + feedback = @$(evaluation_scoring).find('textarea.feedback-on-feedback')[0].value + submission_id = @$(external_grader_message).find('input.submission_id')[0].value + grader_id = @$(external_grader_message).find('input.grader_id')[0].value + score = @$(evaluation_scoring).find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) fd.append('submission_id', submission_id) @@ -474,11 +488,11 @@ class @CombinedOpenEnded @errors_area.html(@out_of_sync_message) gentle_alert: (msg) => - if @el.find('.open-ended-alert').length - @el.find('.open-ended-alert').remove() + if @$el.find('.open-ended-alert').length + @$el.find('.open-ended-alert').remove() alert_elem = "
" + msg + "
" - @el.find('.open-ended-action').after(alert_elem) - @el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) + @$el.find('.open-ended-action').after(alert_elem) + @$el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) queueing: => if @child_state=="assessing" and @child_type=="openended" From 3509aea2b1578a42741485724581c17b8fa703ca Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 19 Jul 2013 14:21:01 -0400 Subject: [PATCH 004/179] Redo js --- .../js/src/combinedopenended/display.coffee | 175 +++++++++++------- .../peergrading/peer_grading_problem.coffee | 4 +- .../src/staff_grading/staff_grading.coffee | 4 +- 3 files changed, 107 insertions(+), 76 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 7eb4f06805..bf569c984a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -3,22 +3,17 @@ class @Rubric @initialize: (location,el) -> @el = el - @$el = $(el) - @$('.rubric').data("location", location) - @$('input[class="score-selection"]').change @tracking_callback + $('.rubric',@el).data("location", location) + $('input[class="score-selection"]',@el).change @tracking_callback # set up the hotkeys $(window).unbind('keydown', @keypress_callback) $(window).keydown @keypress_callback # display the 'current' carat - @categories = $('.rubric-category') - @category = $(@categories.first()) + @categories = $('.rubric-category',el) + @category = $(@categories.first(),el) @category.prepend('> ') @category_index = 0 - # locally scoped jquery. - $: (selector) -> - $(selector, @el) - @keypress_callback: (event) => # don't try to do this when user is typing in a text input if $(event.target).is('input, textarea') @@ -47,14 +42,14 @@ class @Rubric old_category_text = @category.html().substring(5) @category.html(old_category_text) @category_index++ - @category = $(@categories[@category_index]) + @category = $(@categories[@category_index],@el) @category.prepend('> ') @tracking_callback: (event) -> target_selection = $(event.target).val() # chop off the beginning of the name so that we can get the number of the category category = $(event.target).data("category") - location = $('.rubric').data('location') + location = $('.rubric',@el).data('location') # probably want the original problem location as well data = {location: location, selection: target_selection, category: category} @@ -64,7 +59,7 @@ class @Rubric # finds the scores for each rubric category @get_score_list: () => # find the number of categories: - num_categories = $('.rubric-category').length + num_categories = $('.rubric-category',@el).length score_lst = [] # get the score for each one @@ -83,14 +78,43 @@ class @Rubric @check_complete: () -> # check to see whether or not any categories have not been scored - num_categories = $('.rubric-category').length + num_categories = $('.rubric-category',@el).length for i in [0..(num_categories-1)] - score = $("input[name='score-selection-#{i}']:checked").val() + score = $("input[name='score-selection-#{i}']:checked",@el).val() if score == undefined return false return true class @CombinedOpenEnded + + wrapper_sel: 'section.xmodule_CombinedOpenEndedModule' + coe_sel: 'section.combined-open-ended' + reset_button_sel: '.reset-button' + next_step_sel: '.next-step-button' + status_container_sel: '.status-elements' + show_results_sel: '.show-results-button' + question_header_sel: '.question-header' + submit_evaluation_sel: '.submit-evaluation-button' + result_container_sel: 'div.result-container' + combined_rubric_sel: '.combined-rubric-container' + legend_sel: '.legend-container' + open_ended_child_sel: 'section.open-ended-child' + error_sel: '.error' + answer_area_sel: 'textarea.answer' + prompt_sel: '.prompt' + rubric_wrapper_sel: '.rubric-wrapper' + hint_wrapper_sel: '.hint-wrapper' + message_wrapper_sel: '.message-wrapper' + submit_button_sel: '.submit-button' + skip_button_sel: '.skip-button' + file_upload_sel: '.file-upload' + file_upload_box_sel: '.file-upload-box' + file_upload_preview_sel: '.file-upload-preview' + fof_sel: 'textarea.feedback-on-feedback' + sub_id_sel: 'input.submission_id' + grader_id_sel: 'input.grader_id' + grader_status_sel: '.grader-status' + constructor: (el) -> @el=el @$el = $(el) @@ -103,8 +127,8 @@ class @CombinedOpenEnded $(selector, @el) reinitialize: (element) -> - @wrapper=@$('section.xmodule_CombinedOpenEndedModule') - @coe = @$('section.combined-open-ended') + @wrapper=@$(@wrapper_sel) + @coe = @$(@coe_sel) #Get data from combinedopenended @allow_reset = @coe.data('allow_reset') @@ -118,55 +142,61 @@ class @CombinedOpenEnded @child_state = @coe.data('state') @child_type = @coe.data('child-type') + console.log(@child_state) + # set up handlers for click tracking - Rubric.initialize(@location) + Rubric.initialize(@location,@coe) @is_ctrl = false - + console.log("init rubric") #Setup reset - @reset_button = @$('.reset-button') + @reset_button = @$(@reset_button_sel) @reset_button.click @reset - + console.log("init reset") #Setup next problem - @next_problem_button = @$('.next-step-button') + @next_problem_button = @$(@next_step_sel) @next_problem_button.click @next_problem - @status_container = @$('.status-elements') + @status_container = @$(@status_container_sel) #setup show results - @show_results_button=@$('.show-results-button') + @show_results_button=@$(@show_results_sel) @show_results_button.click @show_results - @question_header = @$('.question-header') + @question_header = @$(@question_header_sel) @question_header.click @collapse_question # valid states: 'initial', 'assessing', 'post_assessment', 'done' - Collapsible.setCollapsibles(@el) - @submit_evaluation_button = @$('.submit-evaluation-button') + console.log("init collapse") + Collapsible.setCollapsibles(@$el) + console.log("finish collapse") + @submit_evaluation_button = @$(@submit_evaluation_sel) @submit_evaluation_button.click @message_post - @results_container = @$('.result-container') - @combined_rubric_container = @$('.combined-rubric-container') + @results_container = @$(@result_container_sel) + console.log(@results_container) + @combined_rubric_container = @$(@combined_rubric_sel) - @legend_container= @$('.legend-container') + @legend_container= @$(@legend_sel) @show_legend_current() # Where to put the rubric once we load it - - @oe = @$('section.open-ended-child') - @errors_area = @$(@oe).find('.error') - @answer_area = @$(@oe).find('textarea.answer') - @prompt_container = @$(@oe).find('.prompt') - @rubric_wrapper = @$(@oe).find('.rubric-wrapper') - @hint_wrapper = @$(@oe).find('.hint-wrapper') - @message_wrapper = @$(@oe).find('.message-wrapper') - @submit_button = @$(@oe).find('.submit-button') + console.log("started child") + @oe = @$(@open_ended_child_sel) + @errors_area = @$(@oe).find(@error_sel) + @answer_area = @$(@oe).find(@answer_area_sel) + @prompt_container = @$(@oe).find(@prompt_sel) + @rubric_wrapper = @$(@oe).find(@rubric_wrapper_sel) + @hint_wrapper = @$(@oe).find(@hint_wrapper_sel) + @message_wrapper = @$(@oe).find(@message_wrapper_sel) + @submit_button = @$(@oe).find(@submit_button_sel) if @child_type=="openended" - @skip_button = @$(@oe).find('.skip-button') + @skip_button = @$(@oe).find(@skip_button_sel) @skip_button.click @skip_post_assessment - @file_upload_area = $(@oe).find('.file-upload') + @file_upload_area = $(@oe).find(@file_upload_sel) @can_upload_files = false - @open_ended_child= $(@oe).find('.open-ended-child') + @open_ended_child= $(@oe).find(@open_ended_child_sel) + console.log("Passed child") @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' @@ -188,9 +218,10 @@ class @CombinedOpenEnded data = {'task_number' : @task_number-1} $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => if response.success - @results_container.after(response.html).remove() - @results_container = $('div.result-container') - @submit_evaluation_button = $('.submit-evaluation-button') + if (results_container?) + @results_container.after(response.html).remove() + @results_container = @$(@result_container_sel) + @submit_evaluation_button = @$(@submit_evaluation_sel) @submit_evaluation_button.click @message_post Collapsible.setCollapsibles(@results_container) # make sure we still have click tracking @@ -204,8 +235,8 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => if response.success @results_container.after(response.html).remove() - @results_container = $('div.result-container') - @submit_evaluation_button = $('.submit-evaluation-button') + @results_container = @$(@result_container_sel) + @submit_evaluation_button = @$(@submit_evaluation_sel) @submit_evaluation_button.click @message_post Collapsible.setCollapsibles(@results_container) else @@ -216,30 +247,30 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_combined_rubric", data, (response) => if response.success @combined_rubric_container.after(response.html).remove() - @combined_rubric_container= $('div.combined_rubric_container') + @combined_rubric_container= $(@combined_rubric_sel) show_status_current: () => data = {} $.postWithPrefix "#{@ajax_url}/get_status", data, (response) => if response.success @status_container.after(response.html).remove() - @status_container= $('.status-elements') + @status_container= $(@status_container_sel) show_legend_current: () => data = {} $.postWithPrefix "#{@ajax_url}/get_legend", data, (response) => if response.success @legend_container.after(response.html).remove() - @legend_container= $('.legend-container') + @legend_container= $(@legend_sel) message_post: (event)=> external_grader_message=$(event.target).parent().parent().parent() evaluation_scoring = $(event.target).parent() fd = new FormData() - feedback = @$(evaluation_scoring).find('textarea.feedback-on-feedback')[0].value - submission_id = @$(external_grader_message).find('input.submission_id')[0].value - grader_id = @$(external_grader_message).find('input.grader_id')[0].value + feedback = @$(evaluation_scoring).find(@fof_sel)[0].value + submission_id = @$(external_grader_message).find(@sub_id_sel)[0].value + grader_id = @$(external_grader_message).find(@grader_id_sel)[0].value score = @$(evaluation_scoring).find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) @@ -304,7 +335,7 @@ class @CombinedOpenEnded @submit_button.hide() @queueing() if @task_number==1 and @task_count==1 - @grader_status = $('.grader-status') + @grader_status = @$(@grader_status_sel) @grader_status.html("

Response submitted for scoring.

") else if @child_state == 'post_assessment' if @child_type=="openended" @@ -347,7 +378,7 @@ class @CombinedOpenEnded if @child_state == 'initial' files = "" if @can_upload_files == true - files = $('.file-upload-box')[0].files[0] + files = @$(@file_upload_box_sel)[0].files[0] if files != undefined if files.size > max_filesize @can_upload_files = false @@ -369,7 +400,7 @@ class @CombinedOpenEnded if response.success @rubric_wrapper.html(response.rubric_html) @rubric_wrapper.show() - Rubric.initialize(@location) + Rubric.initialize(@location,@coe) @answer_area.html(response.student_response) @child_state = 'assessing' @find_assessment_elements() @@ -396,6 +427,7 @@ class @CombinedOpenEnded @is_ctrl=false save_assessment: (event) => + console.log("callback save assessment") event.preventDefault() if @child_state == 'assessing' && Rubric.check_complete() checked_assessment = Rubric.get_total_score() @@ -454,7 +486,7 @@ class @CombinedOpenEnded @hint_wrapper.html('') @message_wrapper.html('') @child_state = 'initial' - @combined_open_ended.after(response.html).remove() + @coe.after(response.html).remove() @allow_reset="False" @reinitialize(@element) @rebind() @@ -473,7 +505,7 @@ class @CombinedOpenEnded @hint_wrapper.html('') @message_wrapper.html('') @child_state = 'initial' - @combined_open_ended.after(response.html).remove() + @coe.after(response.html).remove() @reinitialize(@element) @rebind() @next_problem_button.hide() @@ -514,8 +546,8 @@ class @CombinedOpenEnded @can_upload_files = true @file_upload_area.html('Uploaded image') @file_upload_area.show() - $('.file-upload-preview').hide() - $('.file-upload-box').change @preview_image + @$(@file_upload_preview_sel).hide() + @$(@file_upload_box_sel).change @preview_image else @gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.' @@ -559,7 +591,7 @@ class @CombinedOpenEnded @question_header.text("(Show)") log_feedback_click: (event) -> - link_text = $(event.target).html() + link_text = @$(event.target).html() if link_text == 'See full feedback' Logger.log 'oe_show_full_feedback', {} else if link_text == 'Respond to Feedback' @@ -567,32 +599,31 @@ class @CombinedOpenEnded else generated_event_type = link_text.toLowerCase().replace(" ","_") Logger.log "oe_" + generated_event_type, {} - log_feedback_selection: (event) -> - target_selection = $(event.target).val() + target_selection = @$(event.target).val() Logger.log 'oe_feedback_response_selected', {value: target_selection} remove_attribute: (name) => - if $('.file-upload-preview').attr(name) - $('.file-upload-preview')[0].removeAttribute(name) + if @$(@file_upload_preview_sel).attr(name) + @$(@file_upload_preview_sel)[0].removeAttribute(name) preview_image: () => - if $('.file-upload-box')[0].files && $('.file-upload-box')[0].files[0] + if @$(@file_upload_box_sel)[0].files && @$(@file_upload_box_sel)[0].files[0] reader = new FileReader() reader.onload = (e) => max_dim = 150 @remove_attribute('src') @remove_attribute('height') @remove_attribute('width') - $('.file-upload-preview').attr('src', e.target.result) - height_px = $('.file-upload-preview')[0].height - width_px = $('.file-upload-preview')[0].width + @$(@file_upload_preview_sel).attr('src', e.target.result) + height_px = @$(@file_upload_preview_sel)[0].height + width_px = @$(@file_upload_preview_sel)[0].width scale_factor = 0 if height_px>width_px scale_factor = height_px/max_dim else scale_factor = width_px/max_dim - $('.file-upload-preview')[0].width = width_px/scale_factor - $('.file-upload-preview')[0].height = height_px/scale_factor - $('.file-upload-preview').show() - reader.readAsDataURL($('.file-upload-box')[0].files[0]) + @$(@file_upload_preview_sel)[0].width = width_px/scale_factor + @$(@file_upload_preview_sel)[0].height = height_px/scale_factor + @$(@file_upload_preview_sel).show() + reader.readAsDataURL(@$(@file_upload_box_sel)[0].files[0]) diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 9483932f80..050d525a13 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -162,7 +162,7 @@ class @PeerGradingProblem @prompt_wrapper = $('.prompt-wrapper') @backend = backend @is_ctrl = false - + @el = $('.peer-grading-container') # get the location of the problem @location = $('.peer-grading').data('location') @@ -463,7 +463,7 @@ class @PeerGradingProblem @submit_button.hide() @action_button.hide() @calibration_feedback_panel.hide() - Rubric.initialize(@location) + Rubric.initialize(@location,@el) render_calibration_feedback: (response) => diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index f4a3360d1e..a57a8ebdb1 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -152,7 +152,7 @@ class @StaffGrading @backend = backend # all the jquery selectors - + @el = $('.staff-grading') @problem_list_container = $('.problem-list-container') @problem_list = $('.problem-list') @@ -224,7 +224,7 @@ class @StaffGrading setup_score_selection: => @score_selection_container.html(@rubric) $('input[class="score-selection"]').change => @graded_callback() - Rubric.initialize(@location) + Rubric.initialize(@location, @el) graded_callback: () => From e2326802106c7c88c69078ebf6c379aa918bc2f2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 19 Jul 2013 15:15:10 -0400 Subject: [PATCH 005/179] Move child attributes --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index bf569c984a..e7084d315b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -139,8 +139,6 @@ class @CombinedOpenEnded @task_number = @coe.data('task-number') @accept_file_upload = @coe.data('accept-file-upload') @location = @coe.data('location') - @child_state = @coe.data('state') - @child_type = @coe.data('child-type') console.log(@child_state) @@ -189,6 +187,8 @@ class @CombinedOpenEnded @hint_wrapper = @$(@oe).find(@hint_wrapper_sel) @message_wrapper = @$(@oe).find(@message_wrapper_sel) @submit_button = @$(@oe).find(@submit_button_sel) + @child_state = @oe.data('state') + @child_type = @oe.data('child-type') if @child_type=="openended" @skip_button = @$(@oe).find(@skip_button_sel) @skip_button.click @skip_post_assessment From a47fe070583f5b41f22f57b0bde385f4699bc373 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 19 Jul 2013 15:50:28 -0400 Subject: [PATCH 006/179] Hide legend, move display name --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 ++ lms/templates/combinedopenended/combined_open_ended.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index e7084d315b..e976b75828 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -303,6 +303,7 @@ class @CombinedOpenEnded @reset_button.hide() @next_problem_button.hide() @hide_file_upload() + @legend_container.show() @hint_area.attr('disabled', false) if @task_number>1 or @child_state!='initial' @show_status_current() @@ -325,6 +326,7 @@ class @CombinedOpenEnded @submit_button.prop('value', 'Submit') @submit_button.click @save_answer @setup_file_upload() + @legend_container.hide() else if @child_state == 'assessing' @answer_area.attr("disabled", true) @replace_text_inputs() diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 5d8ef859aa..0dc5ec914f 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -1,8 +1,8 @@
+

${display_name}

${status|n}
-

${display_name}

Prompt (Hide)

From ca0cefd260d73cec70b73295e22ab4524a74ce7a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 19 Jul 2013 16:26:23 -0400 Subject: [PATCH 007/179] Move legend, make status elements smaller --- .../lib/xmodule/xmodule/css/combinedopenended/display.scss | 2 +- lms/templates/combinedopenended/combined_open_ended.html | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 86c028a8b0..b6f78612f1 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -62,7 +62,7 @@ section.combined-open-ended-status { color: #2C2C2C; background-color : #d4d4d4; font-size: .9em; - padding: 20px; + padding: 10px; display: inline; width: 20%; margin-right: 20px; diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 0dc5ec914f..89bfb9ebf7 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -16,11 +16,12 @@
-
-
+
+
+
From 2dd5338378d4cd0706434ddbdf185af5f2236992 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 19 Jul 2013 16:43:01 -0400 Subject: [PATCH 008/179] Add in progress text --- .../xmodule/css/combinedopenended/display.scss | 13 ++++++++++++- .../combined_open_ended_modulev1.py | 8 ++++++++ .../combinedopenended/combined_open_ended.html | 9 +++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index b6f78612f1..f2d8e5c497 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -22,9 +22,20 @@ h2 { section.combined-open-ended { @include clearfix; - .status-container + .status-bar { + + .status-container + { padding-bottom: 5px; + display: inline-block; + padding-right: 20px + } + .progress-container + { + padding-bottom: 5px; + display: inline-block; + } } .item-container { 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 f7ae9af9d3..724c650cce 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 @@ -37,6 +37,13 @@ HUMAN_TASK_TYPE = { 'peer_grading.conf' : "Peer Assessment", } +HUMAN_STATES = { + 'intitial' : "Not started.", + 'assessing' : "Being scored.", + 'intermediate_done' : "Scoring finished.", + 'done' : "Complete." +} + # Default value that controls whether or not to skip basic spelling checks in the controller # Metadata overrides this SKIP_BASIC_CHECKS = False @@ -291,6 +298,7 @@ class CombinedOpenEndedV1Module(): 'accept_file_upload': self.accept_file_upload, 'location': self.location, 'legend_list': LEGEND_LIST, + 'human_state': HUMAN_STATES.get(self.state,"Not started.") } return context diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 89bfb9ebf7..e09fb29482 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -1,7 +1,12 @@

${display_name}

-
- ${status|n} +
+
+ ${status|n} +
+
+ Progress: ${human_state} +
From 579b66d9dd641994aeaa99cdfd682e14fb302665 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 11:44:42 -0400 Subject: [PATCH 009/179] Redesign header css --- .../css/combinedopenended/display.scss | 29 +++++++++++++++---- .../combined_open_ended.html | 15 ++++++---- .../combined_open_ended_status.html | 3 -- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index f2d8e5c497..2b20943d8b 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -16,6 +16,21 @@ h2 { } } +div.name{ + padding-bottom: 15px; + h2{ + display: inline; + } + + .progress-container + { + display: inline; + float: right; + padding-top: 3px; + } + +} + .inline-error { color: darken($error-red, 10%); } @@ -24,6 +39,13 @@ section.combined-open-ended { @include clearfix; .status-bar { + float: right; + display: inline-block; + + .problemtype{ + display: inline; + margin-right: 140px; + } .status-container { @@ -31,15 +53,11 @@ section.combined-open-ended { display: inline-block; padding-right: 20px } - .progress-container - { - padding-bottom: 5px; - display: inline-block; - } } .item-container { padding-bottom: 10px; + display: inline-block; } .result-container @@ -76,7 +94,6 @@ section.combined-open-ended-status { padding: 10px; display: inline; width: 20%; - margin-right: 20px; .show-results { margin-top: .3em; text-align:right; diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index e09fb29482..c4623b781d 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -1,13 +1,18 @@
-

${display_name}

-
-
- ${status|n} -
+
+

${display_name}

Progress: ${human_state}
+
+
+ Open Response Assessment +
+ Assessments:
+ ${status|n} +
+

Prompt (Hide)

diff --git a/lms/templates/combinedopenended/combined_open_ended_status.html b/lms/templates/combinedopenended/combined_open_ended_status.html index 66afb79021..50c0459ce9 100644 --- a/lms/templates/combinedopenended/combined_open_ended_status.html +++ b/lms/templates/combinedopenended/combined_open_ended_status.html @@ -10,9 +10,6 @@ ${status['human_task']}
- %if i - %endif %endfor
From 5ae6ce9d47434c893e122f8e3790d0cd78cf61fd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 12:10:16 -0400 Subject: [PATCH 010/179] Add in css borders --- .../css/combinedopenended/display.scss | 26 +++++++--- .../js/src/combinedopenended/display.coffee | 9 ++-- .../combined_open_ended.html | 48 ++++++++++--------- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 2b20943d8b..f38d33d300 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -36,28 +36,40 @@ div.name{ } section.combined-open-ended { - @include clearfix; + @include clearfix; +} + +div.problemwrapper { + border: 1px solid; + border-color: lightgray; + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + border-radius: 20px; + padding: 5px; .status-bar { float: right; display: inline-block; + padding: 5px; .problemtype{ display: inline; - margin-right: 140px; + margin-right: 135px; } .status-container { padding-bottom: 5px; display: inline-block; - padding-right: 20px } + border-bottom: 1px solid; + border-color: lightgray; } .item-container { padding-bottom: 10px; display: inline-block; + padding: 15px; } .result-container @@ -66,10 +78,6 @@ section.combined-open-ended { width: 100%; position:relative; } - h4 - { - margin-bottom:10px; - } } section.legend-container { @@ -694,4 +702,8 @@ section.open-ended-child { } } + div.prompt{ + padding: 10px; + background-color:white; + } } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index e976b75828..f89a124c09 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -140,8 +140,6 @@ class @CombinedOpenEnded @accept_file_upload = @coe.data('accept-file-upload') @location = @coe.data('location') - console.log(@child_state) - # set up handlers for click tracking Rubric.initialize(@location,@coe) @is_ctrl = false @@ -174,8 +172,6 @@ class @CombinedOpenEnded console.log(@results_container) @combined_rubric_container = @$(@combined_rubric_sel) - @legend_container= @$(@legend_sel) - @show_legend_current() # Where to put the rubric once we load it console.log("started child") @@ -205,6 +201,10 @@ class @CombinedOpenEnded else if @task_number==1 and @child_state!='initial' @prompt_hide() + if @child_state!="initial" + @show_legend_current() + @legend_container= @$(@legend_sel) + @find_assessment_elements() @find_hint_elements() @@ -339,6 +339,7 @@ class @CombinedOpenEnded if @task_number==1 and @task_count==1 @grader_status = @$(@grader_status_sel) @grader_status.html("

Response submitted for scoring.

") + @legend_container.hide() else if @child_state == 'post_assessment' if @child_type=="openended" @skip_button.show() diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index c4623b781d..ad96afc2df 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -5,34 +5,36 @@ Progress: ${human_state}
-
-
- Open Response Assessment -
- Assessments:
- ${status|n} -
-
- -
-

Prompt (Hide)

-
- % for item in items: -
${item['content'] | n}
- % endfor +
+
+
+ Open Response Assessment +
+ Assessments:
+ ${status|n} +
- - -
+
+

Prompt (Hide)

+
+ % for item in items: +
${item['content'] | n}
+ % endfor +
-
-
+ + +
-
-
+
+
-
+
+
+ +
+
From 9cb7d448c61a713aaf5e7cb38c04637a42164c9a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 12:15:05 -0400 Subject: [PATCH 011/179] Border between prompt element --- .../xmodule/xmodule/css/combinedopenended/display.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index f38d33d300..bb48b29996 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -702,8 +702,14 @@ section.open-ended-child { } } + div.prompt{ - padding: 10px; background-color:white; + border-bottom: 1px solid; + border-color: lightgray; + } + + h4{ + margin-top: 10px; } } From c059a31623b1c27368ca040f64149d1d46402301 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 12:33:12 -0400 Subject: [PATCH 012/179] Fix border locations, remove open ended response text --- .../xmodule/css/combinedopenended/display.scss | 14 +++++++++----- .../combinedopenended/combined_open_ended.html | 3 --- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index bb48b29996..2c53e51918 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -62,14 +62,18 @@ div.problemwrapper { padding-bottom: 5px; display: inline-block; } - border-bottom: 1px solid; - border-color: lightgray; } .item-container { padding-bottom: 10px; display: inline-block; padding: 15px; + + h4 { + padding-top: 10px; + border-top: 1px solid; + border-color: lightgray; + } } .result-container @@ -705,11 +709,11 @@ section.open-ended-child { div.prompt{ background-color:white; - border-bottom: 1px solid; - border-color: lightgray; } h4{ - margin-top: 10px; + padding-top: 10px; + border-top: 1px solid; + border-color: lightgray; } } diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index ad96afc2df..21008d83db 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -7,9 +7,6 @@
-
- Open Response Assessment -
Assessments:
${status|n}
From 172bc5cfa51989d7deeb66d3c811daf529792a00 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 14:23:11 -0400 Subject: [PATCH 013/179] Change naming, edit border radius --- .../css/combinedopenended/display.scss | 24 +++++++++++++++---- .../combined_open_ended_modulev1.py | 8 +++---- .../combined_open_ended.html | 15 +++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 2c53e51918..66b9c708c6 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -42,9 +42,9 @@ section.combined-open-ended { div.problemwrapper { border: 1px solid; border-color: lightgray; - -webkit-border-radius: 20px; - -moz-border-radius: 20px; - border-radius: 20px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; padding: 5px; .status-bar { @@ -54,7 +54,7 @@ div.problemwrapper { .problemtype{ display: inline; - margin-right: 135px; + float: left; } .status-container @@ -62,9 +62,18 @@ div.problemwrapper { padding-bottom: 5px; display: inline-block; } + + .problemtype-container{ + width: 40% + } + + .assessments-container{ + width: 30% + } } .item-container { + @clearfix padding-bottom: 10px; display: inline-block; padding: 15px; @@ -98,8 +107,13 @@ section.legend-container { section.combined-open-ended-status { padding: 20px 20px 20px 0px; - .statusitem { + &:first-child{ + border-bottom-right-radius: 10px; + } + &:last-child{ + border-top-left-radius: 10px; + } color: #2C2C2C; background-color : #d4d4d4; font-size: .9em; 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 724c650cce..2fc19fb6d7 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 @@ -31,10 +31,10 @@ ACCEPT_FILE_UPLOAD = False TRUE_DICT = ["True", True, "TRUE", "true"] HUMAN_TASK_TYPE = { - 'selfassessment': "Self Assessment", - 'openended': "edX Assessment", - 'ml_grading.conf' : "AI Assessment", - 'peer_grading.conf' : "Peer Assessment", + 'selfassessment': "Self", + 'openended': "edX", + 'ml_grading.conf' : "AI", + 'peer_grading.conf' : "Peer", } HUMAN_STATES = { diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 21008d83db..799abf2711 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -7,9 +7,18 @@
- Assessments:
- ${status|n} -
+ + + +
+
+ Open Response Assessment +
+
+ Assessments:
+ ${status|n} +
+
From d23d354ab36202e34a63bd0ade416d5498e467f6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 14:41:54 -0400 Subject: [PATCH 014/179] Change top text --- .../css/combinedopenended/display.scss | 15 ++++++++--- .../combined_open_ended.html | 25 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 66b9c708c6..0373ce1aa8 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -55,6 +55,9 @@ div.problemwrapper { .problemtype{ display: inline; float: left; + background-color: #d4d4d4; + padding: 10px; + border-radius: 5px; } .status-container @@ -64,11 +67,15 @@ div.problemwrapper { } .problemtype-container{ - width: 40% + width: 40%; + padding-top: 12px; } .assessments-container{ - width: 30% + width: 30%; + .assessment-text{ + display: inline-block; + } } } .item-container @@ -109,10 +116,10 @@ section.combined-open-ended-status { padding: 20px 20px 20px 0px; .statusitem { &:first-child{ - border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; } &:last-child{ - border-top-left-radius: 10px; + border-top-right-radius: 10px; } color: #2C2C2C; background-color : #d4d4d4; diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 799abf2711..da9e8ab04f 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -8,16 +8,21 @@
- - + + + +
-
- Open Response Assessment -
-
- Assessments:
- ${status|n} -
-
+
+ Open Response Assessment +
+
+
+ Assessments: +
+
+ ${status|n} +
+
From b6ff513e335ac4031ff2c39178b0f871cc20126b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 15:06:40 -0400 Subject: [PATCH 015/179] Add in get last response functionality --- .../xmodule/js/src/combinedopenended/display.coffee | 9 +++++++++ .../combined_open_ended_modulev1.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index f89a124c09..730a9462c7 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -196,6 +196,8 @@ class @CombinedOpenEnded @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' + @get_last_response() + if @task_number>1 @prompt_hide() else if @task_number==1 and @child_state!='initial' @@ -263,6 +265,13 @@ class @CombinedOpenEnded @legend_container.after(response.html).remove() @legend_container= $(@legend_sel) + get_last_response: () => + data = {} + $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => + if response.success + console.log(response) + console.log(response.response) + message_post: (event)=> external_grader_message=$(event.target).parent().parent().parent() evaluation_scoring = $(event.target).parent() 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 2fc19fb6d7..3ceafdca65 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 @@ -7,6 +7,7 @@ from xmodule.progress import Progress from xmodule.stringify import stringify_children import self_assessment_module import open_ended_module +from functools import partial from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST log = logging.getLogger("mitx.courseware") @@ -445,6 +446,7 @@ class CombinedOpenEndedV1Module(): 'feedback_dicts': feedback_dicts, 'grader_ids': grader_ids, 'submission_ids': submission_ids, + 'success' : True } return last_response_dict @@ -608,6 +610,7 @@ class CombinedOpenEndedV1Module(): 'get_combined_rubric': self.get_rubric, 'get_status': self.get_status_ajax, 'get_legend': self.get_legend, + 'get_last_response': self.get_last_response_ajax, } if dispatch not in handlers: @@ -617,6 +620,9 @@ class CombinedOpenEndedV1Module(): d = handlers[dispatch](data) return json.dumps(d, cls=ComplexEncoder) + def get_last_response_ajax(self,data): + return self.get_last_response(self.current_task_number) + def next_problem(self, _data): """ Called via ajax to advance to the next problem. From 29bd64555f55d534a7113d7c4910e9db75098d97 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 15:30:11 -0400 Subject: [PATCH 016/179] Get table to take up the width of the page --- common/lib/xmodule/xmodule/css/combinedopenended/display.scss | 4 ++++ .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 3 --- lms/templates/combinedopenended/combined_open_ended.html | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 0373ce1aa8..b1ac3d773c 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -60,6 +60,10 @@ div.problemwrapper { border-radius: 5px; } + .statustable{ + width: 750px; + } + .status-container { padding-bottom: 5px; diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 730a9462c7..dcb3cd24fc 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -196,8 +196,6 @@ class @CombinedOpenEnded @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' - @get_last_response() - if @task_number>1 @prompt_hide() else if @task_number==1 and @child_state!='initial' @@ -269,7 +267,6 @@ class @CombinedOpenEnded data = {} $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => if response.success - console.log(response) console.log(response.response) message_post: (event)=> diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index da9e8ab04f..f66088be8a 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -7,7 +7,7 @@
- +
From 1a4be13d4c693dc002a53fc42fc16c4a873ded4c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 15:40:09 -0400 Subject: [PATCH 017/179] Improve rubric look and feel --- .../xmodule/css/combinedopenended/display.scss | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index b1ac3d773c..e91765b50a 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -87,7 +87,7 @@ div.problemwrapper { @clearfix padding-bottom: 10px; display: inline-block; - padding: 15px; + margin: 0px 15px 0px 15px; h4 { padding-top: 10px; @@ -112,7 +112,7 @@ section.legend-container { display: inline; width: 20%; } - margin-bottom: 5px; + margin: 15px; } section.combined-open-ended-status { @@ -182,10 +182,11 @@ section.combined-open-ended-status { } div.combined-rubric-container { + margin:15px; ul.rubric-list{ list-style-type: none; padding:0; - margin:0; + margin:4px; li { &.rubric-list-item{ margin-bottom: 2px; @@ -194,6 +195,12 @@ div.combined-rubric-container { } } + h4{ + padding-top: 10px; + border-top: 1px solid; + border-color: lightgray; + } + span.rubric-category { font-size: .9em; font-weight: bold; From 2d966209f40935f9712ad9d27a4f4ab9a2615050 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 15:55:34 -0400 Subject: [PATCH 018/179] fix test --- .../xmodule/open_ended_grading_classes/peer_grading_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py index f80aff610a..415298edf3 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py @@ -111,7 +111,7 @@ class MockPeerGradingService(object): 'max_score': 4} def save_grade(self, location, grader_id, submission_id, - score, feedback, submission_key, rubric_scores, submission_flagged): + score, feedback, submission_key, rubric_scores, submission_flagged, answer_unknown): return {'success': True} def is_student_calibrated(self, problem_location, grader_id): From 12a2c06fb13f53290e5bed53c3482c2e87c490e3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 25 Jul 2013 15:57:21 -0400 Subject: [PATCH 019/179] Remove console.log statements --- .../xmodule/js/src/combinedopenended/display.coffee | 8 -------- lms/static/coffee/src/staff_grading/staff_grading.coffee | 1 - 2 files changed, 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index dcb3cd24fc..96ad75f088 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -143,11 +143,9 @@ class @CombinedOpenEnded # set up handlers for click tracking Rubric.initialize(@location,@coe) @is_ctrl = false - console.log("init rubric") #Setup reset @reset_button = @$(@reset_button_sel) @reset_button.click @reset - console.log("init reset") #Setup next problem @next_problem_button = @$(@next_step_sel) @next_problem_button.click @next_problem @@ -162,19 +160,15 @@ class @CombinedOpenEnded @question_header.click @collapse_question # valid states: 'initial', 'assessing', 'post_assessment', 'done' - console.log("init collapse") Collapsible.setCollapsibles(@$el) - console.log("finish collapse") @submit_evaluation_button = @$(@submit_evaluation_sel) @submit_evaluation_button.click @message_post @results_container = @$(@result_container_sel) - console.log(@results_container) @combined_rubric_container = @$(@combined_rubric_sel) # Where to put the rubric once we load it - console.log("started child") @oe = @$(@open_ended_child_sel) @errors_area = @$(@oe).find(@error_sel) @answer_area = @$(@oe).find(@answer_area_sel) @@ -192,7 +186,6 @@ class @CombinedOpenEnded @file_upload_area = $(@oe).find(@file_upload_sel) @can_upload_files = false @open_ended_child= $(@oe).find(@open_ended_child_sel) - console.log("Passed child") @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' @@ -436,7 +429,6 @@ class @CombinedOpenEnded @is_ctrl=false save_assessment: (event) => - console.log("callback save assessment") event.preventDefault() if @child_state == 'assessing' && Rubric.check_complete() checked_assessment = Rubric.get_total_score() diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index a57a8ebdb1..f22fe9c7fd 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -103,7 +103,6 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t else if cmd == 'save_grade' - console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") response = @mock('get_next', {location: data.location}) # get_problem_list From 77e57448b354f5d438e3b5fd61648eaa696b6adb Mon Sep 17 00:00:00 2001 From: marco Date: Thu, 1 Aug 2013 12:42:11 -0400 Subject: [PATCH 020/179] cleaned up sass document format and added baseline variable throughout capa and combined open ended problems. no style rule changes, simply whitespace, ordering of properties, and the addition of baseline over direct pixels for padding and margins. --- .../lib/xmodule/xmodule/css/capa/display.scss | 562 +++++++++--------- .../css/combinedopenended/display.scss | 494 ++++++++------- 2 files changed, 537 insertions(+), 519 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 48912795f0..a35dc01633 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -19,10 +19,10 @@ h2 { iframe[seamless]{ - background-color: transparent; - border: 0px none transparent; - padding: 0px; - overflow: hidden; + overflow: hidden; + padding: 0px; + border: 0px none transparent; + background-color: transparent; } .inline-error { @@ -31,17 +31,17 @@ iframe[seamless]{ section.problem-progress { display: inline-block; - color: #999; - font-size: em(16); - font-weight: 100; padding-left: 5px; + color: #999; + font-weight: 100; + font-size: em(16); } section.problem { @media print { display: block; - width: auto; padding: 0; + width: auto; canvas, img { page-break-inside: avoid; @@ -49,30 +49,29 @@ section.problem { } .inline { - display: inline; + display: inline; } .choicegroup { @include clearfix; - - label.choicegroup_correct{ - &:after{ - content: url('../images/correct-icon.png'); - margin-left:15px - } - } - - label.choicegroup_incorrect{ - &:after{ - content: url('../images/incorrect-icon.png'); - margin-left:15px; - } - } - - min-width:100px; + min-width: 100px; width: auto !important; width: 100px; + label.choicegroup_correct { + &:after { + margin-left: 15px; + content: url('../images/correct-icon.png'); + } + } + + label.choicegroup_incorrect { + &:after { + margin-left: 15px; + content: url('../images/incorrect-icon.png'); + } + } + .indicator_container { float: left; width: 25px; @@ -82,9 +81,9 @@ section.problem { fieldset { @include box-sizing(border-box); + margin: 0px 0px $baseline; + padding-left: $baseline; border-left: 1px solid #ddd; - padding-left: 20px; - margin: 0px 0px 20px; } input[type="radio"], @@ -102,21 +101,21 @@ section.problem { ol.enumerate { li { &:before { - content: " "; display: block; - height: 0; visibility: hidden; + height: 0; + content: " "; } } } .solution-span { > span { - margin: 20px 0; + margin: $baseline 0; display: block; border: 1px solid #ddd; - padding: 9px 15px 20px; - background: #FFF; + padding: 9px 15px $baseline; + background: #fff; position: relative; box-shadow: inset 0 0 0 1px #eee; border-radius: 3px; @@ -133,26 +132,26 @@ section.problem { margin-top: -2px; } &.status { + margin: 8px 0 0 $baseline/2; text-indent: -9999px; - margin: 8px 0 0 10px; } } &.unanswered { p.status { @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; } } &.correct, &.ui-icon-check { p.status { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } input { @@ -163,9 +162,9 @@ section.problem { &.processing { p.status { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } input { @@ -176,9 +175,9 @@ section.problem { &.incorrect, &.ui-icon-close { p.status { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; text-indent: -9999px; } @@ -195,12 +194,12 @@ section.problem { p.answer { @include inline-block(); margin-bottom: 0; - margin-left: 10px; + margin-left: $baseline/2; &:before { + display: inline; content: "Answer: "; font-weight: bold; - display: inline; } &:empty { @@ -218,12 +217,12 @@ section.problem { margin-bottom: 0; &.math { - padding: 6px; - background: #f1f1f1; - border: 1px solid #e3e3e3; @include inline-block; - border-radius: 4px; + padding: 6px; min-width: 30px; + border: 1px solid #e3e3e3; + border-radius: 4px; + background: #f1f1f1; } } } @@ -231,98 +230,92 @@ section.problem { span { &.unanswered, &.ui-icon-bullet { @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; position: relative; top: 4px; width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; } &.processing, &.ui-icon-processing { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } &.correct, &.ui-icon-check { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; position: relative; top: 3px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } &.partially-correct { @include inline-block(); - background: url('../images/partially-correct-icon.png') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/partially-correct-icon.png') center center no-repeat; } &.incorrect, &.ui-icon-close { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; position: relative; top: 3px; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; } } - .reload - { + .reload { float:right; - margin: 10px; + margin: $baseline/2; } .grader-status { + @include clearfix; + margin-bottom: $baseline; padding: 9px; - background: #F6F6F6; border: 1px solid #ddd; border-top: 0; - margin-bottom: 20px; - @include clearfix; + background: #F6F6F6; span { - text-indent: -9999px; - overflow: hidden; display: block; float: left; + overflow: hidden; margin: -7px 7px 0 0; + text-indent: -9999px; } .grading { - background: url('../images/info-icon.png') left center no-repeat; - padding-left: 25px; - text-indent: 0px; margin: 0px 7px 0 0; + padding-left: 25px; + background: url('../images/info-icon.png') left center no-repeat; + text-indent: 0px; } p { - line-height: 20px; - text-transform: capitalize; - margin-bottom: 0; float: left; + margin-bottom: 0; + text-transform: capitalize; + line-height: 20px; } &.file { - background: #FFF; - margin-top: 20px; - padding: 20px 0 0 0; - - border: { - top: 1px solid #eee; - right: 0; - bottom: 0; - left: 0; - } + background: #fff; + margin-top: $baseline; + padding: $baseline 0 0 0; + border: 0; + border-top: 1px solid #eee; p.debug { display: none; @@ -335,54 +328,54 @@ section.problem { } .evaluation { - p { - margin-bottom: 4px; - } + p { + margin-bottom: 4px; + } } .feedback-on-feedback { - height: 100px; - margin-right: 20px; + height: 100px; + margin-right: $baseline; } .evaluation-response { - header { - text-align: right; - a { - font-size: .85em; - } + header { + text-align: right; + a { + font-size: .85em; } + } } .evaluation-scoring { - .scoring-list { - list-style-type: none; - margin-left: 3px; + .scoring-list { + margin-left: 3px; + list-style-type: none; - li { - &:first-child { - margin-left: 0px; - } - display:inline; - margin-left: 50px; + li { + display:inline; + margin-left: 50px; + + &:first-child { + margin-left: 0px; + } - label { - font-size: .9em; - } - - } + label { + font-size: .9em; + } } - + } } + .submit-message-container { - margin: 10px 0px ; + margin: $baseline 0px ; } } form.option-input { - margin: -10px 0 20px; - padding-bottom: 20px; + margin: -$baseline/2 0 $baseline; + padding-bottom: $baseline; select { margin-right: flex-gutter(); @@ -390,17 +383,17 @@ section.problem { } ul { - list-style: disc outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: disc outside none; } ol { - list-style: decimal outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: decimal outside none; } dl { @@ -421,8 +414,8 @@ section.problem { } li { - line-height: 1.4em; margin-bottom: lh(.5); + line-height: 1.4em; &:last-child { margin-bottom: 0; @@ -439,8 +432,8 @@ section.problem { table-layout: auto; th { - font-weight: bold; text-align: left; + font-weight: bold; } td { @@ -453,44 +446,43 @@ section.problem { } caption { - background: #f1f1f1; margin-bottom: .75em; margin-bottom: .75rem; padding: .75em 0; padding: .75rem 0; + background: #f1f1f1; } tr, td, th { vertical-align: middle; } - } code { margin: 0 2px; padding: 0px 5px; - white-space: nowrap; - border: 1px solid #EAEAEA; - background-color: #F8F8F8; + border: 1px solid #eaeaea; border-radius: 3px; + background-color: #f8f8f8; + white-space: nowrap; font-size: .9em; } pre { - background-color: #F8F8F8; - border: 1px solid #CCC; + overflow: auto; + padding: 6px $baseline/2; + border: 1px solid #ccc; + border-radius: 3px; + background-color: #f8f8f8; font-size: .9em; line-height: 1.4; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; > code { margin: 0; padding: 0; - white-space: pre; border: none; background: transparent; + white-space: pre; } } @@ -507,26 +499,25 @@ section.problem { } pre { - border-radius: 0; - border-radius: 0; - border-width: 0; + overflow: hidden; margin: 0; padding: 0; + border-width: 0; + border-radius: 0; background: transparent; - font-family: inherit; - font-size: inherit; white-space: pre; word-wrap: normal; - overflow: hidden; + font-size: inherit; + font-family: inherit; resize: none; &.CodeMirror-cursor { - z-index: 10; position: absolute; + z-index: 10; visibility: hidden; - border-left: 1px solid black; - border-right: none; width: 0; + border-right: none; + border-left: 1px solid black; } } } @@ -536,14 +527,14 @@ section.problem { } hr { - background: #ddd; - border: none; - clear: both; - color: #ddd; float: none; - height: 1px; + clear: both; margin: 0 0 .75rem; width: 100%; + height: 1px; + border: none; + background: #ddd; + color: #ddd; } .hidden { @@ -560,17 +551,17 @@ section.problem { center { display: block; margin: lh() 0; - border: 1px solid #ccc; padding: lh(); + border: 1px solid #ccc; } section.action { - margin-top: 20px; + margin-top: $baseline; .save, .check, .show { height: ($baseline*2); - font-weight: 600; vertical-align: middle; + font-weight: 600; } .save { @@ -580,8 +571,8 @@ section.problem { .show { .show-label { - font-size: 1.0em; font-weight: 600; + font-size: 1.0em; } } @@ -592,20 +583,20 @@ section.problem { // padding: 8px 12px; // margin-top: 10px; @include inline-block; - font-style: italic; - margin: 8px 0 0 10px; + margin: 8px 0 0 $baseline/2; color: #777; + font-style: italic; -webkit-font-smoothing: antialiased; } } .detailed-solution { > p:first-child { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } p:last-child { @@ -614,12 +605,12 @@ section.problem { } div.capa_alert { + margin-top: $baseline; padding: 8px 12px; - border: 1px solid #EBE8BF; + border: 1px solid #ebe8bf; border-radius: 3px; - background: #FFFCDD; + background: #fffcdd; font-size: 0.9em; - margin-top: 10px; } div.capa_reset { @@ -628,12 +619,14 @@ section.problem { background-color: lighten($error-red, 25%); border-radius: 3px; font-size: 1em; - margin-top: 10px; - margin-bottom: 10px; + margin-top: $baseline/2; + margin-bottom: $baseline/2; } + .capa_reset>h2 { - color: #AA0000; + color: #aa0000; } + .capa_reset li { font-size: 0.9em; } @@ -642,10 +635,10 @@ section.problem { border: 1px solid #ccc; h3 { - border-bottom: 1px solid #e3e3e3; - text-shadow: 0 1px 0 #fff; padding: 9px; + border-bottom: 1px solid #e3e3e3; background: #eee; + text-shadow: 0 1px 0 #fff; font-weight: bold; font-size: em(16); } @@ -665,7 +658,7 @@ section.problem { a { display: block; padding: 9px; - background: #F6F6F6; + background: #f6f6f6; box-shadow: inset 0 0 0 1px #fff; } } @@ -683,22 +676,22 @@ section.problem { margin-bottom: 12px; h3 { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } } > section { - border: 1px solid #ddd; - padding: 9px 9px 20px; - margin-bottom: 10px; - background: #FFF; position: relative; - box-shadow: inset 0 0 0 1px #eee; + margin-bottom: $baseline/2; + padding: 9px 9px $baseline; + border: 1px solid #ddd; border-radius: 3px; + background: #fff; + box-shadow: inset 0 0 0 1px #eee; p:last-of-type { margin-bottom: 0; @@ -709,28 +702,29 @@ section.problem { } a.full { - @include position(absolute, 0 0 1px 0px); - font-size: .8em; - padding: 4px; - text-align: right; - width: 100%; - display: block; - background: #F3F3F3; + @include position(absolute, 0 0 1px 0); @include box-sizing(border-box); + display: block; + padding: 4px; + width: 100%; + background: #f3f3f3; + text-align: right; + font-size: .8em; } } } .external-grader-message { section { - padding-left: 20px; - background-color: #FAFAFA; - color: #2C2C2C; - font-family: monospace; + padding-top: $baseline/2; + padding-left: $baseline; + background-color: #fafafa; + color: #2c2c2c; font-size: 1em; - padding-top: 10px; + font-family: monospace; + header { - font-size: 1.4em; + font-size: 1.4em; } .shortform { @@ -738,35 +732,36 @@ section.problem { } .longform { - padding: 0px; - margin: 0px; + margin: 0; + padding: 0; .result-errors { - margin: 5px; - padding: 10px 10px 10px 40px; + margin: $baseline/4; + padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2; background: url('../images/incorrect-icon.png') center left no-repeat; + li { - color: #B00; - } + color: #b00; + } } .result-output { - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: $baseline solid #fafafa; h4 { - font-family: monospace; font-size: 1em; + font-family: monospace; } dl { - margin: 0px; + margin: 0; } dt { - margin-top: 20px; + margin-top: $baseline; } dd { @@ -776,6 +771,7 @@ section.problem { .result-correct { background: url('../images/correct-icon.png') left 20px no-repeat; + .result-actual-output { color: #090; } @@ -783,6 +779,7 @@ section.problem { .result-incorrect { background: url('../images/incorrect-icon.png') left 20px no-repeat; + .result-actual-output { color: #B00; } @@ -790,16 +787,16 @@ section.problem { .markup-text{ margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; bs { - color: #BB0000; + color: #bb0000; } bg { - color: #BDA046; + color: #bda046; } } } @@ -807,96 +804,111 @@ section.problem { } .rubric { - tr { - margin:10px 0px; - height: 100%; - } - td { - padding: 20px 0px; - margin: 10px 0px; - height: 100%; - } - th { - padding: 5px; - margin: 5px; - } - label, - .view-only { - margin:3px; - position: relative; - padding: 15px; - width: 150px; - height:100%; - display: inline-block; - min-height: 50px; - min-width: 50px; - background-color: #CCC; - font-size: .9em; - } - .grade { - position: absolute; - bottom:0px; - right:0px; - margin:10px; - } - .selected-grade { - background: #666; - color: white; - } - input[type=radio]:checked + label { - background: #666; - color: white; } - input[class='score-selection'] { - display: none; - } + tr { + margin: $baseline/2 0; + height: 100%; + } + + td { + margin: $baseline/2 0; + padding: $baseline 0; + height: 100%; + } + + th { + margin: $baseline/4; + padding: $baseline/4; + } + + label, + .view-only { + position: relative; + display: inline-block; + margin: 3px; + padding: 15px; + min-width: 50px; + min-height: 50px; + width: 150px; + height: 100%; + background-color: #ccc; + font-size: .9em; + } + + .grade { + position: absolute; + right: 0; + bottom: 0; + margin: $baseline/2; + } + + .selected-grade { + background: #666; + color: white; + } + + input[type=radio]:checked + label { + background: #666; + color: white; + } + + input[class='score-selection'] { + display: none; + } } .annotation-input { $yellow: rgba(255,255,10,0.3); - + margin: 0 0 1em 0; border: 1px solid #ccc; border-radius: 1em; - margin: 0 0 1em 0; .annotation-header { - font-weight: bold; - border-bottom: 1px solid #ccc; padding: .5em 1em; + border-bottom: 1px solid #ccc; + font-weight: bold; } + .annotation-body { padding: .5em 1em; } + a.annotation-return { float: right; font: inherit; font-weight: normal; } + a.annotation-return:after { content: " \2191" } .block, ul.tags { margin: .5em 0; padding: 0; } + .block-highlight { padding: .5em; + border: 1px solid darken($yellow, 10%); + background-color: $yellow; color: #333; font-style: normal; - background-color: $yellow; - border: 1px solid darken($yellow, 10%); } + .block-comment { font-style: italic; } ul.tags { display: block; - list-style-type: none; margin-left: 1em; + list-style-type: none; + li { + position: relative; display: block; margin: 1em 0 0 0; - position: relative; + .tag { display: inline-block; - cursor: pointer; + margin-left: $baseline; border: 1px solid rgb(102,102,102); - margin-left: 40px; + cursor: pointer; + &.selected { background-color: $yellow; } @@ -908,42 +920,49 @@ section.problem { .tag-status, .tag { padding: .25em .5em; } } } + textarea.comment { $num-lines-to-show: 5; $line-height: 1.4em; $padding: .2em; - width: 100%; padding: $padding (2 * $padding); - line-height: $line-height; + width: 100%; height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2); + line-height: $line-height; } + .answer-annotation { display: block; margin: 0; } /* for debugging the input value field. enable the debug flag on the inputtype */ .debug-value { - color: #fff; - padding: 1em; - margin: 1em 0; - background-color: #999; - border: 1px solid #000; - input[type="text"] { width: 100%; } - pre { background-color: #CCC; color: #000; } - &:before { - display: block; - content: "debug input value"; - text-transform: uppercase; - font-weight: bold; - font-size: 1.5em; - } + margin: 1em 0; + padding: 1em; + border: 1px solid #000; + background-color: #999; + color: #fff; + + input[type="text"] { width: 100%; } + + pre { background-color: #CCC; color: #000; } + + &:before { + display: block; + content: "debug input value"; + text-transform: uppercase; + font-weight: bold; + font-size: 1.5em; + } } } + .choicetextgroup{ + @extend .choicegroup; + input[type="text"]{ margin-bottom: 0.5em; } - @extend .choicegroup; - label.choicetextgroup_correct, section.choicetextgroup_correct{ + label.choicetextgroup_correct, section.choicetextgroup_correct { @extend label.choicegroup_correct; input[type="text"] { @@ -951,17 +970,18 @@ section.problem { } } - label.choicetextgroup_incorrect, section.choicetextgroup_incorrect{ + label.choicetextgroup_incorrect, section.choicetextgroup_incorrect { @extend label.choicegroup_incorrect; } - label.choicetextgroup_show_correct, section.choicetextgroup_show_correct{ - &:after{ - content: url('../images/correct-icon.png'); + label.choicetextgroup_show_correct, section.choicetextgroup_show_correct { + &:after { margin-left:15px; + content: url('../images/correct-icon.png'); } } - span.mock_label{ + + span.mock_label { cursor : default; } } diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index e91765b50a..61b1901866 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -17,18 +17,17 @@ h2 { } div.name{ - padding-bottom: 15px; - h2{ - display: inline; - } - - .progress-container - { - display: inline; - float: right; - padding-top: 3px; - } + padding-bottom: 15px; + + h2{ + display: inline; + } + .progress-container { + display: inline; + float: right; + padding-top: 3px; + } } .inline-error { @@ -36,177 +35,178 @@ div.name{ } section.combined-open-ended { - @include clearfix; + @include clearfix; } div.problemwrapper { + padding: $baseline/4; border: 1px solid; border-color: lightgray; - -webkit-border-radius: 10px; - -moz-border-radius: 10px; - border-radius: 10px; - padding: 5px; - .status-bar - { - float: right; + border-radius: 10px; + + .status-bar { display: inline-block; + float: right; padding: 5px; .problemtype{ - display: inline; - float: left; - background-color: #d4d4d4; - padding: 10px; - border-radius: 5px; + display: inline; + float: left; + padding: $baseline/2; + border-radius: 5px; + background-color: #d4d4d4; } - .statustable{ - width: 750px; + .statustable { + width: 750px; } - .status-container - { - padding-bottom: 5px; + .status-container { display: inline-block; + padding-bottom: $baseline/4; } - .problemtype-container{ - width: 40%; - padding-top: 12px; + .problemtype-container { + width: 40%; + padding-top: 12px; } - .assessments-container{ - width: 30%; - .assessment-text{ - display: inline-block; - } - } - } - .item-container - { - @clearfix - padding-bottom: 10px; - display: inline-block; - margin: 0px 15px 0px 15px; + .assessments-container { + width: 30%; - h4 { - padding-top: 10px; - border-top: 1px solid; - border-color: lightgray; + .assessment-text { + display: inline-block; } + } + } + .item-container { + @clearfix + padding-bottom: 10px; + display: inline-block; + margin: 0px 15px 0px 15px; + + h4 { + padding-top: $baseline/2; + border-top: 1px solid; + border-color: lightgray; + } } - .result-container - { - float:left; - width: 100%; - position:relative; + .result-container { + float:left; + width: 100%; + position:relative; } } section.legend-container { + margin: 15px; + .legenditem { + display: inline; + padding: 2px; + width: 20%; background-color : #d4d4d4; font-size: .9em; - padding: 2px; - display: inline; - width: 20%; } - margin: 15px; } section.combined-open-ended-status { + padding: $baseline $baseline $baseline 0; - padding: 20px 20px 20px 0px; - .statusitem { - &:first-child{ - border-bottom-left-radius: 10px; - } - &:last-child{ - border-top-right-radius: 10px; - } - color: #2C2C2C; - background-color : #d4d4d4; - font-size: .9em; - padding: 10px; - display: inline; - width: 20%; - .show-results { - margin-top: .3em; - text-align:right; - } - .show-results-button { - font: 1em monospace; - } + .statusitem { + display: inline; + padding: $baseline/2; + width: 20%; + background-color : #d4d4d4; + color: #2c2c2c; + font-size: .9em; + + &:first-child { + border-bottom-left-radius: 10px; } - .statusitem-current { - background-color: #B2B2B2; - color: #222; - } - - span { - &.unanswered { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - position: relative; - width: 14px; - float: right; - } - - &.correct { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - position: relative; - width: 25px; - float: right; - } - - &.incorrect { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - position: relative; - float: right; - } + &:last-child { + border-top-right-radius: 10px; + } + + .show-results { + margin-top: .3em; + text-align:right; } - .icon-caret-right { - display: inline-block; - vertical-align: baseline; - margin-right: ($baseline/4); + .show-results-button { + font: 1em monospace; } + } + + .statusitem-current { + background-color: #B2B2B2; + color: #222; + } + + span { + &.unanswered { + @include inline-block(); + position: relative; + float: right; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + + &.correct { + @include inline-block(); + position: relative; + float: right; + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + &.incorrect { + @include inline-block(); + position: relative; + float: right; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + } + } + + .icon-caret-right { + display: inline-block; + vertical-align: baseline; + margin-right: ($baseline/4); + } } div.combined-rubric-container { - margin:15px; - ul.rubric-list{ + margin: 15px; + padding-bottom: 5px; + padding-top: 10px; + + ul.rubric-list { + margin: 4px; + padding: 0; list-style-type: none; - padding:0; - margin:4px; li { - &.rubric-list-item{ + &.rubric-list-item { margin-bottom: 2px; padding: 0px; } } } - - h4{ - padding-top: 10px; - border-top: 1px solid; - border-color: lightgray; - } + h4 { + padding-top: 10px; + border-top: 1px solid; + border-color: lightgray; + } span.rubric-category { font-size: .9em; font-weight: bold; } - padding-bottom: 5px; - padding-top: 10px; } div.result-container { @@ -342,8 +342,8 @@ div.result-container { section.open-ended-child { @media print { display: block; - width: auto; padding: 0; + width: auto; canvas, img { page-break-inside: avoid; @@ -351,30 +351,30 @@ section.open-ended-child { } .inline { - display: inline; + display: inline; } ol.enumerate { li { &:before { - content: " "; display: block; - height: 0; visibility: hidden; + height: 0; + content: " "; } } } .solution-span { > span { - margin: 20px 0; - display: block; - border: 1px solid #ddd; - padding: 9px 15px 20px; - background: #FFF; position: relative; - box-shadow: inset 0 0 0 1px #eee; + display: block; + margin: $baseline 0; + padding: 9px 15px $baseline; + border: 1px solid #ddd; border-radius: 3px; + background: #fff; + box-shadow: inset 0 0 0 1px #eee; &:empty { display: none; @@ -387,8 +387,8 @@ section.open-ended-child { margin-top: -2px; } &.status { + margin: 8px 0 0 $baseline/2; text-indent: -9999px; - margin: 8px 0 0 10px; } } @@ -404,9 +404,9 @@ section.open-ended-child { div.correct, div.ui-icon-check { p.status { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } input { @@ -417,9 +417,9 @@ section.open-ended-child { div.processing { p.status { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } input { @@ -430,9 +430,9 @@ section.open-ended-child { div.incorrect, div.ui-icon-close { p.status { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; text-indent: -9999px; } @@ -467,96 +467,90 @@ section.open-ended-child { span { &.unanswered, &.ui-icon-bullet { @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; position: relative; top: 4px; width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; } &.processing, &.ui-icon-processing { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } &.correct, &.ui-icon-check { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } &.incorrect, &.ui-icon-close { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; position: relative; top: 6px; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; } } - .reload - { + .reload { float:right; margin: 10px; } div.short-form-response { - background: #F6F6F6; - border: 1px solid #ddd; - margin-bottom: 0px; - overflow-y: auto; - height: 200px; - @include clearfix; - } + @include clearfix; + overflow-y: auto; + margin-bottom: 0; + height: 200px; + border: 1px solid #ddd; + background: #f6f6f6; + } .grader-status { - padding: 9px; - background: #F6F6F6; - border: 1px solid #ddd; - border-top: 0; - margin-bottom: 20px; @include clearfix; + margin-bottom: $baseline; + padding: 9px; + border: 1px solid #ddd; + border-top: 0px; + background: #F6F6F6; span { - text-indent: -9999px; - overflow: hidden; display: block; float: left; + overflow: hidden; margin: -7px 7px 0 0; + text-indent: -9999px; } .grading { - background: url('../images/info-icon.png') left center no-repeat; + margin: 0 7px 0 0; padding-left: 25px; - text-indent: 0px; - margin: 0px 7px 0 0; + background: url('../images/info-icon.png') left center no-repeat; + text-indent: 0; } p { - line-height: 20px; - margin-bottom: 0; float: left; + margin-bottom: 0; + line-height: 20px; } &.file { - background: #FFF; - margin-top: 20px; - padding: 20px 0 0 0; - - border: { - top: 1px solid #eee; - right: 0; - bottom: 0; - left: 0; - } + margin-top: $baseline; + padding: $baseline 0 0 0; + border: 0; + border-top: 1px solid #eee; + background: #fff; p.debug { display: none; @@ -570,8 +564,8 @@ section.open-ended-child { } form.option-input { - margin: -10px 0 20px; - padding-bottom: 20px; + margin: -$baseline/2 0 $baseline; + padding-bottom: $baseline; select { margin-right: flex-gutter(); @@ -579,29 +573,30 @@ section.open-ended-child { } ul { - list-style: disc outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: disc outside none; } ul.rubric-list{ - list-style-type: none; - padding:0; - margin:0; - li { - &.rubric-list-item{ - margin-bottom: 0px; - padding: 0px; - } - } + margin: 0; + padding: 0; + list-style-type: none; + + li { + &.rubric-list-item { + margin-bottom: 0; + padding: 0; + } + } } ol { - list-style: decimal outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: decimal outside none; } dl { @@ -622,8 +617,9 @@ section.open-ended-child { } li { - margin-bottom: 0px; - padding: 0px; + margin-bottom: 0; + padding: 0; + &:last-child { margin-bottom: 0; } @@ -634,14 +630,14 @@ section.open-ended-child { } hr { - background: #ddd; - border: none; - clear: both; - color: #ddd; float: none; - height: 1px; + clear: both; margin: 0 0 .75rem; width: 100%; + height: 1px; + border: none; + background: #ddd; + color: #ddd; } .hidden { @@ -655,7 +651,7 @@ section.open-ended-child { } section.action { - margin-top: 20px; + margin-top: $baseline; input.save { @extend .blue-button !optional; @@ -663,20 +659,20 @@ section.open-ended-child { .submission_feedback { @include inline-block; - font-style: italic; - margin: 8px 0 0 10px; + margin: 8px 0 0 $baseline/2; color: #777; + font-style: italic; -webkit-font-smoothing: antialiased; } } .detailed-solution { > p:first-child { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } p:last-child { @@ -686,66 +682,68 @@ section.open-ended-child { div.open-ended-alert, .save_message { + margin-top: $baseline/2; + margin-bottom: $baseline/4; padding: 8px 12px; - border: 1px solid #EBE8BF; + border: 1px solid #ebe8bf; border-radius: 3px; - background: #FFFCDD; + background: #fffcdd; font-size: 0.9em; - margin-top: 10px; - margin-bottom:5px; } div.capa_reset { + margin-top: $baseline/2; + margin-bottom: $baseline/2; padding: 25px; border: 1px solid $error-red; - background-color: lighten($error-red, 25%); border-radius: 3px; + background-color: lighten($error-red, 25%); font-size: 1em; - margin-top: 10px; - margin-bottom: 10px; } - .capa_reset>h2 { - color: #AA0000; + + .capa_reset > h2 { + color: #aa0000; } + .capa_reset li { font-size: 0.9em; } .assessment-container { - margin: 40px 0px 30px 0px; - .scoring-container - { - p - { - margin-bottom: 1em; - } - label { - margin: 10px; - padding: 5px; - display: inline-block; - min-width: 50px; - background-color: #CCC; - text-size: 1.5em; - } - - input[type=radio]:checked + label { - background: #666; - color: white; - } - input[class='grade-selection'] { - display: none; - } + margin: $baseline*2 0px 30px 0px; + .scoring-container { + p { + margin-bottom: 1em; } + + label { + display: inline-block; + margin: $baseline/2; + padding: $baseline/4; + min-width: 50px; + background-color: #ccc; + text-size: 1.5em; + } + + input[type=radio]:checked + label { + background: #666; + color: white; + } + + input[class='grade-selection'] { + display: none; + } + } } - div.prompt{ - background-color:white; + div.prompt { + background-color: white; } - h4{ - padding-top: 10px; - border-top: 1px solid; + h4 { + padding-top: $baseline/2; border-color: lightgray; + border-top: 1px solid; } } From 178be44333659f4d2c178a6ce9b727ea9effe48d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 1 Aug 2013 16:25:21 -0400 Subject: [PATCH 021/179] Remove legend, start to edit rubric --- .../css/combinedopenended/display.scss | 14 +++++++++ .../js/src/combinedopenended/display.coffee | 31 ++++++++----------- .../combined_open_ended_rubric.py | 10 ++++++ .../openendedchild.py | 6 ++-- .../combined_open_ended_results.html | 2 +- .../openended/open_ended_combined_rubric.html | 25 ++++++++------- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 61b1901866..5e217e5ee0 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -207,6 +207,20 @@ div.combined-rubric-container { font-size: .9em; font-weight: bold; } + + label.choicegroup_correct { + &:before { + margin-right: 15px; + content: url('../images/correct-icon.png'); + } + } + + label.choicegroup_incorrect { + &:before { + margin-right: 15px; + content: url('../images/incorrect-icon.png'); + } + } } div.result-container { diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 96ad75f088..6022bdd4cc 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -55,7 +55,6 @@ class @Rubric data = {location: location, selection: target_selection, category: category} Logger.log 'rubric_select', data - # finds the scores for each rubric category @get_score_list: () => # find the number of categories: @@ -97,7 +96,6 @@ class @CombinedOpenEnded submit_evaluation_sel: '.submit-evaluation-button' result_container_sel: 'div.result-container' combined_rubric_sel: '.combined-rubric-container' - legend_sel: '.legend-container' open_ended_child_sel: 'section.open-ended-child' error_sel: '.error' answer_area_sel: 'textarea.answer' @@ -114,6 +112,8 @@ class @CombinedOpenEnded sub_id_sel: 'input.submission_id' grader_id_sel: 'input.grader_id' grader_status_sel: '.grader-status' + info_rubric_elements_sel: '.rubric-elements-info' + rubric_collapse_sel: '.rubric-collapse' constructor: (el) -> @el=el @@ -194,10 +194,6 @@ class @CombinedOpenEnded else if @task_number==1 and @child_state!='initial' @prompt_hide() - if @child_state!="initial" - @show_legend_current() - @legend_container= @$(@legend_sel) - @find_assessment_elements() @find_hint_elements() @@ -240,7 +236,10 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_combined_rubric", data, (response) => if response.success @combined_rubric_container.after(response.html).remove() - @combined_rubric_container= $(@combined_rubric_sel) + @combined_rubric_container= @$(@combined_rubric_sel) + @toggle_rubric("") + @rubric_collapse = @$(@rubric_collapse_sel) + @rubric_collapse.click @toggle_rubric show_status_current: () => data = {} @@ -249,13 +248,6 @@ class @CombinedOpenEnded @status_container.after(response.html).remove() @status_container= $(@status_container_sel) - show_legend_current: () => - data = {} - $.postWithPrefix "#{@ajax_url}/get_legend", data, (response) => - if response.success - @legend_container.after(response.html).remove() - @legend_container= $(@legend_sel) - get_last_response: () => data = {} $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => @@ -302,7 +294,6 @@ class @CombinedOpenEnded @reset_button.hide() @next_problem_button.hide() @hide_file_upload() - @legend_container.show() @hint_area.attr('disabled', false) if @task_number>1 or @child_state!='initial' @show_status_current() @@ -325,7 +316,6 @@ class @CombinedOpenEnded @submit_button.prop('value', 'Submit') @submit_button.click @save_answer @setup_file_upload() - @legend_container.hide() else if @child_state == 'assessing' @answer_area.attr("disabled", true) @replace_text_inputs() @@ -338,7 +328,6 @@ class @CombinedOpenEnded if @task_number==1 and @task_count==1 @grader_status = @$(@grader_status_sel) @grader_status.html("

Response submitted for scoring.

") - @legend_container.hide() else if @child_state == 'post_assessment' if @child_type=="openended" @skip_button.show() @@ -568,7 +557,7 @@ class @CombinedOpenEnded reload: -> location.reload() - collapse_question: () => + collapse_question: (event) => @prompt_container.slideToggle() @prompt_container.toggleClass('open') if @question_header.text() == "(Hide)" @@ -578,6 +567,7 @@ class @CombinedOpenEnded Logger.log 'oe_show_question', {location: @location} new_text = "(Hide)" @question_header.text(new_text) + return false prompt_show: () => if @prompt_container.is(":hidden")==true @@ -628,3 +618,8 @@ class @CombinedOpenEnded @$(@file_upload_preview_sel)[0].height = height_px/scale_factor @$(@file_upload_preview_sel).show() reader.readAsDataURL(@$(@file_upload_box_sel)[0].files[0]) + + toggle_rubric: (event) => + info_rubric_elements = @$(@info_rubric_elements_sel) + info_rubric_elements.slideToggle() + return false diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py index 6245d4d31c..0b2b0fdaec 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py @@ -208,6 +208,7 @@ class CombinedOpenEndedRubric(object): feedback_types) rubric_categories = self.extract_categories(rubric_xml) max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories) + actual_scores = [] max_score = max(max_scores) for i in xrange(0, len(rubric_categories)): category = rubric_categories[i] @@ -217,9 +218,18 @@ class CombinedOpenEndedRubric(object): if tuple[1] == i and tuple[2] == j: for grader_type in tuple[3]: rubric_categories[i]['options'][j]['grader_types'].append(grader_type) + if len(actual_scores)<=i: + actual_scores.append([j]) + else: + actual_scores[i] += [j] + + actual_scores = [sum(i)/len(i) for i in actual_scores] + correct = [int(a>.66) for a in actual_scores] html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR), {'categories': rubric_categories, + 'max_scores': max_scores, + 'correct' : correct, 'has_score': True, 'view_only': True, 'max_score': max_score, diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 10f939b270..555505d611 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -58,7 +58,7 @@ class OpenEndedChild(object): 'assessing': 'In progress', 'post_assessment': 'Done', 'done': 'Done', - } + } def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): @@ -229,7 +229,7 @@ class OpenEndedChild(object): 'max_score': self._max_score, 'child_attempts': self.child_attempts, 'child_created': False, - } + } return json.dumps(state) def _allow_reset(self): @@ -485,4 +485,4 @@ class OpenEndedChild(object): else: eta_string = "" - return eta_string + return eta_string \ No newline at end of file diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 0a03737b8f..f1e9289221 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -1,4 +1,4 @@
-

${task_name}

+

${task_name}

(Show) ${results | n}
diff --git a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html index 61393cdc95..9ae148442f 100644 --- a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html @@ -7,18 +7,19 @@ <% option = category['options'][j] %>
  • - %for grader_type in category['options'][j]['grader_types']: - % if grader_type in grader_type_image_dict: - <% grader_image = grader_type_image_dict[grader_type] %> - % if grader_type in human_grader_types: - <% human_title = human_grader_types[grader_type] %> - % else: - <% human_title = grader_type %> - % endif - - % endif - %endfor - ${option['points']} points : ${option['text']} + %if len(category['options'][j]['grader_types'])>0: + %if correct[i]==1: +
  • % endfor From 0e8c29989c8c4ddc95e06b03abf58a962d183621 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 2 Aug 2013 00:57:31 -0400 Subject: [PATCH 022/179] style cleanup for open ended assessments, cleaning up workflow header, style definitions for rubrics are commented out momentarily, and an open issue for unicode rendering on the show and hide prompt area still remains --- .../css/combinedopenended/display.scss | 128 ++++++++++++------ .../js/src/combinedopenended/display.coffee | 8 +- .../combined_open_ended.html | 8 +- 3 files changed, 93 insertions(+), 51 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 61b1901866..32e88d23e2 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -39,56 +39,74 @@ section.combined-open-ended { } div.problemwrapper { - padding: $baseline/4; - border: 1px solid; - border-color: lightgray; - border-radius: 10px; + border: 1px solid lightgray; + border-radius: $baseline/2; .status-bar { - display: inline-block; - float: right; - padding: 5px; - - .problemtype{ - display: inline; - float: left; - padding: $baseline/2; - border-radius: 5px; - background-color: #d4d4d4; - } + background-color: #eee; + border-radius: $baseline/2 $baseline/2 0 0; + border-bottom: 1px solid lightgray; .statustable { - width: 750px; + width: 100%; + padding: $baseline; } .status-container { - display: inline-block; - padding-bottom: $baseline/4; + display: table-cell; + text-align: center; + + .status-elements { + border-radius: $baseline/4; + border: 1px solid lightgray; + } } .problemtype-container { - width: 40%; - padding-top: 12px; + padding: $baseline/2; + width: 60%; + } + + .problemtype{ + padding: $baseline/2; } .assessments-container { - width: 30%; + padding: $baseline/2 $baseline $baseline/2 $baseline/2; + float: right; .assessment-text { display: inline-block; + display: table-cell; + padding-right: $baseline/2; } } } .item-container { - @clearfix - padding-bottom: 10px; - display: inline-block; - margin: 0px 15px 0px 15px; + padding-bottom: $baseline/2; + margin: 15px; - h4 { + .visibility-control-prompt { + display: block; + height: 40px; + width: 100%; + + .inner { + float: left; + height: 5px; + margin-top: 17px; + border-top: 1px dotted #ddd; + width: 85%; + } + } + + a { + display: block; + text-align: center; + width: 15%; + float: right; padding-top: $baseline/2; - border-top: 1px solid; - border-color: lightgray; + font-size: .9em; } } @@ -101,33 +119,36 @@ div.problemwrapper { section.legend-container { margin: 15px; + border-radius: $baseline/4; .legenditem { display: inline; - padding: 2px; + padding: $baseline/2; width: 20%; - background-color : #d4d4d4; + background-color : #eee; font-size: .9em; } } section.combined-open-ended-status { - padding: $baseline $baseline $baseline 0; + vertical-align: center; .statusitem { - display: inline; + display: table-cell; padding: $baseline/2; - width: 20%; - background-color : #d4d4d4; + width: 30px; + background-color: #eee; color: #2c2c2c; font-size: .9em; + border-right: 1px solid lightgray; &:first-child { - border-bottom-left-radius: 10px; + border-radius: $baseline/4 0 0 $baseline/4; } &:last-child { - border-top-right-radius: 10px; + border-radius: 0 $baseline/4 $baseline/4 0; + border-right: 0px; } .show-results { @@ -141,8 +162,9 @@ section.combined-open-ended-status { } .statusitem-current { - background-color: #B2B2B2; - color: #222; + background-color: #fff; + box-shadow: inset 0 1px 1px gray; + color: #222; } span { @@ -187,15 +209,29 @@ div.combined-rubric-container { padding-top: 10px; ul.rubric-list { - margin: 4px; + margin: 0 $baseline $baseline/2 $baseline; padding: 0; list-style-type: none; + li { + &.rubric-list-item { margin-bottom: 2px; padding: 0px; } } + + .score-selection { + //display: inline-block; + //padding-right: $baseline/2; + //width: 5%; + //vertical-align: center; + } + + .wrappable { + //display: inline-block; + //width: 94%; + } } h4 { padding-top: 10px; @@ -204,8 +240,11 @@ div.combined-rubric-container { } span.rubric-category { - font-size: .9em; + display: block; + width: 100%; + border-bottom: 1px solid lightgray; font-weight: bold; + font-size: .9em; } } @@ -511,6 +550,7 @@ section.open-ended-child { @include clearfix; overflow-y: auto; margin-bottom: 0; + padding: $baseline/2; height: 200px; border: 1px solid #ddd; background: #f6f6f6; @@ -521,8 +561,8 @@ section.open-ended-child { margin-bottom: $baseline; padding: 9px; border: 1px solid #ddd; - border-top: 0px; - background: #F6F6F6; + border-top: 0; + background: #f6f6f6; span { display: block; @@ -742,8 +782,6 @@ section.open-ended-child { } h4 { - padding-top: $baseline/2; - border-color: lightgray; - border-top: 1px solid; + padding: $baseline/2 0; } } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 96ad75f088..0a06c29d86 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -572,24 +572,24 @@ class @CombinedOpenEnded @prompt_container.slideToggle() @prompt_container.toggleClass('open') if @question_header.text() == "(Hide)" - new_text = "(Show)" + new_text = "↧ Show Prompt" Logger.log 'oe_hide_question', {location: @location} else Logger.log 'oe_show_question', {location: @location} - new_text = "(Hide)" + new_text = "↥ Hide Prompt" @question_header.text(new_text) prompt_show: () => if @prompt_container.is(":hidden")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("(Hide)") + @question_header.text("↥ Hide Prompt") prompt_hide: () => if @prompt_container.is(":visible")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("(Show)") + @question_header.text("↧ Show Prompt") log_feedback_click: (event) -> link_text = @$(event.target).html() diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index f66088be8a..5f1c80fc5f 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -11,7 +11,7 @@
    - Open Response Assessment + Open Response
    @@ -27,7 +27,11 @@
    -

    Prompt (Hide)

    +
    +
    +
    + ↥ Hide Prompt +
    % for item in items:
    ${item['content'] | n}
    From 30e506777efe6eb33c3989d1fe6950ae757eff1e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 12:12:38 -0400 Subject: [PATCH 023/179] Render multiple rubrics, collapse rubrics --- .../js/src/combinedopenended/display.coffee | 6 +++ .../combined_open_ended_modulev1.py | 42 +++++++++++-------- .../combined_open_ended_results.html | 11 +++-- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 6022bdd4cc..db41288324 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -622,4 +622,10 @@ class @CombinedOpenEnded toggle_rubric: (event) => info_rubric_elements = @$(@info_rubric_elements_sel) info_rubric_elements.slideToggle() + @rubric_header = @$(@rubric_collapse_sel) + if @rubric_header.text() == "(Hide)" + new_text = "(Show)" + else + new_text = "(Hide)" + @rubric_header.text(new_text) return false 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 3ceafdca65..d9d296175e 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 @@ -503,27 +503,35 @@ class CombinedOpenEndedV1Module(): """ all_responses = [] loop_up_to_task = self.current_task_number + 1 + contexts = [] for i in xrange(0, loop_up_to_task): - all_responses.append(self.get_last_response(i)) - rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0, len(all_responses)) if - len(all_responses[i]['rubric_scores']) > 0 and all_responses[i]['grader_types'][ - 0] in HUMAN_GRADER_TYPE.keys()] - grader_types = [all_responses[i]['grader_types'] for i in xrange(0, len(all_responses)) if - len(all_responses[i]['grader_types']) > 0 and all_responses[i]['grader_types'][ - 0] in HUMAN_GRADER_TYPE.keys()] - feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0, len(all_responses)) if - len(all_responses[i]['feedback_items']) > 0 and all_responses[i]['grader_types'][ - 0] in HUMAN_GRADER_TYPE.keys()] - rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), - rubric_scores, - grader_types, feedback_items) + response = self.get_last_response(i) + rubric_scores = None + if len(response['rubric_scores']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): + rubric_scores = [response['rubric_scores']] + grader_types = None + if len(response['grader_types']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): + grader_types = [response['grader_types']] + feedback_items = None + if len(response['feedback_items']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): + feedback_items = [response['feedback_items']] + if feedback_items is not None and grader_types is not None and rubric_scores is not None: + rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), + rubric_scores, + grader_types, feedback_items) + contexts.append({ + 'result': rubric_html, + 'task_name': 'Scored Rubric', + 'class_name': 'combined-rubric-container' + }) - response_dict = all_responses[-1] context = { - 'results': rubric_html, - 'task_name': 'Scored Rubric', - 'class_name': 'combined-rubric-container' + 'results': contexts, + 'name' : 'name', } + + log.info(contexts) + html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index f1e9289221..90fca863b1 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -1,4 +1,7 @@ -
    -

    ${task_name}

    (Show) - ${results | n} -
    + +% for (i,result) in enumerate(results): +
    +

    ${result['task_name']}(Hide)

    + ${result['result'] | n} +
    +% endfor From a939c152c80a6d271cf2632ad24f745d296176cf Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 12:56:31 -0400 Subject: [PATCH 024/179] Add in support for multiple rubrics --- .../combined_open_ended_modulev1.py | 4 +--- .../combined_open_ended_results.html | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 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 d9d296175e..a943ccda49 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 @@ -521,13 +521,11 @@ class CombinedOpenEndedV1Module(): grader_types, feedback_items) contexts.append({ 'result': rubric_html, - 'task_name': 'Scored Rubric', - 'class_name': 'combined-rubric-container' + 'task_name': 'Scored Rubric' }) context = { 'results': contexts, - 'name' : 'name', } log.info(contexts) diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 90fca863b1..84f186fbb6 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -1,7 +1,19 @@ - % for (i,result) in enumerate(results): -
    -

    ${result['task_name']}(Hide)

    - ${result['result'] | n} -
    + % if 'task_name' in result and 'result' in result: +
    0: + status="shown"> + % else: + status="hidden"> + % endif +

    ${result['task_name']} from grader ${i+1} (Hide)

    + ${result['result'] | n} +
    + %endif + % endfor +% if len(results)>1: + Previous + Next +% endif + From bca308e70efc312139a416d1c1d59ba8fd5b05ab Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 13:10:03 -0400 Subject: [PATCH 025/179] No more log.info --- .../combined_open_ended_modulev1.py | 4 ---- 1 file changed, 4 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 a943ccda49..dd88a23077 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 @@ -452,7 +452,6 @@ class CombinedOpenEndedV1Module(): def extract_human_name_from_task(self, task_xml): tree = etree.fromstring(task_xml) - log.info(etree.tostring(tree)) payload = tree.xpath("/openended/openendedparam/grader_payload") if len(payload)==0: task_name = "selfassessment" @@ -527,9 +526,6 @@ class CombinedOpenEndedV1Module(): context = { 'results': contexts, } - - log.info(contexts) - html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} From 932bbd259247f5b69f2d39c0a1945951816fedeb Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 13:23:03 -0400 Subject: [PATCH 026/179] Fix text issue --- .../xmodule/js/src/combinedopenended/display.coffee | 10 +++++----- .../combined_open_ended_modulev1.py | 2 +- .../combinedopenended/combined_open_ended.html | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index e82ebeb4f5..b539c72554 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -560,12 +560,12 @@ class @CombinedOpenEnded collapse_question: (event) => @prompt_container.slideToggle() @prompt_container.toggleClass('open') - if @question_header.text() == "(Hide)" - new_text = "↧ Show Prompt" + if @question_header.text() == "Hide Prompt" + new_text = "Show Prompt" Logger.log 'oe_hide_question', {location: @location} else Logger.log 'oe_show_question', {location: @location} - new_text = "↥ Hide Prompt" + new_text = "Hide Prompt" @question_header.text(new_text) return false @@ -573,13 +573,13 @@ class @CombinedOpenEnded if @prompt_container.is(":hidden")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("↥ Hide Prompt") + @question_header.text("Hide Prompt") prompt_hide: () => if @prompt_container.is(":visible")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("↧ Show Prompt") + @question_header.text("Show Prompt") log_feedback_click: (event) -> link_text = @$(event.target).html() 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 dd88a23077..ae3d01c686 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 @@ -520,7 +520,7 @@ class CombinedOpenEndedV1Module(): grader_types, feedback_items) contexts.append({ 'result': rubric_html, - 'task_name': 'Scored Rubric' + 'task_name': 'Scored rubric' }) context = { diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 5f1c80fc5f..b517e60976 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -30,7 +30,7 @@
    % for item in items: From ec99e20a356b778bb35674d05faf155448d6e8b3 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 2 Aug 2013 13:37:53 -0400 Subject: [PATCH 027/179] styled rubric, dividers between sections --- .../css/combinedopenended/display.scss | 100 +++++++++++------- .../js/src/combinedopenended/display.coffee | 8 +- .../src/staff_grading/test_grading.html | 6 +- .../combined_open_ended.html | 2 +- .../openended/open_ended.html | 6 +- .../openended/open_ended_rubric.html | 8 +- .../self_assessment_prompt.html | 7 +- 7 files changed, 88 insertions(+), 49 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 224f554101..2873e37e73 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -1,3 +1,6 @@ +// lms - xmodule - combinedopenended +// ==================== + h2 { margin-top: 0; margin-bottom: 15px; @@ -16,6 +19,7 @@ h2 { } } + // Problem Header div.name{ padding-bottom: 15px; @@ -38,6 +42,7 @@ section.combined-open-ended { @include clearfix; } + div.problemwrapper { border: 1px solid lightgray; border-radius: $baseline/2; @@ -85,29 +90,6 @@ div.problemwrapper { .item-container { padding-bottom: $baseline/2; margin: 15px; - - .visibility-control-prompt { - display: block; - height: 40px; - width: 100%; - - .inner { - float: left; - height: 5px; - margin-top: 17px; - border-top: 1px dotted #ddd; - width: 85%; - } - } - - a { - display: block; - text-align: center; - width: 15%; - float: right; - padding-top: $baseline/2; - font-size: .9em; - } } .result-container { @@ -163,7 +145,6 @@ section.combined-open-ended-status { .statusitem-current { background-color: #fff; - box-shadow: inset 0 1px 1px gray; color: #222; } @@ -203,6 +184,63 @@ section.combined-open-ended-status { } } + // Problem Section Controls + +.visibility-control, .visibility-control-prompt { + display: block; + height: 40px; + width: 100%; + + .inner { + float: left; + height: 5px; + margin-top: $baseline; + border-top: 1px dotted #ddd; + width: 85%; + } +} + +.section-header { + display: block; + text-align: center; + width: 15%; + float: right; + padding-top: $baseline/2; + font-size: .9em; +} + + +// Rubric Styling + +.wrapper-score-selection { + display: table-cell; + padding: 0 $baseline/2; + width: 20px; + vertical-align: middle; + +} + +.wrappable { + display: table-cell; + padding: $baseline/4 0 ; +} + +.rubric-list-item { + margin-bottom: 2px; + padding: $baseline/2; + + &:hover { + background-color: #eee; + } +} + +span.rubric-category { + display: block; + width: 100%; + border-bottom: 1px solid lightgray; + font-size: .9em; +} + div.combined-rubric-container { margin: 15px; padding-bottom: 5px; @@ -217,21 +255,9 @@ div.combined-rubric-container { &.rubric-list-item { margin-bottom: 2px; - padding: 0px; + padding: $baseline/2; } } - - .score-selection { - //display: inline-block; - //padding-right: $baseline/2; - //width: 5%; - //vertical-align: center; - } - - .wrappable { - //display: inline-block; - //width: 94%; - } } h4 { padding-top: 10px; diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index bbf4a0eacc..eded6b0391 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -561,11 +561,11 @@ class @CombinedOpenEnded @prompt_container.slideToggle() @prompt_container.toggleClass('open') if @question_header.text() == "(Hide)" - new_text = "↧ Show Prompt" + new_text = "Show Prompt" Logger.log 'oe_hide_question', {location: @location} else Logger.log 'oe_show_question', {location: @location} - new_text = "↥ Hide Prompt" + new_text = "Hide Prompt" @question_header.text(new_text) return false @@ -573,13 +573,13 @@ class @CombinedOpenEnded if @prompt_container.is(":hidden")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("↥ Hide Prompt") + @question_header.text("Hide Prompt") prompt_hide: () => if @prompt_container.is(":visible")==true @prompt_container.slideToggle() @prompt_container.toggleClass('open') - @question_header.text("↧ Show Prompt") + @question_header.text("Show Prompt") log_feedback_click: (event) -> link_text = @$(event.target).html() diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index 9b84d0703b..6d3a6e3637 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -23,7 +23,11 @@
    -

    Rubric

    +
    +
    +
    + Rubric +
    diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 5f1c80fc5f..60bdd38a1b 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -30,7 +30,7 @@
    - ↥ Hide Prompt + Prompt
    % for item in items: diff --git a/lms/templates/combinedopenended/openended/open_ended.html b/lms/templates/combinedopenended/openended/open_ended.html index 909ef15838..f2d59c4049 100644 --- a/lms/templates/combinedopenended/openended/open_ended.html +++ b/lms/templates/combinedopenended/openended/open_ended.html @@ -3,7 +3,11 @@
    ${prompt|n}
    -

    Response

    +
    +
    +
    + Response +
    diff --git a/lms/templates/combinedopenended/openended/open_ended_rubric.html b/lms/templates/combinedopenended/openended/open_ended_rubric.html index 144cd829d9..c015a32d2d 100644 --- a/lms/templates/combinedopenended/openended/open_ended_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_rubric.html @@ -1,5 +1,9 @@
    -

    Rubric

    +
    +
    +
    + Rubric +

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

    % for i in range(len(categories)): @@ -14,7 +18,7 @@
  • % endif
  • diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html index 5347e23844..5755eeec3a 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html @@ -3,10 +3,11 @@
    ${prompt} +
    +
    +
    + Response
    - -

    Response

    -
    From 5da5d556303f4d6fa86eedd5b11aa46431e00e75 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 13:41:37 -0400 Subject: [PATCH 028/179] Fix peer grading feedback issue --- .../xmodule/js/src/peergrading/peer_grading_problem.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 050d525a13..47b2652020 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -482,7 +482,7 @@ class @PeerGradingProblem if response.actual_rubric != undefined calibration_wrapper.append("
    Instructor Scored Rubric: #{response.actual_rubric}
    ") - if response.actual_feedback!=undefined + if response.actual_feedback.feedback!=undefined calibration_wrapper.append("
    Instructor Feedback: #{response.actual_feedback}
    ") # disable score selection and submission from the grading interface From aa09006247210f73ea112e04861aca82cc5e9d6e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 13:53:07 -0400 Subject: [PATCH 029/179] Hide and show multiple rubrics --- .../js/src/combinedopenended/display.coffee | 4 +++ .../combined_open_ended_modulev1.py | 30 +++++++++---------- .../combined_open_ended_results.html | 4 +-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b539c72554..0db6521d27 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -569,6 +569,10 @@ class @CombinedOpenEnded @question_header.text(new_text) return false + hide_rubrics: () => + @$(combined_rubric_sel + ' > [data-status="hidden"]').hide() + @$(combined_rubric_sel + ' > [data-status="shown"]').show() + prompt_show: () => if @prompt_container.is(":hidden")==true @prompt_container.slideToggle() 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 ae3d01c686..ad9d7a287f 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 @@ -506,22 +506,20 @@ class CombinedOpenEndedV1Module(): for i in xrange(0, loop_up_to_task): response = self.get_last_response(i) rubric_scores = None - if len(response['rubric_scores']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): - rubric_scores = [response['rubric_scores']] - grader_types = None - if len(response['grader_types']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): - grader_types = [response['grader_types']] - feedback_items = None - if len(response['feedback_items']) > 0 and response['grader_types'][0] in HUMAN_GRADER_TYPE.keys(): - feedback_items = [response['feedback_items']] - if feedback_items is not None and grader_types is not None and rubric_scores is not None: - rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), - rubric_scores, - grader_types, feedback_items) - contexts.append({ - 'result': rubric_html, - 'task_name': 'Scored rubric' - }) + score_length = len(response['grader_types']) + log.info(response) + for z in xrange(0,score_length): + if response['grader_types'][z] in HUMAN_GRADER_TYPE.keys(): + rubric_scores = [[response['rubric_scores'][z]]] + grader_types = [[response['grader_types'][z]]] + feedback_items = [[response['feedback_items'][z]]] + rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), + rubric_scores, + grader_types, feedback_items) + contexts.append({ + 'result': rubric_html, + 'task_name': 'Scored rubric' + }) context = { 'results': contexts, diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 84f186fbb6..24934be3d2 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -2,9 +2,9 @@ % if 'task_name' in result and 'result' in result:
    0: - status="shown"> + data-status="shown" data-number="${i}"> % else: - status="hidden"> + data-status="hidden" data-number="${i}"> % endif

    ${result['task_name']} from grader ${i+1} (Hide)

    ${result['result'] | n} From ea2b9191c14b39f521d40fbfb33dc98ed2c26437 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 2 Aug 2013 15:30:17 -0400 Subject: [PATCH 030/179] Allow student to switch between rubrics, tell user once peer grading required amount is done --- .../js/src/combinedopenended/display.coffee | 37 ++++++++++++++++++- .../peergrading/peer_grading_problem.coffee | 6 ++- .../xmodule/xmodule/peer_grading_module.py | 5 +++ .../combined_open_ended_results.html | 4 +- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 0db6521d27..8aa1af96f6 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -114,6 +114,8 @@ class @CombinedOpenEnded grader_status_sel: '.grader-status' info_rubric_elements_sel: '.rubric-elements-info' rubric_collapse_sel: '.rubric-collapse' + next_rubric_sel: '.rubric-next-button' + previous_rubric_sel: '.rubric-previous-button' constructor: (el) -> @el=el @@ -240,6 +242,9 @@ class @CombinedOpenEnded @toggle_rubric("") @rubric_collapse = @$(@rubric_collapse_sel) @rubric_collapse.click @toggle_rubric + @hide_rubrics() + @$(@previous_rubric_sel).click @previous_rubric + @$(@next_rubric_sel).click @next_rubric show_status_current: () => data = {} @@ -570,8 +575,36 @@ class @CombinedOpenEnded return false hide_rubrics: () => - @$(combined_rubric_sel + ' > [data-status="hidden"]').hide() - @$(combined_rubric_sel + ' > [data-status="shown"]').show() + rubrics = @$(@combined_rubric_sel) + for rub in rubrics + if @$(rub).data('status')=="shown" + @$(rub).show() + else + @$(rub).hide() + + next_rubric: => + @shift_rubric(1) + return false + + previous_rubric: => + @shift_rubric(-1) + return false + + shift_rubric: (i) => + rubrics = @$(@combined_rubric_sel) + number = 0 + for rub in rubrics + if @$(rub).data('status')=="shown" + number = @$(rub).data('number') + @$(rub).data('status','hidden') + if i==1 and number < rubrics.length - 1 + number = number + i + + if i==-1 and number>0 + number = number + i + + @$(rubrics[number]).data('status', 'shown') + @hide_rubrics() prompt_show: () => if @prompt_container.is(":hidden")==true diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 47b2652020..c02fa3f390 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -344,7 +344,11 @@ class @PeerGradingProblem if response.success @is_calibrated_check() @grading_message.fadeIn() - @grading_message.html("

    Successfully saved your feedback. Fetched the next essay.

    ") + message = "

    Successfully saved your feedback. Fetched the next essay." + if response.required_done + message = message + " You have completed the required number of gradings." + message = message + "

    " + @grading_message.html(message) else if response.error @render_error(response.error) diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 44114136aa..b21e4865ec 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -327,6 +327,11 @@ class PeerGradingModule(PeerGradingFields, XModule): try: response = self.peer_gs.save_grade(location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged, answer_unknown) + + success, location_data = self.query_data_for_location() + response.update({'required_done' : False}) + if 'count_graded' in location_data and 'count_required' in location_data: + response['required_done'] = True return response except GradingServiceError: # This is a dev_facing_error diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 24934be3d2..71dbea4ec5 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -2,9 +2,9 @@ % if 'task_name' in result and 'result' in result:
    0: - data-status="shown" data-number="${i}"> - % else: data-status="hidden" data-number="${i}"> + % else: + data-status="shown" data-number="${i}"> % endif

    ${result['task_name']} from grader ${i+1} (Hide)

    ${result['result'] | n} From 56dee21142d6a453370983cc9373e9cb93b99523 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 7 Aug 2013 16:35:14 -0400 Subject: [PATCH 031/179] partial cleanup for submitted rubric state, along with sass format cleanup for rubric.scss --- .../css/combinedopenended/display.scss | 10 ++- .../js/src/combinedopenended/display.coffee | 6 +- lms/static/sass/course/_rubric.scss | 76 ++++++++++++------- .../combined_open_ended.html | 6 +- .../combined_open_ended_results.html | 10 ++- .../openended/open_ended_combined_rubric.html | 19 ++--- .../openended/open_ended_rubric.html | 2 +- 7 files changed, 80 insertions(+), 49 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 2873e37e73..b63e04d20e 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -222,7 +222,7 @@ section.combined-open-ended-status { .wrappable { display: table-cell; - padding: $baseline/4 0 ; + padding: $baseline/4; } .rubric-list-item { @@ -238,7 +238,9 @@ span.rubric-category { display: block; width: 100%; border-bottom: 1px solid lightgray; - font-size: .9em; + font-size: 1.1em; + padding-top: $baseline/2; + margin-bottom: $baseline/2; } div.combined-rubric-container { @@ -591,7 +593,8 @@ section.open-ended-child { overflow-y: auto; margin-bottom: 0; padding: $baseline/2; - height: 200px; + height: auto; + min-height: 20px; border: 1px solid #ddd; background: #f6f6f6; } @@ -668,6 +671,7 @@ section.open-ended-child { &.rubric-list-item { margin-bottom: 0; padding: 0; + border-radius: $baseline/4; } } } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b539c72554..4ae1f9156a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -623,9 +623,9 @@ class @CombinedOpenEnded info_rubric_elements = @$(@info_rubric_elements_sel) info_rubric_elements.slideToggle() @rubric_header = @$(@rubric_collapse_sel) - if @rubric_header.text() == "(Hide)" - new_text = "(Show)" + if @rubric_header.text() == "Show Score Only" + new_text = "Show Full Rubric" else - new_text = "(Hide)" + new_text = "Show Score Only" @rubric_header.text(new_text) return false diff --git a/lms/static/sass/course/_rubric.scss b/lms/static/sass/course/_rubric.scss index 294ac86d78..6c1467bfb8 100644 --- a/lms/static/sass/course/_rubric.scss +++ b/lms/static/sass/course/_rubric.scss @@ -1,56 +1,78 @@ +.rubric-header { + .rubric-collapse { + float: right; + } +} + .rubric { - margin: 0px 0px; + margin: 0; color: #3C3C3C; + tr { - margin:0px 0px; - height: 100%; + margin: 0; + height: 100%; } + td { - height: 100%; - border: 1px black solid; - text-align: center; + height: 100%; + border: 1px black solid; + text-align: center; } + th { - padding: 5px; - margin: 5px; - text-align: center; + margin: $baseline/4; + padding: $baseline/4; + text-align: center; } + .points-header th { - padding: 0px; + padding: 0px; } - .rubric-label - { - position: relative; - font-size: .9em; - display: block; + + .rubric-label { + position: relative; + display: block; + font-size: .9em; + + .choicegroup-correct { + //nothing + } + + .choicegroup-incorrect { + display:none; + } } + .grade { position: absolute; - bottom:0px; - right:0px; + bottom: 0; + right: 0; } .selected-grade, .selected-grade .rubric-label { background: #666; color: white; } - input[type=radio]:checked + .rubric-label { + + input[type=radio]:checked + .rubric-label { background: white; color: $base-font-color; white-space:nowrap; - } + } + .wrappable { - white-space:normal; + white-space:normal; } + input[class='score-selection'] { - position: relative; - font-size: 16px; + position: relative; + font-size: 16px; } - ul.rubric-list - { - list-style-type: none; - padding:0; - margin:0; + + ul.rubric-list { + margin: 0; + padding: 0; + list-style-type: none; } } diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 7668312e0e..ef73a09bcc 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -30,11 +30,7 @@
    -<<<<<<< HEAD - Prompt -======= - Hide Prompt ->>>>>>> 932bbd259247f5b69f2d39c0a1945951816fedeb + Hide Prompt
    % for item in items: diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 84f186fbb6..125bab2444 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -6,7 +6,15 @@ % else: status="hidden"> % endif -

    ${result['task_name']} from grader ${i+1} (Hide)

    +
    +
    +
    + Submitted Rubric +
    +
    + ${result['task_name']} from grader ${i+1} + +
    ${result['result'] | n}
    %endif diff --git a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html index 9ae148442f..ab9e751672 100644 --- a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html @@ -8,17 +8,18 @@
  • %if len(category['options'][j]['grader_types'])>0: - %if correct[i]==1: -
  • diff --git a/lms/templates/combinedopenended/openended/open_ended_rubric.html b/lms/templates/combinedopenended/openended/open_ended_rubric.html index c015a32d2d..df0704a263 100644 --- a/lms/templates/combinedopenended/openended/open_ended_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_rubric.html @@ -8,7 +8,7 @@
    % for i in range(len(categories)): <% category = categories[i] %> - ${category['description']}
    + ${category['description']}
      % for j in range(len(category['options'])): <% option = category['options'][j] %> From 203b1176cdee75f4e1096db6d906d36cb082ce01 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 12:14:57 -0400 Subject: [PATCH 032/179] Don't check for ETA, or check peer grading data in self assessment --- .../open_ended_grading_classes/open_ended_module.py | 3 +-- .../self_assessment_module.py | 11 +++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 2e7a3eaf89..bd873cb5ae 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -649,7 +649,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # add new history element with answer and empty score and hint. success, data = self.append_image_to_student_answer(data) - error_message = "" if success: success, allowed_to_submit, error_message = self.check_if_student_can_submit() if allowed_to_submit: @@ -698,7 +697,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): score = self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' if self.child_state == self.ASSESSING: - eta_string = self.get_eta() + eta_string = "" else: post_assessment = "" correct = "" diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index 1262e1f68f..baba66eb23 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -184,14 +184,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): # add new history element with answer and empty score and hint. success, data = self.append_image_to_student_answer(data) if success: - success, allowed_to_submit, error_message = self.check_if_student_can_submit() - if allowed_to_submit: - data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer']) - self.new_history_entry(data['student_answer']) - self.change_state(self.ASSESSING) - else: - # Error message already defined - success = False + data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer']) + self.new_history_entry(data['student_answer']) + self.change_state(self.ASSESSING) else: # This is a student_facing_error error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." From 45d94c17f7d024c4717a2e593b2979d96be91037 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 12:46:43 -0400 Subject: [PATCH 033/179] Add in next button, get rid of error with deleted peer grading module --- .../js/src/combinedopenended/display.coffee | 4 +- .../combined_open_ended_modulev1.py | 40 +++++++++++++++++++ .../open_ended_module.py | 13 ++---- .../openendedchild.py | 32 --------------- .../xmodule/xmodule/peer_grading_module.py | 9 ++++- 5 files changed, 53 insertions(+), 45 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 68c1f17ef6..b67b6dca93 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -297,8 +297,8 @@ class @CombinedOpenEnded @submit_button.unbind('click') @submit_button.show() @reset_button.hide() - @next_problem_button.hide() @hide_file_upload() + @next_problem_button.hide() @hint_area.attr('disabled', false) if @task_number>1 or @child_state!='initial' @show_status_current() @@ -353,7 +353,7 @@ class @CombinedOpenEnded if @child_type=="openended" @skip_button.hide() if @task_number<@task_count - @next_problem() + @next_problem_button.show() else if @task_number==1 and @task_count==1 @show_combined_rubric_current() 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 ad9d7a287f..eee2e93312 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 @@ -9,6 +9,7 @@ import self_assessment_module import open_ended_module from functools import partial from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST +from peer_grading_service import PeerGradingService, MockPeerGradingService log = logging.getLogger("mitx.courseware") @@ -116,6 +117,11 @@ class CombinedOpenEndedV1Module(): self.accept_file_upload = instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.skip_basic_checks = instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT + if system.open_ended_grading_interface: + self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system) + else: + self.peer_gs = MockPeerGradingService() + due_date = instance_state.get('due', None) grace_period_string = instance_state.get('graceperiod', None) @@ -147,6 +153,8 @@ class CombinedOpenEndedV1Module(): self.location = location self.setup_next_task() + + def get_tag_name(self, xml): """ Gets the tag name of a given xml block. @@ -494,6 +502,34 @@ class CombinedOpenEndedV1Module(): pass return return_html + def check_if_student_has_done_needed_grading(self): + student_id = self.system.anonymous_student_id + success = False + allowed_to_submit = True + error_string = ("You need to peer grade {0} more in order to make another submission. " + "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.") + try: + response = self.peer_gs.get_data_for_location(self.location, student_id) + count_graded = response['count_graded'] + count_required = response['count_required'] + student_sub_count = response['student_sub_count'] + success = True + except: + # This is a dev_facing_error + log.error("Could not contact external open ended graders for location {0} and student {1}".format( + self.location, student_id)) + # This is a student_facing_error + error_message = "Could not contact the graders. Please notify course staff." + return success, allowed_to_submit, error_message + if count_graded >= count_required: + return success, allowed_to_submit, "" + else: + allowed_to_submit = False + # This is a student_facing_error + error_message = error_string.format(count_required - count_graded, count_graded, count_required, + student_sub_count) + return success, allowed_to_submit, error_message + def get_rubric(self, _data): """ Gets the results of a given grader via ajax. @@ -501,6 +537,7 @@ class CombinedOpenEndedV1Module(): Output: Dictionary to be rendered via ajax that contains the result html. """ all_responses = [] + loop_up_to_task = self.current_task_number + 1 contexts = [] for i in xrange(0, loop_up_to_task): @@ -546,6 +583,9 @@ class CombinedOpenEndedV1Module(): Output: Dictionary to be rendered via ajax that contains the result html. """ self.update_task_states() + success, can_see_rubric, error = self.check_if_student_has_done_needed_grading() + if not can_see_rubric: + return {'html' : error, 'success' : False} loop_up_to_task = self.current_task_number + 1 all_responses = [] for i in xrange(0, loop_up_to_task): diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index bd873cb5ae..ec3bebac7f 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -650,15 +650,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # add new history element with answer and empty score and hint. success, data = self.append_image_to_student_answer(data) if success: - success, allowed_to_submit, error_message = self.check_if_student_can_submit() - if allowed_to_submit: - data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer']) - self.new_history_entry(data['student_answer']) - self.send_to_grader(data['student_answer'], system) - self.change_state(self.ASSESSING) - else: - # Error message already defined - success = False + data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer']) + self.new_history_entry(data['student_answer']) + self.send_to_grader(data['student_answer'], system) + self.change_state(self.ASSESSING) else: # This is a student_facing_error error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." 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 555505d611..71b48aed54 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -433,38 +433,6 @@ class OpenEndedChild(object): return success, string - def check_if_student_can_submit(self): - location = self.location_string - - student_id = self.system.anonymous_student_id - success = False - allowed_to_submit = True - response = {} - # This is a student_facing_error - error_string = ("You need to peer grade {0} more in order to make another submission. " - "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.") - try: - response = self.peer_gs.get_data_for_location(self.location_string, student_id) - count_graded = response['count_graded'] - count_required = response['count_required'] - student_sub_count = response['student_sub_count'] - success = True - except: - # This is a dev_facing_error - log.error("Could not contact external open ended graders for location {0} and student {1}".format( - self.location_string, student_id)) - # This is a student_facing_error - error_message = "Could not contact the graders. Please notify course staff." - return success, allowed_to_submit, error_message - if count_graded >= count_required: - return success, allowed_to_submit, "" - else: - allowed_to_submit = False - # This is a student_facing_error - error_message = error_string.format(count_required - count_graded, count_graded, count_required, - student_sub_count) - return success, allowed_to_submit, error_message - def get_eta(self): if self.controller_qs: response = self.controller_qs.check_for_eta(self.location_string) diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index b21e4865ec..059b66b615 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -535,9 +535,13 @@ class PeerGradingModule(PeerGradingFields, XModule): log.error("Problem {0} does not exist in this course".format(location)) raise + good_problem_list = [] for problem in problem_list: problem_location = problem['location'] - descriptor = _find_corresponding_module_for_location(problem_location) + try: + descriptor = _find_corresponding_module_for_location(problem_location) + except: + continue if descriptor: problem['due'] = descriptor._model_data.get('due', None) grace_period_string = descriptor._model_data.get('graceperiod', None) @@ -554,13 +558,14 @@ class PeerGradingModule(PeerGradingFields, XModule): # if we can't find the due date, assume that it doesn't have one problem['due'] = None problem['closed'] = False + good_problem_list.append(problem) ajax_url = self.ajax_url html = self.system.render_template('peer_grading/peer_grading.html', { 'course_id': self.system.course_id, 'ajax_url': ajax_url, 'success': success, - 'problem_list': problem_list, + 'problem_list': good_problem_list, 'error_text': error_text, # Checked above 'staff_access': False, From bd9097596cac8085e3bc6cdb4e4956a68c7acd94 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 13:16:32 -0400 Subject: [PATCH 034/179] Remove annoying rubric parsing error, redo how notifications work --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ---- .../open_ended_grading_classes/self_assessment_module.py | 2 -- lms/djangoapps/open_ended_grading/open_ended_notifications.py | 3 +-- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b67b6dca93..ee2c3589df 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -201,10 +201,6 @@ class @CombinedOpenEnded @rebind() - if @task_number>1 - @show_combined_rubric_current() - @show_results_current() - show_results_current: () => data = {'task_number' : @task_number-1} $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index baba66eb23..674fab0d30 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -267,8 +267,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): try: rubric_scores = json.loads(latest_post_assessment) except: - # This is a dev_facing_error - log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment)) rubric_scores = [] return [rubric_scores] diff --git a/lms/djangoapps/open_ended_grading/open_ended_notifications.py b/lms/djangoapps/open_ended_grading/open_ended_notifications.py index 1d6fa22929..b1617bbe53 100644 --- a/lms/djangoapps/open_ended_grading/open_ended_notifications.py +++ b/lms/djangoapps/open_ended_grading/open_ended_notifications.py @@ -93,7 +93,6 @@ def peer_grading_notifications(course, user): log.info( "Problem with getting notifications from peer grading service for course {0} user {1}.".format(course_id, student_id)) - if pending_grading: img_path = "/static/images/grading_notification.png" @@ -166,7 +165,7 @@ def combined_notifications(course, user): last_time_viewed) notifications = json.loads(controller_response) if notifications['success']: - if notifications['overall_need_to_check']: + if notifications['staff_needs_to_grade'] or notifications['student_needs_to_peer_grade']: pending_grading = True except: #Non catastrophic error, so no real action From fd38223244056975ad5eea80067061ea4661277b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 13:31:43 -0400 Subject: [PATCH 035/179] Fix grading submitted messages --- .../xmodule/js/src/combinedopenended/display.coffee | 10 ++++------ .../open_ended_grading_classes/open_ended_module.py | 3 ++- .../combinedopenended/openended/open_ended.html | 10 +++++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index ee2c3589df..8c29af2c0b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -326,9 +326,8 @@ class @CombinedOpenEnded if @child_type == "openended" @submit_button.hide() @queueing() - if @task_number==1 and @task_count==1 - @grader_status = @$(@grader_status_sel) - @grader_status.html("

      Response submitted for scoring.

      ") + @grader_status = @$(@grader_status_sel) + @grader_status.html("Your response has been submitted. Please check back later for your grade. ") else if @child_state == 'post_assessment' if @child_type=="openended" @skip_button.show() @@ -341,6 +340,8 @@ class @CombinedOpenEnded else @submit_button.click @message_post else if @child_state == 'done' + @show_combined_rubric_current() + @show_results_current() @rubric_wrapper.hide() @answer_area.attr("disabled", true) @replace_text_inputs() @@ -351,9 +352,6 @@ class @CombinedOpenEnded if @task_number<@task_count @next_problem_button.show() else - if @task_number==1 and @task_count==1 - @show_combined_rubric_current() - @show_results_current() @reset_button.show() diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index ec3bebac7f..e1e4643afe 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -640,6 +640,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """ # Once we close the problem, we should not allow students # to save answers + error_message = "" closed, msg = self.check_if_closed() if closed: return msg @@ -692,7 +693,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): score = self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' if self.child_state == self.ASSESSING: - eta_string = "" + eta_string = "Your response has been submitted. Please check back later for your grade." else: post_assessment = "" correct = "" diff --git a/lms/templates/combinedopenended/openended/open_ended.html b/lms/templates/combinedopenended/openended/open_ended.html index f2d59c4049..b76db8cecf 100644 --- a/lms/templates/combinedopenended/openended/open_ended.html +++ b/lms/templates/combinedopenended/openended/open_ended.html @@ -3,11 +3,11 @@
      ${prompt|n}
      -
      -
      +
      +
      +
      + Response
      - Response -
      @@ -15,7 +15,7 @@ % if state == 'initial': Unanswered % elif state == 'assessing': - Submitted for grading. + % if eta_message is not None: ${eta_message} % endif From 15b0e120686179276110c08a095e4d2f7bce0879 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 14:02:33 -0400 Subject: [PATCH 036/179] Remove progress indicators, make rubric display properly --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- lms/templates/combinedopenended/combined_open_ended.html | 3 +-- .../openended/open_ended_combined_rubric.html | 6 +++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 8c29af2c0b..49c056172a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -112,7 +112,7 @@ class @CombinedOpenEnded sub_id_sel: 'input.submission_id' grader_id_sel: 'input.grader_id' grader_status_sel: '.grader-status' - info_rubric_elements_sel: '.rubric-elements-info' + info_rubric_elements_sel: '.rubric-info-item' rubric_collapse_sel: '.rubric-collapse' next_rubric_sel: '.rubric-next-button' previous_rubric_sel: '.rubric-previous-button' diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index ef73a09bcc..1a5c7e3c7d 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -2,7 +2,6 @@

      ${display_name}

      - Progress: ${human_state}
      @@ -30,7 +29,7 @@
      % for item in items: diff --git a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html index ab9e751672..158ec1f981 100644 --- a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html @@ -5,7 +5,11 @@
        % for j in range(len(category['options'])): <% option = category['options'][j] %> -
      • + %if len(category['options'][j]['grader_types'])>0: +
      • + %else: +
      • + %endif
        %if len(category['options'][j]['grader_types'])>0: %if correct[i]==1: From 803b7eb2af4a833bf78c1c7b1164e2142575e1a2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 16:31:36 -0400 Subject: [PATCH 037/179] Fix peer grading rubric issue. Redo peer grading JS to scope locally --- .../css/combinedopenended/display.scss | 19 ++- .../js/src/peergrading/peer_grading.coffee | 32 +++-- .../peergrading/peer_grading_problem.coffee | 130 ++++++++++++------ 3 files changed, 125 insertions(+), 56 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index b63e04d20e..32558e32bb 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -419,6 +419,22 @@ div.result-container { } } +div.rubric { + ul.rubric-list{ + margin: 0; + padding: 0; + list-style-type: none; + list-style: none; + li { + &.rubric-list-item { + margin-bottom: 0; + padding: 0; + border-radius: $baseline/4; + } + } + } +} + section.open-ended-child { @media print { @@ -659,14 +675,13 @@ section.open-ended-child { margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; - list-style: disc outside none; } ul.rubric-list{ margin: 0; padding: 0; list-style-type: none; - + list-style: none; li { &.rubric-list-item { margin-bottom: 0; diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading.coffee index 48980c7d88..7196a5d7a6 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading.coffee @@ -3,10 +3,20 @@ # Can (and should be) expanded upon when our problem list # becomes more sophisticated class @PeerGrading + + peer_grading_sel: '.peer-grading' + peer_grading_container_sel: '.peer-grading-container' + error_container_sel: '.error-container' + message_container_sel: '.message-container' + problem_button_sel: '.problem-button' + problem_list_sel: '.problem-list' + progress_bar_sel: '.progress-bar' + constructor: (element) -> - @peer_grading_container = $('.peer-grading') + @el = element + @peer_grading_container = @$(@peer_grading_sel) @use_single_location = @peer_grading_container.data('use-single-location') - @peer_grading_outer_container = $('.peer-grading-container') + @peer_grading_outer_container = @$(@peer_grading_container_sel) @ajax_url = @peer_grading_container.data('ajax-url') if @use_single_location.toLowerCase() == "true" @@ -14,23 +24,27 @@ class @PeerGrading @activate_problem() else #Otherwise, activate the panel view. - @error_container = $('.error-container') + @error_container = @$(@error_container_sel) @error_container.toggle(not @error_container.is(':empty')) - @message_container = $('.message-container') + @message_container = @$(@message_container_sel) @message_container.toggle(not @message_container.is(':empty')) - @problem_button = $('.problem-button') + @problem_button = @$(@problem_button_sel) @problem_button.click @show_results - @problem_list = $('.problem-list') + @problem_list = @$(@problem_list_sel) @construct_progress_bar() + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + construct_progress_bar: () => problems = @problem_list.find('tr').next() problems.each( (index, element) => problem = $(element) - progress_bar = problem.find('.progress-bar') + progress_bar = problem.find(@progress_bar_sel) bar_value = parseInt(problem.data('graded')) bar_max = parseInt(problem.data('required')) + bar_value progress_bar.progressbar({value: bar_value, max: bar_max}) @@ -43,10 +57,10 @@ class @PeerGrading if response.success @peer_grading_outer_container.after(response.html).remove() backend = new PeerGradingProblemBackend(@ajax_url, false) - new PeerGradingProblem(backend) + new PeerGradingProblem(backend, @el) else @gentle_alert response.error activate_problem: () => backend = new PeerGradingProblemBackend(@ajax_url, false) - new PeerGradingProblem(backend) \ No newline at end of file + new PeerGradingProblem(backend, @el) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index c02fa3f390..b60f363649 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -158,11 +158,47 @@ class @PeerGradingProblemBackend return response class @PeerGradingProblem - constructor: (backend) -> - @prompt_wrapper = $('.prompt-wrapper') + + prompt_wrapper_sel: '.prompt-wrapper' + peer_grading_container_sel: '.peer-grading-container' + submission_container_sel: '.submission-container' + prompt_container_sel: '.prompt-container' + rubric_container_sel: '.rubric-container' + flag_student_container_sel: '.flag-student-container' + answer_unknown_container_sel: '.answer-unknown-container' + calibration_panel_sel: '.calibration-panel' + grading_panel_sel: '.grading-panel' + content_panel_sel: '.content-panel' + grading_message_sel: '.grading-message' + question_header_sel: '.question-header' + flag_submission_confirmation_sel: '.flag-submission-confirmation' + flag_submission_confirmation_button_sel: '.flag-submission-confirmation-button' + flag_submission_removal_button_sel: '.flag-submission-removal-button' + grading_wrapper_sel: '.grading-wrapper' + calibration_feedback_sel: '.calibration-feedback' + interstitial_page_sel: '.interstitial-page' + calibration_interstitial_page_sel: '.calibration-interstitial-page' + error_container_sel: '.error-container' + feedback_area_sel: '.feedback-area' + score_selection_container_sel: '.score-selection-container' + rubric_selection_container_sel: '.rubric-selection-container' + submit_button_sel: '.submit-button' + action_button_sel: '.action-button' + calibration_feedback_button_sel: '.calibration-feedback-button' + interstitial_page_button_sel: '.interstitial-page-button' + calibration_interstitial_page_button_sel: '.calibration-interstitial-page-button' + flag_checkbox_sel: '.flag-checkbox' + answer_unknown_checkbox_sel: '.answer-unknown-checkbox' + calibration_text_sel: '.calibration-text' + grading_text_sel: '.grading-text' + calibration_feedback_wrapper_sel: '.calibration-feedback-wrapper' + + constructor: (backend, el) -> + @el = el + @prompt_wrapper = $(@prompt_wrapper_sel) @backend = backend @is_ctrl = false - @el = $('.peer-grading-container') + @el = $(@peer_grading_container_sel) # get the location of the problem @location = $('.peer-grading').data('location') @@ -172,51 +208,51 @@ class @PeerGradingProblem return # get the other elements we want to fill in - @submission_container = $('.submission-container') - @prompt_container = $('.prompt-container') - @rubric_container = $('.rubric-container') - @flag_student_container = $('.flag-student-container') - @answer_unknown_container = $('.answer-unknown-container') - @calibration_panel = $('.calibration-panel') - @grading_panel = $('.grading-panel') - @content_panel = $('.content-panel') - @grading_message = $('.grading-message') + @submission_container = @$(@submission_container_sel) + @prompt_container = @$(@prompt_container_sel) + @rubric_container = @$(@rubric_container_sel) + @flag_student_container = @$(@flag_student_container_sel) + @answer_unknown_container = @$(@answer_unknown_container_sel) + @calibration_panel = @$(@calibration_panel_sel) + @grading_panel = @$(@grading_panel_sel) + @content_panel = @$(@content_panel_sel) + @grading_message = @$(@grading_message_sel) @grading_message.hide() - @question_header = $('.question-header') + @question_header = @$(@question_header_sel) @question_header.click @collapse_question - @flag_submission_confirmation = $('.flag-submission-confirmation') - @flag_submission_confirmation_button = $('.flag-submission-confirmation-button') - @flag_submission_removal_button = $('.flag-submission-removal-button') + @flag_submission_confirmation = @$(@flag_submission_confirmation_sel) + @flag_submission_confirmation_button = @$(@flag_submission_confirmation_button_sel) + @flag_submission_removal_button = @$(@flag_submission_removal_button_sel) @flag_submission_confirmation_button.click @close_dialog_box @flag_submission_removal_button.click @remove_flag - @grading_wrapper =$('.grading-wrapper') - @calibration_feedback_panel = $('.calibration-feedback') - @interstitial_page = $('.interstitial-page') + @grading_wrapper = @$(@grading_wrapper_sel) + @calibration_feedback_panel = @$(@calibration_feedback_sel) + @interstitial_page = @$(@interstitial_page_sel) @interstitial_page.hide() - @calibration_interstitial_page = $('.calibration-interstitial-page') + @calibration_interstitial_page = @$(@calibration_interstitial_page_sel) @calibration_interstitial_page.hide() - @error_container = $('.error-container') + @error_container = @$(@error_container_sel) @submission_key_input = $("input[name='submission-key']") - @essay_id_input = $("input[name='essay-id']") - @feedback_area = $('.feedback-area') + @essay_id_input = @$("input[name='essay-id']") + @feedback_area = @$(@feedback_area_sel) - @score_selection_container = $('.score-selection-container') - @rubric_selection_container = $('.rubric-selection-container') + @score_selection_container = @$(@score_selection_container_sel) + @rubric_selection_container = @$(@rubric_selection_container_sel) @grade = null @calibration = null - @submit_button = $('.submit-button') - @action_button = $('.action-button') - @calibration_feedback_button = $('.calibration-feedback-button') - @interstitial_page_button = $('.interstitial-page-button') - @calibration_interstitial_page_button = $('.calibration-interstitial-page-button') - @flag_student_checkbox = $('.flag-checkbox') - @answer_unknown_checkbox = $('.answer-unknown-checkbox') + @submit_button = @$(@submit_button_sel) + @action_button = @$(@action_button_sel) + @calibration_feedback_button = @$(@calibration_feedback_button_sel) + @interstitial_page_button = @$(@interstitial_page_button_sel) + @calibration_interstitial_page_button = @$(@calibration_interstitial_page_button_sel) + @flag_student_checkbox = @$(@flag_checkbox_sel) + @answer_unknown_checkbox = @$(@answer_unknown_checkbox_sel) $(window).keydown @keydown_handler $(window).keyup @keyup_handler @@ -251,6 +287,10 @@ class @PeerGradingProblem @is_calibrated_check() + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + ########## # @@ -300,11 +340,11 @@ class @PeerGradingProblem @close_dialog_box() close_dialog_box: () => - $( ".flag-submission-confirmation" ).dialog('close') + @$(@flag_submission_confirmation_sel).dialog('close') flag_box_checked: () => if @flag_student_checkbox.is(':checked') - $( ".flag-submission-confirmation" ).dialog({ height: 400, width: 400 }) + @$(@flag_submission_confirmation_sel).dialog({ height: 400, width: 400 }) # called after we perform an is_student_calibrated check calibration_check_callback: (response) => @@ -399,10 +439,10 @@ class @PeerGradingProblem # Display the right text # both versions of the text are written into the template itself # we only need to show/hide the correct ones at the correct time - @calibration_panel.find('.calibration-text').show() - @grading_panel.find('.calibration-text').show() - @calibration_panel.find('.grading-text').hide() - @grading_panel.find('.grading-text').hide() + @calibration_panel.find(@calibration_text_sel).show() + @grading_panel.find(@calibration_text_sel).show() + @calibration_panel.find(@grading_text_sel).hide() + @grading_panel.find(@grading_text_sel).hide() @flag_student_container.hide() @answer_unknown_container.hide() @@ -429,10 +469,10 @@ class @PeerGradingProblem # Display the correct text # both versions of the text are written into the template itself # we only need to show/hide the correct ones at the correct time - @calibration_panel.find('.calibration-text').hide() - @grading_panel.find('.calibration-text').hide() - @calibration_panel.find('.grading-text').show() - @grading_panel.find('.grading-text').show() + @calibration_panel.find(@calibration_text_sel).hide() + @grading_panel.find(@calibration_text_sel).hide() + @calibration_panel.find(@grading_text_sel).show() + @grading_panel.find(@grading_text_sel).show() @flag_student_container.show() @answer_unknown_container.show() @feedback_area.val("") @@ -473,7 +513,7 @@ class @PeerGradingProblem render_calibration_feedback: (response) => # display correct grade @calibration_feedback_panel.slideDown() - calibration_wrapper = $('.calibration-feedback-wrapper') + calibration_wrapper = @$(@calibration_feedback_wrapper_sel) calibration_wrapper.html("

        The score you gave was: #{@grade}. The actual score is: #{response.actual_score}

        ") score = parseInt(@grade) @@ -490,7 +530,7 @@ class @PeerGradingProblem calibration_wrapper.append("
        Instructor Feedback: #{response.actual_feedback}
        ") # disable score selection and submission from the grading interface - $("input[name='score-selection']").attr('disabled', true) + @$("input[name='score-selection']").attr('disabled', true) @submit_button.hide() @calibration_feedback_button.show() @@ -516,7 +556,7 @@ class @PeerGradingProblem setup_score_selection: (max_score) => # And now hook up an event handler again - $("input[class='score-selection']").change @graded_callback + @$("input[class='score-selection']").change @graded_callback gentle_alert: (msg) => @grading_message.fadeIn() From f59045c3f9cc81365a3c521f0ee1fa7252968a96 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 16:54:39 -0400 Subject: [PATCH 038/179] Scope rubrics locally, make them into classes --- .../js/src/combinedopenended/display.coffee | 58 +++++++++++-------- .../peergrading/peer_grading_problem.coffee | 11 ++-- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 49c056172a..7d299b5f68 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -1,20 +1,28 @@ class @Rubric - constructor: () -> - @initialize: (location,el) -> + rubric_category_sel: '.rubric-category' + rubric_sel: '.rubric' + + constructor: (el) -> @el = el - $('.rubric',@el).data("location", location) - $('input[class="score-selection"]',@el).change @tracking_callback + + initialize: (location) -> + @$(@rubric_sel).data("location", location) + @$('input[class="score-selection"]').change @tracking_callback # set up the hotkeys $(window).unbind('keydown', @keypress_callback) $(window).keydown @keypress_callback # display the 'current' carat - @categories = $('.rubric-category',el) - @category = $(@categories.first(),el) + @categories = @$(@rubric_category_sel) + @category = @$(@categories.first()) @category.prepend('> ') @category_index = 0 - @keypress_callback: (event) => + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + + keypress_callback: (event) => # don't try to do this when user is typing in a text input if $(event.target).is('input, textarea') return @@ -31,7 +39,7 @@ class @Rubric # if we actually have a current category (not past the end) if(@category_index <= @categories.length) # find the valid selections for this category - inputs = $("input[name='score-selection-#{@category_index}']") + inputs = @$("input[name='score-selection-#{@category_index}']") max_score = inputs.length - 1 if selected > max_score or selected < 0 @@ -42,44 +50,44 @@ class @Rubric old_category_text = @category.html().substring(5) @category.html(old_category_text) @category_index++ - @category = $(@categories[@category_index],@el) + @category = @$(@categories[@category_index]) @category.prepend('> ') - @tracking_callback: (event) -> + tracking_callback: (event) -> target_selection = $(event.target).val() # chop off the beginning of the name so that we can get the number of the category category = $(event.target).data("category") - location = $('.rubric',@el).data('location') + location = @$(@rubric_sel).data('location') # probably want the original problem location as well data = {location: location, selection: target_selection, category: category} Logger.log 'rubric_select', data # finds the scores for each rubric category - @get_score_list: () => + get_score_list: () => # find the number of categories: - num_categories = $('.rubric-category',@el).length + num_categories = @$(@rubric_category_sel).length score_lst = [] # get the score for each one for i in [0..(num_categories-1)] - score = $("input[name='score-selection-#{i}']:checked").val() + score = @$("input[name='score-selection-#{i}']:checked").val() score_lst.push(score) return score_lst - @get_total_score: () -> + get_total_score: () -> score_lst = @get_score_list() tot = 0 for score in score_lst tot += parseInt(score) return tot - @check_complete: () -> + check_complete: () -> # check to see whether or not any categories have not been scored - num_categories = $('.rubric-category',@el).length + num_categories = @$(@rubric_category_sel).length for i in [0..(num_categories-1)] - score = $("input[name='score-selection-#{i}']:checked",@el).val() + score = @$("input[name='score-selection-#{i}']:checked").val() if score == undefined return false return true @@ -143,7 +151,8 @@ class @CombinedOpenEnded @location = @coe.data('location') # set up handlers for click tracking - Rubric.initialize(@location,@coe) + @rub = new Rubric(@coe) + @rub.initialize(@location) @is_ctrl = false #Setup reset @reset_button = @$(@reset_button_sel) @@ -390,7 +399,8 @@ class @CombinedOpenEnded if response.success @rubric_wrapper.html(response.rubric_html) @rubric_wrapper.show() - Rubric.initialize(@location,@coe) + @rub = new Rubric(@coe) + @rub.initialize(@location) @answer_area.html(response.student_response) @child_state = 'assessing' @find_assessment_elements() @@ -408,7 +418,7 @@ class @CombinedOpenEnded #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed. if event.which == 17 && @is_ctrl==false @is_ctrl=true - else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && Rubric.check_complete() + else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && @rub.check_complete() @save_assessment(event) keyup_handler: (event) => @@ -418,9 +428,9 @@ class @CombinedOpenEnded save_assessment: (event) => event.preventDefault() - if @child_state == 'assessing' && Rubric.check_complete() - checked_assessment = Rubric.get_total_score() - score_list = Rubric.get_score_list() + if @child_state == 'assessing' && @rub.check_complete() + checked_assessment = @rub.get_total_score() + score_list = @rub.get_score_list() data = {'assessment' : checked_assessment, 'score_list' : score_list} $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => if response.success diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index b60f363649..4f644a4ec1 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -309,8 +309,8 @@ class @PeerGradingProblem construct_data: () -> data = - rubric_scores: Rubric.get_score_list() - score: Rubric.get_total_score() + rubric_scores: @rub.get_score_list() + score: @rub.get_total_score() location: @location submission_id: @essay_id_input.val() submission_key: @submission_key_input.val() @@ -398,11 +398,11 @@ class @PeerGradingProblem # called after a grade is selected on the interface graded_callback: (event) => # check to see whether or not any categories have not been scored - if Rubric.check_complete() + if @rub.check_complete() # show button if we have scores for all categories @grading_message.hide() @show_submit_button() - @grade = Rubric.get_total_score() + @grade = @rub.get_total_score() keydown_handler: (event) => #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed. @@ -507,7 +507,8 @@ class @PeerGradingProblem @submit_button.hide() @action_button.hide() @calibration_feedback_panel.hide() - Rubric.initialize(@location,@el) + @rub = new Rubric(@el) + @rub.initialize(@location) render_calibration_feedback: (response) => From d44b2e62a00f20bd01b42bf4a16571dbb65f0f30 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 17:19:06 -0400 Subject: [PATCH 039/179] Proper partial credit checks --- .../xmodule/xmodule/css/combinedopenended/display.scss | 7 +++++++ .../combined_open_ended_rubric.py | 9 ++++++++- .../open_ended_grading_classes/open_ended_module.py | 2 +- .../openended/open_ended_combined_rubric.html | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 32558e32bb..8c49417af9 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -282,6 +282,13 @@ div.combined-rubric-container { } } + label.choicegroup_partialcorrect { + &:before { + margin-right: 15px; + content: url('../images/partially-correct-icon.png'); + } + } + label.choicegroup_incorrect { &:before { margin-right: 15px; diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py index 0b2b0fdaec..a072d5ad5e 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_rubric.py @@ -224,7 +224,14 @@ class CombinedOpenEndedRubric(object): actual_scores[i] += [j] actual_scores = [sum(i)/len(i) for i in actual_scores] - correct = [int(a>.66) for a in actual_scores] + correct = [] + for (i,a) in enumerate(actual_scores): + if int(a)/max_scores[i]==1: + correct.append(1) + elif int(a)==0: + correct.append(0) + else: + correct.append(.5) html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR), {'categories': rubric_categories, diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index e1e4643afe..afaa657937 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -697,7 +697,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): else: post_assessment = "" correct = "" - previous_answer = self.initial_display + previous_answer = "" context = { 'prompt': self.child_prompt, diff --git a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html index 158ec1f981..1ad11be024 100644 --- a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html @@ -14,6 +14,8 @@ %if len(category['options'][j]['grader_types'])>0: %if correct[i]==1: + %elif correct[i]==.5: + %else: %endif From 900377e4eecea5520b7c886a431e3c0ef7e5c616 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 17:53:29 -0400 Subject: [PATCH 040/179] Return written feedback --- .../combined_open_ended_modulev1.py | 9 ++++++--- .../combinedopenended/combined_open_ended_results.html | 3 +++ 2 files changed, 9 insertions(+), 3 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 eee2e93312..54cfda514c 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 @@ -537,15 +537,17 @@ class CombinedOpenEndedV1Module(): Output: Dictionary to be rendered via ajax that contains the result html. """ all_responses = [] + success, can_see_rubric, error = self.check_if_student_has_done_needed_grading() + if not can_see_rubric: + return {'html' : error, 'success' : False} loop_up_to_task = self.current_task_number + 1 contexts = [] for i in xrange(0, loop_up_to_task): response = self.get_last_response(i) - rubric_scores = None score_length = len(response['grader_types']) - log.info(response) for z in xrange(0,score_length): + feedback = response['feedback_dicts'][z].get('feedback', '') if response['grader_types'][z] in HUMAN_GRADER_TYPE.keys(): rubric_scores = [[response['rubric_scores'][z]]] grader_types = [[response['grader_types'][z]]] @@ -555,7 +557,8 @@ class CombinedOpenEndedV1Module(): grader_types, feedback_items) contexts.append({ 'result': rubric_html, - 'task_name': 'Scored rubric' + 'task_name': 'Scored rubric', + 'feedback' : feedback }) context = { diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 688354ff4c..7bbf992137 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -16,6 +16,9 @@
        ${result['result'] | n} +
        + ${result['feedback'] | n} +
      %endif From a9926f04604c4dc1f4d1ab141a0eab50594976ee Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 18:43:54 -0400 Subject: [PATCH 041/179] Display message to student when they cannot see their peer grading feedback yet. --- .../xmodule/js/src/combinedopenended/display.coffee | 3 +++ .../combined_open_ended_modulev1.py | 13 ++++++++----- .../combined_open_ended_hidden_results.html | 10 ++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 lms/templates/combinedopenended/combined_open_ended_hidden_results.html diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 7d299b5f68..dc6cd57730 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -250,6 +250,9 @@ class @CombinedOpenEnded @hide_rubrics() @$(@previous_rubric_sel).click @previous_rubric @$(@next_rubric_sel).click @next_rubric + if response.hide_reset + @reset_button.hide() + show_status_current: () => data = {} 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 54cfda514c..19c9aeb6df 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 @@ -506,10 +506,13 @@ class CombinedOpenEndedV1Module(): student_id = self.system.anonymous_student_id success = False allowed_to_submit = True - error_string = ("You need to peer grade {0} more in order to make another submission. " - "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.") + error_string = ("

      Feedback not available yet

      " + "

      You need to peer grade {0} more submissions in order to see your feedback.

      " + "

      You have graded responses from {1} students, and {2} students have graded your submissions.

      " + "

      You have made {3} submissions.

      ") try: - response = self.peer_gs.get_data_for_location(self.location, student_id) + response = self.peer_gs.get_data_for_location(self.location.url(), student_id) + log.info(response) count_graded = response['count_graded'] count_required = response['count_required'] student_sub_count = response['student_sub_count'] @@ -539,7 +542,7 @@ class CombinedOpenEndedV1Module(): all_responses = [] success, can_see_rubric, error = self.check_if_student_has_done_needed_grading() if not can_see_rubric: - return {'html' : error, 'success' : False} + return {'html' : self.system.render_template('{0}/combined_open_ended_hidden_results.html'.format(self.TEMPLATE_DIR), {'error' : error}), 'success' : True, 'hide_reset' : True} loop_up_to_task = self.current_task_number + 1 contexts = [] @@ -565,7 +568,7 @@ class CombinedOpenEndedV1Module(): 'results': contexts, } html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) - return {'html': html, 'success': True} + return {'html': html, 'success': True, 'hide_reset' : False} def get_legend(self, _data): """ diff --git a/lms/templates/combinedopenended/combined_open_ended_hidden_results.html b/lms/templates/combinedopenended/combined_open_ended_hidden_results.html new file mode 100644 index 0000000000..396a657273 --- /dev/null +++ b/lms/templates/combinedopenended/combined_open_ended_hidden_results.html @@ -0,0 +1,10 @@ +
      +
      +
      +
      + Submitted Rubric +
      +
      + ${error} +
      +
      From e4bcfa5c219eb2270eaea15754e82e3103245c60 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 19:31:34 -0400 Subject: [PATCH 042/179] Fix indents and copy behavior --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 6 ++---- common/lib/xmodule/xmodule/peer_grading_module.py | 9 ++++----- lms/djangoapps/courseware/model_data.py | 3 ++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index e01ae49149..256f96a3a7 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,7 +8,7 @@ from .x_module import XModule from xblock.core import Integer, Scope, String, List, Float, Boolean from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from collections import namedtuple -from .fields import Date +from .fields import Date, Timedelta import textwrap log = logging.getLogger("mitx.courseware") @@ -226,12 +226,10 @@ class CombinedOpenEndedFields(object): ) due = Date( help="Date that this problem is due by", - default=None, scope=Scope.settings ) - graceperiod = String( + graceperiod = Timedelta( help="Amount of time after the due date that submissions will be accepted", - default=None, scope=Scope.settings ) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index c29b6a8069..aa742f6a0b 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -46,7 +46,6 @@ class PeerGradingFields(object): ) due = Date( help="Due date that should be displayed.", - default=None, scope=Scope.settings) graceperiod = Timedelta( help="Amount of grace to give on the due date.", @@ -525,10 +524,10 @@ class PeerGradingModule(PeerGradingFields, XModule): good_problem_list = [] for problem in problem_list: problem_location = problem['location'] - try: - descriptor = _find_corresponding_module_for_location(problem_location) - except: - continue + try: + descriptor = _find_corresponding_module_for_location(problem_location) + except: + continue if descriptor: problem['due'] = descriptor.lms.due grace_period = descriptor.lms.graceperiod diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index 44be16e441..b72c0e90c6 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -353,7 +353,8 @@ class LmsKeyValueStore(KeyValueStore): for field in kv_dict: # Check field for validity if field.field_name in self._descriptor_model_data: - raise InvalidWriteError("Not allowed to overwrite descriptor model data", field.field_name) + if field.field_name not in ["due","graceperiod"]: + raise InvalidWriteError("Not allowed to overwrite descriptor model data", field.field_name) if field.scope not in self._allowed_scopes: raise InvalidScopeError(field.scope) From e39abe11db79440d3cb0ace2c6abec98b95bf998 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 19:50:55 -0400 Subject: [PATCH 043/179] Fix rubric with staff grading --- .../js/src/combinedopenended/display.coffee | 8 ++++---- .../src/staff_grading/staff_grading.coffee | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index dc6cd57730..6b79ec9e3b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -6,7 +6,7 @@ class @Rubric constructor: (el) -> @el = el - initialize: (location) -> + initialize: (location) => @$(@rubric_sel).data("location", location) @$('input[class="score-selection"]').change @tracking_callback # set up the hotkeys @@ -53,7 +53,7 @@ class @Rubric @category = @$(@categories[@category_index]) @category.prepend('> ') - tracking_callback: (event) -> + tracking_callback: (event) => target_selection = $(event.target).val() # chop off the beginning of the name so that we can get the number of the category category = $(event.target).data("category") @@ -76,14 +76,14 @@ class @Rubric return score_lst - get_total_score: () -> + get_total_score: () => score_lst = @get_score_list() tot = 0 for score in score_lst tot += parseInt(score) return tot - check_complete: () -> + check_complete: () => # check to see whether or not any categories have not been scored num_categories = @$(@rubric_category_sel).length for i in [0..(num_categories-1)] diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index f22fe9c7fd..e7e7c189ac 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -223,12 +223,12 @@ class @StaffGrading setup_score_selection: => @score_selection_container.html(@rubric) $('input[class="score-selection"]').change => @graded_callback() - Rubric.initialize(@location, @el) - + @rub = new Rubric(@el) + @rub.initialize(@location) graded_callback: () => # show button if we have scores for all categories - if Rubric.check_complete() + if @rub.check_complete() @state = state_graded @submit_button.show() @@ -236,7 +236,7 @@ class @StaffGrading #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed. if event.which == 17 && @is_ctrl==false @is_ctrl=true - else if @is_ctrl==true && event.which == 13 && !@list_view && Rubric.check_complete() + else if @is_ctrl==true && event.which == 13 && !@list_view && @rub.check_complete() @submit_and_get_next() keyup_handler: (event) => @@ -271,8 +271,8 @@ class @StaffGrading skip_and_get_next: () => data = - score: Rubric.get_total_score() - rubric_scores: Rubric.get_score_list() + score: @rub.get_total_score() + rubric_scores: @rub.get_score_list() feedback: @feedback_area.val() submission_id: @submission_id location: @location @@ -286,8 +286,8 @@ class @StaffGrading submit_and_get_next: () -> data = - score: Rubric.get_total_score() - rubric_scores: Rubric.get_score_list() + score: @rub.get_total_score() + rubric_scores: @rub.get_score_list() feedback: @feedback_area.val() submission_id: @submission_id location: @location From adbfdccee8cdb7127600eefe4ae7f2ace37a1c6e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 9 Aug 2013 08:18:23 -0400 Subject: [PATCH 044/179] Prevent student from resetting problem unless they have completed required peer grading. Grant an exemption if there is no available peer grading. --- .../combined_open_ended_modulev1.py | 7 +++++-- .../open_ended_grading_classes/peer_grading_service.py | 2 +- 2 files changed, 6 insertions(+), 3 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 19c9aeb6df..c0a19ae2ab 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 @@ -516,6 +516,7 @@ class CombinedOpenEndedV1Module(): count_graded = response['count_graded'] count_required = response['count_required'] student_sub_count = response['student_sub_count'] + count_available = response['count_available'] success = True except: # This is a dev_facing_error @@ -524,7 +525,7 @@ class CombinedOpenEndedV1Module(): # This is a student_facing_error error_message = "Could not contact the graders. Please notify course staff." return success, allowed_to_submit, error_message - if count_graded >= count_required: + if count_graded >= count_required or count_available==0: return success, allowed_to_submit, "" else: allowed_to_submit = False @@ -687,7 +688,9 @@ class CombinedOpenEndedV1Module(): if self.state != self.DONE: if not self.ready_to_reset: return self.out_of_sync_error(data) - + success, can_reset, error = self.check_if_student_has_done_needed_grading() + if not can_reset: + return {'error' : error, 'success' : False} if self.student_attempts >= self.max_attempts-1: if self.student_attempts==self.max_attempts-1: self.student_attempts +=1 diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py index 3c25b301ab..0e5c9cdda1 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py @@ -124,4 +124,4 @@ class MockPeerGradingService(object): ]} def get_data_for_location(self, problem_location, student_id): - return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1} + return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1, 'submissions_available' : 0} From f929b4930acbdc72adf6d3128bc65535275134d8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 9 Aug 2013 08:42:02 -0400 Subject: [PATCH 045/179] Add in jasmine test for rubric, fix combinedopenended jasmine failures --- .../xmodule/xmodule/js/fixtures/rubric.html | 325 ++++++++++++++++++ .../combinedopenended/display_spec.coffee | 33 +- 2 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/fixtures/rubric.html diff --git a/common/lib/xmodule/xmodule/js/fixtures/rubric.html b/common/lib/xmodule/xmodule/js/fixtures/rubric.html new file mode 100644 index 0000000000..9802e3bf8d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/rubric.html @@ -0,0 +1,325 @@ +
      +
      +

      Open Response Assessment

      +
      +
      +
      +
      +
      + + + + + +
      +
      + Open Response +
      +
      +
      + Assessments: +
      +
      + + + +
      +
      + +
      + Peer +
      +
      +
      + + + +
      +
      +
      + +
      +
      +
      +
      + Show Prompt +
      +
      +
      +
      +
      + +
      +
      +
      + Response +
      +
      + +
      +
      + +
      + +
      + + + + +
      + + +
      +
      +
      + + + +
      + +
      +
      +
      +
      + Submitted Rubric +
      +
      + Scored rubric from grader 1 + +
      +
      + + +Ideas +
      +
        + +
      • +
        + + 0 points : +Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus. + + +
        +
      • + + + + + + +
      + + +Content +
      +
        + +
      • +
        + + 0 points : +Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic. + + +
        +
      • + + + + + + +
      + + +Organization +
      +
        + +
      • +
        + + 0 points : +Ideas organized illogically, transitions weak, and response difficult to follow. + + +
        +
      • + + + + +
      + + +Style +
      +
        + +
      • +
        + + 0 points : +Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns. + + +
        +
      • + + + + +
      + + +Voice +
      +
        + +
      • +
        + + 0 points : +Demonstrates language and tone that may be inappropriate to task and reader. + + +
        +
      • + + + + +
      +
      + + +
      + +
      +
      + + + + +
      +
      + +
      +
      +
      +
      \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee index f2e8da7990..293d6405ad 100644 --- a/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee @@ -1,3 +1,30 @@ +describe 'Rubric', -> + beforeEach -> + spyOn Logger, 'log' + # load up some fixtures + loadFixtures 'rubric.html' + jasmine.Clock.useMock() + @element = $('.combined-open-ended') + @location = @element.data('location') + + describe 'constructor', -> + beforeEach -> + @rub = new Rubric @element + + it 'rubric should properly grab the element', -> + expect(@rub.el).toEqual @element + + describe 'initialize', -> + beforeEach -> + @rub = new Rubric @element + @rub.initialize @location + + it 'rubric correctly sets location', -> + expect($(@rub.rubric_sel).data('location')).toEqual @location + + it 'rubric correctly read', -> + expect(@rub.categories.length).toEqual 5 + describe 'CombinedOpenEnded', -> beforeEach -> spyOn Logger, 'log' @@ -13,7 +40,7 @@ describe 'CombinedOpenEnded', -> @combined = new CombinedOpenEnded @element it 'set the element', -> - expect(@combined.element).toEqual @element + expect(@combined.el).toEqual @element it 'get the correct values from data fields', -> expect(@combined.ajax_url).toEqual '/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE' @@ -77,7 +104,7 @@ describe 'CombinedOpenEnded', -> @combined.child_state = 'done' @combined.rebind() expect(@combined.answer_area.attr("disabled")).toBe("disabled") - expect(@combined.next_problem).toHaveBeenCalled() + expect(@combined.next_problem_button).toBe(":visible") describe 'next_problem', -> beforeEach -> @@ -109,3 +136,5 @@ describe 'CombinedOpenEnded', -> + + From 5d16bc9eb613a4f268eeb4ca7824b5394cc051a7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 9 Aug 2013 11:24:07 -0400 Subject: [PATCH 046/179] Remove ad-hoc patch for xblock issue --- lms/djangoapps/courseware/model_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index b72c0e90c6..44be16e441 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -353,8 +353,7 @@ class LmsKeyValueStore(KeyValueStore): for field in kv_dict: # Check field for validity if field.field_name in self._descriptor_model_data: - if field.field_name not in ["due","graceperiod"]: - raise InvalidWriteError("Not allowed to overwrite descriptor model data", field.field_name) + raise InvalidWriteError("Not allowed to overwrite descriptor model data", field.field_name) if field.scope not in self._allowed_scopes: raise InvalidScopeError(field.scope) From e1cba4b0b4927ad7b44876d472135cb191371976 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 9 Aug 2013 11:26:59 -0400 Subject: [PATCH 047/179] cleaned up html structure for various pages, style fixed for show/hide prompt item --- .../css/combinedopenended/display.scss | 4 + .../js/fixtures/combined-open-ended.html | 121 +++++++++--------- lms/static/sass/course/_rubric.scss | 1 + .../combined_open_ended.html | 3 +- .../combined_open_ended_results.html | 10 +- .../openended/open_ended_combined_rubric.html | 2 +- 6 files changed, 77 insertions(+), 64 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index b63e04d20e..4565f73bc8 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -132,6 +132,10 @@ section.combined-open-ended-status { border-radius: 0 $baseline/4 $baseline/4 0; border-right: 0px; } + + &:only-child { + border-radius: $baseline/4; + } .show-results { margin-top: .3em; diff --git a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html index abea783ae8..5cd1113103 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html +++ b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html @@ -1,75 +1,78 @@
      -
      +
      - -

      Problem 1

      -
      -

      Status

      -
      -
      - -
      - - Step 1 (Problem complete) : 1 / 1 +

      Problem 1

      +
      +

      Status

      +
      +
      +
      + Step 1 (Problem complete) : 1 / 1 - -
      - -
      - - Step 2 (Being scored) : None / 1 +
      +
      + Step 2 (Being scored) : None / 1 - +
      +
      +
      -
      -
      - -
      - -
      -

      Problem

      +
      +

      Problem

      -
      -
      - - Some prompt. - -
      -
      -
      - Submitted for grading. - -
      - -
      - - -
      +
      +
      +
      +
      +
      + Some prompt. +
      + +
      +
      + Submitted for grading. +
      + + +
      + +
      - - - - +
      + +
      - -
      -
      -
      - - -
      - - + +
      + + +
      + + + - + })" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA + +
      +
      Submitted Rubric
    -
    - ${result['task_name']} from grader ${i+1} +
    + % if len(results)>1: + Previous + % endif + ${result['task_name']} from grader ${i+1} + % if len(results)>1: + Next + % endif
    ${result['result'] | n} diff --git a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html index 158ec1f981..5f763f371d 100644 --- a/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_combined_rubric.html @@ -1,7 +1,7 @@
    % for i in range(len(categories)): <% category = categories[i] %> - ${category['description']}
    + ${category['description']}
      % for j in range(len(category['options'])): <% option = category['options'][j] %> From 33da3ec1cfa5080250bf70a5b10f41c7ccc8e069 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 9 Aug 2013 14:50:57 -0400 Subject: [PATCH 048/179] Fix self assessment template --- .../openended/open_ended.html | 4 ++-- .../selfassessment/self_assessment_prompt.html | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lms/templates/combinedopenended/openended/open_ended.html b/lms/templates/combinedopenended/openended/open_ended.html index 6e2de66823..c6a8cb2253 100644 --- a/lms/templates/combinedopenended/openended/open_ended.html +++ b/lms/templates/combinedopenended/openended/open_ended.html @@ -31,8 +31,8 @@
      - - + +
      diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html index 99782e3185..ee2dd643df 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html @@ -4,25 +4,23 @@
      ${prompt} +
      - Response + ${_("Response")}
      -

      ${_("Response")}

      -
      -
      -
      +
      +
      ${initial_rubric}
      -
      - -
      -
      - + + +
      +
    From 68199da821410205ef3c9741b8329555695caa4f Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 9 Aug 2013 18:44:15 -0400 Subject: [PATCH 049/179] added oe-tools header areas to eventually consolitate all messaging --- .../css/combinedopenended/display.scss | 20 +++++++++++++++++++ .../js/fixtures/combined-open-ended.html | 8 +++++--- .../xmodule/xmodule/js/fixtures/rubric.html | 12 +++++------ lms/static/sass/course/_rubric.scss | 10 ++++++++-- .../combined_open_ended.html | 10 ++++++++-- .../combined_open_ended_results.html | 9 +++------ .../self_assessment_prompt.html | 2 -- 7 files changed, 50 insertions(+), 21 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 3500e6e47d..d4a79ff825 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -855,3 +855,23 @@ section.open-ended-child { padding: $baseline/2 0; } } + +//OE Tool Area Styling + +.oe-tools { + display: inline-block; + width: 100%; + border-radius: 5px; + + .oe-tools-label { + font-size: 0.8em; + display: inline-block; + vertical-align: middle; + padding: 10px; + } + + .reset-button { + vertical-align: middle; + } +} + diff --git a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html index 5cd1113103..91d09f7922 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html +++ b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html @@ -41,7 +41,10 @@
    - +
    + Response: + +
    @@ -65,8 +68,7 @@ 'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A', 'category': 'CombinedOpenEndedModule', 'user': 'blah' - })" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA - + })" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA - - - +
    + Response: + +
    @@ -89,8 +90,8 @@ Submitted Rubric
    - Scored rubric from grader 1 + Scored rubric from grader 1
    @@ -313,8 +314,7 @@ Demonstrates effective adjustment of language and tone to task and reader.
    - - +
    diff --git a/lms/static/sass/course/_rubric.scss b/lms/static/sass/course/_rubric.scss index 59aabdb917..b97eea5cd3 100644 --- a/lms/static/sass/course/_rubric.scss +++ b/lms/static/sass/course/_rubric.scss @@ -1,10 +1,16 @@ .rubric-header { - padding: $baseline/2 0; + background-color: #fafafa; + border-radius: 5px; + .rubric-collapse { - float: right; + margin-right: $baseline/2; } } +.button { + display: inline-block; +} + .rubric { margin: 0; color: #3C3C3C; diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 7d46df806e..84ac00fab3 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -37,12 +37,18 @@
    ${item['content'] | n}
    % endfor - - +
    + Response: + +
    +
    + + +
    diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 5dc79ed5d7..99ea6dc46d 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -11,7 +11,9 @@ Submitted Rubric -
    +
    + Assessment Tools: + % if len(results)>1: Previous % endif @@ -19,7 +21,6 @@ % if len(results)>1: Next % endif -
    ${result['result'] | n}
    @@ -29,8 +30,4 @@ %endif % endfor -% if len(results)>1: - Previous - Next -% endif diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html index 99782e3185..543b8899f5 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html @@ -9,8 +9,6 @@
    Response
    - -

    ${_("Response")}

    From 1b111b1d2963aaa6967fd1ee3bf4bbf1d3a703dd Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 12 Aug 2013 20:00:46 +0000 Subject: [PATCH 050/179] add ability to import course (into CMS / edge) without static content, and without rewriting static links. changes xml_importer.py and import.py --- .../management/commands/import.py | 16 ++++-- .../xmodule/modulestore/xml_importer.py | 51 ++++++++++++++++--- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 46f439b055..520e36f4d2 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -2,7 +2,7 @@ Script for importing courseware from XML format """ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError, make_option from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore @@ -14,18 +14,26 @@ class Command(BaseCommand): """ help = 'Import the specified data directory into the default ModuleStore' + option_list = BaseCommand.option_list + ( + make_option('--nostatic', + action='store_true', + help='Skip import of static content'), + ) + def handle(self, *args, **options): "Execute the command" if len(args) == 0: - raise CommandError("import requires at least one argument: [...]") + raise CommandError("import requires at least one argument: [--nostatic] [...]") data_dir = args[0] + do_import_static = not (options.get('nostatic', False)) if len(args) > 1: course_dirs = args[1:] else: course_dirs = None print("Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, - courses=course_dirs)) + courses=course_dirs, + dis=do_import_static)) import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, - static_content_store=contentstore(), verbose=True) + static_content_store=contentstore(), verbose=True, do_import_static=do_import_static) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 0b30a884be..0073863883 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -51,7 +51,10 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ content.thumbnail_location = thumbnail_location #then commit the content - static_content_store.save(content) + try: + static_content_store.save(content) + except Exception as err: + log.exception('Error importing {0}'.format(fullname_with_subpath)) #store the remapping information which will be needed to subsitute in the module data remap_dict[fullname_with_subpath] = content_loc.name @@ -64,7 +67,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor', load_error_modules=True, static_content_store=None, target_location_namespace=None, - verbose=False, draft_store=None): + verbose=False, draft_store=None, + do_import_static=True): """ Import the specified xml data_dir into the "store" modulestore, using org and course as the location org and course. @@ -76,6 +80,10 @@ def import_from_xml(store, data_dir, course_dirs=None, after import off disk. We do this remapping as a post-processing step because there's logic in the importing which expects a 'url_name' as an identifier to where things are on disk e.g. ../policies//policy.json as well as metadata keys in the policy.json. so we need to keep the original url_name during import + + do_import_static: if False, then static files are not imported into the static content store. This can be employed for courses which + have substantial unchanging static content, which is to inefficient to import every time the course is loaded. + Static content for some courses may also be served directly by nginx, instead of going through django. """ @@ -116,8 +124,17 @@ def import_from_xml(store, data_dir, course_dirs=None, course_data_path = path(data_dir) / module.data_dir course_location = module.location + log.debug('======> IMPORTING course to location {0}'.format(course_location)) + module = remap_namespace(module, target_location_namespace) + if not do_import_static: + module.lms.static_asset_path = module.data_dir # for old-style xblock where this was actually linked to kvs + module._model_data['static_asset_path'] = module.data_dir + log.debug('course static_asset_path={0}'.format(module.lms.static_asset_path)) + + log.debug('course data_dir={0}'.format(module.data_dir)) + # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - @@ -129,18 +146,36 @@ def import_from_xml(store, data_dir, course_dirs=None, {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge import_module(module, store, course_data_path, static_content_store, course_location, - target_location_namespace or course_location) + target_location_namespace or course_location, do_import_static=do_import_static) course_items.append(module) # then import all the static content - if static_content_store is not None: + if static_content_store is not None and do_import_static: _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location # first pass to find everything in /static/ import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, _namespace_rename, subpath='static', verbose=verbose) + elif verbose and not do_import_static: + log.debug('Skipping import of static content, since do_import_static={0}'.format(do_import_static)) + + # no matter what do_import_static is, import "static_import" directory + # + # This is needed because the "about" pages (eg "overview") are loaded via load_extra_content, and + # do not inherit the lms metadata from the course module, and thus do not get "static_content_store" + # properly defined. Static content referenced in those extra pages thus need to come through the + # c4x:// contentstore, unfortunately. Tell users to copy that content into the "static_import" subdir. + + simport = 'static_import' + if os.path.exists(course_data_path / simport): + _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location + + import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, + _namespace_rename, subpath=simport, verbose=verbose) + + # finally loop through all the modules for module in xml_module_store.modules[course_id].itervalues(): if module.category == 'course': @@ -156,7 +191,8 @@ def import_from_xml(store, data_dir, course_dirs=None, log.debug('importing module location {0}'.format(module.location)) import_module(module, store, course_data_path, static_content_store, course_location, - target_location_namespace if target_location_namespace else course_location) + target_location_namespace if target_location_namespace else course_location, + do_import_static=do_import_static) # now import any 'draft' items if draft_store is not None: @@ -176,7 +212,8 @@ def import_from_xml(store, data_dir, course_dirs=None, def import_module(module, store, course_data_path, static_content_store, - source_course_location, dest_course_location, allow_not_found=False): + source_course_location, dest_course_location, allow_not_found=False, + do_import_static=True): logging.debug('processing import of module {0}...'.format(module.location.url())) @@ -196,7 +233,7 @@ def import_module(module, store, course_data_path, static_content_store, else: module_data = content - if isinstance(module_data, basestring): + if isinstance(module_data, basestring) and do_import_static: # we want to convert all 'non-portable' links in the module_data (if it is a string) to # portable strings (e.g. /static/) module_data = rewrite_nonportable_content_links( From a5bb971c9b46269beb7accb9cae9a6f3c45cf791 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 12 Aug 2013 20:26:20 +0000 Subject: [PATCH 051/179] add static_asset_path metadata to course, and honor its use in link rewriting --- common/djangoapps/static_replace/__init__.py | 9 +++++---- common/djangoapps/xmodule_modifiers.py | 4 ++-- common/lib/xmodule/xmodule/modulestore/inheritance.py | 3 ++- lms/djangoapps/courseware/courses.py | 4 ++-- lms/djangoapps/courseware/module_render.py | 4 +++- lms/xmodule_namespace.py | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index 9e50d73b26..a34db1a5e0 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -90,7 +90,7 @@ def replace_course_urls(text, course_id): return re.sub(_url_replace_regex('/course/'), replace_course_url, text) -def replace_static_urls(text, data_directory, course_namespace=None): +def replace_static_urls(text, data_directory, course_namespace=None, static_asset_path=''): """ Replace /static/$stuff urls either with their correct url as generated by collectstatic, (/static/$md5_hashed_stuff) or by the course-specific content static url @@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): text: The source text to do the substitution in data_directory: The directory in which course data is stored course_namespace: The course identifier used to distinguish static content for this course in studio + static_asset_path: Path for static assets, which overrides data_directory and course_namespace, if nonempty """ def replace_static_url(match): @@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): if settings.DEBUG and finders.find(rest, True): return original # if we're running with a MongoBacked store course_namespace is not None, then use studio style urls - elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): + elif (not static_asset_path) and course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): # first look in the static file pipeline and see if we are trying to reference # a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule) if staticfiles_storage.exists(rest): @@ -127,7 +128,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): url = StaticContent.convert_legacy_static_url(rest, course_namespace) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: - course_path = "/".join((data_directory, rest)) + course_path = "/".join((static_asset_path or data_directory, rest)) try: if staticfiles_storage.exists(rest): @@ -143,7 +144,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): return "".join([quote, url, quote]) return re.sub( - _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)), + _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=static_asset_path or data_directory)), replace_static_url, text ) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index dd40b5139d..8c1a5ab0e6 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id): return _get_html -def replace_static_urls(get_html, data_dir, course_namespace=None): +def replace_static_urls(get_html, data_dir, course_namespace=None, static_asset_path=''): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... @@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_namespace=None): @wraps(get_html) def _get_html(): - return static_replace.replace_static_urls(get_html(), data_dir, course_namespace) + return static_replace.replace_static_urls(get_html(), data_dir, course_namespace, static_asset_path=static_asset_path) return _get_html diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 1314c72094..2ad08b8350 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -9,7 +9,8 @@ INHERITABLE_METADATA = ( # intended to be set per-course, but can be overridden in for specific # elements. Can be a float. 'days_early_for_beta', - 'giturl' # for git edit link + 'giturl', # for git edit link + 'static_asset_path', # for static assets placed outside xcontent contentstore ) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 086f92a123..c06a2f39c4 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -82,8 +82,8 @@ def get_opt_course_with_access(user, course_id, action): def course_image_url(course): """Try to look up the image url for the course. If it's not found, log an error and return the dead link""" - if isinstance(modulestore(), XMLModuleStore): - return '/static/' + course.data_dir + "/images/course_image.jpg" + if course.lms.static_asset_path or isinstance(modulestore(), XMLModuleStore): + return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg" else: loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg') path = StaticContent.get_url_path_from_location(loc) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0a48c56f87..3a43473f00 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -348,6 +348,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), + static_asset_path=descriptor.lms.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, @@ -405,7 +406,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), - course_namespace=module.location._replace(category=None, name=None) + course_namespace=module.location._replace(category=None, name=None), + static_asset_path=descriptor.lms.static_asset_path ) # Allow URLs of the form '/course/' refer to the root of multicourse directory diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index d57ad9ce52..ad3f634977 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -56,3 +56,4 @@ class LmsNamespace(Namespace): default=None, scope=Scope.settings ) + static_asset_path = String(help="Path to use for static assets - overrides Studio c4x://", scope=Scope.settings, default='') From ab810006651a03cc9500a4d5bc376ba27cb65c7e Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 13 Aug 2013 14:42:34 -0400 Subject: [PATCH 052/179] visual styling changes to staff grading and peer grading, along with additional instances of the use of the baseline variable instead of pixel definitions for padding and margins. --- .../lib/xmodule/xmodule/css/capa/display.scss | 11 +- .../css/combinedopenended/display.scss | 693 ++++++++++-------- lms/static/sass/course/_staff_grading.scss | 200 +++-- .../combined_open_ended_results.html | 2 +- .../peer_grading/peer_grading_problem.html | 6 +- 5 files changed, 483 insertions(+), 429 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index a35dc01633..4bc8fb3f55 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -282,10 +282,9 @@ section.problem { .grader-status { @include clearfix; - margin-bottom: $baseline; - padding: 9px; - border: 1px solid #ddd; - border-top: 0; + margin: $baseline/2 0; + padding: $baseline/2; + border-radius: 5px; background: #F6F6F6; span { @@ -311,11 +310,11 @@ section.problem { } &.file { - background: #fff; margin-top: $baseline; padding: $baseline 0 0 0; border: 0; border-top: 1px solid #eee; + background: #fff; p.debug { display: none; @@ -335,8 +334,8 @@ section.problem { .feedback-on-feedback { - height: 100px; margin-right: $baseline; + height: 100px; } .evaluation-response { diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index d4a79ff825..c0235d5df9 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -23,8 +23,8 @@ h2 { div.name{ padding-bottom: 15px; - h2{ - display: inline; + h2 { + display: inline; } .progress-container { @@ -77,8 +77,8 @@ div.problemwrapper { } .assessments-container { - padding: $baseline/2 $baseline $baseline/2 $baseline/2; float: right; + padding: $baseline/2 $baseline $baseline/2 $baseline/2; .assessment-text { display: inline-block; @@ -93,9 +93,9 @@ div.problemwrapper { } .result-container { - float:left; + float: left; width: 100%; - position:relative; + position: relative; } } @@ -107,7 +107,7 @@ section.legend-container { display: inline; padding: $baseline/2; width: 20%; - background-color : #eee; + background-color: #eee; font-size: .9em; } } @@ -119,18 +119,18 @@ section.combined-open-ended-status { display: table-cell; padding: $baseline/2; width: 30px; + border-right: 1px solid lightgray; background-color: #eee; color: #2c2c2c; font-size: .9em; - border-right: 1px solid lightgray; &:first-child { border-radius: $baseline/4 0 0 $baseline/4; } &:last-child { - border-radius: 0 $baseline/4 $baseline/4 0; border-right: 0px; + border-radius: 0 $baseline/4 $baseline/4 0; } &:only-child { @@ -183,37 +183,36 @@ section.combined-open-ended-status { .icon-caret-right { display: inline-block; - vertical-align: baseline; margin-right: ($baseline/4); + vertical-align: baseline; } } - // Problem Section Controls +// Problem Section Controls .visibility-control, .visibility-control-prompt { display: block; - height: 40px; - width: 100%; + width: 100%; + height: 40px; .inner { float: left; - height: 5px; margin-top: $baseline; - border-top: 1px dotted #ddd; - width: 85%; + width: 85%; + height: 5px; + border-top: 1px dotted #ddd; } } .section-header { display: block; - text-align: center; - width: 15%; float: right; padding-top: $baseline/2; + width: 15%; + text-align: center; font-size: .9em; } - // Rubric Styling .wrapper-score-selection { @@ -221,7 +220,6 @@ section.combined-open-ended-status { padding: 0 $baseline/2; width: 20px; vertical-align: middle; - } .wrappable { @@ -240,17 +238,17 @@ section.combined-open-ended-status { span.rubric-category { display: block; + margin-bottom: $baseline/2; + padding-top: $baseline/2; width: 100%; border-bottom: 1px solid lightgray; font-size: 1.1em; - padding-top: $baseline/2; - margin-bottom: $baseline/2; } div.combined-rubric-container { margin: 15px; - padding-bottom: 5px; - padding-top: 10px; + padding-top: $baseline/2; + padding-bottom: $baseline/4; ul.rubric-list { margin: 0 $baseline $baseline/2 $baseline; @@ -265,10 +263,11 @@ div.combined-rubric-container { } } } + h4 { - padding-top: 10px; - border-top: 1px solid; + padding-top: $baseline/2; border-color: lightgray; + border-top: 1px solid; } span.rubric-category { @@ -286,12 +285,12 @@ div.combined-rubric-container { } } - label.choicegroup_partialcorrect { - &:before { - margin-right: 15px; - content: url('../images/partially-correct-icon.png'); - } + label.choicegroup_partialcorrect { + &:before { + margin-right: 15px; + content: url('../images/partially-correct-icon.png'); } + } label.choicegroup_incorrect { &:before { @@ -300,150 +299,184 @@ div.combined-rubric-container { } } } - + div.result-container { - padding-top: 10px; - padding-bottom: 5px; - .evaluation { + padding-top: $baseline/2; + padding-bottom: $baseline/4; - p { - margin-bottom: 1px; + .evaluation { + p { + margin-bottom: 1px; + } + } + + .feedback-on-feedback { + height: 100px; + margin-right: 0; + } + + .evaluation-response { + margin-bottom: 2px; + + header { + a { + font-size: .85em; + } + } + } + + .evaluation-scoring { + .scoring-list { + margin-left: 3px; + list-style-type: none; + + li { + display:inline; + margin-left: 0; + + &:first-child { + margin-left: 0; } - } - .feedback-on-feedback { - height: 100px; - margin-right: 0px; - } - - .evaluation-response { - margin-bottom: 2px; - header { - a { - font-size: .85em; - } + label { + font-size: .9em; } + } } - .evaluation-scoring { - .scoring-list { - list-style-type: none; - margin-left: 3px; + } - li { - &:first-child { - margin-left: 0px; - } - display:inline; - margin-left: 0px; + .submit-message-container { + margin: $baseline/2 0; + } - label { - font-size: .9em; - } - } + .external-grader-message { + margin-bottom: $baseline/4; + + section { + padding-left: $baseline; + background-color: #fafafa; + color: #2c2c2c; + font-family: monospace; + font-size: 1em; + padding-top: $baseline/2; + padding-bottom:30px; + + header { + font-size: 1.4em; + } + + .shortform { + font-weight: bold; + } + + .longform { + padding: 0; + margin: 0; + + .result-errors { + margin: $baseline/4; + padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2; + background: url('../images/incorrect-icon.png') center left no-repeat; + + li { + color: #B00; + } } - } - .submit-message-container { - margin: 10px 0px ; - } - .external-grader-message { - margin-bottom: 5px; - section { - padding-left: 20px; - background-color: #FAFAFA; - color: #2C2C2C; - font-family: monospace; - font-size: 1em; - padding-top: 10px; - padding-bottom:30px; - header { - font-size: 1.4em; + .result-output { + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; + + h4 { + font-size: 1em; + font-family: monospace; } - .shortform { - font-weight: bold; + dl { + margin: 0; } - .longform { - padding: 0px; - margin: 0px; + dt { + margin-top: $baseline; + } - .result-errors { - margin: 5px; - padding: 10px 10px 10px 40px; - background: url('../images/incorrect-icon.png') center left no-repeat; - li { - color: #B00; - } - } + dd { + margin-left: 24pt; + } + } - .result-output { - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + .markup-text{ + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; - h4 { - font-family: monospace; - font-size: 1em; - } + bs { + color: #bb0000; + } - dl { - margin: 0px; - } - - dt { - margin-top: 20px; - } - - dd { - margin-left: 24pt; - } - } - - .markup-text{ - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; - - bs { - color: #BB0000; - } - - bg { - color: #BDA046; - } - } + bg { + color: #bda046; } } } + } + } + .rubric-result-container { + padding: 2px; + margin: 0px; + display : inline; + .rubric-result { font-size: .9em; padding: 2px; display: inline-table; } - padding: 2px; - margin: 0px; - display : inline; } } div.rubric { - ul.rubric-list{ - margin: 0; - padding: 0; - list-style-type: none; - list-style: none; - li { - &.rubric-list-item { - margin-bottom: 0; - padding: 0; - border-radius: $baseline/4; - } + ul.rubric-list{ + margin: 0 $baseline $baseline/2 $baseline; + padding: 0; + list-style: none; + list-style-type: none; + + li { + &.rubric-list-item { + margin-bottom: 2px; + padding: $baseline/2; + border-radius: $baseline/4; + + &:hover { + background-color: #eee; + } + + .wrapper-score-selection { + display: table-cell; + padding: 0 $baseline/2; + width: 20px; + vertical-align: middle; + } + + .wrappable { + display: table-cell; + padding: $baseline/4; + } } } + } + + span.rubric-category { + display: block; + width: 100%; + border-bottom: 1px solid lightgray; + font-weight: bold; + font-size: .9em; + } } @@ -490,188 +523,186 @@ section.open-ended-child { } } - p { - &.answer { - margin-top: -2px; - } - &.status { - margin: 8px 0 0 $baseline/2; - text-indent: -9999px; - } + p { + &.answer { + margin-top: -2px; } - - div.unanswered { - p.status { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - width: 14px; - } + &.status { + margin: 8px 0 0 $baseline/2; + text-indent: -9999px; } + } - div.correct, div.ui-icon-check { - p.status { - @include inline-block(); - width: 25px; - height: 20px; - background: url('../images/correct-icon.png') center center no-repeat; - } - - input { - border-color: green; - } - } - - div.processing { - p.status { - @include inline-block(); - width: 20px; - height: 20px; - background: url('../images/spinner.gif') center center no-repeat; - } - - input { - border-color: #aaa; - } - } - - div.incorrect, div.ui-icon-close { - p.status { - @include inline-block(); - width: 20px; - height: 20px; - background: url('../images/incorrect-icon.png') center center no-repeat; - text-indent: -9999px; - } - - input { - border-color: red; - } - } - - > span { - display: block; - margin-bottom: lh(.5); - } - - p.answer { + div.unanswered { + p.status { @include inline-block(); - margin-bottom: 0; - margin-left: 10px; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + } + div.correct, div.ui-icon-check { + p.status { + @include inline-block(); + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + input { + border-color: green; + } + } + + div.processing { + p.status { + @include inline-block(); + width: 20px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; + } + + input { + border-color: #aaa; + } + } + + div.incorrect, div.ui-icon-close { + p.status { + @include inline-block(); + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + text-indent: -9999px; + } + + input { + border-color: red; + } + } + + > span { + display: block; + margin-bottom: lh(.5); + } + + p.answer { + @include inline-block(); + margin-bottom: 0; + margin-left: $baseline/2; + + &:before { + content: "Answer: "; + font-weight: bold; + display: inline; + + } + &:empty { &:before { - content: "Answer: "; - font-weight: bold; - display: inline; - - } - &:empty { - &:before { - display: none; - } + display: none; } } + } + + span { + &.unanswered, &.ui-icon-bullet { + @include inline-block(); + position: relative; + top: 4px; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + + &.processing, &.ui-icon-processing { + @include inline-block(); + position: relative; + top: 6px; + width: 25px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; + } + + &.correct, &.ui-icon-check { + @include inline-block(); + position: relative; + top: 6px; + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + &.incorrect, &.ui-icon-close { + @include inline-block(); + position: relative; + top: 6px; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + } + } + + .reload { + float:right; + margin: $baseline/2; + } + + div.short-form-response { + @include clearfix; + overflow-y: auto; + margin-bottom: 0; + padding: $baseline/2; + min-height: 20px; + height: auto; + border: 1px solid #ddd; + background: #f6f6f6; + } + + .grader-status { + @include clearfix; + margin: $baseline/2 0; + padding: $baseline/2; + border-radius: 5px; + background: #f6f6f6; span { - &.unanswered, &.ui-icon-bullet { - @include inline-block(); - position: relative; - top: 4px; - width: 14px; - height: 14px; - background: url('../images/unanswered-icon.png') center center no-repeat; - } - - &.processing, &.ui-icon-processing { - @include inline-block(); - position: relative; - top: 6px; - width: 25px; - height: 20px; - background: url('../images/spinner.gif') center center no-repeat; - } - - &.correct, &.ui-icon-check { - @include inline-block(); - position: relative; - top: 6px; - width: 25px; - height: 20px; - background: url('../images/correct-icon.png') center center no-repeat; - } - - &.incorrect, &.ui-icon-close { - @include inline-block(); - position: relative; - top: 6px; - width: 20px; - height: 20px; - background: url('../images/incorrect-icon.png') center center no-repeat; - } + display: block; + float: left; + overflow: hidden; + margin: -7px 7px 0 0; + text-indent: -9999px; } - .reload { - float:right; - margin: 10px; + .grading { + margin: 0 7px 0 0; + padding-left: 25px; + background: url('../images/info-icon.png') left center no-repeat; + text-indent: 0; } - div.short-form-response { - @include clearfix; - overflow-y: auto; + p { + float: left; margin-bottom: 0; - padding: $baseline/2; - height: auto; - min-height: 20px; - border: 1px solid #ddd; - background: #f6f6f6; + line-height: 20px; } - .grader-status { - @include clearfix; - margin-bottom: $baseline; - padding: 9px; - border: 1px solid #ddd; - border-top: 0; - background: #f6f6f6; + &.file { + margin-top: $baseline; + padding: $baseline 0 0 0; + border: 0; + border-top: 1px solid #eee; + background: #fff; - span { - display: block; + p.debug { + display: none; + } + + input { float: left; - overflow: hidden; - margin: -7px 7px 0 0; - text-indent: -9999px; } - - .grading { - margin: 0 7px 0 0; - padding-left: 25px; - background: url('../images/info-icon.png') left center no-repeat; - text-indent: 0; - } - - p { - float: left; - margin-bottom: 0; - line-height: 20px; - } - - &.file { - margin-top: $baseline; - padding: $baseline 0 0 0; - border: 0; - border-top: 1px solid #eee; - background: #fff; - - p.debug { - display: none; - } - - input { - float: left; - } - } - } + } form.option-input { margin: -$baseline/2 0 $baseline; @@ -688,19 +719,20 @@ section.open-ended-child { margin-left: .75rem; } - ul.rubric-list{ - margin: 0; - padding: 0; - list-style-type: none; - list-style: none; - li { - &.rubric-list-item { - margin-bottom: 0; - padding: 0; - border-radius: $baseline/4; - } + ul.rubric-list{ + margin: 0; + padding: 0; + list-style-type: none; + list-style: none; + + li { + &.rubric-list-item { + margin-bottom: 0; + padding: 0; + border-radius: $baseline/4; } } + } ol { margin-bottom: lh(); @@ -860,18 +892,49 @@ section.open-ended-child { .oe-tools { display: inline-block; + padding-left: $baseline; width: 100%; border-radius: 5px; .oe-tools-label { - font-size: 0.8em; display: inline-block; + padding: $baseline/2; vertical-align: middle; - padding: 10px; + font-size: 0.8em; } + .next-step-button { + margin: $baseline/2; + } .reset-button { vertical-align: middle; } } +// Staff Grading +.problem-list-container { + margin: $baseline/2; + + .instructions { + padding-bottom: $baseline/2; + } +} + +.staff-grading { + + .breadcrumbs { + padding: $baseline/10; + background-color: #f6f6f6; + border-radius: 5px; + margin-bottom: $baseline/2; + } + + .prompt-wrapper { + padding-top: $baseline/2; + + .meta-info-wrapper { + padding: $baseline/2; + border-radius: $baseline/4; + } + } +} diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index b387d753d1..4dd225199c 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -1,46 +1,50 @@ div.staff-grading, div.peer-grading{ + padding: $baseline; + border: none; + textarea.feedback-area { + margin: 0; height: 75px; - margin: 0px; } ul.rubric-list{ + margin: 0; + padding: 0; list-style-type: none; - padding:0; - margin:0; + li { - &.rubric-list-item{ - margin-bottom: 0px; - padding: 0px; - } + &.rubric-list-item{ + margin-bottom: 0; + padding: 0; + } } } h1 { - margin : 0 0 0 10px; + margin: 0 0 0 $baseline/2; } - h2{ - a - { + h2 { + a { text-size: .5em; } } div { - margin: 0px; + margin: 0; + &.submission-container{ - overflow-y: auto; - height: 150px; - background: #F6F6F6; - border: 1px solid #ddd; - @include clearfix; + @include clearfix; + overflow-y: auto; + height: 150px; + border: 1px solid #ddd; + background: #F6F6F6; } } label { - margin: 0px; + margin: 0; padding: 2px; min-width: 50px; background-color: white; @@ -58,143 +62,127 @@ div.peer-grading{ display: none; } - .problem-list - { - text-align: center; - table-layout: auto; + .problem-list { width:100%; - th - { + table-layout: auto; + text-align: center; + + th { padding: 2px; } - td - { - padding:2px; + + td { + padding: 2px; } - td.problem-name - { - text-align:left; + + td.problem-name { + text-align: left; } - .ui-progressbar - { - height:1em; - margin:0px; - padding:0px; + + .ui-progressbar { + margin: 0; + padding: 0; + height: 1em; } } .prompt-information-container, .rubric-wrapper, .calibration-feedback-wrapper, - .grading-container - { - padding: 2px; + .grading-container { + padding: $baseline/2 0; } - .error-container - { - background-color: #FFCCCC; + + .error-container { + margin-left: 0; padding: 2px; - margin-left: 0px; + background-color: #ffcccc; } - .submission-wrapper - { - h3 - { - margin-bottom: 2px; - } - p - { - margin-left:2px; - } + + .submission-wrapper { padding: 2px; padding-bottom: 15px; + + h3 { + margin-bottom: 2px; + } + + p { + margin-left: 2px; + } } - .meta-info-wrapper - { - background-color: #eee; + .meta-info-wrapper { padding:2px; - div - { - display : inline; + background-color: #eee; + + div { + display: inline; } } .message-container, - .grading-message - { - background-color: $yellow; + .grading-message { + margin-left: 0; padding: 2px; - margin-left:0px; + background-color: $yellow; } - .breadcrumbs - { - margin-top:2px; - margin-left:0px; - margin-bottom:2px; + .breadcrumbs { + margin: $baseline/2 $baseline/4; font-size: .8em; } - .instructions-panel - { - + .instructions-panel { + @include clearfix; margin-right:2px; - > div - { - padding: 2px; + + > div { margin-bottom: 5px; + padding: 2px; + width: 47.6%; background: #eee; - width:47.6%; - h3 - { - text-align:center; - text-transform:uppercase; + + h3 { color: #777; + text-align: center; + text-transform: uppercase; } - p - { + + p{ color: #777; } } - .calibration-panel - { - float:left; + .calibration-panel { + float: left; } - .grading-panel - { - float:right; + + .grading-panel { + float: right; } - .current-state - { - background: #1D9DD9; - h3, p - { + .current-state { + background: #1d9dd9; + + h3, p { color: white; } } - @include clearfix; } + .collapsible { + margin-left: 0; - .collapsible - { - margin-left: 0px; - header - { - margin-top:2px; - margin-bottom:2px; + header { + margin-top: 2px; + margin-bottom: 2px; font-size: 1.2em; } } - .interstitial-page - { + .interstitial-page { text-align: center; - input[type=button] - { - margin-top: 20px; + + input[type=button] { + margin-top: $baseline; } } - padding: 15px; - border: none; } diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 99ea6dc46d..95817e4588 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -12,7 +12,7 @@ Submitted Rubric
    - Assessment Tools: + % if len(results)>1: Previous diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index d99e14c706..a46f9135a0 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -14,7 +14,11 @@
    -

    ${_('Prompt')} ${_('(Hide)')}

    +
    +
    +
    + ${_('Hide Prompt')} +
    From 3a970d14904c831fe6727e4ae6966361cf707deb Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 13 Aug 2013 20:56:53 -0400 Subject: [PATCH 053/179] restyled controls area for motion through multiple scored peer reviews --- .../css/combinedopenended/display.scss | 14 +++++++++++++- .../combined_open_ended_results.html | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index c0235d5df9..ef39d8a1b6 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -896,13 +896,25 @@ section.open-ended-child { width: 100%; border-radius: 5px; - .oe-tools-label { + .oe-tools-label, .oe-tools-scores-label { display: inline-block; padding: $baseline/2; vertical-align: middle; font-size: 0.8em; } + .rubric-button { + padding: 8px $baseline/4; + } + + .rubric-previous-button { + margin-right: $baseline/4; + } + + .rubric-next-button { + margin-left: $baseline/4; + } + .next-step-button { margin: $baseline/2; } diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 95817e4588..b013586d4f 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -12,15 +12,18 @@ Submitted Rubric
    - + Rubric: - % if len(results)>1: - Previous - % endif - ${result['task_name']} from grader ${i+1} - % if len(results)>1: - Next - % endif + + Scores: + % if len(results)>1: + + % endif + ${result['task_name']} from grader ${i+1} + % if len(results)>1: + + % endif +
    ${result['result'] | n}
    From c9807db308fa7f17d8eb77564cc916ccf53e8ab6 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 14 Aug 2013 14:12:04 -0400 Subject: [PATCH 054/179] styling of peer review interface for students along with minimal adjustments to the staff grading interface as well. added internationalization of peer grading strings. --- lms/static/sass/course/_staff_grading.scss | 44 +++-- .../combined_open_ended.html | 8 +- .../combined_open_ended_results.html | 8 +- lms/templates/instructor/staff_grading.html | 98 +++++----- .../peer_grading/peer_grading_problem.html | 167 +++++++++--------- 5 files changed, 166 insertions(+), 159 deletions(-) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 4dd225199c..739cdc6ca5 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -1,7 +1,6 @@ div.staff-grading, div.peer-grading{ - padding: $baseline; - border: none; + border: 1px solid lightgray; textarea.feedback-area { margin: 0; @@ -37,7 +36,8 @@ div.peer-grading{ &.submission-container{ @include clearfix; overflow-y: auto; - height: 150px; + height: auto; + max-height: 300px; border: 1px solid #ddd; background: #F6F6F6; } @@ -133,12 +133,14 @@ div.peer-grading{ .instructions-panel { @include clearfix; - margin-right:2px; + padding: $baseline/2; + background-color: #eee; + font-size: .8em; > div { margin-bottom: 5px; - padding: 2px; - width: 47.6%; + padding: $baseline/2; + width: 49%; background: #eee; h3 { @@ -152,18 +154,19 @@ div.peer-grading{ } } .calibration-panel { - float: left; + display: inline-block; + width: 20%; + border-radius: 3px; } .grading-panel { - float: right; + display: inline-block; + width: 20%; + border-radius: 3px; } .current-state { - background: #1d9dd9; + background: #fff; - h3, p { - color: white; - } } } @@ -186,3 +189,20 @@ div.peer-grading{ } } +div.peer-grading { + border-radius: $baseline/2; + padding: 0; + + .prompt-wrapper { + padding: $baseline; + } + + .grading-wrapper { + padding: $baseline; + } +} + +div.staff-grading { + padding: $baseline; +} + diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 84ac00fab3..098f6d456a 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -11,12 +11,12 @@
    - Open Response + ${_("Open Response")}
    - Assessments: + ${_("Assessments:")}
    ${status|n} @@ -30,7 +30,7 @@
    % for item in items: @@ -38,7 +38,7 @@ % endfor
    - Response: + ${_("Response: ")}
    diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index b013586d4f..94208d43dc 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -9,13 +9,13 @@
    - Submitted Rubric + ${_("Submitted Rubric")}
    - Rubric: - + ${_("Rubric: ")} + - Scores: + ${_("Scores:")} % if len(results)>1: % endif diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index 40e80de11e..8595b0d722 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -19,75 +19,69 @@

    ${_("Staff grading")}

    - -
    -
    -
    -
    + +
    +
    -

    ${_("Instructions")}

    -
    -

    ${_("This is the list of problems that currently need to be graded in order to train the machine learning models. Each problem needs to be trained separately, and we have indicated the number of student submissions that need to be graded in order for a model to be generated. You can grade more than the minimum required number of submissions--this will improve the accuracy of machine learning, though with diminishing returns. You can see the current accuracy of machine learning while grading.")}

    -
    +

    ${_("Instructions")}

    +
    +

    ${_("This is the list of problems that currently need to be graded in order to train the machine learning models. Each problem needs to be trained separately, and we have indicated the number of student submissions that need to be graded in order for a model to be generated. You can grade more than the minimum required number of submissions--this will improve the accuracy of machine learning, though with diminishing returns. You can see the current accuracy of machine learning while grading.")}

    +
    -

    ${_("Problem List")}

    - -
    +

    ${_("Problem List")}

    + +
    -

    -
    -
    -
    -
    -
    -
    -
    +

    +

    +
    +
    +
    +
    +
    +
    +

    ${_('Prompt')} ${_("(Hide)")}

    -
    -
    +
    - +
    - +
    +
    +
    +

    ${_("Student Response")}

    +
    +
    +
    +
    +

    +

    +

    +

    +

    ${_("Written Feedback")}

    + +

    + ${_("Flag as inappropriate content for later review")} +

    +
    +
    + + +
    -
    -
    -

    ${_("Student Response")}

    -
    -
    -
    -
    -

    -

    -

    -

    -

    ${_("Written Feedback")}

    - -

    - ${_("Flag as inappropriate content for later review")} -

    -
    +
    - -
    - - -
    - -
    - -
    +
    diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index a46f9135a0..a0078b8868 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -4,102 +4,95 @@
    -
    -
    -

    ${_("Learning to Grade")}

    -
    -
    -

    ${_("Peer Grading")}

    -
    -
    - -
    -
    -
    +
    +
    +

    ${_("Learning to Grade")}

    +
    +
    +

    ${_("Peer Grading")}

    - ${_('Hide Prompt')}
    -
    -
    -
    + +
    +
    +
    +
    + ${_('Hide Prompt')} +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    ${_("Student Response")}

    +
    +
    +

    +
    +
    + + +
    +
    +

    +

    +

    ${_("Written Feedback")}

    +

    ${_("Please include some written feedback as well.")}

    + +
    ${_("This submission has explicit or pornographic content : ")} + +
    +
    ${_("I do not know how to grade this question : ")} + +
    +
    +
    + +
    +
    +
    +
    + +
    +

    ${_("How did I do?")}

    +
    + +
    -
    + +
    +

    ${_("Ready to grade!")}

    +

    ${_("You have finished learning to grade, which means that you are now ready to start grading.")}

    + +
    + +
    +

    ${_("Learning to grade")}

    +

    ${_("You have not yet finished learning to grade this problem.")}

    +

    ${_("You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.")}

    +

    ${_("Once you can score the essays similarly to an instructor, you will be ready to grade your peers.")}

    + +
    -
    -

    ${_("Student Response")}

    - -
    -
    -

    -
    + +
    +

    ${_("Are you sure that you want to flag this submission?")}

    +

    + ${_("You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it.")} +

    +
    + +
    - - -
    -
    -

    -

    -

    -

    ${_("Written Feedback")}

    -

    ${_("Please include some written feedback as well.")}

    - -
    ${_("This submission has explicit or pornographic content : ")}
    -
    ${_("I do not know how to grade this question : ")}
    -
    +
    - -
    - -
    - -
    -
    -
    - -
    - - -
    -

    ${_("How did I do?")}

    -
    -
    - -
    - - -
    -

    ${_("Ready to grade!")}

    -

    ${_("You have finished learning to grade, which means that you are now ready to start grading.")}

    - -
    - - -
    -

    ${_("Learning to grade")}

    -

    ${_("You have not yet finished learning to grade this problem.")}

    -

    ${_("You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.")}

    -

    ${_("Once you can score the essays similarly to an instructor, you will be ready to grade your peers.")}

    - -
    - - -
    -

    ${_("Are you sure that you want to flag this submission?")}

    -

    - ${_("You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it.")} -

    -
    - - -
    -
    - - - + From 6f657c1124190ab388f24f93b156d6b217344007 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 10:30:16 -0400 Subject: [PATCH 055/179] Import to fix internationalization --- lms/templates/combinedopenended/combined_open_ended_results.html | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 94208d43dc..3fa4d32e8d 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> % for (i,result) in enumerate(results): % if 'task_name' in result and 'result' in result:
    Date: Thu, 15 Aug 2013 13:56:17 -0400 Subject: [PATCH 056/179] cleaned up padding for peer review training and grading states, removed extraneous response label,fixed style issue on rubric for peer grading where rubric items had a background color overridding the intended style --- .../css/combinedopenended/display.scss | 5 +- .../js/fixtures/combined-open-ended.html | 2 +- .../xmodule/xmodule/js/fixtures/rubric.html | 2 +- lms/static/sass/course/_staff_grading.scss | 21 +++- .../combined_open_ended.html | 2 +- lms/templates/peer_grading/peer_grading.html | 106 +++++++++--------- 6 files changed, 75 insertions(+), 63 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index ef39d8a1b6..704b00812a 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -266,8 +266,6 @@ div.combined-rubric-container { h4 { padding-top: $baseline/2; - border-color: lightgray; - border-top: 1px solid; } span.rubric-category { @@ -892,7 +890,6 @@ section.open-ended-child { .oe-tools { display: inline-block; - padding-left: $baseline; width: 100%; border-radius: 5px; @@ -910,7 +907,7 @@ section.open-ended-child { .rubric-previous-button { margin-right: $baseline/4; } - + .rubric-next-button { margin-left: $baseline/4; } diff --git a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html index 91d09f7922..e5eb0858f7 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html +++ b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html @@ -42,7 +42,7 @@
    - Response: +
    diff --git a/common/lib/xmodule/xmodule/js/fixtures/rubric.html b/common/lib/xmodule/xmodule/js/fixtures/rubric.html index 6b867cc52b..bdb572d11b 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/rubric.html +++ b/common/lib/xmodule/xmodule/js/fixtures/rubric.html @@ -78,7 +78,7 @@
    - Response: +
    diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 739cdc6ca5..1087aae67a 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -36,10 +36,10 @@ div.peer-grading{ &.submission-container{ @include clearfix; overflow-y: auto; - height: auto; max-height: 300px; + height: auto; border: 1px solid #ddd; - background: #F6F6F6; + background: #f6f6f6; } } @@ -47,7 +47,6 @@ div.peer-grading{ margin: 0; padding: 2px; min-width: 50px; - background-color: white; text-size: 1.5em; } @@ -63,7 +62,7 @@ div.peer-grading{ } .problem-list { - width:100%; + width: 100%; table-layout: auto; text-align: center; @@ -193,6 +192,20 @@ div.peer-grading { border-radius: $baseline/2; padding: 0; + .peer-grading-tools { + padding: $baseline; + } + + .error-container { + margin: $baseline; + border-radius: $baseline/4; + padding: $baseline/2; + } + + .interstitial-page, .calibration -feedback, .calibration-interstitial-page { + padding: $baseline; + } + .prompt-wrapper { padding: $baseline; } diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 098f6d456a..65b44b0aa1 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -38,7 +38,7 @@ % endfor
    - ${_("Response: ")} +
    diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index f423de1c6b..468c2f300f 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -2,59 +2,61 @@
    ${error_text}
    -

    ${_("Peer Grading")}

    -

    ${_("Instructions")}

    -

    ${_("Here are a list of problems that need to be peer graded for this course.")}

    - % if success: - % if len(problem_list) == 0: -
    - ${_("Nothing to grade!")} -
    - %else: -
    - - - - - - - - +
    +

    ${_("Peer Grading")}

    +

    ${_("Instructions")}

    +

    ${_("Here are a list of problems that need to be peer graded for this course.")}

    + % if success: + % if len(problem_list) == 0: +
    + ${_("Nothing to grade!")} +
    + %else: +
    +
    ${_("Problem Name")}${_("Due date")}${_("Graded")}${_("Available")}${_("Required")}${_("Progress")}
    + + + + + + + + + %for problem in problem_list: + + + + + + + - %for problem in problem_list: - - - - - - - - - %endfor -
    ${_("Problem Name")}${_("Due date")}${_("Graded")}${_("Available")}${_("Required")}${_("Progress")}
    + %if problem['closed']: + ${problem['problem_name']} + %else: + ${problem['problem_name']} + %endif + + % if problem['due']: + ${problem['due']} + % else: + ${_("No due date")} + % endif + + ${problem['num_graded']} + + ${problem['num_pending']} + + ${problem['num_required']} + +
    +
    +
    - %if problem['closed']: - ${problem['problem_name']} - %else: - ${problem['problem_name']} - %endif - - % if problem['due']: - ${problem['due']} - % else: - ${_("No due date")} - % endif - - ${problem['num_graded']} - - ${problem['num_pending']} - - ${problem['num_required']} - -
    -
    -
    -
    + %endfor +
    +
    + %endif %endif - %endif +
    From f8dab92ba62f81ad68d9719fd2e8db0b4026af19 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 14:15:19 -0400 Subject: [PATCH 057/179] Hide submit button after student submits --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 1 + .../xmodule/open_ended_grading_classes/openendedchild.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 6b79ec9e3b..64401b647b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -375,6 +375,7 @@ class @CombinedOpenEnded save_answer: (event) => event.preventDefault() + @submit_button.hide() max_filesize = 2*1000*1000 #2MB pre_can_upload_files = @can_upload_files if @child_state == 'initial' 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 8635f75db6..3e12b4656d 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -182,7 +182,7 @@ class OpenEndedChild(object): whitelist_tags=set(['embed', 'iframe', 'a', 'img'])) clean_html = cleaner.clean_html(answer) clean_html = re.sub(r'

    $', '', re.sub(r'^

    ', '', clean_html)) - except: + except Exception: clean_html = answer return clean_html From ac4d3124151a3c3d34cf07f833ee988a134a5246 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 14:26:42 -0400 Subject: [PATCH 058/179] Make rubric text generic, fix right arrow --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 6 ------ .../combinedopenended/combined_open_ended_results.html | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 64401b647b..f2888c0144 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -667,10 +667,4 @@ class @CombinedOpenEnded toggle_rubric: (event) => info_rubric_elements = @$(@info_rubric_elements_sel) info_rubric_elements.slideToggle() - @rubric_header = @$(@rubric_collapse_sel) - if @rubric_header.text() == "Show Score Only" - new_text = "Show Full Rubric" - else - new_text = "Show Score Only" - @rubric_header.text(new_text) return false diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 3fa4d32e8d..8b0a7d3b48 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -14,7 +14,7 @@

    ${_("Rubric: ")} - + ${_("Scores:")} % if len(results)>1: @@ -22,7 +22,7 @@ % endif ${result['task_name']} from grader ${i+1} % if len(results)>1: - + % endif
    From a084d4486d6985e1560911c1df7699af323e6e0a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 14:28:52 -0400 Subject: [PATCH 059/179] Get rid of labels --- .../combinedopenended/combined_open_ended_results.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index 8b0a7d3b48..c1b79bca95 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -13,10 +13,10 @@ ${_("Submitted Rubric")}
    - ${_("Rubric: ")} + - ${_("Scores:")} + % if len(results)>1: % endif From f8b5e6736323795d92d8646048329a07b45f476b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 14:52:51 -0400 Subject: [PATCH 060/179] Text and styling cleanup --- .../lib/xmodule/xmodule/css/combinedopenended/display.scss | 5 +++++ .../xmodule/js/src/peergrading/peer_grading_problem.coffee | 4 ++-- .../combinedopenended/combined_open_ended_results.html | 2 +- lms/templates/peer_grading/peer_grading.html | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 704b00812a..120ea0c52d 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -296,6 +296,11 @@ div.combined-rubric-container { content: url('../images/incorrect-icon.png'); } } + + div.written-feedback { + background: #f6f6f6; + padding: 15px; + } } div.result-container { diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 4f644a4ec1..a026cafd61 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -266,7 +266,7 @@ class @PeerGradingProblem @calibration_feedback_button.click => @calibration_feedback_panel.hide() @grading_wrapper.show() - @gentle_alert "Calibration essay saved. Fetched the next essay." + @gentle_alert "Calibration essay saved. Fetching the next essay." @is_calibrated_check() @interstitial_page_button.click => @@ -384,7 +384,7 @@ class @PeerGradingProblem if response.success @is_calibrated_check() @grading_message.fadeIn() - message = "

    Successfully saved your feedback. Fetched the next essay." + message = "

    Successfully saved your feedback. Fetching the next essay." if response.required_done message = message + " You have completed the required number of gradings." message = message + "

    " diff --git a/lms/templates/combinedopenended/combined_open_ended_results.html b/lms/templates/combinedopenended/combined_open_ended_results.html index c1b79bca95..5e5294bf60 100644 --- a/lms/templates/combinedopenended/combined_open_ended_results.html +++ b/lms/templates/combinedopenended/combined_open_ended_results.html @@ -14,7 +14,7 @@
    - + % if len(results)>1: diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 468c2f300f..162c9273b0 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -9,7 +9,7 @@ % if success: % if len(problem_list) == 0:
    - ${_("Nothing to grade!")} + ${_("You currently do not having any peer grading to do. In order to have peer grading to do, you need to have submitted a response to a peer grading problem. The instructor also needs to score the essays that are used in the 'learning to grade' process.")}
    %else:
    From 35a540a0f64b2fb26cdb82a5585cc9a15e2ac87d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 17:25:04 -0400 Subject: [PATCH 061/179] Fix bug with leaving open ended response page and coming back --- .../js/src/combinedopenended/display.coffee | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index f2888c0144..a1792a3a3d 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -124,6 +124,7 @@ class @CombinedOpenEnded rubric_collapse_sel: '.rubric-collapse' next_rubric_sel: '.rubric-next-button' previous_rubric_sel: '.rubric-previous-button' + oe_alert_sel: '.open-ended-alert' constructor: (el) -> @el=el @@ -137,6 +138,7 @@ class @CombinedOpenEnded $(selector, @el) reinitialize: (element) -> + @has_been_reset = false @wrapper=@$(@wrapper_sel) @coe = @$(@coe_sel) @@ -253,6 +255,30 @@ class @CombinedOpenEnded if response.hide_reset @reset_button.hide() + get_last_response: () => + @submit_button.hide() + @answer_area.attr("disabled", true) + data = {} + $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => + if response.success && response.response != "" + @answer_area.html(response.response) + if response.state!='initial' + @submit_button.hide() + @answer_area.attr("disabled", true) + if @has_been_reset + @submit_button.show() + @answer_area.attr("disabled", false) + @gentle_alert "Here is your previous answer to this qu + estion." + else if @allow_reset=="True" + @reset_button.show() + @gentle_alert "You may reset and answer this question again." + else + @gentle_alert "You have answered this question." + else + @submit_button.show() + @answer_area.attr("disabled", false) + @$(@oe_alert_sel).animate(opacity: 0, 700) show_status_current: () => data = {} @@ -261,12 +287,6 @@ class @CombinedOpenEnded @status_container.after(response.html).remove() @status_container= $(@status_container_sel) - get_last_response: () => - data = {} - $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => - if response.success - console.log(response.response) - message_post: (event)=> external_grader_message=$(event.target).parent().parent().parent() evaluation_scoring = $(event.target).parent() @@ -301,6 +321,7 @@ class @CombinedOpenEnded rebind: () => + @get_last_response() # rebind to the appropriate function for the current state @submit_button.unbind('click') @submit_button.show() @@ -308,6 +329,7 @@ class @CombinedOpenEnded @hide_file_upload() @next_problem_button.hide() @hint_area.attr('disabled', false) + if @task_number>1 or @child_state!='initial' @show_status_current() @@ -376,6 +398,7 @@ class @CombinedOpenEnded save_answer: (event) => event.preventDefault() @submit_button.hide() + @answer_area.attr("disabled", true) max_filesize = 2*1000*1000 #2MB pre_can_upload_files = @can_upload_files if @child_state == 'initial' @@ -414,7 +437,6 @@ class @CombinedOpenEnded @gentle_alert response.error $.ajaxWithPrefix("#{@ajax_url}/save_answer",settings) - else @errors_area.html(@out_of_sync_message) @@ -492,6 +514,7 @@ class @CombinedOpenEnded @coe.after(response.html).remove() @allow_reset="False" @reinitialize(@element) + @has_been_reset = true @rebind() @reset_button.hide() else @@ -523,11 +546,11 @@ class @CombinedOpenEnded @errors_area.html(@out_of_sync_message) gentle_alert: (msg) => - if @$el.find('.open-ended-alert').length - @$el.find('.open-ended-alert').remove() + if @$el.find(@oe_alert_sel).length + @$el.find(@oe_alert_sel).remove() alert_elem = "
    " + msg + "
    " @$el.find('.open-ended-action').after(alert_elem) - @$el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) + @$el.find(@oe_alert_sel).css(opacity: 0).animate(opacity: 1, 700) queueing: => if @child_state=="assessing" and @child_type=="openended" From b6a58085484216ab810f9c2095b7cdd699ff63f3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 17:50:36 -0400 Subject: [PATCH 062/179] Show submit button correctly for self assessment --- .../xmodule/js/src/combinedopenended/display.coffee | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a1792a3a3d..fc1e2243bc 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -273,8 +273,6 @@ class @CombinedOpenEnded else if @allow_reset=="True" @reset_button.show() @gentle_alert "You may reset and answer this question again." - else - @gentle_alert "You have answered this question." else @submit_button.show() @answer_area.attr("disabled", false) @@ -362,6 +360,8 @@ class @CombinedOpenEnded @queueing() @grader_status = @$(@grader_status_sel) @grader_status.html("Your response has been submitted. Please check back later for your grade. ") + else if @child_type == "selfassessment" + @setup_score_selection() else if @child_state == 'post_assessment' if @child_type=="openended" @skip_button.show() @@ -691,3 +691,10 @@ class @CombinedOpenEnded info_rubric_elements = @$(@info_rubric_elements_sel) info_rubric_elements.slideToggle() return false + + setup_score_selection: () => + @$("input[class='score-selection']").change @graded_callback + + graded_callback: () => + if @rub.check_complete() + @submit_button.show() From a49da5d530ccd0e61ac17358362ad9b918908723 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 18:55:42 -0400 Subject: [PATCH 063/179] Fix rubrics, strip out get results --- .../js/src/combinedopenended/display.coffee | 94 ++++--------------- .../combined_open_ended_modulev1.py | 57 ++--------- 2 files changed, 28 insertions(+), 123 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index fc1e2243bc..8be83acd23 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -15,7 +15,6 @@ class @Rubric # display the 'current' carat @categories = @$(@rubric_category_sel) @category = @$(@categories.first()) - @category.prepend('> ') @category_index = 0 # locally scoped jquery. @@ -46,12 +45,8 @@ class @Rubric return inputs.filter("input[value=#{selected}]").click() - # move to the next category - old_category_text = @category.html().substring(5) - @category.html(old_category_text) @category_index++ @category = @$(@categories[@category_index]) - @category.prepend('> ') tracking_callback: (event) => target_selection = $(event.target).val() @@ -98,7 +93,6 @@ class @CombinedOpenEnded coe_sel: 'section.combined-open-ended' reset_button_sel: '.reset-button' next_step_sel: '.next-step-button' - status_container_sel: '.status-elements' show_results_sel: '.show-results-button' question_header_sel: '.question-header' submit_evaluation_sel: '.submit-evaluation-button' @@ -137,15 +131,18 @@ class @CombinedOpenEnded $: (selector) -> $(selector, @el) - reinitialize: (element) -> + reinitialize: () -> @has_been_reset = false @wrapper=@$(@wrapper_sel) @coe = @$(@coe_sel) + @ajax_url = @coe.data('ajax-url') + @get_html() + @coe = @$(@coe_sel) + #Get data from combinedopenended @allow_reset = @coe.data('allow_reset') @id = @coe.data('id') - @ajax_url = @coe.data('ajax-url') @state = @coe.data('state') @task_count = @coe.data('task-count') @task_number = @coe.data('task-number') @@ -163,8 +160,6 @@ class @CombinedOpenEnded @next_problem_button = @$(@next_step_sel) @next_problem_button.click @next_problem - @status_container = @$(@status_container_sel) - #setup show results @show_results_button=@$(@show_results_sel) @show_results_button.click @show_results @@ -183,6 +178,7 @@ class @CombinedOpenEnded # Where to put the rubric once we load it @oe = @$(@open_ended_child_sel) + @errors_area = @$(@oe).find(@error_sel) @answer_area = @$(@oe).find(@answer_area_sel) @prompt_container = @$(@oe).find(@prompt_sel) @@ -212,33 +208,18 @@ class @CombinedOpenEnded @rebind() - show_results_current: () => - data = {'task_number' : @task_number-1} - $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => - if response.success - if (results_container?) - @results_container.after(response.html).remove() - @results_container = @$(@result_container_sel) - @submit_evaluation_button = @$(@submit_evaluation_sel) - @submit_evaluation_button.click @message_post - Collapsible.setCollapsibles(@results_container) - # make sure we still have click tracking - $('.evaluation-response a').click @log_feedback_click - $('input[name="evaluation-score"]').change @log_feedback_selection + get_html_callback: (response) => + @coe.replaceWith(response.html) - show_results: (event) => - status_item = $(event.target).parent() - status_number = status_item.data('status-number') - data = {'task_number' : status_number} - $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => - if response.success - @results_container.after(response.html).remove() - @results_container = @$(@result_container_sel) - @submit_evaluation_button = @$(@submit_evaluation_sel) - @submit_evaluation_button.click @message_post - Collapsible.setCollapsibles(@results_container) - else - @gentle_alert response.error + get_html: () => + url = "#{@ajax_url}/get_html" + $.ajaxWithPrefix({ + type: 'POST', + url: url, + data: {}, + success: @get_html_callback, + async:false + }); show_combined_rubric_current: () => data = {} @@ -255,36 +236,6 @@ class @CombinedOpenEnded if response.hide_reset @reset_button.hide() - get_last_response: () => - @submit_button.hide() - @answer_area.attr("disabled", true) - data = {} - $.postWithPrefix "#{@ajax_url}/get_last_response", data, (response) => - if response.success && response.response != "" - @answer_area.html(response.response) - if response.state!='initial' - @submit_button.hide() - @answer_area.attr("disabled", true) - if @has_been_reset - @submit_button.show() - @answer_area.attr("disabled", false) - @gentle_alert "Here is your previous answer to this qu - estion." - else if @allow_reset=="True" - @reset_button.show() - @gentle_alert "You may reset and answer this question again." - else - @submit_button.show() - @answer_area.attr("disabled", false) - @$(@oe_alert_sel).animate(opacity: 0, 700) - - show_status_current: () => - data = {} - $.postWithPrefix "#{@ajax_url}/get_status", data, (response) => - if response.success - @status_container.after(response.html).remove() - @status_container= $(@status_container_sel) - message_post: (event)=> external_grader_message=$(event.target).parent().parent().parent() evaluation_scoring = $(event.target).parent() @@ -319,7 +270,6 @@ class @CombinedOpenEnded rebind: () => - @get_last_response() # rebind to the appropriate function for the current state @submit_button.unbind('click') @submit_button.show() @@ -328,9 +278,6 @@ class @CombinedOpenEnded @next_problem_button.hide() @hint_area.attr('disabled', false) - if @task_number>1 or @child_state!='initial' - @show_status_current() - if @task_number==1 and @child_state=='assessing' @prompt_hide() if @child_state == 'done' @@ -338,7 +285,7 @@ class @CombinedOpenEnded if @child_type=="openended" @skip_button.hide() if @allow_reset=="True" - @show_results_current + @show_combined_rubric_current() @reset_button.show() @submit_button.hide() @answer_area.attr("disabled", true) @@ -375,7 +322,6 @@ class @CombinedOpenEnded @submit_button.click @message_post else if @child_state == 'done' @show_combined_rubric_current() - @show_results_current() @rubric_wrapper.hide() @answer_area.attr("disabled", true) @replace_text_inputs() @@ -539,7 +485,7 @@ class @CombinedOpenEnded @gentle_alert "Moved to next step." else @gentle_alert "Your score did not meet the criteria to move to the next step." - @show_results_current() + @show_combined_rubric_current() else @errors_area.html(response.error) else @@ -591,7 +537,7 @@ class @CombinedOpenEnded # wrap this so that it can be mocked reload: -> - location.reload() + @reinitialize() collapse_question: (event) => @prompt_container.slideToggle() 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 0294a022b9..7e95e27089 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 @@ -67,7 +67,6 @@ class CombinedOpenEndedV1Module(): ajax actions implemented by combined open ended module are: 'reset' -- resets the whole combined open ended module and returns to the first child moduleresource_string 'next_problem' -- moves to the next child module - 'get_results' -- gets results from a given child module Types of children. Task is synonymous with child module, so each combined open ended module incorporates multiple children (tasks): @@ -351,6 +350,9 @@ class CombinedOpenEndedV1Module(): self.update_task_states() return self.current_task.get_html(self.system) + def get_html_ajax(self, data): + return {'html' : self.get_html()} + def get_current_attributes(self, task_number): """ Gets the min and max score to attempt attributes of the specified task. @@ -592,53 +594,6 @@ class CombinedOpenEndedV1Module(): html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} - def get_results(self, _data): - """ - Gets the results of a given grader via ajax. - Input: AJAX data dictionary - Output: Dictionary to be rendered via ajax that contains the result html. - """ - self.update_task_states() - success, can_see_rubric, error = self.check_if_student_has_done_needed_grading() - if not can_see_rubric: - return {'html' : error, 'success' : False} - loop_up_to_task = self.current_task_number + 1 - all_responses = [] - for i in xrange(0, loop_up_to_task): - all_responses.append(self.get_last_response(i)) - context_list = [] - for ri in all_responses: - for i in xrange(0, len(ri['rubric_scores'])): - feedback = ri['feedback_dicts'][i].get('feedback', '') - rubric_data = self.rubric_renderer.render_rubric(stringify_children(self.static_data['rubric']), - ri['rubric_scores'][i]) - if rubric_data['success']: - rubric_html = rubric_data['html'] - else: - rubric_html = '' - context = { - 'rubric_html': rubric_html, - 'grader_type': ri['grader_type'], - 'feedback': feedback, - 'grader_id': ri['grader_ids'][i], - 'submission_id': ri['submission_ids'][i], - } - context_list.append(context) - feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), { - 'context_list': context_list, - 'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT, - 'human_grader_types': HUMAN_GRADER_TYPE, - 'rows': 50, - 'cols': 50, - }) - context = { - 'results': feedback_table, - 'task_name': "Feedback", - 'class_name': "result-container", - } - html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) - return {'html': html, 'success': True} - def get_status_ajax(self, _data): """ Gets the results of a given grader via ajax. @@ -662,11 +617,12 @@ class CombinedOpenEndedV1Module(): handlers = { 'next_problem': self.next_problem, 'reset': self.reset, - 'get_results': self.get_results, 'get_combined_rubric': self.get_rubric, 'get_status': self.get_status_ajax, 'get_legend': self.get_legend, 'get_last_response': self.get_last_response_ajax, + 'get_current_state': self.get_current_state, + 'get_html': self.get_html_ajax, } if dispatch not in handlers: @@ -676,6 +632,9 @@ class CombinedOpenEndedV1Module(): d = handlers[dispatch](data) return json.dumps(d, cls=ComplexEncoder) + def get_current_state(self, data): + return self.get_context() + def get_last_response_ajax(self,data): return self.get_last_response(self.current_task_number) From b97dbc37a6d10e43bc48efe4694f202c4c648834 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 18:57:32 -0400 Subject: [PATCH 064/179] Fix test, strip out more of show results --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 5 ----- common/lib/xmodule/xmodule/tests/test_combined_open_ended.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 8be83acd23..ec9ddb1329 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -93,7 +93,6 @@ class @CombinedOpenEnded coe_sel: 'section.combined-open-ended' reset_button_sel: '.reset-button' next_step_sel: '.next-step-button' - show_results_sel: '.show-results-button' question_header_sel: '.question-header' submit_evaluation_sel: '.submit-evaluation-button' result_container_sel: 'div.result-container' @@ -160,10 +159,6 @@ class @CombinedOpenEnded @next_problem_button = @$(@next_step_sel) @next_problem_button.click @next_problem - #setup show results - @show_results_button=@$(@show_results_sel) - @show_results_button.click @show_results - @question_header = @$(@question_header_sel) @question_header.click @collapse_question diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index 05e8df0ad8..83f6dc6833 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -636,7 +636,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): self.assertTrue(isinstance(legend, basestring)) #Get all results - module.handle_ajax("get_results", {}) + module.handle_ajax("get_combined_rubric", {}) #reset the problem module.handle_ajax("reset", {}) From 8218a4a220bbdaae1eaddafdca08557166be11c6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 19:09:09 -0400 Subject: [PATCH 065/179] Show the right rubric, be more explicit about "next step" --- .../js/src/combinedopenended/display.coffee | 2 + .../combined_open_ended_modulev1.py | 44 +++++++------------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index ec9ddb1329..2fe8979652 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -286,6 +286,8 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @replace_text_inputs() @hint_area.attr('disabled', true) + if @task_number<@task_count + @gentle_alert "Your score did not meet the criteria to move to the next step." else if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') 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 7e95e27089..bdcbc7054c 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 @@ -556,25 +556,23 @@ class CombinedOpenEndedV1Module(): if not can_see_rubric: return {'html' : self.system.render_template('{0}/combined_open_ended_hidden_results.html'.format(self.TEMPLATE_DIR), {'error' : error}), 'success' : True, 'hide_reset' : True} - loop_up_to_task = self.current_task_number + 1 contexts = [] - for i in xrange(0, loop_up_to_task): - response = self.get_last_response(i) - score_length = len(response['grader_types']) - for z in xrange(0,score_length): - feedback = response['feedback_dicts'][z].get('feedback', '') - if response['grader_types'][z] in HUMAN_GRADER_TYPE.keys(): - rubric_scores = [[response['rubric_scores'][z]]] - grader_types = [[response['grader_types'][z]]] - feedback_items = [[response['feedback_items'][z]]] - rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), - rubric_scores, - grader_types, feedback_items) - contexts.append({ - 'result': rubric_html, - 'task_name': 'Scored rubric', - 'feedback' : feedback - }) + response = self.get_last_response(self.current_task_number + 1) + score_length = len(response['grader_types']) + for z in xrange(0,score_length): + feedback = response['feedback_dicts'][z].get('feedback', '') + if response['grader_types'][z] in HUMAN_GRADER_TYPE.keys(): + rubric_scores = [[response['rubric_scores'][z]]] + grader_types = [[response['grader_types'][z]]] + feedback_items = [[response['feedback_items'][z]]] + rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), + rubric_scores, + grader_types, feedback_items) + contexts.append({ + 'result': rubric_html, + 'task_name': 'Scored rubric', + 'feedback' : feedback + }) context = { 'results': contexts, @@ -594,15 +592,6 @@ class CombinedOpenEndedV1Module(): html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} - def get_status_ajax(self, _data): - """ - Gets the results of a given grader via ajax. - Input: AJAX data dictionary - Output: Dictionary to be rendered via ajax that contains the result html. - """ - html = self.get_status(True) - return {'html': html, 'success': True} - def handle_ajax(self, dispatch, data): """ This is called by courseware.module_render, to handle an AJAX call. @@ -618,7 +607,6 @@ class CombinedOpenEndedV1Module(): 'next_problem': self.next_problem, 'reset': self.reset, 'get_combined_rubric': self.get_rubric, - 'get_status': self.get_status_ajax, 'get_legend': self.get_legend, 'get_last_response': self.get_last_response_ajax, 'get_current_state': self.get_current_state, From 7b9dde78fec860dfd26195aae5c6391e0d800406 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Aug 2013 19:21:28 -0400 Subject: [PATCH 066/179] Fetch the correct rubric --- .../combined_open_ended_modulev1.py | 5 ++++- 1 file changed, 4 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 bdcbc7054c..23fa9c28c8 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 @@ -557,7 +557,10 @@ class CombinedOpenEndedV1Module(): return {'html' : self.system.render_template('{0}/combined_open_ended_hidden_results.html'.format(self.TEMPLATE_DIR), {'error' : error}), 'success' : True, 'hide_reset' : True} contexts = [] - response = self.get_last_response(self.current_task_number + 1) + rubric_number = self.current_task_number + if self.ready_to_reset: + rubric_number+=1 + response = self.get_last_response(rubric_number) score_length = len(response['grader_types']) for z in xrange(0,score_length): feedback = response['feedback_dicts'][z].get('feedback', '') From be3291eeb075e574de689c95814aa503b47b79f4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 13:22:34 -0400 Subject: [PATCH 067/179] Proper scoping, show status to staff, fix line break issues, multiple self assessments on one page work --- .../js/src/combinedopenended/display.coffee | 15 +++++++++------ .../open_ended_module.py | 2 +- .../self_assessment_module.py | 1 + .../coffee/src/staff_grading/staff_grading.coffee | 11 ++++++++++- .../openended/open_ended_rubric.html | 6 ++++-- lms/templates/instructor/staff_grading.html | 4 ++-- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 2fe8979652..e846717030 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -23,7 +23,7 @@ class @Rubric keypress_callback: (event) => # don't try to do this when user is typing in a text input - if $(event.target).is('input, textarea') + if @$(event.target).is('input, textarea') return # for when we select via top row if event.which >= 48 and event.which <= 57 @@ -49,9 +49,9 @@ class @Rubric @category = @$(@categories[@category_index]) tracking_callback: (event) => - target_selection = $(event.target).val() + target_selection = @$(event.target).val() # chop off the beginning of the name so that we can get the number of the category - category = $(event.target).data("category") + category = @$(event.target).data("category") location = @$(@rubric_sel).data('location') # probably want the original problem location as well @@ -187,9 +187,9 @@ class @CombinedOpenEnded @skip_button = @$(@oe).find(@skip_button_sel) @skip_button.click @skip_post_assessment - @file_upload_area = $(@oe).find(@file_upload_sel) + @file_upload_area = @$(@oe).find(@file_upload_sel) @can_upload_files = false - @open_ended_child= $(@oe).find(@open_ended_child_sel) + @open_ended_child= @$(@oe).find(@open_ended_child_sel) @out_of_sync_message = 'The problem state got out of sync. Try reloading the page.' @@ -257,7 +257,7 @@ class @CombinedOpenEnded contentType: false success: (response) => @gentle_alert response.msg - $('section.evaluation').slideToggle() + @$('section.evaluation').slideToggle() @message_wrapper.html(response.message_html) @@ -299,6 +299,7 @@ class @CombinedOpenEnded @hide_file_upload() @submit_button.prop('value', 'Submit assessment') @submit_button.click @save_assessment + @submit_button.attr("disabled",false) if @child_type == "openended" @submit_button.hide() @queueing() @@ -339,6 +340,7 @@ class @CombinedOpenEnded @hint_area = @$('textarea.post_assessment') save_answer: (event) => + @submit_button.attr("disabled",true) event.preventDefault() @submit_button.hide() @answer_area.attr("disabled", true) @@ -640,4 +642,5 @@ class @CombinedOpenEnded graded_callback: () => if @rub.check_complete() + @submit_button.attr("disabled",false) @submit_button.show() diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 04eff33159..dc2c3fb01b 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -699,7 +699,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): post_assessment = "" correct = "" previous_answer = "" - + previous_answer = previous_answer.replace("\n","
    ") context = { 'prompt': self.child_prompt, 'previous_answer': previous_answer, diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index 674fab0d30..5c40aca116 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -61,6 +61,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): else: previous_answer = '' + previous_answer = previous_answer.replace("\n","
    ") context = { 'prompt': self.child_prompt, 'previous_answer': previous_answer, diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index e7e7c189ac..e8cee60001 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -146,6 +146,8 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t class @StaffGrading + grading_message_sel: '.grading-message' + constructor: (backend) -> AjaxPrefix.addAjaxPrefix(jQuery, -> "") @backend = backend @@ -278,6 +280,7 @@ class @StaffGrading location: @location skipped: true submission_flagged: false + @gentle_alert "Skipped the submission." @backend.post('save_grade', data, @ajax_callback) get_problem_list: () -> @@ -292,9 +295,15 @@ class @StaffGrading submission_id: @submission_id location: @location submission_flagged: @flag_submission_checkbox.is(':checked') - + @gentle_alert "Grades saved. Fetching the next submission to grade." @backend.post('save_grade', data, @ajax_callback) + gentle_alert: (msg) => + @grading_message = $(@grading_message_sel) + @grading_message.html("") + @grading_message.fadeIn() + @grading_message.html("

    " + msg + "

    ") + error: (msg) -> @error_msg = msg @state = state_error diff --git a/lms/templates/combinedopenended/openended/open_ended_rubric.html b/lms/templates/combinedopenended/openended/open_ended_rubric.html index ea374c5173..55194a9318 100644 --- a/lms/templates/combinedopenended/openended/open_ended_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_rubric.html @@ -1,4 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> +<% from random import randint %>
    @@ -9,6 +10,7 @@
    % for i in range(len(categories)): <% category = categories[i] %> + <% m = randint(0,1000) %> ${category['description']}
      % for j in range(len(category['options'])): @@ -18,8 +20,8 @@ %else:
    • % endif -
    • diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index 8595b0d722..bb60bddeb0 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -80,8 +80,8 @@
    - -
    +
    +
    \ No newline at end of file From 4f3ec68fb744565af929da3eae5f9dc2c5262bf7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 14:12:37 -0400 Subject: [PATCH 068/179] Scroll to top in peer and staff grading, preserve line breaks in submissions --- .../js/src/combinedopenended/display.coffee | 30 +++++++++++-------- .../peergrading/peer_grading_problem.coffee | 8 ++++- .../open_ended_module.py | 2 +- .../openendedchild.py | 3 +- .../self_assessment_module.py | 2 +- .../src/staff_grading/staff_grading.coffee | 8 ++++- lms/templates/instructor/staff_grading.html | 6 ++-- lms/templates/peer_grading/peer_grading.html | 2 +- .../peer_grading/peer_grading_problem.html | 4 +-- 9 files changed, 43 insertions(+), 22 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index e846717030..b4f72a3e1d 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -100,6 +100,7 @@ class @CombinedOpenEnded open_ended_child_sel: 'section.open-ended-child' error_sel: '.error' answer_area_sel: 'textarea.answer' + answer_area_div_sel : 'div.answer' prompt_sel: '.prompt' rubric_wrapper_sel: '.rubric-wrapper' hint_wrapper_sel: '.hint-wrapper' @@ -339,6 +340,21 @@ class @CombinedOpenEnded find_hint_elements: -> @hint_area = @$('textarea.post_assessment') + replace_answer: (response) => + if response.success + @rubric_wrapper.html(response.rubric_html) + @rubric_wrapper.show() + @rub = new Rubric(@coe) + @rub.initialize(@location) + @child_state = 'assessing' + @find_assessment_elements() + @rebind() + answer_area_div = @$(@answer_area_div_sel) + answer_area_div.html(response.student_response) + else + @can_upload_files = pre_can_upload_files + @gentle_alert response.error + save_answer: (event) => @submit_button.attr("disabled",true) event.preventDefault() @@ -367,19 +383,9 @@ class @CombinedOpenEnded data: fd processData: false contentType: false + async: false success: (response) => - if response.success - @rubric_wrapper.html(response.rubric_html) - @rubric_wrapper.show() - @rub = new Rubric(@coe) - @rub.initialize(@location) - @answer_area.html(response.student_response) - @child_state = 'assessing' - @find_assessment_elements() - @rebind() - else - @can_upload_files = pre_can_upload_files - @gentle_alert response.error + @replace_answer(response) $.ajaxWithPrefix("#{@ajax_url}/save_answer",settings) else diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index a026cafd61..f96704037e 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -450,7 +450,7 @@ class @PeerGradingProblem @submit_button.unbind('click') @submit_button.click @submit_calibration_essay - + @scroll_to_top() else if response.error @render_error(response.error) else @@ -479,6 +479,7 @@ class @PeerGradingProblem @submit_button.unbind('click') @submit_button.click @submit_grade + @scroll_to_top() else if response.error @render_error(response.error) else @@ -573,3 +574,8 @@ class @PeerGradingProblem Logger.log 'peer_grading_show_question', {location: @location} new_text = "(Hide)" @question_header.text(new_text) + + scroll_to_top: () => + $('html, body').animate({ + scrollTop: $(".peer-grading").offset().top + }, 200) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index dc2c3fb01b..800789b56a 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -663,7 +663,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return { 'success': success, 'error': error_message, - 'student_response': data['student_answer'] + 'student_response': data['student_answer'].replace("\n","
    ") } def update_score(self, data, system): 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 3e12b4656d..d99e466886 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -179,9 +179,10 @@ class OpenEndedChild(object): answer = autolink_html(answer) cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True, host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS, - whitelist_tags=set(['embed', 'iframe', 'a', 'img'])) + whitelist_tags=set(['embed', 'iframe', 'a', 'img', 'br'])) clean_html = cleaner.clean_html(answer) clean_html = re.sub(r'

    $', '', re.sub(r'^

    ', '', clean_html)) + clean_html = re.sub("\n","
    ", clean_html) except Exception: clean_html = answer return clean_html diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index 5c40aca116..2485fc77ea 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -196,7 +196,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'success': success, 'rubric_html': self.get_rubric_html(system), 'error': error_message, - 'student_response': data['student_answer'], + 'student_response': data['student_answer'].replace("\n","
    ") } def save_assessment(self, data, _system): diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index e8cee60001..31c084ffd0 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -253,7 +253,7 @@ class @StaffGrading # always clear out errors and messages on transition. @error_msg = '' @message = '' - + if response.success if response.problem_list @problems = response.problem_list @@ -265,6 +265,7 @@ class @StaffGrading @error(response.error) @render_view() + @scroll_to_top() get_next_submission: (location) -> @location = location @@ -474,6 +475,11 @@ class @StaffGrading new_text = "(Hide)" @question_header.text(new_text) + scroll_to_top: () => + $('html, body').animate({ + scrollTop: $(".staff-grading").offset().top + }, 200) + # for now, just create an instance and load it... diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index bb60bddeb0..15ea97332a 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -38,6 +38,7 @@

    +

    @@ -80,8 +81,9 @@
    -
    - \ No newline at end of file + + + diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 162c9273b0..b0dffe8f9d 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -9,7 +9,7 @@ % if success: % if len(problem_list) == 0:
    - ${_("You currently do not having any peer grading to do. In order to have peer grading to do, you need to have submitted a response to a peer grading problem. The instructor also needs to score the essays that are used in the 'learning to grade' process.")} + ${_("You currently do not having any peer grading to do. In order to have peer grading to do, you need to have submitted a response to a peer grading problem. The instructor also needs to score the essays that are used to help you better understand the grading criteria.")}
    %else:
    diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index a0078b8868..b945e030cb 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -27,6 +27,8 @@
    +
    +

    ${_("Student Response")}

    @@ -53,8 +55,6 @@
    -
    -
    From bc8244b70950e651088e083fc45a06df14cc6f24 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 14:43:37 -0400 Subject: [PATCH 069/179] Rename reset to try again, hide and disable submit buttons --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 +++- .../xmodule/js/src/peergrading/peer_grading_problem.coffee | 6 +++++- lms/templates/combinedopenended/combined_open_ended.html | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b4f72a3e1d..7065a54d55 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -357,8 +357,8 @@ class @CombinedOpenEnded save_answer: (event) => @submit_button.attr("disabled",true) - event.preventDefault() @submit_button.hide() + event.preventDefault() @answer_area.attr("disabled", true) max_filesize = 2*1000*1000 #2MB pre_can_upload_files = @can_upload_files @@ -404,6 +404,8 @@ class @CombinedOpenEnded @is_ctrl=false save_assessment: (event) => + @submit_button.attr("disabled",true) + @submit_button.hide() event.preventDefault() if @child_state == 'assessing' && @rub.check_complete() checked_assessment = @rub.get_total_score() diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index f96704037e..bdb02c44b9 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -322,10 +322,12 @@ class @PeerGradingProblem submit_calibration_essay: ()=> data = @construct_data() + @submit_button.hide() @backend.post('save_calibration_essay', data, @calibration_callback) submit_grade: () => data = @construct_data() + @submit_button.hide() @backend.post('save_grade', data, @submission_callback) @@ -448,6 +450,7 @@ class @PeerGradingProblem @feedback_area.val("") + @submit_button.show() @submit_button.unbind('click') @submit_button.click @submit_calibration_essay @scroll_to_top() @@ -476,7 +479,8 @@ class @PeerGradingProblem @flag_student_container.show() @answer_unknown_container.show() @feedback_area.val("") - + + @submit_button.show() @submit_button.unbind('click') @submit_button.click @submit_grade @scroll_to_top() diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 65b44b0aa1..5f088265a2 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -39,7 +39,7 @@
    - +
    From 8248c331386708100c94247387729fb0a45a3f74 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 14:48:11 -0400 Subject: [PATCH 070/179] Grab the correct location to report data for --- common/lib/xmodule/xmodule/peer_grading_module.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index aa742f6a0b..e8409948e9 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -188,9 +188,8 @@ class PeerGradingModule(PeerGradingFields, XModule): return json.dumps(d, cls=ComplexEncoder) - def query_data_for_location(self): + def query_data_for_location(self, location): student_id = self.system.anonymous_student_id - location = self.link_to_location success = False response = {} @@ -322,7 +321,7 @@ class PeerGradingModule(PeerGradingFields, XModule): try: response = self.peer_gs.save_grade(**data_dict) - success, location_data = self.query_data_for_location() + success, location_data = self.query_data_for_location(data_dict['location']) response.update({'required_done' : False}) if 'count_graded' in location_data and 'count_required' in location_data and int(location_data['count_graded'])>=int(location_data['count_required']): response['required_done'] = True From 48b0e570bc131e60647b543d7965061dc9dd3b5e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 15:10:55 -0400 Subject: [PATCH 071/179] Disable submit buttons until rubric totally filled out --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- .../xmodule/js/src/peergrading/peer_grading_problem.coffee | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 7065a54d55..1732bfbe71 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -300,7 +300,7 @@ class @CombinedOpenEnded @hide_file_upload() @submit_button.prop('value', 'Submit assessment') @submit_button.click @save_assessment - @submit_button.attr("disabled",false) + @submit_button.attr("disabled",true) if @child_type == "openended" @submit_button.hide() @queueing() diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index bdb02c44b9..0cbccad548 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -453,6 +453,7 @@ class @PeerGradingProblem @submit_button.show() @submit_button.unbind('click') @submit_button.click @submit_calibration_essay + @submit_button.attr('disabled', true) @scroll_to_top() else if response.error @render_error(response.error) @@ -479,10 +480,11 @@ class @PeerGradingProblem @flag_student_container.show() @answer_unknown_container.show() @feedback_area.val("") - + @submit_button.show() @submit_button.unbind('click') @submit_button.click @submit_grade + @submit_button.attr('disabled', true) @scroll_to_top() else if response.error @render_error(response.error) @@ -558,6 +560,7 @@ class @PeerGradingProblem @action_button.show() show_submit_button: () => + @submit_button.attr('disabled', false) @submit_button.show() setup_score_selection: (max_score) => From 59336ee03caab6d398cd72eebc1666d466510ed1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 15:24:38 -0400 Subject: [PATCH 072/179] Fix prompt hiding issue in peer grading --- .../src/peergrading/peer_grading_problem.coffee | 15 +++++++-------- .../peer_grading/peer_grading_problem.html | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 0cbccad548..8771b4fe91 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -257,8 +257,6 @@ class @PeerGradingProblem $(window).keydown @keydown_handler $(window).keyup @keyup_handler - @collapse_question() - Collapsible.setCollapsibles(@content_panel) # Set up the click event handlers @@ -571,16 +569,17 @@ class @PeerGradingProblem @grading_message.fadeIn() @grading_message.html("

    " + msg + "

    ") - collapse_question: () => + collapse_question: (event) => @prompt_container.slideToggle() @prompt_container.toggleClass('open') - if @question_header.text() == "(Hide)" - Logger.log 'peer_grading_hide_question', {location: @location} - new_text = "(Show)" + if @question_header.text() == "Hide Prompt" + new_text = "Show Prompt" + Logger.log 'oe_hide_question', {location: @location} else - Logger.log 'peer_grading_show_question', {location: @location} - new_text = "(Hide)" + Logger.log 'oe_show_question', {location: @location} + new_text = "Hide Prompt" @question_header.text(new_text) + return false scroll_to_top: () => $('html, body').animate({ diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index b945e030cb..80b230e2aa 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -14,10 +14,10 @@
    -
    +
    - ${_('Hide Prompt')} + ${_('Hide Prompt')}
    From f17987c13ae7b2745376649baf304579af19dc3e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 16 Aug 2013 17:37:53 -0400 Subject: [PATCH 073/179] Fix flagging, select rubric labels in peer grading --- .../xmodule/css/combinedopenended/display.scss | 11 +++++++++++ .../js/src/peergrading/peer_grading_problem.coffee | 9 ++++++++- common/lib/xmodule/xmodule/peer_grading_module.py | 9 ++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 120ea0c52d..551d1c181f 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -234,6 +234,9 @@ section.combined-open-ended-status { &:hover { background-color: #eee; } + .rubric-label-selected{ + border: 2px solid #666; + } } span.rubric-category { @@ -952,3 +955,11 @@ section.open-ended-child { } } } + +section.peer-grading-container{ + div.peer-grading{ + section.calibration-feedback { + padding: 20px; + } + } +} diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index 8771b4fe91..4151bda33d 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -338,13 +338,15 @@ class @PeerGradingProblem remove_flag: () => @flag_student_checkbox.removeAttr("checked") @close_dialog_box() + @submit_button.attr('disabled', true) close_dialog_box: () => - @$(@flag_submission_confirmation_sel).dialog('close') + $(@flag_submission_confirmation_sel).dialog('close') flag_box_checked: () => if @flag_student_checkbox.is(':checked') @$(@flag_submission_confirmation_sel).dialog({ height: 400, width: 400 }) + @submit_button.attr('disabled', false) # called after we perform an is_student_calibrated check calibration_check_callback: (response) => @@ -397,6 +399,10 @@ class @PeerGradingProblem # called after a grade is selected on the interface graded_callback: (event) => + ev = @$(event.target).parent().parent() + ul = ev.parent().parent() + ul.find(".rubric-label-selected").removeClass('rubric-label-selected') + ev.addClass('rubric-label-selected') # check to see whether or not any categories have not been scored if @rub.check_complete() # show button if we have scores for all categories @@ -479,6 +485,7 @@ class @PeerGradingProblem @answer_unknown_container.show() @feedback_area.val("") + @flag_student_checkbox.removeAttr("checked") @submit_button.show() @submit_button.unbind('click') @submit_button.click @submit_grade diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index e8409948e9..d60f448d3b 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -310,13 +310,16 @@ class PeerGradingModule(PeerGradingFields, XModule): error: if there was an error in the submission, this is the error message """ - required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged', 'answer_unknown']) - success, message = self._check_required(data, required) + required = ['location', 'submission_id', 'submission_key', 'score', 'feedback', 'submission_flagged', 'answer_unknown'] + if 'submission_flagged' not in data or data['submission_flagged'] in ["false", False, "False"]: + required.append("rubric_scores[]") + success, message = self._check_required(data, set(required)) if not success: return self._err_response(message) data_dict = {k:data.get(k) for k in required} - data_dict['rubric_scores'] = data.getlist('rubric_scores[]') + if 'rubric_scores[]' in required: + data_dict['rubric_scores'] = data.getlist('rubric_scores[]') data_dict['grader_id'] = self.system.anonymous_student_id try: From 06977c198ebf3cbd3cc8d24cbc243ace635c5d01 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 19 Aug 2013 08:47:53 -0400 Subject: [PATCH 074/179] Created new test configuration for MixedModuleStore --- .../xmodule/modulestore/tests/django_utils.py | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 4f998d57fb..8b8d61c85a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -1,4 +1,3 @@ - import copy from uuid import uuid4 from django.test import TestCase @@ -8,6 +7,41 @@ import xmodule.modulestore.django from unittest.util import safe_repr +def mixed_store_config(data_dir, mappings): + """ + Return a `MixedModuleStore` configuration, which provides + access to both Mongo- and XML-backed courses. + + `data_dir` is the directory from which to load XML-backed courses. + `mappings` is a dictionary mapping course IDs to modulestores, for example: + + { + 'MITx/2.01x/2013_Spring': 'xml', + 'edx/999/2013_Spring': 'default' + } + + where 'xml' and 'default' are the two options provided by this configuration, + mapping (respectively) to XML-backed and Mongo-backed modulestores.. + """ + mongo_config = mongo_store_config(data_dir) + xml_config = xml_store_config(data_dir) + + store = { + 'default': { + 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', + 'OPTIONS': { + 'mappings': mappings, + 'stores': { + 'default': mongo_config['default'], + 'xml': xml_config['default'] + } + } + } + } + store['direct'] = store['default'] + return store + + def mongo_store_config(data_dir): """ Defines default module store using MongoModuleStore. @@ -27,6 +61,7 @@ def mongo_store_config(data_dir): } } } + store['direct'] = store['default'] return store @@ -45,23 +80,22 @@ def draft_mongo_store_config(data_dir): 'render_template': 'mitxmako.shortcuts.render_to_string' } - return { + store = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore', 'OPTIONS': modulestore_options - }, - 'direct': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options } } + store['direct'] = store['default'] + return store + def xml_store_config(data_dir): """ Defines default module store using XMLModuleStore. """ - return { + store = { 'default': { 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'OPTIONS': { @@ -71,6 +105,9 @@ def xml_store_config(data_dir): } } + store['direct'] = store['default'] + return store + class ModuleStoreTestCase(TestCase): """ Subclass for any test case that uses the mongodb From 05b34098abfce0c3c1e0d9d2bbacd7ca623a148e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 19 Aug 2013 09:27:36 -0400 Subject: [PATCH 075/179] Refactored ModuleStoreTestCase to use modulestore interface for clearing _MODULESTORES --- .../lib/xmodule/xmodule/modulestore/django.py | 9 +++++++++ .../xmodule/modulestore/tests/django_utils.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index 2f0cd126f9..cd0166e4b0 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -53,3 +53,12 @@ def modulestore(name='default'): settings.MODULESTORE[name]['OPTIONS']) return _MODULESTORES[name] + +def clear_existing_modulestores(): + """ + Clear the existing modulestore instances, causing + them to be re-created when accessed again. + + This is useful for flushing state between unit tests. + """ + _MODULESTORES.clear() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 8b8d61c85a..a38cab79f6 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -3,7 +3,7 @@ from uuid import uuid4 from django.test import TestCase from django.conf import settings -import xmodule.modulestore.django +from xmodule.modulestore.django import modulestore, clear_existing_modulestores from unittest.util import safe_repr @@ -126,7 +126,7 @@ class ModuleStoreTestCase(TestCase): 'data' is a dictionary with an entry for each CourseField we want to update. """ - store = xmodule.modulestore.django.modulestore() + store = modulestore() store.update_metadata(course.location, data) updated_course = store.get_instance(course.id, course.location) return updated_course @@ -136,15 +136,15 @@ class ModuleStoreTestCase(TestCase): """ Delete everything in the module store except templates. """ - modulestore = xmodule.modulestore.django.modulestore() + store = modulestore() # This query means: every item in the collection # that is not a template query = {"_id.course": {"$ne": "templates"}} # Remove everything except templates - modulestore.collection.remove(query) - modulestore.collection.drop() + store.collection.remove(query) + store.collection.drop() @classmethod def setUpClass(cls): @@ -160,7 +160,7 @@ class ModuleStoreTestCase(TestCase): settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - xmodule.modulestore.django._MODULESTORES.clear() + clear_existing_modulestores() print settings.MODULESTORE @@ -173,10 +173,10 @@ class ModuleStoreTestCase(TestCase): """ # Clean up by dropping the collection - modulestore = xmodule.modulestore.django.modulestore() - modulestore.collection.drop() + store = modulestore() + store.collection.drop() - xmodule.modulestore.django._MODULESTORES.clear() + clear_existing_modulestores() # Restore the original modulestore settings settings.MODULESTORE = cls.orig_modulestore From bd65cfa813266a31b18c670c06c10f4928d85bd8 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 19 Aug 2013 09:35:30 -0400 Subject: [PATCH 076/179] Removed outdated template logic in ModuleStoreTestCase --- .../xmodule/modulestore/tests/django_utils.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index a38cab79f6..8e4655cf3f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -110,10 +110,11 @@ def xml_store_config(data_dir): class ModuleStoreTestCase(TestCase): - """ Subclass for any test case that uses the mongodb - module store. This populates a uniquely named modulestore - collection with templates before running the TestCase - and drops it they are finished. """ + """ + Subclass for any test case that uses a ModuleStore. + + Ensures that the ModuleStore is cleaned before/after each test. + """ @staticmethod def update_course(course, data): @@ -132,24 +133,19 @@ class ModuleStoreTestCase(TestCase): return updated_course @staticmethod - def flush_mongo_except_templates(): + def drop_mongo_collection(): """ - Delete everything in the module store except templates. + If using a Mongo-backed modulestore, drop the collection. """ store = modulestore() - # This query means: every item in the collection - # that is not a template - query = {"_id.course": {"$ne": "templates"}} - - # Remove everything except templates - store.collection.remove(query) - store.collection.drop() + if hasattr(store, 'collection'): + store.collection.drop() @classmethod def setUpClass(cls): """ - Flush the mongo store and set up templates. + Flush the ModuleStore. """ # Use a uuid to differentiate @@ -173,8 +169,7 @@ class ModuleStoreTestCase(TestCase): """ # Clean up by dropping the collection - store = modulestore() - store.collection.drop() + cls.drop_mongo_collection() clear_existing_modulestores() @@ -183,21 +178,20 @@ class ModuleStoreTestCase(TestCase): def _pre_setup(self): """ - Remove everything but the templates before each test. + Flush the ModuleStore before each test. """ - # Flush anything that is not a template - ModuleStoreTestCase.flush_mongo_except_templates() + # Flush the Mongo modulestore + ModuleStoreTestCase.drop_mongo_collection() # Call superclass implementation super(ModuleStoreTestCase, self)._pre_setup() def _post_teardown(self): """ - Flush everything we created except the templates. + Flush the ModuleStore after each test. """ - # Flush anything that is not a template - ModuleStoreTestCase.flush_mongo_except_templates() + ModuleStoreTestCase.drop_mongo_collection() # Call superclass implementation super(ModuleStoreTestCase, self)._post_teardown() From 39b646558ac1e77e106427b0bad7932790030eaf Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:15:26 -0400 Subject: [PATCH 077/179] pep8 in inheritance.py --- common/lib/xmodule/xmodule/modulestore/inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 2ad08b8350..aeec53cc29 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -10,7 +10,7 @@ INHERITABLE_METADATA = ( # elements. Can be a float. 'days_early_for_beta', 'giturl', # for git edit link - 'static_asset_path', # for static assets placed outside xcontent contentstore + 'static_asset_path', # for static assets placed outside xcontent contentstore ) From 13bb3bf0a99cd62ab0cd270e6c3a79ac9ab930a5 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:18:04 -0400 Subject: [PATCH 078/179] pylint --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 4d9b332012..0f672b86d2 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -80,7 +80,7 @@ def import_from_xml(store, data_dir, course_dirs=None, after import off disk. We do this remapping as a post-processing step because there's logic in the importing which expects a 'url_name' as an identifier to where things are on disk e.g. ../policies//policy.json as well as metadata keys in the policy.json. so we need to keep the original url_name during import - + do_import_static: if False, then static files are not imported into the static content store. This can be employed for courses which have substantial unchanging static content, which is to inefficient to import every time the course is loaded. Static content for some courses may also be served directly by nginx, instead of going through django. @@ -162,7 +162,7 @@ def import_from_xml(store, data_dir, course_dirs=None, log.debug('Skipping import of static content, since do_import_static={0}'.format(do_import_static)) # no matter what do_import_static is, import "static_import" directory - # + # This is needed because the "about" pages (eg "overview") are loaded via load_extra_content, and # do not inherit the lms metadata from the course module, and thus do not get "static_content_store" # properly defined. Static content referenced in those extra pages thus need to come through the From 2249692f9a7168e2b60be9fe65dfb1ca3d1058d8 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:19:14 -0400 Subject: [PATCH 079/179] pylint --- cms/djangoapps/contentstore/management/commands/import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 520e36f4d2..1d77e9cb54 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" From e9ef45076d900d98bcdb0f613ca6a2df01350789 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:21:10 -0400 Subject: [PATCH 080/179] pylint --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 0f672b86d2..1706de7ed4 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -54,7 +54,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ try: static_content_store.save(content) except Exception as err: - log.exception('Error importing {0}'.format(fullname_with_subpath)) + log.exception('Error importing {0}, error={1}'.format(fullname_with_subpath, err)) #store the remapping information which will be needed to subsitute in the module data remap_dict[fullname_with_subpath] = content_loc.name From d782278d5caba66b4903ab5529ec1567d997abcc Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 20 Aug 2013 01:33:06 -0400 Subject: [PATCH 081/179] minor cleanup of pull request comments within scss files, along with removal of some whitespace in text fixture file for rubrics --- common/lib/xmodule/xmodule/css/capa/display.scss | 2 +- .../xmodule/css/combinedopenended/display.scss | 11 ++++++----- common/lib/xmodule/xmodule/js/fixtures/rubric.html | 4 ---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 2fa3c315ef..fcc067c51d 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -914,7 +914,7 @@ section.problem { .tag { display: inline-block; - margin-left: $baseline; + margin-left: $baseline*2; border: 1px solid rgb(102,102,102); cursor: pointer; diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 551d1c181f..7ca99be5c4 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -129,7 +129,7 @@ section.combined-open-ended-status { } &:last-child { - border-right: 0px; + border-right: 0; border-radius: 0 $baseline/4 $baseline/4 0; } @@ -235,7 +235,8 @@ section.combined-open-ended-status { background-color: #eee; } .rubric-label-selected{ - border: 2px solid #666; + border-radius: $baseline/4; + background-color: #eee; } } @@ -365,7 +366,7 @@ div.result-container { font-family: monospace; font-size: 1em; padding-top: $baseline/2; - padding-bottom:30px; + padding-bottom: 30px; header { font-size: 1.4em; @@ -433,8 +434,8 @@ div.result-container { .rubric-result-container { padding: 2px; - margin: 0px; - display : inline; + margin: 0; + display: inline; .rubric-result { font-size: .9em; diff --git a/common/lib/xmodule/xmodule/js/fixtures/rubric.html b/common/lib/xmodule/xmodule/js/fixtures/rubric.html index bdb572d11b..76ad59b8ff 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/rubric.html +++ b/common/lib/xmodule/xmodule/js/fixtures/rubric.html @@ -19,8 +19,6 @@
    - -
    @@ -30,8 +28,6 @@
    - -
    From 210fa112f325a96d8a207b3538d994c1375e4c63 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 08:56:03 -0400 Subject: [PATCH 082/179] modify handling of info/handouts and module_render to honor static_asset_path --- lms/djangoapps/courseware/courses.py | 5 ++++- lms/djangoapps/courseware/module_render.py | 26 +++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index eb7d690116..b63828eba5 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -196,6 +196,8 @@ def get_course_info_section(request, course, section_key): loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) + log.debug("in get_course_info, static_asset_path=%s" % course.lms.static_asset_path) + # Use an empty cache model_data_cache = ModelDataCache([], course.id, request.user) info_module = get_module( @@ -204,7 +206,8 @@ def get_course_info_section(request, course, section_key): loc, model_data_cache, course.id, - wrap_xmodule_display=False + wrap_xmodule_display=False, + static_asset_path=course.lms.static_asset_path ) html = '' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ba4654ce9c..981ccde441 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -124,7 +124,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_ def get_module(user, request, location, model_data_cache, course_id, position=None, not_found_ok=False, wrap_xmodule_display=True, - grade_bucket_type=None, depth=0): + grade_bucket_type=None, depth=0, + static_asset_path=''): """ Get an instance of the xmodule class identified by location, setting the state based on an existing StudentModule, or creating one if none @@ -141,6 +142,10 @@ def get_module(user, request, location, model_data_cache, course_id, position within module - depth : number of levels of descendents to cache when loading this module. None means cache all descendents + - static_asset_path : static asset path to use (overrides descriptor's value); needed + by get_course_info_section, because info section modules + do not have a course as the parent module, and thus do not + inherit this lms key value. Returns: xmodule instance, or None if the user does not have access to the module. If there's an error, will try to return an instance of ErrorModule @@ -152,7 +157,8 @@ def get_module(user, request, location, model_data_cache, course_id, return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position=position, wrap_xmodule_display=wrap_xmodule_display, - grade_bucket_type=grade_bucket_type) + grade_bucket_type=grade_bucket_type, + static_asset_path=static_asset_path) except ItemNotFoundError: if not not_found_ok: log.exception("Error in get_module") @@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request): def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, - position=None, wrap_xmodule_display=True, grade_bucket_type=None): + position=None, wrap_xmodule_display=True, grade_bucket_type=None, + static_asset_path=''): """ Implements get_module, extracting out the request-specific functionality. @@ -194,12 +201,14 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, - position, wrap_xmodule_display, grade_bucket_type) + position, wrap_xmodule_display, grade_bucket_type, + static_asset_path) def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, - position=None, wrap_xmodule_display=True, grade_bucket_type=None): + position=None, wrap_xmodule_display=True, grade_bucket_type=None, + static_asset_path=''): """ Actually implement get_module, without requiring a request. @@ -282,7 +291,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, make_xqueue_callback, - position, wrap_xmodule_display, grade_bucket_type) + position, wrap_xmodule_display, grade_bucket_type, + static_asset_path) def xblock_model_data(descriptor): return DbModel( @@ -349,7 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, - static_asset_path=descriptor.lms.static_asset_path, + static_asset_path=static_asset_path or descriptor.lms.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, @@ -409,7 +419,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours _get_html, getattr(descriptor, 'data_dir', None), course_id=course_id, - static_asset_path=descriptor.lms.static_asset_path + static_asset_path=static_asset_path or descriptor.lms.static_asset_path ) # Allow URLs of the form '/course/' refer to the root of multicourse directory From 2fe4895e43a24e23995968fe2e673665893568ed Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 17:03:44 -0400 Subject: [PATCH 083/179] more static_asset_path handling in courses.py --- lms/djangoapps/courseware/courses.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index b63828eba5..23890ba8a7 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -156,7 +156,8 @@ def get_course_about_section(course, section_key): model_data_cache, course.id, not_found_ok=True, - wrap_xmodule_display=False + wrap_xmodule_display=False, + static_asset_path=course.lms.static_asset_path ) html = '' @@ -245,7 +246,8 @@ def get_course_syllabus_section(course, section_key): return replace_static_urls( htmlFile.read().decode('utf-8'), getattr(course, 'data_dir', None), - course_id=course.location.course_id + course_id=course.location.course_id, + static_asset_path=course.lms.static_asset_path, ) except ResourceNotFoundError: log.exception("Missing syllabus section {key} in course {url}".format( From fedfa7cab7a37049c7057ab504da7ae6a7c4f580 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 17:57:46 -0400 Subject: [PATCH 084/179] fix tabs.py to properly handle static_asset_path --- lms/djangoapps/courseware/tabs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 38f81a476c..d7e1158e99 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -380,7 +380,8 @@ def get_static_tab_contents(request, course, tab): loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug']) model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id, request.user, modulestore().get_instance(course.id, loc), depth=0) - tab_module = get_module(request.user, request, loc, model_data_cache, course.id) + tab_module = get_module(request.user, request, loc, model_data_cache, course.id, + static_asset_path=course.lms.static_asset_path) logging.debug('course_module = {0}'.format(tab_module)) From 91bf6ad86dd4de9f9263f9bb2b2fde585a66ba2d Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 18:11:40 -0400 Subject: [PATCH 085/179] remove extra debugging line from courses.py --- lms/djangoapps/courseware/courses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 23890ba8a7..676fdf4941 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -197,8 +197,6 @@ def get_course_info_section(request, course, section_key): loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) - log.debug("in get_course_info, static_asset_path=%s" % course.lms.static_asset_path) - # Use an empty cache model_data_cache = ModelDataCache([], course.id, request.user) info_module = get_module( From 5a7bcd7bb3f5b3ad5269b173397b1c56692575fb Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 22:43:53 -0400 Subject: [PATCH 086/179] always serialize out HTML content to a separate .html file --- .../contentstore/tests/test_contentstore.py | 25 +++++++++++++++++++ common/lib/xmodule/xmodule/html_module.py | 7 +----- common/test/data/toy/course/2012_Fall.xml | 1 + 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..282753fcf9 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1057,6 +1057,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # It should now contain empty data self.assertEquals(imported_word_cloud.data, '') + def test_html_export_roundtrip(self): + """ + Test that a course which has HTML that has style formatting is preserved in export/import + """ + module_store = modulestore('direct') + content_store = contentstore() + + import_from_xml(module_store, 'common/test/data/', ['toy']) + + location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') + + # Export the course + root_dir = path(mkdtemp_clean()) + export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip') + + # Reimport and get the video back + import_from_xml(module_store, root_dir) + + # get the sample HTML with styling information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'with_styling']) + ) + self.assertIn('

    ', html_module.data) + def test_course_handouts_rewrites(self): module_store = modulestore('direct') diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 567f5c7eef..726cac77f8 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -164,14 +164,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # TODO (vshnayder): make export put things in the right places. def definition_to_xml(self, resource_fs): - '''If the contents are valid xml, write them to filename.xml. Otherwise, - write just to filename.xml, and the html + ''' Write to filename.xml, and the html string to filename.html. ''' - try: - return etree.fromstring(self.data) - except etree.XMLSyntaxError: - pass # Not proper format. Write html to file, return an empty tag pathname = name_to_pathname(self.url_name) diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index ec75ef0b9d..ebbc2bb75e 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -8,6 +8,7 @@ +

    Red text here

    \ No newline at end of file diff --git a/common/test/data/toy/html/with_styling.xml b/common/test/data/toy/html/with_styling.xml new file mode 100644 index 0000000000..1ee6ca5c24 --- /dev/null +++ b/common/test/data/toy/html/with_styling.xml @@ -0,0 +1 @@ + \ No newline at end of file From b8a8b202a46c20a68b7a556557729fdf737fa8ef Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 22:53:03 -0400 Subject: [PATCH 088/179] update comments and some code violation drive by fixes --- common/lib/xmodule/xmodule/html_module.py | 31 +++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 726cac77f8..7a68c42ac9 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -33,11 +33,13 @@ class HtmlFields(object): class HtmlModule(HtmlFields, 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') - ] - } + 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') + ] + } js_module_name = "HTMLModule" css = {'scss': [resource_string(__name__, 'css/html/display.scss')]} @@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # from .html # 'filename' in html pointers is a relative path # (not same as 'html/blah.html' when the pointer is in a directory itself) - pointer_path = "{category}/{url_path}".format(category='html', - url_path=name_to_pathname(location.name)) + pointer_path = "{category}/{url_path}".format( + category='html', + url_path=name_to_pathname(location.name) + ) base = path(pointer_path).dirname() # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) filepath = "{base}/{name}.html".format(base=base, name=filename) @@ -168,10 +172,12 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): string to filename.html. ''' - # Not proper format. Write html to file, return an empty tag + # Write html to file, return an empty tag pathname = name_to_pathname(self.url_name) - filepath = u'{category}/{pathname}.html'.format(category=self.category, - pathname=pathname) + filepath = u'{category}/{pathname}.html'.format( + category=self.category, + pathname=pathname + ) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) with resource_fs.open(filepath, 'w') as filestream: @@ -185,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): elt.set("filename", relname) return elt + class AboutFields(object): display_name = String( help="Display name for this module", @@ -197,12 +204,14 @@ class AboutFields(object): scope=Scope.content ) + class AboutModule(AboutFields, HtmlModule): """ Overriding defaults but otherwise treated as HtmlModule. """ pass + class AboutDescriptor(AboutFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located @@ -211,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor): template_dir_name = "about" module_class = AboutModule + class StaticTabFields(object): """ The overrides for Static Tabs @@ -236,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule): """ pass + class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located From d523167bddd852a69f28891d8a7ef3ba75473356 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 23:16:39 -0400 Subject: [PATCH 089/179] add another piece of test data. just an img tag. --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++++++ common/test/data/toy/course/2012_Fall.xml | 1 + common/test/data/toy/html/just_img.html | 1 + common/test/data/toy/html/just_img.xml | 1 + 4 files changed, 10 insertions(+) create mode 100644 common/test/data/toy/html/just_img.html create mode 100644 common/test/data/toy/html/just_img.xml diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 282753fcf9..93f353015d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1082,6 +1082,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ) self.assertIn('

    ', html_module.data) + # get the sample HTML with just a simple tag information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'just_img']) + ) + self.assertIn('', html_module.data) + def test_course_handouts_rewrites(self): module_store = modulestore('direct') diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index ebbc2bb75e..ce8a2399b5 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -9,6 +9,7 @@ +

    gG6rIoyo6k4l>d(sMD_3bQ@Pzii|Majd&5f7&rK=o1zs;^8men{` z#!V8@AB?L_?z086?&W>swObgnjIkwev%LNO({j+8${9<2{7PloMx8WJBh7$_6r>S&jh|oo ztCrCvt?_Hi<@4Y9d)Z*|!T0`(El_n?vLjj&beWkXq-z0f?5@Ur6gAV=mdY$UNcQmW zbjF&W@rGL{jYd!{41H@kYAYq4kF}aLxPGcIlB*vHv?xAG*xH|4l+`A5uw7z z&J~1xeQmP5iV56f_F&ooTK@FmY_Ebryhz zr65k}Bm@;K4s8C}?1PH<4esGMcnsAZux7e}Vqtf0x9mW82m4M+5nIu6KY4~&4s6fB z)dBj_l~>_dBPb#$7o1hi>8^Itej^Zl=$c&vP!2&NQ*+aAvuj{5g;%E%w**JSx7diAOl^i1;xNho_*p6a7-1rd= z>|FlQ0;#``m&60sxSeN1-1|BOz$DQ;k1+DMys`vwPT)MC+G&r?uSb-9%z$`w@T6Sr zzs4zeoG^bnU!Jn2ytnzZtY%3EPD}eu=-hsi$up%qWUxEqaJ;{G>lbC?GR_FtSG`NV z2iT5hxh%^=^u?!M4P##VNbs+I`PGn@Qq?p>b6IMXeVS(s7#W>|0H*g(?5r5t@Uqd- z%kYfeQq)19rb|5f&Hix4-UHp%-ndbI{WrgfbGz-b^Y(k?DeWgsuudxov#fU!wUP>V zIwu!##xMo^*6;jwnK~RPfBt{`VcFkie5b`Q2tUtfcw3w)y=RZg2k*XD&KSv8-@H@i)>q5v(?=kp zTizzi?dw-Fr}$*^pgh~9F&4+*7Ko0nxgws)z2vOUEh8d#8JSmXb$M63%vLl8xJ@>1 zyEX1$`)PT`#sqg3dB~I^t@}gea{4@S9_qldyZx-(zyBc0G99VQHMeme`T~ys=+Ra* z+iu*qOb7g3=46DYqq}HPIcAeN`#5t#*PIWLtQRoz)3N#y&dOZ_ouGXtf#;!GSPy2t zEV1>7A5%gk2nvfN;C48cz98ZQMPaO}w&NL5g&A>ahdsW-NZAQR#esX6xX$+xw;8-Y zdHAqAe)b$GV708URO4Xsd~=JtQw|3bm%kpCd0en69XxpO0O^2oKiR^g$Q%RAKV)ewTaVo#)0=@cq*V9c4{R@4R&n zmr8&2Bc^fB9x}-71m`T2(ixY8(Fnm;3)GPwi3M`DdsyE2{*ORp6eDmG3ltI7=n@ux z5|(iTD-n42{_+(HGiJR)e7s&DLO#9xejeYaPXSD3HkJR}z&>{RtnA-^Sa42J?tk>S zeC9X4RTfrp%gNE49C$Ik~3A&hy}DS^F$wtEi4kRFJAdO}w- zz|3K)sHCprwi(jcyCaCN<8WR5IE_H0YLnyV8ICsQT%UnODZsfjj2$0L_+moLH9rT} zJvizvguS)LcL+v~tcdIq*cx2$&KpnK)>Sqt}4=`z9yj7w@B`W#~D0xDeL_-CZ^d&`T!!3dT?!T{?4Tu1D6 z6iIAtPg3mtOAj}#@BGPMmOJ16VwqxXEH=4_ejX6D03x1Eu`GdB^uR%h`QF`+nB#(| zYnV4KU?36uUL;+WF^=?>EBipBQ64GJP`vEMi=)djb)Gtd@xwuHygWy&ajr6t_d$fy z@)XY;vxs_n(|227-;)P8Mx~GcsfnpY=H2~r^>YjYh=?x`wU61fKSA4_plOtd53rTq zW_|;mg7EsUVo-qSj`5|XMv}3GJK{3jUr|^?ifI&Tik`R_YgM^IbKqQnMA9m zTW#^-8Pf=)5S3j=a7wwV68>H zIC(-*e?sjyBK$TmPN-{^`9uV|!M-0{_R9c4hQbGN&cn{RBm`?3h(z=hXkrcHCtyz* zY)?52!^;Vh9C46U@H3x=eqv?g`qYZEi?hX?fVW4-8=F*$u4fv?4qcCoWA42D#4*-B^7WKi^wXy8@Fw#8pe&rR_@1EwGY>VN@v3 zrnMzo?dsifdYv~yea}e6oXocc#L)iPE}|d}&btSKojR}l=##Sc>P_@Oa31DTXw{-T zS!CYV19==#rzW;p@|h`jKiMu|`(9 zyi>mQ-OrUD{ormH6s~!kKSIizB;8e#S}L7bfWz(}s6cO`}wipPk-2PZ1c*xiKIcZoKF{)x|khG^fzA=@eQ z&W$H${Oy+a-@cCsOuI0rwY!jRAzbI~Q)OitU&d&b*HEC1Db6eMG6=n36Tm~<=Pt12 zZf+JL03m0HzqY(wcFALTWeo-4Q>F+9Xws`W=nj6y9E3bZb=a3u1`A%; zG^04q|7`iBQ8R|{VGexMN`a9h-;-e-9a^Dnj1HVP2{j-I2Y|p$FYtU$fg1!r*2@ry z{yYugBuMl`N$L(@O;a1~;&bXD5KEEF+js78S2<4_PlP416?X)W#TUq}-{L-VJbmhp zf{dq7xe6MWb*51?u~{!$^5j-)^Qe|aXUh-X-YUz>m&*;b(Ml3kJOVD5smOVP;eZ5p z#s-8QoJ1mAoLMQ~`FH*~1IARj{D?W-^b^G14HQbx*s!o&Ub%IHbyEnAN}P~RuNIy@ zc=UqPpUYs+>(>AZQkY}027;QV`v9aS@^>PIkw+1O?t%Ag^)cQ5kV z|^A!ixEcp*ZSE1=mNFC7XRj2tR)Q>(|DX^x#;^R=^fd=7X+3LLt$0mf#T8U)Ia@18SZO?4 z4aJ8`4RS;5=u01NJEh_SYnx*bSf^>|hv@rp9JgVJzJ9fAF#iJQVQpjOwX4(2p~lMh zfAT^3r~mM4WgY|mGnQ^*5d=cXd7XE?=~QFPEq9{;X`Uws`RP9`0jR zDj^Dk(4G_Y_`ws>F;{avn7lMJIDvcZ;^2Mzg?7O#?X1Yx;B0Wf132gb3aN1{sWi9L zvxdHt7W5mnw$A(;<<9PAxjK*72`AIy$ho-^fq1PI)e-qS5LnkmltnKP_cqWAd(1abiY?{VWq}2G6iYn8*a!v^P?E2mjosQnYZ;j-!YRM=#U(BXof%j*;{Z^Y?@K5tiUDqC{vCG;RJ>q2*AU=w7XS&@4;=A2u>-Z zc|&y&<)YNaG6)=>umia4F_X*L3Cu2!Bwp5%0~1XvODB6vm&(JfC*{=z1{RKlJmtW! zsi{@wh6}WpJ7`2Wqz&ervv9l>6gnE-@320+ht<{r$UDg#YV(lO=@1n^`s8_eaW-8Z zp&)z0Ne(VCDUmn|9HJ3dOkF{#=Vl5CS1Y9*oX~G#OtM2|cAt+#gTKN-XP)9fC8!P( z0fXftoOXVGqx|p}_o-vQeC75^`8n&bU;ErOd|L062t)C=3U|J#I6VmhUtU=*Km7SeATbEX^VG^UmO-fFk%k9{jyT)TAdo$jR(mLvUN8vgZfy$fHjEX; znsw`EE~wVKngOECrhF@@r-E z`kgYee6yU)Lg*7XyqjJv`^@E4Zs~$Y*+r3gb8DA%G z3XV{A^%(3HH0q~)$0&yeOR~o!=E}OgRpaSkA&1xY_{tTQ-5~g5M9pLBeDBF2b6)0Z zh|6F8hySG9`PT20pZ@&q^58K(j*%KZe`TUvTbVBpS^D|jFSb~(W$EAo%3be%gdYL^-qb6b|hT3k*keNzC?+^-6oLES_5PZ>IzlIqxQvAjvC&%x_fRm zKprf?;8Tq^h-^{3glc3Xc3~EpFs-r@;!ofW|vhtJ>1MZeRKj>(+Z0CM<{}@iqcr&3g@v-vhB_TtTh32spQa2@rtMXX=SBI zr(#Qm$|qY$8BbZl>iu<@JKBK3Ar3FEluIbIc3y0AQXvcV*dD+C!M*a6kGING#Q$kD zk=N%K>=0$uLZ7Nlr~Qv(6HFUm6O3SAo?^4DCOI3{!6>4Z9h}Htq3v`i?_A?JR#CJu zioHH_{Ax!HvY{wbVKzs751qTCJ@3!6g!A;Ey#L5k^PZO9|NZ~5tY3STHR|_S!`>>N z!&}9xuU_Mz^2xIO_6EiUo)Evl^abwD01-^-@F6G>4B;s_&j+=v^#h3CaKr@ll1N3f z*4x25D7bOGB;_aC>cDPk+Z{Nj5AI1^yaMo)DbOj0l~1g##w+E>lg%=&lZB3Ul6&x! zCo>`3*WtNr%^@H2%`}Dyf;C)G)7%IlPD6g9=Q1pic{4={=wnTj)Lo$m33y2Q!1U+S zu6*o1406l6h+8k?G!p7y`e6XFrfZNKAH3A4e(-p=+}*pLRi}Hy2xMlS&BJ$!Tc3XB^<~#M2#4hv3vbzzy5mp;l~^0CqI3+ zymII34N{@x+>sdpGXk|}v z7jtL6l=TJhP+ zD%`lifm<9g`WTUWZ}bvMAO^PHv$EBjF6#^cw1NRh#=-Aau<>qgG1A%9~++~Bx2}haGkb~$oz3|UL7#3Qpl^||| zZ=NYtO~$X=qw9+zLa@GjA4C~?piu2<#O-?vtto4B8s3P=(k4#Q6Dz%I46l~j{E0V| zNBt%N*En{N#XpBNoaVgT4z!ybA@coqH_GR}zF3y8q0ZmIqaSl3t&V2kNbYH!L=1ic zR}%4iEW^w*kKE(PkU8eP<7^9aeHZ2vEwLl}wM!%Aw^)<>@q16p!$13$k$) zC3a=k7)4yWT)zJIzf>Onug@TIcKgH!GMrluOSyAakfIpH^+2t9QXnS`!abpL#8XUI z75#{y6_!N#@T2?GlR<{VveeLfOl2Q}mMgkZVG#w_%^T}cQmK%7cz+u$KEeSaeGh-i zlh|Y5yfTkL%L1ni;D?%}ttFfmjB@sp?YPC@=kXzA9DJ~Bl(ynh@Iox}75wP7CmQDh zI`O?CkDD5hAnv>#Vmi9}L5N)3szB(wiew}Z>iN~+2|JODmyCGRP93YMZ~x=JU*7oa z?egKH{qm0D2m@CJ0u$GSx!0vu(+kU+4*{5RXSs)<5N(S?dQE&1AU-)BQKvkC;7C`?(aKf7wN$N|mEe<}bbZcE z{qa4F8@_eD%rd%bJ!jVm_F7)U^gQw~2(tVG% z+EcVywig0xVdjL57?;o%uS8P#@X>C0gr?f1B>h5Ny{6?E4)=NvsvLc=bX9 z2`_68N=&NJHBnT?YhMjnEZk2o~`~VmqxBfcU;!xld63xZ~!kmOMD- z*4g+T8p$1;yKQ5h!ro^-sJSFY@gRrD>>N|0444zf11zJ!LFVDC zUtJq3i~X7M94nwc3a@$QaTE)~0y$~tEdujNiG?vI9Bhq~0FTisOK>ozD8+29afa}D zt&0$AX&?v{F_64n0xOdMWdIuAdtN^Kh3k}cwrs)4pz74YB`FOtynDKygd+?O4V$Aa z)|cJ9-}$_fgP@tVURpot$UQEAh&x936a$sc^lsg{g3n^~2@v@qT=2mLbAGudgGirs zZPZtk35_^w;p13o^?Q3*LZKYIcNbQCo3+X5^7U`tDnCU5_Tk;99oE%;78yd2K5(7m zi1>W|-UznF>LSw5BMdU5OG}j7a~5np6Zzek4Oe~kN?`CSuS2M9zGKw&^RHhkYYgl= ztSWdI^y+1F2^zN0;bB(6Rm(0o;hh5!i~Tzf1FO3VS^oUdY}JgB57LU_Y$HkU!;K-V z5Qe$&KcGAKX6v7?#%XDJts#EM)g@@J!}pF@Thyw7K93Oig9?yO_iT)mGp%5i`Nuco zZ|se6cI{cYhDFXa8`0gxa)2^scjGyP3&Efv-9!1b%iM7i)5cMjOXkp~F3^e37bdBOi6~WgaY`CG~aOPNWE_=&5j={}! zz^ws+b3;g_EaPo(5BBWl0*~exVY%oGPIJm!yN_j4#AXP`wNe#nF0;(CdEhyO|NMwi zA0k?W(@URo3(F9l=9dtyBQu1L7(xCJ2;z2ONo!?kmQ53@#)0j6>tYF#z z59pNOok(m0^PX>Zmhb5`?>wbq1S7hSF`qrV`ypkfjSM%waRQ(|5C68bhE)_aO~pAk zqAV@1Wk2*|2Bu%Uf4^+J_Yr$xnKOf5dbJ!AAHbV>b(ytH2lwu)py`Xmq7LHF9P{-~ zdrWXwQKw>F`YkfD?-?Pw?^zTGfq{I)$;G#D#xAXd<7EXKZ3!!(9dYuthICoC*TLr& z-&6!f5b~T#MP!4R+%~5pw-Xwz&!{gak^8Fr*W7?2SWPHm~;))||xqi`X93@i`Lo8ySz>NjTPznXlNK`(cK5I}g_gRnf(6YrVg^kEqOb$0j35GH6KU^`GpGOT zoy+A<-hPyWw)XLOs9$F{M5wURu30gCOfJE|Gf6wl&_)yED9g~o=g=_3XS*O{0}_LS z!x)3lENj!tOY>!Y9j!X+va=X=>%L{I;6~xZXTSV8HUM^J zgXO)#PKb|w1-?|FT#SStclK|#F}PJbf0(Kzo4oCS#im#r4s3FEB!K^*#iKp1_vW>5;WCt(-TE- z%^InZ%O#%W>ReU>3H!PS(Yx8fxYH1j8_xH(*(xSy#t)Ng%%gBEcgbj{tmE<8`!*w) zL(mi!KUFUqh7-9cx7#kbZ}0`3?r zy@y_(Evaa;EUcyPa}YUNFu9HGF80)ayyTs_1;;E$3$yo(+g(TpRF$SAP7ur_#-O!r z8~zgCXG8fGT*~_eNXaUZ_;L6!;V9bm5B~Ts%g3xPplUB?&sd@gffDIC>sm+(mN|(B zg{pZ1f#8mqQzjyGS$0JPkNOyxunEH~YZ;AE|$_k_1%;#bn(H=WvIF-9d z)WA9)mD3*XYPb#;0wF|3mtLl93WlF$B@0|1@P${5@kXt28}|)ZiwOvk{b0*cOkTz%kiOz;bn9 zdW4HDtt6s|uR(zpXKI?=YA5HN7-t}J5TTr#tkG^`_&@G+1ZBVG59weX)%!Jwdk^0OXya>?|=2g+exb+?6pJXL{>l zK27Wpk{DC>t!YiMqj9O1;sCEhOY~+ zgU>Z~N@w2Azb;&M%6V9;SYY|XpN-J{A9P;A_|YJ6D+E` zW+_JiDUUSrr~LYDl8fL21`vqD0EE{=P}MiEM0^UY9Mec8wYua0g$x4Dolg+W0SN){ z1H5rYjh?tEcWP z6G46b>EaLIbXf#eSL$;RmbWq=6`x+#-K&CO3n-g5x;ZU@gTmAY z-&^C$Fh(Delya{7wYYX^7(G?Ly-v)F0B?PAwkkl55Q=H<>n5$R}5_U%aXGjdwjJ`8$!xOQbudZN#p-&}E z2t9UEZXBf#0OfFFgxQ|(B9IFr1B+f^G5xKd;l1!^0aZW5#kWNi651uZN!@`iheacz zA7J9BOH)l5br>*a$7)xP-B;xwFE zF}!dIiVbrCIYN*tEHEk2HU9lgLa)-!w~B$0ek4#GJ9#=4fO&|{t0e%?mxwK z4iu{<`OQ~m$`@X{Uf%jQ-!EUeig{z|&%lJm*wtHC5e1P~p$? zzK2tCuVm*NFNs!hBQFjt1m_U53|>aa|9n3G5o%2zY%)PR($k> zSIQdFy%=6URy^*06EyfSBVklHGA*%6sq6U$YxqI>WKv5sT(J6Fo1+16iN&iCOlvN~ zDuMdo+t3A|Hqs0}4b`-jVUT_(XX73)S94?dB1)GJ zFtY#J8>_gtVJT=A?-ke_%Z=T;tCX_4y&0qWeRjH7R-01T*4sbVkmDs$&WeV$M;RqL zHOZP^%C#0j_SZtf8IVv8(IX;$I+Q{+) ziXqo$&Hw89rE-gwyKH4YUT6JF_{X%Pvs!ysN_5b%pU(9m6!E2NKzoMs5;CH_^2*Pz zipJ?3a;Zof-1U)sM6RSafBq*%jV5ArDTHiR_G=ynune#Go+p1QEpd0SH?INBQ@3_C z`Z;FW5KbFnF=GqSQnc79hMAlD`S<#s5)ER${E!qDysJv9+spBMZ}8hU%TdcUG-6u7 zPr1y&EAVS8CBzX5Txb4|M+&y0DQYNoX>HVF^qz$KxTD3*+VdP6A_vlq>KTR^?mJb} zdy3e=jyyWRaxf1@tac9MatEvu#TJ%R5W<%}bESOmuOF7rvQR&c8RWtKCJ2Ww4C3u= zJYz=*{uOUY}5i>vItQ9P>}dB@wDO z!k}^aMtSn*Kf-j8J*sfuWeo4-d^SKIMO2@u09u_Gj<_xwjGzq2xi&h8ID*1xw%kN3 zd4vS;FOSs zL1>SCzflC9c`V8*O0wtWKl*on3k4nusI3R(H^2BQcHf(kge2NYmeZCmtzy~4G8lIj zeOVS#5(0}O##M|Gr)41(b%%e&2CEv3psZp@!yOyOXa6An1OtygeHjOZVZ6*SjYmIZ zuwX?ZR|UaiCA1DyJ|$-Y1_OqJ1^Kozna0asFw3UORpU4;wHU)`L_~175r_5Y($&;| z!|$h6tZSPKDbC{w72x&%g*Y|cr{_9IZ@Gvvcprk0w(s7%T#}FRZ3vY`hO11HYm9;n zGuX5qVQtgLLEQw-v}fm*AueJw-_-5s6vT?21i}DOU2}Aa0)Qfjh#F%K-gFoj5tK?tH{1{KRpp@R%! zjs)w8@288{<}yHG6w;@Twvp%VUdH&rT}SWYtdLqJ`R3u*qXONFXx|VNmBFv)bFO_uZxi+aw7|W#N5y&7ZK2P!YM@BIh0ND zxx>EG5C$@@ZaQ`2DN)#rS)YU>M|@?TR)I6hfsk@+mwZIz9K8Xu@4nb9-+E)Y{KdPQ z zb*x+3>@}v~T7fC*9WmdQ5YIH7#HH#YW|{x-Z`>}sPac)`|Kxk+H$Jz_2Vo7kbA==@{!EaJhC=C6 z20BfwM<|onmn+eErrkqtDFa_Ah!(LG7B{QfB}uyaQ!~nMS9lBMb%A(!Z%tKUG)NZ< znS_I%3ftG#FqLEy!cY5N3A4vq$R#(G%F9Fau2K`vYc}d(N5x?XN~sT zuU{&E{r->2A3eEIzWntsv+aXqjxEV613u{iOw1DTC~LPUgU}liamCgB@t~c9uJqK!G0Of z?F02L66~qY5CC9UmpggaI&=k0>1Zu0QZuoTS~;0r&6Ia5Y!p9ofy2C?hZ#2!Qd*wO zgpj1`zNG+N`Y0|2MQVY9u+vCq8h%>-dF`Slbf_#Bzxl>xT;jMcJI2z*mL3oDtmg#G z>n>epIbyMlJ$hL7S#mhV1aKVVdw0kj>tGFcDRa|o8-vS^p%|ez5MkvgG$myWv5|=e z1jktGcm}OyU4UTS{W67>({F$EdU=NVPs-2mQh4p!db!08mjw`UYWbi{Vaj)m zk%x}X6v=h6rr32z$J@N_$5c{CJ6ZzPnI~q@2(xj0lfAYdfAA5D_4mtb?6o~)NpK1U zl_&IB2Psz5NXw)#!;uzY3#EB^8NK&0A%Uv z>vvYzB6m>k-g{c^%`UR7ZndnfETUjq3`f;!%%!b3jw4UodWt#xF$0myY}+W?wl^`B z|L8uOGTue0^sHQ2JT2e4z5o%Pl^0u&Gv7U?jAKqkY+KK}r2(cevL7F3DuLc>ZwG%- zSh7uwEr+w(!Qx^FRYLbcd`!nXePEDk+>lT&+d>=^H#Jtdwl-h>v)}#gvQIz!{(t?) z?DVQzGi&Vj>nP_{^UI?hl6>+8)O@Wk`L!(hpIBs<7}1Axu^OWrpfoYhkn)V$zL_p` zQ~~%Ush#i%xDu^FM&u@)hsbI?<8(=k#MOoSXguR5sD*QH1X}0hegNYpR!iO=|=Z03HRgZem-!#Yt;dK!Cr21CC>$|Fv*(w&whC0`4O6D4OcHTm{w&9QL=r zd=+mLW91VzDE#GLJOI(A*?EJDROV^3xPqBt&OCyv#Sz4GMP)fathAgvLwFvexeu%( zkoo{a`)zzUPaJKQ8`JoUS{lJ-8XwANeOE7EhKbIHYeqv(TPwyYsyTPoP~wdJoTDmn z##oPD;Otj>fPq3zt|r=bmBV65l$gU#h{={=ov~Nul+_9^NE`q5a?Z}$d3D#_VoE=zC0LB<9-)(tt*bAxz$7goChd ztnmFJ;nq0LN+8;y%a~nBD#7`!hDn_Caw1EnX|zF7-bk0^&YifDo`Ae{KSB9KG_`@i z^h$%a=Ni-sVuN4IuZu^tl!CM^M3FZzJJ5c4xOr0EX5;#;FM>2ZJR;uz5M{w`S-E<( ztlqp)i4pU^6EwcZbhMkjYhwY)fsJ?9XI&|afr1j6h{xQ^WuX!6j9IG`S^BKo$~9tV z3|H@+?lM1HF5i6PYWWhdw@^?#XQ#@Od(X-y%Nd?NaKNF-q^#n$e7-DMfST=rt!AJ$5s+H z2Koi=?`C=Dqz2_8Z@v0|)HF$MseAxiZd0p$7`+J9&gT?-p-5)&QZ`SZEM#o%}i7U$2eid!|^ zd1#UwJ^0(gsT1$@PuPCvhsW`ZLI#+UfZX{FS(&-Fd1^nEz<5=qzT63(%SlvL(0^I&-&&J5roy=X7_FBe@}R*5wVsGw+aF!I|B(Z3LHrs!1RO z0C&vthaxHzKc)#G7%BqVol9^X&%F6OVQNBMbK8di<0_T1>l0YT4y(0T3E?q?{t2!l=f%v+&J* z!IF>pHssIet}=rhc`&>+SbY|)&kK<%`=PsPg?(YXgB#f}rk5KyIHV%^9frdfVAOmX zr1oCFwNM#RYp|LvB_J#vz!&q;hWD?Tmu(+ksgAeHDuxd%#o#Y^1}@?Nt+`yzxN&|3 zft(?>*0!)lmfg;;rB&Qw)|C091m>|DsSlskQhPS7#I5$$d9h`z8etVbkRu#UgLO>| zT~D3EAVOctv5sPyL>pNZQw4ARa&woKJXTV>V%?}p1hcH!&La+wW60pi2S+N_AUX{k z%vZSX0nXGS+*Shx_6tLl6Qk_cMXc^KPZWoAZ%US_om}zx#wMOP9>$W3uQ9`vpoQ1N z1oY&H@-kjIw?D;tOY5@J90=wbv}?$2K0ryQ(>KayTa0oxQd=I<*82dlbss|uiBl=X zmes1szB8@6r8e2;>=7XAH(sNRBjtTG+p$>>a#VaUoycOkI%a@m*Tm18H6>p76nTT!~8czzx6dCftLB@2-92IWE z*>+Ef1=wY4xOnJ$Ho;qPt$@Z5d9pSKJmVU1Ks-FhxmpLrhH_MwlL$L0Qo@(h@lN=# z2+|7HwHIk=(h8*O0D;#l`4}-*GObVbLlZ2i_A(o> z*`_wuiHv6;NFN2t2?*Rnw4HH|1`=zuet;dXz@phrUczDqNQEs$L`5D{>P&$3Cm^9- z2E%cUL@@~f06+jqL_t(&OB*F+Aq+t>@#1<#L?xsR7G%X78p8`%)F@)Du#+)}djL1g zH{u_|nT`=B-BT-4YhC1EGr`)kMCwTZdX31ArG(?u8fie$(>ghNjF^_AEqZV|@h2PH zDX0D3Bfxd!Cvd38c(B}lE~i40X8A_%4rkM7gCWXnX=GC6juSZm(@>nysa(`HV?8z_P6}kEKlwFhGY&Jo&uFj4rH2Yh4v8;oj z5$#$3<#B+c4J9%F@YU^wA=vIihn}f>npp?}A>Y9}57oHE(zMH`+MkJ@;PbB?kST&*Z~5+r|<^R^UB_@dmDqpCOjazHzfG z+`7*36Z>WBhd*a2QA2QZ>>^uf{!;AoQYVi_Y+bIoRYEttSJ%L#Gw+L+zzno;E$lGx zWLcvpT}_;>;xQ?Qqjz(hnwur9u&u_U7ya_DetN%r_IKFS4uY!z6@iYKbGc#tWCw3~ zgio-hIRzIRf#cvFKbcsUhRi2|Ao8;vQvDOU(RH zg-bRQSZ4^xeY|e9JJ#I|h#Ybzn^W>v5hVBRIWI(PcFpq$g4dswM^x(TNlUJ2T)D(1 znK|<@1{QM=zH?i(+S8b2j<6A4t#Mx>>O}t@+x48Ms}*-A5dbo1>pfD$Hr@S)7=SQ^ z(B;l7>&R|qn8kadjglp0+Pla4uhv`+S}uQ$GKf#QG~-@g%6@`&eN3n6l+~j~J@6~i z4|Sbp{;Vcfw?8y6#Hy7RT$=ID`S3BD2K3eCCWDU|K;L^n8#w@xwsyM=#nRL_pL20{ z5D>5OO{8ld+uhHJU-6_RZMazRv|*5u`7#8ihWzTUxDtpRqT5=P-I5)H)G-o~jtNFa z*zUt+zM&r@^4CP-MpKp`1nhSrw?Sw8>RqBWrUNSvKGPmpppBIe-uh)Z z+uSH8kDhX38JYwVMnp)0I(j5`)BlZ^ku7q)%%e6J56CP2lPHpTqSdgPg+;FgY#kt! zj9f%8PEC?ndCzx$xB|&|4X$?zP+>C%fI#m)FXey#%a6-{@$Y{P?#7N2khBL!>xH|3 zOs)~a#dg?*qe~i3Kc8N?!njdAP{>t9O0Ue%5W78a+0vPh+<07y=86>+jay4CM}jCV z4}X3`vV_16K}6sxqVr5eVcR(-n)E-^lBJkQ&WTyMd>t{GGjZ`= z`SHhS)Je04k-spVubxq7H!dy28PF?4&wa=-Vwc}5GZO=8tfDml` z;Ky8veU4BSM*<^)C~*R)i+!Gk5vU7uFn;+8rgecl*CO#=``jW#py7i9nGwVV5!RGZ z7*uE}n;6N<3)-r4Ji=8t zHX>anFfZZr(%w@Eg9$gSS1rR(K?n$88pjAUq;X7}G|Iw$;IG0kt!0KYus-=e|C@*9 z-~P(g^6k%FOXbyS&MvVhm4Bzj}_{ZH6R$F7}GG~GMhUSM0)fyh2MgQ9y1P&L zgtCuQPQ~vjOe|H9Ik;#5;&N2PY5R+C=iDLqw4+*P=78?*p?`NdKINSUc!5BCR%O+2 zgKWA!;lXhGC{Oi6i>KrwqWdm33@!<#B3J-bF0D0}Yqr9p9)rmBp+acC!{3KvTShR3 z!~xHzl#KjaEc4BG^N_O+zSJ}=tzU9=1BU5rGPbhN@P6?-fdZ~C)}_8B$R7$*j~E<) z%cnF*2fqg7xBeu&!l~eZ(KTQy(S6S|DZHn8l%?g8)Yim{yJKE!0>{TRLRUd+kobvf zMBezHgRgc+%762J{<3`L)@6>u^f->{A4*S&wu>T}rg$uTvJ(Y&Ge~hFXIdB-x@U3f zoTBjK99AwWS&L{AqGeW5WNAZ8ih{&FL1a|Zs%V(`E(olP7U#p3QEubbH;pzVsEEU8 zxjkswV=&GjSj#3%#q>pXB`xqbiwR+ES8J`-T5bh`bC1(>=C-aMAG6G*U9b6ACsEHi zqG=^8jUM`#44&Z$(f+}bu-v(GsocB&VGKP^h|}+5M55FV@w)x)6p{E8(Kp9`F!z;! zt(&&Y_QHs50|85{!8Y1KrI`e-3md&+$VtW-xME-dA-PsOr9UZ*ab14;ym_qF9#Lu+6L zf`BHJf{82snJ^GCSQzG;ml4k6Imm?b`bCWz2@{uSzNNi{SSw-WYXm?0OiT0Eu3EI3 zQ4Once)ZWx0;x+_8`X?!eBmYsek;rb7*;=vltY_WBdjQ-n8Rs1s|Sly#Z1I#r5TrVG9p7bYK{%pr;{8<(K+HU zOCugfDMysMy2O)Cqik-0b5plvcNs-47ni9F#jY1?5HD9fMWJ;@UYaxtJ7>#^@QCuG z7(`UAOW8<20+AfTjom~cmwTQ0?O)8kO&rQcQ}!6K+?cNovc@J8%vU{YcZ_8(vdg+M zpC}i-pTP(?)C~<9Wmh2ukrHtnzfh{CC!BhKn2>(j&a}FvaYs}Cl(Ua8g8A{gPYR9= zskfqG#e;OOeJT^NtNA&n;nwl#ii#{gj2D|9ucJq#oIthJ6n4C9DV zV}P&iq5)|$($Hi)8OR(y*Txyxsk1=AVP3CSIJGn0Z$c02`+oX27kzDSGR$9&qvARAi(JGP?iysIt z?{G?57Lp~1XGG(1+C=AabL`RejGj$g&gN8kl3C*;)@MhhQeZjFw;tw~lk9*y&VZsA zXL~q*b(!oK;+?_wm1rt~sgUZAs6U~Cv~6|yLfQs+{{7>?Z#sWzA~YGbrY8Mi(AwY^(N-|Ho!>31pGLxUK}`)mrwgBaRjIu-6vHaV@1 zjx||4?%U1OJEP{e}1GBPg6G{T zD%N8#+20Enoy9azQ$e@i>9B1QSE&vXhn$8tI?AD8m=(G!$vRjU^N8{ZIBK7(k_Te3 z4b2s)!YfGR_+tB69uU|82o-ol^+To$^RqZ*-KoYAwgGdBXuU_+xS;n(iE~63IsT&! z>WTqCS?01mLyW?voook>z3juC`=pf~561>0y12!-g>!lmC!j8rR8>np!4!!BKIftJajfx3nX!1E|6$pDfMHeVNM5MF^* zP$_neq9~CPcb!0Is%;dx&X|k#S^J9?lg9R(SjABHb7~Ev_%&3tj^noM$q$eth>v%% zu(@-2s@z)DTF9$5jL(P60e8`+zhK`dXu{m9x{Hx$MTg4a;EZxH{=&f!KRr=EPGI;U zOCK)bG%lJoNjYh_ZRLz{DFd+{p>%SSforyM+!NsfXA*a%daTg1p>s&Nt;!t<#fo@y znfq1@(e9xAaU;K9NNR7@85rnA;5MW zSd<|6Gam<>5UYJedP`Ed2;adtxv?5*xw?I!q~YLXfz8wHfcAC<5c)M`=DTMsogflZ z*z(~gFUo)S|2!yuA-*UH4}ub0#97v+q$&SeGy^`4t|q1P)C7vCIAa#7wX zwBVmxJSxBTTEG0-*9u3J;@$@1k%OInd5r5G_&-;L=aX%94$+YFtL2lB#2fnNdm>O* z>NkyX&#T}1YT3nI>@jAV4*r%)1*QX*aWkLwA^)qGUds|dDGNlq{Ch*$oIifrQ%zUD zRJgTjL*eaF5hl+xSeh{}?P~=wJX#I1Yw=QWDyx4bv?4u3LeEM$193yRe3x^?L=aJH z3?dm}S!4uc@pAlb4K1i#T|pek4%jyGQ#|Y6Ugi8*?GGS2mq3oOj@rdN%>Mt&*n2fw zmSp#RdA%uf%e%JIJsx8)07(I$1b`43dQb!;0(ubT+g)c>Z5Qx5%U;yEv`ZukJe^(ad%b7Zqg%aQn;pPZ3Xk{qtg|cWvj-0`C0vKT2qk#Z-Xx#&7%~|Cal*9wQ)>C2 z@)2OZmQhYLPp2~u1tZ{=pX9>r9fRQx%UAENwNE}TZzpdA7+TEa+t{5&D~YWQyi4GE+KG5ph}a5m42ZXhI1~P9@r8-YuRlT52Z9N6 zoOoUkOB-6lEU?Jwsq}}~rrK+GX7K?gckXC*$YgN@R0S2PxbhWZ6G)hGTyy=1yIjBf zf4tlN$3ME!{`6}rTyN<*COscVvFu_c^~3j$+h@$D{--w;l8aY#A1?P;} zG$3sL0U~)GBG4*?)Dp|=(Havdd1P4UunKG);%1{kLIlD?!O!uR5d?TTEcSxyIhLs4 zTKj(Dv2Blr>KW(kF0&C~g-gCn=WT$}-(kA=IcMHu?U~F;LND#`9!5E0NbmN=QgIbC5erI$Lyc|MI;xj=W*03d(61QlY{D5_jvK&_h|3;W2?* zM)NDC443(-Ph9w6>|xqDB3vTxYp;E=E!_EAG;%bMch))E>LJq)j6#w)O${JMkSqw_ z4CynL2=veW6hBjsBWD`^d@!myq?Z-UNby8fnup~{Dam}2cY};+1ArIk!Q@7yZ1E~! zL;yc^!P!7STAEPIfT|F~O&ch_sQH7Tr(~KSGDaq;l936jm96{?8M5LT0W+R|Fxd>t z8t2$b=XH^nI6!QH1GRTHSWhL7Q-t^)mQ6=PUJ8ehhFk}zRxCb%11SG=ZnS2v>&|rh zpa0co?QcGK-u}}!IlX~t@+R}j51&o9KY#bAUA}hO{s+ql6M{_2X>XEsU4+bK6i=LB zR2ha;31x3n2{U*1F@*VZ|KgEg+_SRSpRHNSn0v!EH_xEI$gY!{&)O=Y7*2(YwJjDVqV^68fX z>G;YzkturvXK!to<+tIrA)ZCrRkqDhpR&p#bMnu&X4}8M&vMr(=Ui!lBEOka9`)`> z%6uV!A^a;BhWAS$ub>3@S_qO#MEwt9V8Jw~>d_I#E_3;u6>TpoZ%zWKEbxJL7=TEL zIAQT84+l1$#m{4`Dk6B|6&A{m;r^MJ{{ZP+X?)kTYk6ykV@zLlB!SiV6W>}^;M4E z`~sST%P3~N^mvOQ;K%nLw>5g(=L;O-Z0-9uSsr2FV^-!}EnG54F*VIQo;uh5^25XS z{SQ~U#F?HJmF))i!k$cB;D$UlVs9R_JKX3duU3y)!@LYPsbo;e&{ENKZfZH~Ma|Dm z@$!*^5=|it;}Vd9?MUGOVR8r5MXZ~a*bAEZA{A@-?1kJXtH8R)mF}4w=+XG+ywP>r z>zunZMI*M&ZF~1QknIttO?Z#bC9dRfXidg64B$sEy3ZP{FlP>-yMBwiZaJFg7+hn%In5d_7l=57a~Uj51+BBuK6EJ}J0@`-uuD+#g3|^S zJvJf?NJz(qd!{^|uVv5gklqo6hX6d^@X$LAF-T`whdS$AA_sOcmA3vRQnK!`^y=>w z+xia=4v!Pog;%k!fAqlzjMUhN%;NL<-Mj7SQ)p$wGVpiOnRamIJ~icPg5V6TeCpju zJ>PLykK5yn4sZ+1${}Zh@trT=PTBE6cQxjEj-ifL>UFc#r8OjsB>kA zLtL-1_y}#t>tDo(p%PS&ALOg>uDVu|rzV zj*3x-v4|@J`?r90RRx=Cv~#Qldt9qRGa4F_m&|{`TM>wNj-F!|A#hupmQ!FuTb)#hmQ~K69*XYfnv97KA^w;lgl) zQDt_BY-3|-ZKSOH8J-k1d|)r(vXyJK2P|oMjF0a+8!me~%)KzASY>I5v}B`jPu7*A zo9^_V3@iUI8*rqw=_|gqFTULgcb7cgeU6UcZ=SWcKmBpLNFCh&?4bSZ>1^9z_-oS6 zHRCR>Ly<3qH^7u=`8Ee}=&rwy%Ml;qR>;`jejf^s^3@!3ej+?I8T90$8kR&KbG-`W5VV(V*Uco+X$>pZrPmB7#tgz`T)9DAvQN z^%D-gvUl{#jjZ*tF6+*Zy3q}@JHC-@hUCoAP*_++f)S_W0Ib4J_yxY_Qx)0~tFI(A zh5za154bD#mmfZFfBI#5Gyr!&G@cDyDVJ=W5nPA6w2lsmHSvp5CJopCmGY=+H9x^g{z#Tis zj$B4;O73A=8Jyz!m!G1iU*!_eKPs99i8~u+dXGlP+P%+K!@zxL&|AOc zHKZQ_5rhQ`Vpfs^K?K3M-Nog!;E%Z2=-nsJ8`q<^H#pJX`(5@Js%@}8aoXh`wV1fx zbN5CjSph%(z&9%iWOkAySr5Oa7bk~E`n$3H-@pH;{r5X}+Ibi)Mgia^gNm!Yw89z} zNpgr>1{s#Yctci1p-kZDTIjWFS2-s1a?;lEFn}Zd3~Qe=tR;KnfkNy2bis?f0CGb9 zE~8pE+1pFL3ay3$^Yok!W9JZ1g?@^3TL|v6$E(1@8p+*7Gzy2VsUma^ulMLFcd<>j z6HP_J zde5;`rBK#b!*%jO_eofdNMzla7;LPw%6TFaCu~Qv0r7XB=W$KL2dAI!?x&%&WdCoq5;+ zB`@g{ei^XS6IfN&eBhB3d4?Ji21o7IOF_aXh$k#Se5<$uAL8;B`_F+KNkfq0u!$n$ z7ytfO?df};I`+W_%!bT_HFC*kfGM(p?ROO_<qO9NNax21=^_qg2Z z<44==o42}VkyyZyX$xMUBV_beEYH|>xF+g+&UvXM*6)}FeV2kRU%A|_-MA9;%DxnM z=0$JnQKa@j?)-6RJj!^vAvcy+}zG7`qyr9>mI{dvrI2aUNgXRi0c_ni_4B8imqOK zee4b?8xTBgfV6K8eaW-;q~wx@M$<({CpH3(EQI24_z9}xhifz-oakXgXnMo);J4SQ zVV`%FOA=P>dVeuig^hjrCXE~#VI&|p2S&k52_qlH7p4uL=5vHud<`okd@-!2>F?ub z52*~vpFCBJ(dPl8LaQv|VUE;|Q27y^QbdVp;#Oer)C2E~1d_r`(;f$eEuP*Xt3fz- zux!5jxUF+Z@eCDW4A&eB^bjdA<5Q>%i0UH&#kmW@ENilt-ng0Jk|*HrK6=8TXwOlW zV1jn(8Ydnuwhy?x_lPAQwURmu>z^?C6Q~9xJQ;AWt5tZzkXDYC$#c@9!(tUwEK!)3^Ff`xuFZCinW1bt@89i(a2`D6kR8D~qV*6T zX%hTHnkvgY4V1^vAs}=8S&)Pl5QcFb}2sYInj#?{q+xDg<(|3k_vy*4C#i9;}<;6W*8X3ViFZ$1h!z3bCtogm1R&jNf*Wh zr1!Aig1NEB>G4Y<>LalEW^9lsVr|SJLOd(30P^V%;dvB7`bQ~3NFVQej;{)gAe}-~ z5~57F4$EhE1vA7GuHI`KZR^AL+dP*_eTe<**4uC$Li5bga>SKIyH+~v$8 z3@eD

    ={;$SNp35G_5{ z?Ty^@Zc_7}VLm#~J+LPzvt(oi+MY3rCwj6N{u4LrvLq6(2r2>_O)JVEH`UirN*D+V zFkB{bomAY+Q^8dT+1G9qvNbhcQ(|NoIalzImKrzUZ3rHq#LI0Xrj51Fq&*z|oEa++I9+fZrVD*OJSj zzW8Y!=8#)u)ih~L8OHew`oJmZwm$?F=fSz@eFedQ}TE>nbnQGw60McKS20RE&`~hEM8=>Rr zS80!ZtCV?5?V6HIlBiMw&30|lp+rc{`K!xZz{jg?33*$4O9_NScKN9&(P<4isUKg1)CX)KK37LHH>Hrg1O zE9H^l#K0{130@>sc%z;tnmxp?9vpD8BU{#3Udei+jDX9WHB12vTSL56=%~Cor90op z##ZA2mv%he;S!1=uY+`MY^DwqJ&?_%6PHCil@M-*xxorxC&QA9imiR#GY_Pk8uq6m z#S?3>DKx}8+bFo0D&*iag_lFY_gSYp1~y0$(Lb$jd{9yw8(`U^JMZ*Hd$q^v31q+; zIh2xV5BhJhtfYd=M#3J}dUHf!&1F;poy;PFOk?F;y7t;LDD=D=N$Qd%iKq|fw!m<% zD~_K&MGxRVfaQ;9I^WBUUic&vojAV48!$NVvr0x$jSB{*$v>ggd&M3iCULkx?*V?# z#5nz3C_PQ0j(#RmfUS#8X=K{;je1 z&fWXm2lJ#&vEOtWs~{0|;GPz~BgA*d$?!vt9_@wp+b|i|Vd{lfu0fRjw(;Nzb7N1V zNQ5BFTbS_v>QQ_D(Qf_ zpX8%RU$|EME|j@OgzHQ;m@A8K;#ekJdPHE911O=^X)@5$u=4inbIu?mm?F3g^>2L$ zyFL?|nE4sNS6Rd%Z5Q-ccnf0Iv|#{8QFcQ5dQIxW^=obJ5;rY!``|tY@0HL702vN| z%|zkKvmT>z^nztr9O4<7|HkXzWM`2&g2IW9@Dl=J6tJ?G$du5~GG11K!!3t_`h%YW zqT8eO#@c9_;aXbmjw3-;)Bz^`zQtK_Aw)0>Q4q&AclD_K{r~3MoaKA2{l!23Uc38q zmZ^jiV)VoqA<%IA=)Gqsqi%tVM1{KGWKS85GK?;hI=pphpeo8M+4+*N2l)OoF;R0rmoEYdFb8m-bshBy^Q!|R$ zLZA-m4Ly=)mxjS*Cw`o6$N0nRJfnmYVe+y~5${ECP?IgaYW$mR!zCe~^aP4ZFI}L4fRm=-S(F!mITQx&9t#pzIrJQdI1KEzF>3wt1*xEc3# z?%DZMwoAPVVHP;9<$t^X{kHjhT)|1g&lCm$Kl*5+{lmX?tzATGZNFZF8JH9T>!VX} z?J3+`f5PyXU1%s=VA34A22&1Yvyv6kD3lOF*Isi91kA0_y6!2?DtJ^dIj^<#^>#TG zKgaZV_K4{%=_N^xA6OA*9xXI0Vi^{@4R8-VzyU&+lNW;qy!az;el-j z(A&HmVtWeYDZ}pVgX#8g{kYxZfHk-4U06JAx317Ipa|5M!=6Enwili$j4G4VG&=-# zKFh!ZnbFvv`l8&93x4 zgp&{j4hxEC@7Yq2j=;;F69^G3I0+NYV%{MGpcNph?PUNy?-!gpVx$8=$Z2xnh+m!( z06|Hv6hBFbImD|uA|G&v1Qt8z5e`J{?XO*0Y_~4Vx9b?)|NfV6x4(RCp406$(@WI` zs`)BBo}K)wdzHgna(N)co<6`&$Yjc*R|CXT&Y zHWr}q-Q+5a96E{ zTxZT{6~sYaL6BZjtu1w{w%>WP>%1zscAvPl5F{y<%S38(AFc1VyH9u8>ehCabS^S` z*dof45E@fQ_W4db$K{`gnLFHF;Oa(&RjsiC%l|yx?W8rPJF{q)!d_3dPTRYfUruq! znDsJ9(@;-5d=tP6&ie1(R%;hP{d||T_c11E?9b(+onGY9HNsBZJdrr^f`5~41aagU zSc&G3;V-7a2l_eU`Puu=xueKl*24fLwv7Xv@wo!%NAQHR9uOFk&@i*)_S)CqZdYgs zu3@q}HGiISvL3d_?|nqYaA8~lHa?k5;3R4ao#AH!h*Zpv?^9;FJ55fRL)z<{*yFLt>$W_{78>w- zZN~|Mes^QP{rWyyXyRaY%|;3oRSdhAIej8DAdbT=@;}Bfknw!;D{j4cx{@O>L|@ zYufVgy!i^#DFkYRZGGTaZuHVUM$gW7diY+fWc)Et^vL?|acli=K4dzA;91v00%8xi z4lt@SAiY}JPexWVtFUV6Bn47AbTt_g^UQfrDBL{6O<~>HXE^$Fv;E}mPP@A; z6Q8v62+b`nANtyjbM5Nl47c1N2pq767=m*tgJe+oC3pMar&ii|uQtYq_B!Ax(|DAO z!pY#6&YAftqk>~6qTJ21$tH|FEQH>FOz+Ro`5Hocj>hIZz30j$%p2L5?{PQ;kvBav zf(SUY`q)Y%f&Rm8KGo<5CMkBAj2mv&Z_uJy-#xGb%GQs7>Tx`hr(*?IrP<%wq^oe#4L9 z70=}Lem1=FF;e9@QHDv_Aet#0n_spV3jg}Af7EWgcB9>=D63D`+8RCW6g;Aig!WCx z?-=Lq!&6UwgOzW@tN(dFTcC<-tVt*QmxuINMcn>1ewq?DS+IC?X!We%A?^^p3v&0( z)9Cz3vJxA4h+FlZ9JKpS589V*W32>p@20m)P?uE59y!cZtSncw~2zkc4ne&-Iu zMYoT`pmuw_#l($>fT{DK@Hfo)sRBNQB1gC(Cy}&Da_w^uMUuzMIGo!;3ADPp&01({ zH_xHCqFMcn(-fY=^jj!6b~xhGVbdRc6BEc21dR0FP3utC*5iQQS|f>h%oXP}r(_NL z82f3j{j?Q#T5t#gduUeo=;`*4?Ws|qxFZPvM~~UF3Y<&ypo`?Qgfi=#8}gY#EYjV3 z{OLSrJ6Wb`^EIsC)h~KbhsWAatDLjfcMGE@>#NPShc&%cVJ?Y1p!4_?3%VI^sGq23 zh1FV`boufp^;luOp5tkUBMoq4So&EX;IduA@df9PR6f% z1!moL*VA-;ia~&ks}c@^jHgo`8}kLWRyvGdd$xv>^m5zddePla?&ko2@D2Q^x+{PL zHU>C+3<&hh9d0CkFZ9_IBzn=o(f}=5cId|GC6gewP9RV4=Ls@d%okzz?+&Yfli!dP zBrH!KothL{WsNx5D?NO^)&7bLn_hkWc6++E-5zk)4db~edom{{uE{(0Ds6u70jD1P z&T0GN72y(Qhr&~(P`0cgptU@sF#)|9OzN^n6ihI)LtK|`_BB+XM>=GejmG@jjElF^ znoe;E_09GgilQB^#PGJb_qmesLzF9P2>3SNUt$U7wHxQ#$L~L6K8{)B&6V6n=h?;@ z<2!Wrvf@Le{($XwTrQBgvL=hiNZAQO>M<^B&$vNQ!xM++>%0Tsok4T#?OkAWu!Py- z#p~V`gt3VA4{IG`9+?B^D1%BXK+?NHGs^D}*mYNL?o(l=ZL+6fNT`f%h6Yfr_Al>p zoh7!v%pgWiI(L36s>>VtT4&ZocjXr!>mU_WewA*BHwbgm5>LHKK`FlL(E9Ui-W{&F z7u4d{p_LlAqVt9=_)_dcl-=1-}+S!O5hK%K{(BbKVXdAF zil~$~U?9v0Uia0b+*zUO;T@$OtH<>@AjYE?OdpZ&-xXU}6;j{*SHh0dcHgtljz{^9 z>$NJ|!k%d~%*MPxz+Uu(&ENm-dq3uqIu!^9?a8A@@TH14Vgan+fkzOg5rPTU&;8m@ zr|i3PX?fweUAe*eRcL8<*lPFo!9sgNz8}&HKVWa{6L#=S?a#M+_cypKdyhHx8N!Ql|#;1T4d|tGAB3&YD3Tij$hq@nR1c^Ws#w*R|PW`Pc&TokI=?c5g~RC zTWgV+yJDsFo2RTfPhnURVoSucby?o22q1T7-T7bMFf5;WcT$sx)^SgeCvhjho2;BK zh7dh@_ki*n0~x=`6XUNWec@*uF7N3&RP)mCL=C!AqiO(pUW6KvsoJR)aWvS^-~XT; zdzKXR4i7_H8B(jUMD8}mV-RLnt_!P~RN=b6M3Q_ja|5J2j8TL$W&~*>Rp`ZcJ;xXn zflTr891QuKBWlIAQU_Rg$Wl6e+;TSs1Ld%6?}&Y{+5XKx`ymV2Xj&h`NJbr14j(}* zDFa;1?{iNKJaj7;!@_hGhV#d;*MFMmY3%QDFWNXe*IPCW!e@8=@QBKv-k4&;6ZiJeTOYss8QNY9EP%Pj zl^#>vl;8zjNA$A$Xv24yhfgsVou6m9jQlh=MX$yASRVPp{Fh#IiHob}SzGnQ#ASNc zrEBc4Vu)@pwGA`bzZ{|Xa)ZBj@7kO0u*EIL7?dlgQn-tV@B`q>Xf|op%Q@w+B49ci zhmW4^w?`Zvudz&#&6uBeRz**jC)+cf}n({9K+hOs{ zDu6J@!Z>6YumY~IqDf^YGJU908S6157#c_rPLQL(?GDqFwc3%Gb}Yt(?A>o zW0$$H_w&g7!I%8K>sE!|BFeQR8ZnhT9x~??>Im9pS|l?$!K z8DGrrV{1&j?mFMp_fQ#7-QlBrBRm_O1vWM;P`_KxwnFQnb}Jd+x|X*0#_AhJ;s%Vf z`Sjd-t^(kFV1^NF(Fv;lWEku=7a=-@8)X*GG|+~gj?+%=1Hh0kn6#KKhpi-4BH!gu_EQTR(#{~IpA?w^Rm zcO@pI;2LC3x*LMa>Brn!1+jijEdV-XA-UWzQ)_4JVw_JIfGy4q=SJJ-9oMU1pB!+{cTy ztY}Rsoo^zdnTroQT|hZCi=aB<;PgBiF?%zVNISsRqDZ_c zQ&ppsDrM6&y|47tVu}LdP5shWY@|6Oa__yayBkjiJ0AD_ipv$@2m$>+DnUfly25cGt(kGSW%^MHstY6v4Ni^mt zzxzjD;X>by_Q8LA039I&-XYNMeu%}R zDl^0+L-W)bHFcS<`WfXPq{hHfP*QXls72cmMggwP6{DrC3J^DXuiag3|LhVz{_Xaj#<)^qe)coO6Vrd1kO@|JqB&2@?U5mz5Ql zJhU{jw^v}T1cmn;LVup2st2h#TviBFCb=Kio?Gpr`)xS^4NYRrm8X~w9x;+|=)H*Y z!reHB2izG;uX~K39daa;OF9xg^g+;F{_rRrFMnP@ST%V(e}QG1K{4e~tlRESX794S z_7;X8+glun4s7v>B^SMgG|Uc@4xF z@`VqHJOJ%}^<4l`dfQ9AU zOQU@uAJ8+3i#?GIK+a|Iu337%gA*_6;^w77I9-P1B1dMr>xng)n=_z zgZ~)>+~MGyb5oRDt`+Yg*qHXF*K;F6H4pf~VcGTv<4?KwmY(&98wxTKfcd>wXdjrz z;IZq_oA2ByY~7hwictP+0+2Rokg6PW;NOk&BZQO<36Ap0lGsWWy( z-|;tL{4XBh6`}!-dYx{+_&4uy(KfB7i^L>LN-HKbA4AY9L+f}=<~5E}K?C3TVYC2D z$dLa_(n^Rkz0ix_mHu^4pRS`Be?Wya9%Mj`R+6ay?fIB4CA?VPj}o8_wxT$zz{O zppTe8dh((dJiCmdzx0XF$sqKEHPjS$HYq45gnQ9dt;NE82=Ns4G0VIUI|i1OP*9oF z`4j-Xkq$jD3MB@o?5sIpbAxunj&cvEdpFAO(c^nEqe?puOJ88=Cuk3q;E>3U}b1$B#8`T8zXgV;BpnE551*IE4?@4Uzt& zOs~KB0a||I#+W5z7+i;Fya@c*W5i3NxRwj}$=~Nh+%a5BJmCg`)i=p4aCZS! z@ZvqZ_Tb_pJSk8urB>RWm(dF-{`J4w2LhNiRhTXK0mp^zvjh;PAh1}}U)(^DQ^nlM zPi6*4L@M7pAVrWLGD*5oK}4g7XUr%R@QONnh*R+m)fvbi05d1z)x*CMB77r28MH53)t*=~yvDH36C>$;;gu4i#*K}BhG$i0O%k<#@ zTj2wC56v;3Jz zh9lvjA!YJP&ENuV=P%qu2}i>dN{AB)@}oFb1(bk|-+PQap`&fQH0{I5^iHW9se9Jx zo!9|4g!8UH#mLhb^6+nT-ueQ~%eCnp=Y%SI_z7*?C#yw~_NG4VMeL2`e&ah#&GtMC zg>Yr52YSRbkf+=F6k4s1T=Vrx62(J7Es^vIIACV!Nq@?;&(W+7q4~^Gjq_kFm$HGK zvJnMm=+KT*hV1FZ%cmOwY+1Cko(DGlD)4JBZmtp~-2wb!Lvx;yoR@}Krho-*JqJEW zF62gebz^}Ue`Sz9jEuob;bCV=RUgUQp>R1u#lLcv@hFgr2x(Pl?9_ zQO~04SC8}gkd7E|gkc2WCEtnPQ-)RcES{R6Kuo$KVEHH%TTl1eyZaxqF`nHhG9^I; z&liSmq`!EZ!_IGBX%9cX%Xv*TY^{o`n8I|#arMSR`v-saO1pabdVBPAxBcShPun{` z{W#|a%kVlz}i!hz#;V!b7FS@Sum?Y#&Yzb)lqKU(VOvW*&q^z*9DRF%(6N7Nm#0m*d;5002M$Nklt^~D6kba#XT{%S#up>v^<^kNaWZkez3jIHG#Qha*n@_+ z;tKJsD_-d^4gWmwic<~CDF>J*)-%cgB88=|XVZmuse8zgGJ7@5*qx1Qo|PQPXHYQS z*rVj(#WIO-f@Ni~xB565@f-106&hsZ|t$`28!sP)Xp%YF*Wafp?J-vAE(0gibG zZ@&vM=c31Gc16^-@r0`>ihLEP2$)1kGv!8Scb&k%fjWTdD7Ys1(NErQtDNMoOxbHD z-ta3Vb8xBU$|cTjW?A8_OH1urZ@s~my^Z!y+jjfG4;~H7Rro$$$IS0f52yo{3{OJ? zVh%|{)5Na$e$UcafBW}e#V-7?y^8|3;0}@UFyFy-)Wjt~dsu(=cz*lWaV>xI(}#6h z+xOJvfH)O6f?*U3ULKKj873+CwWbZ@|CoRY{M=8{;@h9&2!&V;55~ToQpu~LBp+Kn z@vFu)p+x2Bq?)xv**|Vb)05g$?LFVc)|iW;jUdrRfqavUMl-GEO-u*RBecuY+=`Cr z;Z-b?{^)nlw@*I)tg+c5#g?(3F>Kyo20K=h6|Jf$j>fjRE}ta>!B&cq)kg*{g>30CG528|Vk21y<&l zx71_h(z0iYD{-?#Ka#U25Gh$^hc;qoI zbomx@7_S4Kj4eLpAL2}a9H$t+Rn##YVMl8`_DTUQYlt)gYM#Q8K$X8tHp|(=m$|9$ z;ss9sr?c_yovYl9bL*Am_Ss`Mip!^58M06B&6OcvVySHZfcwgc>j@AiCum(|W*FV| zSeZJs5+52m^U$EfURc7LwL{McBtunuLw_8crW|68fJL-O%i2-s6Z63cDhebFJC3pE z)f&lpv^KuIP)S01t1SvHg3|N)P5h8aBKpUdZLv!_T5T}UK#EyBH3##pz(vT#pp+3kpL`eRAeBM>iP)Va;IaOD` z1!NRByhyj9jH956A84bbepT6vCe$0Q_|!=LRsJ29@Dp@>=O>?{d}PghP?Vak7bW0_ zkAjs@e@0m6WB8e5&2f&CF<`4n;3gPkI4@l`Ekp-)N^3c{;tmq5Wcbsn$ZI;DKW8r) z!fU}QzJdV2o&g~#z(oEjbUY=7e-*=Y!qC-YfJ4tD^CsE|uIY3Yzg~wp`IrbJs;3r? zqM9_x>6v`tzb6np!whhiyKIi<7)k@mjQ~q4x7(M#eB6Hh&b#cy;gC3c8U8-xyxtRz zR#kE2aC64EXp*&*l94rzaQ|w}*~lPh`U8EGYs#uIzDKBP!6f}1T84B)cOBOe09mCc zWj2|fIOjYAKl54z#dNN(Iuu2yEQdh+m-C~{k%l!aY&!BtPe?+0dTDOM;jIl9R=8@D zrK?4hM$3#cra3le`P_xrMZ0X{us#214YIuu7=XkMnf<^Hc;JI6jBPv@$}-tIFQ4Pg zD74hBI0Fr2IB3Ae3A|I4lEgnBly&Unh4Ghfex8YvuK~adft`&;OArKw0Bg>9FM!fBj&-pfAPC*{r>&-)9=0C{_sz}-2V30zuNwn|Mf4phVw7%_oksthdI>h|wy{GMOe)FXL{x`1EL#(vz>7BNwT`hv;g*q~~e=3XK z{cO8!?q6y1(5L>{gVu)dDS8=rRMKIr;vsZaq(gA6fmG&H@0a;C0^rlVp9<3~XF+8j zD14*I;wi(_IJiDB*E_M^{PzR3!y>dOV}dA z@+EpuZ^>J^a*?fg=eg#xth0`>iS=5NB{nWNU-dLbb<(a6+jt#$|-bqQ|RU)G8Vnz_$*acKo@JC<8q;4RH1zjV>{XaDfK*Jb?pF@F6jRO1wZGXbv) zi4|b1N@vmWu* zbYGu?kS0#`xJFBi9u1aUd$iI1#lQS%`;^@>Z@uvfrh?n-HC=7LdFL~hN7%2bQbxf= zz*pS^%M#f9yhAGlgl14-b9gxeu38u^Vt}FadNM^7xdh2weH!(rEZE`Nt2#$IRr5+j z=e2R+Y;Xs@npa@>oRme3H!sT}>c=0nN8G3Ltv~n{ zYlH8$Cm%k5{sNK71VB0w7$H1{s5e%$2_F+f-?L6Ib{7Xv(GUY#xE-7}xgBNwd!>{w zA!(-1!aLg_5G4ShxKD)Q6ou02y}kC=|Ki8(N9TSKqy2g2Xd92nKXW$X^51P`&$jv7 zIc-x?&lX*6iy}I+lV@~LEH;yuEy z!|Bd(_?zpsa~Nr`m`jx$u!{*D!3~a6`3%h-&OEIL>sq>mOZ>=pDk$-WW-RY#eo~1; zd>#Vp^LEQ><%Yz<#6_HWt-9~<>sfw@HvIJP?pHAUDg`QaP93MS8=_X8J^t{N7nAfw zcojbG#R&&b^%-#;=76uwFRuGeyau>GkPiTboVTjYw1>a^r2VUhY{F(pG($!0tZi|j z)YW#0;~t-U^w~fn;hJ~l)VD?jtp3a`OP(NRUMP47S~02s0Wk`X-vGcQC7=~YA@)0< zhT>F6hw&26GZCDl(6g}JYvvNCEwah{h@(`GxWi{x0-Em#guN->cy41@@F~aDeEB-_ zSbI4I7Jr3O`SR&)@rWODN-v)}!2o}kr5ThuL{N9(jqF;0&LSu-8_h8%wk)QN7MaY1 zHAT-hO~arv$a$k{pY!vcm5ktu9wa=+sgm~irgbQK$niJ|tx6^7=Qg|r?40ef=dj#b zTC%rg?Unqq{tA4BRd~XQmYOdc39YkExZK;G-HrRZY}{Ytv_@|p5T7X6sDN71vB9DD zriXQH*15g{x64+>|MH*yn|8SFa!L_>OlBFr z2-6FpfWfD6f;M-Z@QFI%>Z5S#0rgbCCS8QMdys*7jB%)dj>qs-j|t-1>ub<)%t2cA zn&M)bp04LCy7A*h;r-2LyX`;!I=9;)2rlEmd0{$fp_!pV;~JGqBp%@HQj3D?oOS{D zDur@X4MSzEq!3%S%bkZVuFZlbxOj4+jP3?}EsLmx2$jR}S&STdnbhp6h?17mG$I~l znvDktfqBoN8J|Ib6;uWL6w^r0ri#$8&}+{)x;#sukl9|)-AlVPP;N)mfMXYP&CMN- z$l<6}zg;4dKG9A)dP0fg5Pq3nRyxKEl5(Cf{CA5>*9z%<)F1`=9R*- zm&m*bCQX@*h!b_Ox5D+><>*B<%L<}aOBD{IbKJ}l0%p(V@HD$)$ZwkYqzywD0>+cp z8uI5bHHM$2w`WXwK+JQQ7a98cZNssMpk@e+@kaO~1OyTP6ITW{FRiphR4uWZY?VtM zEwfFJzU{U@aP=gM=WEzna|BKAJ#2)ej|w#bEVGU9(h7O9{ZBmgWf;+Z{dkWpuq;pI zoMe}j=Gv1d=n7tcvwiKq{d;Zs%7ymq-H%j5%M^nv$_GZM*!`vFK-bD49x-r{R?v3w z)$8rTYqziv)6fGtQ@^z&NFi~Pko}r;RreKd(mzGi<4jP;j;F7N@l3t)Q?M$Id6sMv ziOQI{e3`N$+|YOgo4SZ83)LAx zFgj_1uE_J9?0D+(?K2VLHU1d_3q3!|C8|GTua4FTUixCz9e&#@!8q<-+TvD+9hQ64 z9y=W0@vt}=9Ic;pp`kd*U^tZh+$Zd?oMUWIbh#Y0=-Msw;W=pIhJ#s#+TMI{nR|#| zxw+FGAFj2>@BXIkesquR+2{hMP}NHnVAWAvLxm-W@sBi12w9uE_2)?+vz&Kzjtj}Z z`g>o2HVf?+Kl}w=jK(k$VV8%lcDK} z4zI%#%;k^`hl(i+X&jO+U^NvpKZNfbTME67(jJSwHKbi?o7{-5CDjs&A*I_Mvo;nW ztfW^c6>xXrxYXosAr(w6?$4pcHV#}`xX_BYeB_~OUejSZ%XUD|Y1d;pyt&>X;}Y{> zY2dXTyM!yO=2ctu)DZIro*Nv?T8bxxJ>{)N&m|peMD4%=je|XG%s3H{Mdr~<%jen} zs}Fk&hfhz>F^H}qwAZ10`HjnM`#0~m2Y>#M4I&Ut(-M#ge@b@i65Ph_%m>~~CY(3Y zYkU6JJXUSD-n!nt`o%X`0eBUf?zOwW;xIdw3|Fquh(L2M7JH7#;-QRl2Ha$3lL6&l zvLShUn9mNwIJ)Aa{#AbC-=vVIEFpEY^T9{xE6{zU-teVSd+YCfy?e@$6>j!Q7 zV~(PMnS>gZqM3Ig6>Mcxnf7cP`ix!0u+@(@pJ&5+Ou}u@`X|qV(tZ3YAAj`?E5dg+Uq7)9tPRh zEkMMtzxjI1_coc6O?e0!|LTCVGL+CKb6Tu(kB>R|(8LhMIc(;iFnV^CAaI`Qu*H%R z^2)FnYhH$j^Zduk`u}rQvB&XvZ0qtYWndnihK3&75yBklfB(%p?R!7_sO>>-=~I;U z(JO=ZGmiYM7JnFuPhDoYirrS1R_5C4cb3|l+|%?HJanED?3NfMeDjr+w$Ae1YZvy~ zH{M|L!pw#Cy&pYkfAJ2q;S>;gXPlF~J^mVJYlhTv_@Q??L1Ktf4NXt!b279d4L%Aj z5zKd*HE&48pMJn6>GBQimw_j29CMs&y^Ry}a_T>( z$*8RdbV2dgUhSF|ZHrRjhwae{muij{HT*DL<{XniZLGiWl z=6rh!%r`EdZ-4lQ-)xsyKVCTfUi%3rSw1*+&J8Yuo8*P=S5IZbx2ofM>M?vzyiUfN zgeU4Llfxd~rGGoe2*Cutvw}Ke+Ma;`UvM4w zQQQ~9DP0MAhy6l{UKSs0EYtx?ny1`ByNK

    yf7jNf^~a4i%Wfb{H>KYWE*KLz2;# zFD6OdpYg2^^C;ZMfquKJVT1Vk)r;+|+gI8S#_w~q@*i+h!FOJnZ5Qo>8QwXh6t={+ z=Mr8NJv=kqMewj|3R6z!l>e$c)5v5S+Z^yj+BxWOkptb73=G-y&=O!P3Sc{c2C<&M0@aZd8+T!hN?aI!h_LWQY zsCUk{xB0$&aU~|UhYW#tFsocX=4Qmh=WGOFm`}sDwt2z#rRXc5;v>udNGeup4W;7#Mb@;k3H`e3`~&)$bN$^h}eQC=IMjovVQ zl@awWv@_&Q#vL%&&^r_!yw?ly$F2*}*vDw&tjyMyrTduY%iMql`LT>d<^Jeje7}9; zcfQyzV?y_Qmt~kUFh37LsAma0#4*1i^HCo8?ih7gLYVaPeT=J1DHqVjzQT1G-*}a^ zT$T|I9Qx6_-s8~oC+rNlw32};$&?a=j0|Pw^t3|rn?HlS|3;>EmU2vQ>+sYyQYobp zXnhmG#kmCIcD-)wpDQ7pzOfmyL2)`DcIQ?uL=&#k z$-KOkVBdxZl7{cZpEA#~l%=+Ji#uxf89L7+%x|x>_Qs`4?JmOmt0#cr3Id6Rh14#0U-RMRr-$NSWawGH$GVr|`fjIv3y zPx;3P`JKq2zcM%bHj58E{KYl{@qP=|KFj?Mk$?5`1wndd`l-xfz8|>`Qwx@dxWN8FW>G2$GoU*me zoAoyL=yAb`KAlF(A@@;xogwopOhI;z*kMK5AAhpe?yhozCi85eGJ#;Y2#q(NvlDBo z-MEU8&k8-NN8~UqkPnI4rZDi>=VCLD@_B$g!H<7*h|M@R#UI#U7Oz{BDoOnUKD6># zWf=SGBOP^}cShf-=>ZZCdAaygKMEG6G9~rk13o$WjI+&rCoFN3ykO`plya2k@7y?K z9>5M25`Kmu{)b!hDU2)9B4PWiGaoB{<<^ZX(m(n1o>&J|dX5gWq8l`c8w=+|pw-uS zVOqwTK+2)d*vTkzodBmWb9?3^t`jGeP2nu|KyYINb0oZcm`uSF-Y9K>GWO_g{2~Am zld1I$k_mnli?8dL3C?ZaWsBa;c5jzqB+8Wqns9rdeHy)c&tUd1Qx?)nJGZA*H zj?zFp!b&_19Z}A2|Dd&Hvlgw&MeYysRs?Sg^!OV$TDTwAMun@B+9ua`s>wc8c?ZsP z(ox2paESRXcl&5Ex<;>Sg)TE1A&bEJ%H`?y_O;vGCitxV_}&hCnXC4vJd>G`%Ixe# z(ztnD3ol18(g~#aDJ2^{@4`h!NawhgWSySdwQ1>DCAMIe&HO&^x~I<(15h((3eRrtqK3p}<&Z|{` z(WubFyD?ywhT%RpBplNrOksz8Ox)x{VsX*3i+kqL3m}9TmfYK7$?Sy91`MCMUJSEY zZUkib{Kt%)zkcUZ`{~`S_AV!utZsn!3b!zvEVEvI1s0?1G-g_4iI$C;BmG1Cx9HWk zrmizY=Z>5y_C`xc%Nv5GSSbfTzs04XBRW+i6H6OftPW^0^#D6wR&>3X0{jL()z7pE zt~gl@z)KoU#+&`UKl>AWVTP^M7=tfg+v4W$N9}JuQUTC2uv{eLZ(m~o{)!v|H0!Z5 zP8ZyfFJPuvi<{=8#~OBxLIfOPwQ_YmZHLqIO+LwLjFY*S6Pt-S3ErlAk$@fckf`E} z@&tx72a-bI+Ug2>N^f1iMrEJ2Pd@sjeTlj3?VC5-uQs?_hsa*GtWxHPBT5{uefH$I zJ>d?WH#lQTp%5dFHkG3_lF+g!N4$WaJ&*WF4@4$IIU9^@H0Mf5uG~RUyqqLj`ySTj%VRH3#rvx{7chtmGdpGxJDUCUpWX zoj!g?U`Mopp1dbVUx=H$c{RO5P!W{|^H^2b_(*@x%5u3U2e{Ltp|WdFP_|4H$K@i+ z?U6LcA_jDoLkBci9zt+|BYZfZwmoN9?1A3vM|15q#u_WEWgc?A_C;>>`v+fLX}`D2 zErx85{1vwFzXXSy*RHqQSKv`j(P&f5Rl&_h!#Vmo+VT63@3#*>{D|pHDF-c!|8%_m z|8~V+)gVZWTHYG?F0jVHI{W~aRuk?_82&HEfea>+%`V*f4h3VZe?&pi6ux!kLHqrC z`|W4HB|b0UhOMbsN-(W<5*}KJfT8GNEjDTLq3ntKgkuE;!5Gunu$KeR>%F7^x78AvBv9=yenncib39vKCBwEz%q0Qj%1u zgWY^8)CeiWEyWnL<6AnER$RqWC>&yS`lK9zuea11C@9T%wXU4QYfv$Vu;jdE8Y-=& z+^CSUaXZ4})oHHkJZ@Zd%Z@O5;Iz~3eiCLuUZ-paoN#e@g)26>A^sWm&C+Cxp81pA z*>-zjw_TrOXw2H>G9!m;*f#%{Z(nKOV5iad?`^jq|NMh?ab~lfym6h|Uzi zTM6%z<~??5J!k9XA$Halsq>d~8h^>x(gm;2xp68r?^iCf-+F74olcBEso#S0Vj_uT zP=0tRz@c5ezL+52(!Y49aP1h*PIEegZf2I<9&>O96@BZKSKFnF2W@SCD@92r18@P& zvcz%YtsCtbljr^CZe;G@0&1eL`jh%h>C3OujKfBZVLqcF1jxb>GKoUypfB7XcxOvn zP)XZS8R3i9L|{P@E18?#fiRZH+6~Tmx{cD~5|{cO&0lMKwDKFAw)o)T6Dnl0eFFx# zbA73O$f*_wRE{lcgaAR{jiW=W<^Bmb#sA%J-Nv{81cE~i5%bQs_uGoT_y5=f*Ab)k z3RDm~O=nxI5G|_}mfmAW1@NI6o(pTGARhp#>5 zZ0ccz5k9K&pXJftCa@ApSHt;pR~FlU_mBP-I3Bfs`hWiWcK`nKjFaRXd&rs|%z_1|Z7dYemQaaDB_O(mxGB*UA!&dbOrr4ZX;-K%D zwz0O}o<4il)>fag{cx+jedijps+qP8qsXfB^ej1GoZj#<$FVFRAgd^f-v4y1t)kGo zhH}rcEwcS>XJUXPPN#8I7g~w7fQZHZawz5>EL;7ic{i2GbD4k>19mYIC5!d6OnC zi0~eKh-YI%8sX$JYs%uc8tVmyxd&*c)viB9nC^4w7FY5CyTF~ID04m?#Ttps*Ih&$ zr$>3W(Iar?HSu#$9*wtiWOo8R+(XfHyvWs`D4!_kHn!W|tz;ek-nHfS;pSBP^{=0| zkMC`@ORqd>=dN9Bdo1;+oxgZ}rEP9-;s!gjm{Ei_V}TVj3-&Une-ERq5PIY3di&{b z-f7?Z_LthNS8ufk_g7O-UD!e@Qdyt6Y-aZv9~*`aFToQ(6@MZ-6ALH~@vDT+V^mgb zZfvwq?mcMF)~4E(l@;tncVG<1F;*bI2+=L^ua1JSv}jo5v6^ zA_{)On1eB2;aJqyuAXZb+;@uMx@SN=r}uia@2vnX`ZK@hkT&;y?l9~*z%s}U?$^$* zFfX!aV59+~DjaHcmzPkEF#C6&c$o{Wes%v*dyi`-uYCPFxNB3)FqW_g0#9*LknE9M z7CCU93sXlCg91nTD!`VX|1tERr7>|nyrgv#nq>uj0l_^+aX8H}C#NvF<&#_)DkFdC z!D;VOb$r{4S?`@DsT&1Oe4v+U6;!prqq@W|g!cJ@OM3jokrXAg^tR5cb&_;B+c48nd*jth*|D-?FH0{>jfEx-RcH9iLOI@xYy|K&xF5f_+P?YLdFG)E z1JMG@jkRVdUJe|DQ;g#78?~e5_X{3HM;&-Rd^8Qok z!h!IdD|NKTol)F}ME{x|_ZdUyee%3bW4DCf*-Ehi*m$zrUcbUI^0%hj{q@84WR*=0 z(DpIAjpjK{XL0@#jm$$7b4Xn;Rthxn!RyPM<<)heJcW-o=(Xc7{REL_umg~`zFV^T zEh^(@-4^KynRGo3!Mp}`j|aVGvvvog9v`f=?WZ5NPd|Cq?yq8`%#dt-V;wrEq-Ss-nf0SJ??$G;l6dDW${ul}{gbkj_BL%VK9b1dcu#h3phwt1C9u7?^ z2_a@P&?lrZ*#P$Bcsq;pfAIPx)}rUzDvSE7z&T)<;|Kcg*7$7QV;L@=rL;_z$69-Q7X>XNyBltaa z&y#oxG|YVt?e6c~SZ;s&%dfYGPdD59&tZ~<xr1gxSnV6Z{cN9YstA-@FHID6{>ZEoTi?5ShOtp zpnP)8-?Nvu(UR}cfILTex8vy=i~=YPuow;pw(#vBJ8fKDW<3>6@g9QgoYw0twEdy0_eJ5^r&u9+w9Akg3epHcBdn=|}MQP3rp6 z$!>e~_#D^0TyDQa&>yU#NTp&UbVU-<;v|WPz0^Su%bsw5dgRW}eve&32VR#qtJUwo zc}h@S_o}Zm--$8)X|tbv{PWD+-uc!2_HTdmsJ-(5D;&4@L)z;0ks+;c+ zP=?J*o<)X{qd?KsikFr#a!_l8 zo5$+O-d2ILx7AtE-je4D1}E+SAe9Eztt)AZbxt96E;;Fq4< zKx3Fce+l5T=NG&p)QMPeOkqC8$2q*OP(DDznlQblgWK4QJCD`k$@!^U`JOPPSzNwE zSuH!{r*Vsd5g=hF3@Oy&yPN5R@_L$OdU<+YwbHT4VN0Rw=X)DZ+w28yPG83Mn{9T} z440+RAwBL6h8WLX7e|wRec5e|SSqt5w1nyDCcCf}7)jk=H~DRpir>0(qy7Bhx%N{G zVjgfQxR;KpOba6qcafhS*(C{$*$>>B#<1UB_=I7yLVXj()$L0hvP}_=Fu&XKR0oFF^9buK z1G%YrH<^1H16EAg+IG+$(6gUIQFMzXfdv``ZHD835dl(IDN~k>c-qq_d@{1jJPNIf zs+5Z|Nh^gyQE`KN4jq@I#L?d#Gsnds*uuBB)umB@_PP5982qn4X-9ti4$i#!PMjXE z8V^#`f5RjK&XndECgInSgmZHhnXVx_bvS~LJf8X^6i|=%=Gbv_fgLvXo-AF#Un&4k z5j1;U3WAj-h9LB&Hi{|(FV7-mj3m$=v~|{0caCU+l0A4aR2J8HdfbdsNOulFKhGT1 z^Pe}l*nA(@R7rZ{C5$pOH{E4ww8S~qbJWSy=96}tA@!Tw?KHo1v8}mL0>$EpFNd;% z9FB}T4+n`VyY%y|(=q53k_qYli6y=8I2(s(ldb~s8NH0&<^S$xo@`$D?xSZ84F|7@ zR<31A&~;9yu#-5%NMiR8%K(JjC7+oYhE}tjm(Nm9&PHZB!GM_!)_C6j_3`Vgj|9y!6o%LXzh zBho;Plu^*T_u!*664SJWRlKWOir%SjUiC{_e_Q5A9-0%3Eoka!w7i|+L_tJxgb5VOSQwM29kz^==fWI@8Ez=x-Hj~l zB1GpP%3uv4<5lkeBJzzWUltwr1JeZN+fGhCzW{Tt1)qxy(O9_>^m$^tO+!IC=$UM=Twn z@IbH@XyIp=CoRzud$E>t#IL`4v7N`(mux5o%)l^BUH<~>uQSYP-TCIQ&A;^K^7p& z+~mOdMb4%2wgZh^G&XTlKPD{gDs$dfubyY8&;=S9c3i<5>Js9lhh^qtz`&NP!s6$< z_0(hZAiukth@WVaad{sHliTz;cp z{rNkkN1Ea+VGqbojSwMF#5r#jZ``^JOogU)jE!-}F6sS&tIngwf9v(vax2^Z7UpR# zoj`0`?D=ySv2bF8G{a!nS@lF)WBB`gbu;G>U1hR#fg$b*hj6={&S9z*MIXxESo#M# zfw%8mM)8A!gNk#IyN3a_AWGkYW0v{rJUc~VafHyH$c2P_he{t4tEp93X`8}` zF1196i34j#@e>?ntw^IHiVF^e_+fn^7~&LIAYvX7XTpsVe62e^)Kn`xX^c=bseIaG zgyELJHMS}~#|-uWrP5`#%f((=ULcLuP%dSu+&enP&;%F`k53O&A|VfyL!n*t9!<)> z4;`N|L`J^TkR%d6(q^8rEJN3&rStT>2%7cCp9nf-Sb_(HKeNBVu`R3$a4+#bjouPx zX{}tKMrV#tn9)G1kfbi{k*}<9&CB_V46*B!kId1@DX)BTR-DDKzP%v3_I! zp*>8w#xaL*A0xS_(A3K~V%}!vFI@g^&p#EGbYumRJ+t2?oW*cwA$Ih78c&*=;?zOL zqH;js$_Cx}HbTsoGf49xzyWB*rNSMW!C2>)mfM@Jze-QW{E61xjovxZWuALbxj@TH zqZkC!+s-3I&tnOrK%S#jzre8R-0>#I*s#W?ki+dZEXQb%RZ2W%LblBE#n*1JM$7j_ z$gx1Lw#`z+)I3J)uCH<(=HZd^EA3+$%EBz-s64Kd-UHBXa|F$Tix|wIy{JqEkl9HD zcMc5{<*uQx!{rJRjs;g}R3jOlB?7)Bn1XF&KkEYed7JhU0&_7r*gBT+x+4Cpa^ z<^3h`X6Dad`mX*~P{k)=5aT;eATLRYpD@p4;+JVFAU2=e9fGzHKjw~UWs;HU24{A@ z=BND9whISdASpSKzw!JdioM|y0`>MAZ!o81O^+QMM;y0#I=93(!!%@K20@x(j&wpL zauEVOK3lwivq0;sMac{dvdp@k@hrUaLWj`8_H>1ZoRsh?7gBv2>zYMi*jsAexCN9G z^30t!^y)`yFJ1bfHMci8pw`{Za^Ov+R500#I-K1$4Ij=ept_ywr3vZ=| z^eRq!ygd*RZff$7p*6@QAFR`knU^{o*CcdhhB>M_D)Kp`&eqsc=h3Ht2p@QQc^Ud^mF5;nIdWie-UKCA`d7Gh{@2HFUE=VOSiQR)7Ir|+Qj7v*y>yr8RF?Ji+g%4*@kWIpk-fCN)+r|{PP2LZbaN|0VecqV3nhVd!QaGD z7~l)j^+FGrWoRh#IM0*iuOUpYz4bOd=nA_%m%MCkw`-G|(+_F+Pv{vB+$0WYJKqS1A4Qbl zBQ2a;+k0jsfz=KUGPlCzeAcjC6e!sc0OM&O@BGm-np8?<*AM9wO5vq`psH1X*H9Cc zzYWG{h>YOJi4y5d4`uSS(NWQ5;oUkXF8k!==JhA5|4&)x*4xOD9pDmgB1KXpMO{Xk znH@W0XV*61tbuIoV1Wb(ke2{KHV;9NhXDC0`_BU8KP1R{FYwM}JTtnCB;F)ayyW{% zbxT^iM;tc0tGcVsy-uAvMII<9|M(-;w!%!#A@2}2s)m*DWnLqRT)1y^U4yzV$RBf0 z;aRQTfM=I6>X*=L<-Op+X|LG9f#Hfl^E)^K8=AxRi}qC50DKDvB9SvhKJBM53+fj) zV=SC3aKD@Fr02LNx~u{MSkyz~ZnYVM^E_~V*sY|){x(VgPcj0z$vF48cE>V6R`alm z>#+L~@zYEZy+4I{rL%%vflsE@2UvPf;NH{seF;titWhb>Ju7kJw}<3bK;OF}Ag=o# zoSRL#qua>q-i$R;CYDT6Tr zA!mf@BznpC@Xvp8oT|NNX;dYLExyJ3C}o>J&9n5g-nF`tKM+G zof-}oH&MPIvg(xmZm`^#f?BfI&LXs}y|#^}kBJNt5O=|k0UwcWO3 zR)KNsOT=`~X4@#K?2uccNu%A)chgq!)}ZgMXRhImo;#MMJz6`ayQvTfR|A|kD=eZ` z*QY>#EW*bN{fZ!rr%cmz(Bv}&+>|l528Wi=Ych}QG+E_Fw#ETDtjEkktch9Z;k;@y zL(r-+7r%Xw_K0jcJwH$H`N+J&Ab=!Y31{FUyq$C2&_1M9^et%IjzqcQyWmB?N;;$} z_{i|}JReahPBD)Mzb-EdP~In8{^Ss;Hl#crPV=?L_j02tNugLA5DaVmxRn3%1pqfc zhjRXlF;rPDdqg*kH~S1)z9jbdAHLX2fAQ(_bh~qiiP}u#i_>&7zQ*xHLNexLZG(;n zO-gg-8Zzi+VPTG{d2U#(#CxHYAQo4Lv`BqwS0y@Lk7rmyCr(up31a>7*?=6qS))1m zzkq4Sm0i};5(EH|x@oeVaK4w#^9W?m!4U%A1Z^$K&)H>pO}^uO#!X_@OnZ%pzyhJP zN5)ih>wu~evul7t)K?Sm4nf6BqCHL<#rr~-$zZ$(V%22U@uVf98S2=U)9gl|etv|w z5()A5fMf6T5}A{~IQ%ZnWpNbaX)E#pLzFK}Z2Jz)$b*{2ug+%a1M6Dd&-UQtDcz<2 zz*7NZu!ZZ0g}>GB&}K^(21Fm}kHIV(0$(b?v`e~E+z@!Bu}YwmHP5-i3mMSU1pojR zS4l)cRE3VFT~y|?1AH9M8q%sSL8cAZfJo<%W!^R3Xcu>E%^zN?h>o(Tf55gH*Du(Nc*aA6QzLq_^oMo}aH?^qF#LrU5&< zT@rrwKR1K)ehQ&Zi2@-sZA1D}5ol@?+XAk583ck;gJ^RI7|DsJ*NJ|mI+U5;Lt(wV zGK|a^vCI(Hig3GNVD6jA&t&A9Q4(zn6RFYIg)n6br*9ZT(?2(#5?wb?`VJ<3#SU;D zOh_~iBLspimg!`$O2{6x5RpR&f&q;+AVZI`8bS}XjiZUt50iUKWUlB@>e_zIV4XSE z%Z!aP1u4FO81%}@Oors?#g&L0W6OYE^Phk5jsP`J;JGDp#@LtwdAf&21fx2R z3dR=DfqP2i-R3bRfycYi8cPBp1N?`F2jB#!8=YaW!}}eZGklPNu?Sq5WIkmD1&_nr zn#GL&bHo_g3Q4ECMTSNDUZ-rGzShXHxD=;$4NENJm-Yc$haHA{ zuF=ACqNujQ;Yaz|3a5~#g1DqfI8pA49bqS{pnpgUCJbZR#pvWp6AG#$c`w+A6 zKp#PedHL4%0@J&Z+Lawl=cn91U>WCmXkRxEk6~QzAbt9`-=uGTdW=N7PXF1uOW*$2 zw+!EOv+=UWhaLFQkxK>21d@I9KaS9}UdtHu`-eyQz=s!QDN!Qt;rb72#_zIvanqPx z4^1wQ_y*BRP|c%Hy6g0-&v( zxBxC^^iUQ4<-!1!n#WOKBuJJ>sxjoAsF>AZEU#idQI4<29^zi1(B zu|^^r2BvWXjZ0k z=5bjne~YAy-17PZ_xaNMCl&839~gIXfhd!u0~N})S)e$)2l>YQ+Q;ox`sdI4>0iF; zrO$}j)to&YZPK@B*Y8$)LHH~y!rUa)B!SJWF^Nq6WkAH?!9hA0 zKEW0vNgC#tW4tMjs}YW|)$EEmCXV#kG z46Sa-yrAR5sf#<1t2wp8JT~?>sGh#bU@nb*U|M%!H-$_H z4PmND7fhH5Hl|kSoz5GzrZAN0Wh~>QQL-?PTE(o5<}S|Wy;Br2mCO}*De(bwXC}=} z4aXN{cw8YABwU3+)QonOxbo#O-lYvPtIwxJF@J<0|YX6|^&P(07>P_Gcmt!DSm9I_`DqPhBV- zD0uWC%;t`7Nn=ZTnE^PIx0eVJmjm`l4(G@Bp@T7*(agg^B=;Nr@%!}ZzkieZ1a18F zS6`$ze|VoR-jm(JHWf$a4}O$5DzD%FrbKk1tb?zkto*}oX(*)YedR|bWWZqDhikw+ zUsTnX=Uj=?C}xFFK~x3lR4|9pL32rVy>9yZPj}N^n+=y)P=C()$2BIDi>BYK$i2i| z=mHa`x}M^Kck4u^dB~Pw?p&W0jtQiB10X`@{0I)Tvsz9KB|+}>F{NN+rL7WHtk!FyK+5s+LEn7&*Sr%We9ukbt>EvI?m z9ZU_aG3R=OR=y+s39;#TkMU?@8PqW)>-8IcWA5=hrMz0qHru`ocAlrEz|5-ySA!M= zs2bQj)#ix`hGX?nWYdH()B;`;)4{ep$f1#k!bCz;pjgiFf8JYG?U5NaYkG-1moEj1 zaLSL7VY7y(N(c@a)^YUl6Bhn6CclPxj0vvtKIf(tA`xb3TOc{*n?8X$0V?4<#0eLX zRlK1YyAIfI!#;Tdf10e0QRl$MX|olZQrpD^WvV!mCV0*}z)>@Gf+miQ$%K*rXsi8Q zQ2z!v&zRF)V^(jNYhRMgP&0l+-mfKVtA_+ywBZPGA$^7#=Bo}v&zT;2_u-bHj1-9! zYCCu7{2YfFc)=b_PtnFL1nW7^nc97FupVCWC&LMeIz& zj2ZY(Cwfg;krOFhC1lwVWa?=|irf5|-al9NK*f%q;HWTb1)S!YPL#*36#*7AEYe)i z?ujGPeU{%bhNE{9zF=7ptGZ@70hS3z1}qatBV(TGTxB4Xd09D1Kgfh zNbaVHjyOmHFEGFL(1dc?;p> z8PBvmGQS^9Rhcu!ix8-NmPoDR;n;W;;f47bGOSGEGZZSkfwDN?(Ck+T9|IM-1km(| z-#zE9?!8irH+0S+qyV>xJm?oS;|Iw7JOz$|Rr*=s2|r~$?D4RkPFVW+mIe3DHdI(Wv|qb~u331` zSojLSjQc zh+PuBU@VkZ_oxN^5Jdx!MUI5(M<1BRFZrv#nJ>JtvB-78;{+P$t#ayFkEM|9{>2(?ral!@=RJh<4mcm1CF$23`bRmZ!NjL>IYDVM;M2Mi_wgepGL>aP@Ia$;k zZ{xt}Bq`3_mt7+b#8Hl+_nu-?V7vXg>vyeO4$MJhP(1jFwqwz$`J^*x86|W!Vz= zoS+Y~UQr8kd8+#nQ@&Rt3n#S4vjai+R`f6*q4lS@r)|%M`QkRyMbAlcKg3k7I@JXI zx+P1%NO1TD{v;c}<7dFnAO7}!{=l?(%BR3JcO}!}cvLVdl-GasO^xDh0^zfEG%V{@f{{A@^80_M#aSGWyDwFQI14Cf2YVLZfJn`?z0CUmO$FGl4mf(1-#gF>|hFJ=k=aBi0}i$&Q(jn6v@PqOXk5ArcXw_tuv= zf4X4W2fr8XMKCdfY8FfN+)&vRqegIfDW-I2gMC&Kdqn=IAT)nvOCU5Azk(Uj4BhYSl-_yo+RYZyfQPA!14hq9^pH39SIW%L2z!&MGEUkXg?-5 z^DZe|dPE=1Fn4b$uke&X&a)i)xUy)w^~+u z#WkDS0 zjB(I4%iPZ9Fe+&=$4#=?%~-b8AQuoJhwhK+(ZTF;3r2SBV zNmy;7Zu)HcBoWS8rJ*Fd1z-ytRHp3=2Y_yUOz0No`8I(g_t~L=Y)c3G=Lt)g0L> zh%ullhXbiGMw@7or~&SN~FGa{NUn3Hu( z_=1S9Enq)_al#1}?~>sZI`qR%p~+B<2skMNk2xl7jdG4$o!+T|-`4yx^;nYEA#||C z?(k}2Gf(t;%=c2K!PMUlK^i7Rx1T5+1U~Cy(X?i4+}iHYK^$_7Rc)_=ugz550EgnB zhruQ10cdu!@}C|d_*{Hr7Mar;3}idn8P)< zSeGG>I~yDx9;K_Rt2Da)fOf@s1+(keTJnrF775^>iR82*y!9Dz07DWMI>a${UYvzb z#CC`iK<0mf4>08m{HWGDy#`Is19%%7`8GrgO&)2nGKy@u{wpeuvS;oOBjiGkzlxF6-M0( zGZ5xy5dkEJ5P?x)+!$X%EzaG>+%fM8{7_d$ESbue#=Is_cB)5n(7L&=N_+?-#=C0x z!i=?kd}s|1Ed^Z0BHD*KkO}40zG~QvYZ*2RIBtRa=#Te!h^FrxL3phr5KZ&5mxp*v zcSD%z7<8(|=h+MYIpp9nu#i|8Q*+168!kq7fq?de(_U&jP2)QL4Yi;js=!!{bhytx zNpH_$HO?GiE&lbCUxT<2n^QY0pp~K)+itd*6`1l~ZJ~q{uosI4`hB9D`efl`Pg+-b z>Buv#*r}sUs~;8>16J@Q^=1_NP09nzc7rY0Nf6CD*E$MknHtv?;yGvKP```$%T^_C z-d@GfO<_hA2WP>&O_+Cy)paNHid!Jz(nR+>diMMf!9r{^v==F!Y(oLGU_Dxt-vmUt z0L%g<#l>ht{AYvn2^BFxt_dzi#A2Z>3uz-gd49Aie?O$Wk! z<6LJ(j1|LtV+@33H0$CT=Fka|1qtIQ&Z}reTpEl>yh{6R2SAb$T<6+s?~xBmGcz-g zOrjK4@5Lcdg{Ff_fy+mea|?uSF(si=aU2E@MS^ zgqr!{2tmrU6e0xcMDq?u5#?Nh=Y5IWalabLCfZJ)afbuvj(o*Odu_A}W+qG;Rs(&u zzALNjgF4Wx84V)n4iBCad@)Y1UcG_=#xT)4d?q((aD%6s7nyjjfq*1@CWa<=2#(eD zDcrf6s@+4p$oyt%(KI%i%ynp42N@WpjyoMNW~cxApr-|s>*Q)<6|f?kJm4ezA!I-? z)Qa*_jD0~huoux4_@1Y?t05_I+5~h!ebDC}K^pdLuz!;Fj|S=N{dM~7Z(pYm7x05f z<_L`&C!B*TNsONwaT!qF^9V%c(;!e8HzS`8bN={{P`oGNk66+2%by*jpFeBizDHt` ztol2=a&N|jgrWU9bTUjgpMhCX0oaQfZhAVA$4@Yu2x0NMt6C6ZXjse{S}x(c#t}#a z0%S6S@7+4m{7OnUwSdl~1w`x^CB}P{*F?(^FOZ@pGSc3y6h)uFjdGm1pj)9~dt{L4 zH`ik|3(pWrQH5)ukFrcTa6#la?r@iO3%Fd62H^3ujntBe0eHX1R0FG79P4TgxUY>) zuuFp0TMZZ0TR*=yoKDnHo91^aSdiGj*=rH7&kg?t;{ms9;s!$KC%C1#wmNH79?c=X zcx=X5^c`Wq^%(PmA9NwOpq5SHBW%3q+@%2$Q?%@7n06oSchdJb>n_=*b1uAfI8ja} z)Tw4#K-*x-nLCL zs?%y)wiWogWY0q%2w@(PHpupiyS(clo?`a(QCpe|tPayI@#JpEY@xHs_pfi6w_l(@ znq*AeJt8P(lp2e12ok|4Ai|2`$Ap|?YlQkiSQ^yOfI4+{_=xVyVM!QGuCz~JsdLLj)iyW8M1gF|o+Zi56(kO2k_zWHr! zZS9|XUsu~kdOfYn|}}B^%Q{r!N=O^3jhg#3IG7a z{}r79aAj@WtbhQU{(mJ%uP{J303Gdr>|c)l-y5_y|6yUEqhnyIu14tDIOjvIWYk-`Two>|9yGw2jF8M(I9<8 zLBaKt@7F zMMXh-^Dm*I{P%AxQ1DR!v;=6pbno8C>hOK`2qi>MDUs8qCu+9x{1KMQ&oEcoB2SEQ zu`?imD0{DG?e)F&!y=>LKOD$NC`hRPp&}#y``14JNchMow7dkU7qUA4dPynyLC5zF zt$A(-AxEf7ZzQ8n1AX3@4FaDq>vt52-c(jv1nl|Ib+Z4IS0%F!P)?j@yK53}bXo;H z)_KEne_Hpj(H1&D=OnVw5_`Q7E3Jtp1@ zKD3X_jcPe+}wj&p%zav$B5E2D}#Q*dm4iX}y2D5S7EuQrHR zeN9~6sQu{b#UnHFZEVSnT0$sCJpP{NkKu!-a+ral_CPuQ;C2GVT!BG6QUNYU$*(v~ zk{Vh#R?JvbC*Zfa3Y(U8ynYrspk~TyKiQ+ywhk$VQDw>0gHWV$fTDOlxZ6VkgRN~S zb(rlQxEHP6WQ;xjlh9Paa#BOc$B;(3xMjKfzn|P+MRUxJkM`P1mq}`K?RLKP`T=Uy z1YQB{^f(=4r^vtuy~>RuZ)s+dS?*& z!q@$f&IAd?Ncwn}olJ9e3(a{%nrh)NnMMa{y3%h8umt3pT|D)a^0B!wws;n$w>#lC zl#=|`{%4ht%@A3`nFbw;BByi<1;H>CI-xxZDCZSNUu6P)A1g-${!DW2*Fh!Hy6J-= zgzx2<;opa6`GObYz|m6^-tHN%QNUKk_wxo+)%AQ@ih=s~vx?RRZ;XHW^jK8EtFDBr zyxf;3aEu)o-+81;PIle0GAwFLDKuDD`@~h&BZ_Ig3p9?I2e&6E6;1c+>H=1hdS2#s zy{Uu~6S#>TSO5TPaAa&PET(O3#`WPfv8U7boD02K4 zZVwUDuzQiBZVhbQ@Y3Ky7OO3lQ%Vd))~7Kd{1on6dBIrZ>Qx;IkMCetqJQ(Ux1~JMj~TGn)-&GYn49 zfA3U0I6Nin!%>QkicV{aIcfuhO8r)>Qj%}CZC2Og;FHG!DWmNiegrUM3yUG{s@^Ou zRs9dC;l-Ny9*3O?$cvv%_^Q7$Whm}6Qa*kQ(PP+f6Bdu-?0g8Gg!AYBoE2` zo{zM1%~Gq@%UZAyL~FW?qV8+!TI~9 zxi(Uoa)8HUv7TaU0Tv7(dL6~7wAzy9Jj(SwHi0{}JO9AP1Xi_4YAvZhNpVG_Ula66 z>*IG4Q|s;(g_p*sdz4$2!y}a>R1kw{1hc~=SN{SH5lQYFfHN@MSL`$craiN~zIDz`2XTvs;F@}7L6nf#EWXikRgW5 zx5R+T$hg$U$OHw`kq2~Cul$cipFE=Abzs3nQF7S$;Fe%ckv=<)uTx)pQNloyRdKjk zfq>u>;(3NO(J*MS0_5aB7wDY&GBV$K&R>@*{gB0uMwYN>Ac{+zVvB*IJ&_-T7=Ma~ z(RCu01uvA3t?G9z4MN_j%Qz&-Mv)tca>l;N_Hh9e3{u8Qnk(4Ictheij@)n0Y!=Ep z)yHMY9Tn#5iz6BSO+3-}gTCpzS&jE2Fu$6dZJ(VM3ecgnJ6UrIE_mLD>=0LiF!_<1@(Eox>DbZdjA!fo zq2|rW=P$6wpKd=LysnUqI-J^4jSpm+TWUG*K>`)5jqlAJY0t%hD{+$N;+dW@|=!$z{By~gibf*+gH%~@3cB$(9w_$EC{hUmd{RfWLl zR8w{1kbm#8MzNFr0)9^Ry#6ML5D~z#wk7DT?w^U1H#gB0B?dR7n-Amq4B1x>EE{uL%W zh63q~Rx1Ap%3-|g51ft~yjWH$H3!$bc)A2{=xIui6|JSQF_g6hN!^EwoxikO<*+zf z6v#t|f<8D(oqAsNW&fEQR#QQ#(t5Y|T}{>pQkimHT4lSDy~w%&QVi6zT2vyI> zw-xEC;intnmP83PUJp^>XqvC=473jxZu5VB3?60za}z&KOHZh;+Um zlqf~wmWzie;BAp~hABYho&<@0KIxA#M|`>5snxkYKiR!SoZrZ-d2~!p+E@Am1MMr@ z!+By=SjZ$ly}^7iZm6}>6j@3&1Nl$g!_VLk6Q<01$>M7ERh4wUI*UfM!^%WIIpqgB zKI<-Xvg1?hOC9X=Mfrtx%jd3JDj7DAoD3S9tolg?^@{Sc z2p5^+!+F+nVoT);kpa#1Ki-`Zjw^>n3%fyk;%4tkDb>jKYaL{7KWAj}R+Abe(aaN> z{>9uCnAD){r`4Hc4~*V>^3cevmBbuS=F`+SEJv$(5yAVF-n_cPYReJiSZuVZ1s^zF zJ@uItZYy_VE0uEoA^Vl|Akj~Mrbu}wcht}2>^Z4fE%*zn;e|{SY}Be%4Q-UI-mV&X zl$^uSIFiE0fvMc;A;Jx+2I+a;rY1Ua^27YY)wedOV8rdaBY3=7lh9pZAMbhZVlXi> z9cAUEYZqlZE7cU{38jW`((TXuH0#a`67|_mXN*T% zY(1Gkbev$2%HBmKU9)Ccaw@J4&n}=ugp^(x>I-^4INbDW(!3-xBd_)i40cmKKdXrO z!onb6f0OG_S#szUqwJ>5aWh+WCpGP3T^iWALkpgPP@W}&ZfHJZVb|^m4uez z6(pItqt-8VI9#*reMMybA%Bc ziAn(PI70?|njV z#!&q7t~O;+A8g{tN5~*tgdY!f^gF*K%Zv-eKlf% z{AW}Dpv|7pPi4W1Uk@%r?h%(4SC`+JIyo4vnj;rdO7|?$Sq2xYTa{zs-lNJ1GJiuT z?9*+3t#0LkY|!Kf6IkO8Lmv$ZT^e&(7HStCd<3O8(ed|dynm$pN#e}kU)(^O{NWQq zDFQA)`I`9qtEKW7)1C?Sq!xVW%Sx!vANa2Ndcz^?j^vzktF`}#?~QhU3y+wIX-R0M zf5zCi-ZydRAufKNu*x0pQ|f^AvQ4ftDiIsIE`T{J--o&~eO1I?JA;J^oy5~GxV`~S zRepo=tR{j93>Skrh3JIulKb(zDn;ByB9y=5eb)B3BF@f8l0aZNf5b*$elPx>vKOt7 z#n%5_HP-!d7@ORO&GWjDBwVmz4gha@H+r!13ixK7ljr3k#KEXSYQrPp{a^0(Lc4Vm z6n`&PsNaogp>PX~CjOMzXoNq2dR zDjlcdg25;7On8IkgOwg-YLtUVZ{cR_hMi-9E2ml%0aecgF{&V$=q4%Kf76&pfBoHm zR5Z3hG->eV2T}h!e%l&Ji!11mQZhsfFlpU3+vrY=L}JBP5|7?LMzio#NOlpOC5r(0 z?=cAiU94XLw$IZ+-d8w1$Rx{vuo)cW?}!m6;P?bl@GGESg*Jw;4bjTaB4?(|MpP>> zSIfTlJKlFvt0y!y$^w697Z0pZw`>;s*u^!7uAsG@T1;< zTQFer&B@Z~2kQ4N%S{;|;Q-&F?QvJ}_g|xG=!xt(QWIj}l(zh%JF8fOe;hB0?2$*}yS|)?U4;f<3|l{bH==@Hw_k6N zaAclD&ke2If{@`$t~IfZ_k$*%(~6Lh%JsmsrE;aTfUA5K<7d5^4{TGTH@F~Ed-NKI z+T4uH#P;-`OQ#EN)Y5-`Cb5O2LK1?SkN$boF5Ne0A)0GK`^aPcvCX1Q^kh5?we}~! z^X_GBWK|cNsfY$|L}wF^zj3MT4CN!-OYE>W}QsLzs`B>NWg> zAvG+W=unj6KTm$X*#`KdUWkNlbgR)Gd;;l8tZQ@ud2r%rj?G?Zo zAECgW(qOU!Z4g^tmph*&TwPwG;9dWB{imS3?v#U@h_+v_3F9XAW!Q<~B>T57HzLI? zMkzQHQQ*r_aGA!8Eov_y1u%6zH^XMHIeF7PuJA{fxQ0ou4^)xSG^Fb|tksw|8>iNz z*bi>+q!)!DL&>ZObI3#U;RPSQ?;M2sDx~DeE5|Lf9mMDu5Ygb-W7O;=2D?m5#=QcV zw?Dq=;SRB1Vil&Y(S(1~e0Dz*=#D#LHUR7vKNs1Gitu(F7zNPREIA}%t&RmYA30!F zk>)Rhi)qq!_Cg=8*94UD->DSeC}uAHLb;cxWZrEX)?+L)!2Q_beTQaKCA+>e4-ivq ziEi`k9vWerrZz3c3)aAU*|3JF`=4A1zXD*t_zgw*2KhexV34lHWObkwD-(+>nNU9X zrGU%MNs!3e6XXnm|03d8*zQ{BP&z+(o}YARHw&B$bjoR)Uk+w#eAkYRZ=8BsE2--L z=pFCHcYXHw2b#11Jx%;6Z>rknvi&%E^i(8tMlkIm5yzn1Fw$U6s2g-SSXZjNVmC80 z#52s|?R0B(%?j~x-6+6%yB)WS()tdow}lipt*DjAo>YG{CzOWmm$s(DAH|^b@4FuA zY(2;@u>r0UhqM}i&#|J4g_9+4u_HF#`WUxS-b65m)Aa6WXu&k9t)*pSOBwD%s?8Do zU6fKjnamKdGMcAn7qPju$`~whbISrhO8n;cbu+uWN4{cZ!z+_Ee`MvfZ(3UEfiA&bF{Bn*DfXu7;qn;%aCL8>T zGxh`D$nWgnzkvp?31R{Jf zjoqrnthk{oltJLffSaE>QuF-b^Rv_;71-WR@+Ydid*T^!{9vF9_Ai;Ku~dhniSl+M zT$_Y~^mwoyrRu4NGx$xpqduP3Jjk8KeEU!OV>-1z*jrl5^B#& z)2r*AOCguteT*TxfS1g8^G-gKOMuNsaArNU48}|)>SEqE9x;qiAkD3nog7{I$W29r7<}l4RI;0ZKZ>3gDRuP$+ePMG=(~c5 zFJPfAFJ=-gxuPD6xr(Xsx>}?p2Rr@1$X$?=e$Dl zhy$&-y*}5?VbG6R@b!CyX;Y>XK5D9pAG=xgvCo_A2aj4wIVdHqgZhoNl-R*zoY4ry zPijh``bXLz&EG*b0@jM+M#WJgQGz4BTA zK#%c*BsjuXwdFu{du8~z_tY!kR7dJ7 zl^Lb%xn%YVV2WLt-Ml>H zJRNo8ABAit(5+I>kr`Ga+r}I}Nu6?ElJ+hby#mCZEQ_rxIMo6Z0)t_F9vz!d;@n-~ zHTbfbKj(o=%NQ-+pS0^Og()u8wS9-*tyrLXIJhqqXIS?vq{GE-!4;S?;egu8vXp$Q zy@K1|bGwwccmCHG!9UW(XLh}3*Ac8}sXIovSW-$f&zkw=cZ3~iCuD8-ay$0lA#rcL zMI;Ps|M}8v2~%oP0(O};y=4g$AG=mX4JF~rcm?p1t9-I&l@kqRR^_@h1cS1CU&I1? zrK^k(t7(RvUv(N2Unq+jf17};BHcfCszvK3R~UY198i#t71TLpu0&$64QeWPc9DV6 z3zvSMX%}Fc=CyBSmB84EJ~r(bHA~5N#1OSwk_C_I{-@}sp)?-)g1Y18F)8O0lVO9Y ztYf-B`Vdh9Eg#2T{c6Sfl3UUEa>#O?%y?qj#-nJKp3aprwl4}?CTwoZu!kx(A%d5; zyA1aB4&Y2!^+m~#8Rup5X0B3HaDl6_hN;x4)2Z0EJJy37RBWBqZ>x``4qzYNCm`AK zD;Dd1J7HK!5TO4&PRi-OxTE}IVQI?W>K9|?TW%R!6>IgZ2(3PR{j8tptnc?f1z*2R zeMHXmik%v&_x%;oc+5{07#mC7hgzL1bZpU<6mlU&+tuN}Jiq!M)rVLX+C(K^KAsPR zUrQKs!VonKdf4j2xx{CZJzaqUQWJ2g>vd=#*KTlZ;E5Uv$2QBA8SIAnNWg>MXJ4)a zXcHWuOj!M6r%jaaMz~J1wZ^db!EBox+*{&+u|aIoP|_?+oYLOb0c82l0ic^2BWlvE zmBCxtBbS{Fm0dm!3`HjA(RL=o%I&Vt-6i-lD?))a08B+47+|6M>c z&pnRow3M#;(ZsktA5tTvZ0ZVYcdz1gV|dSe8m7WCBe$ex>Eq6akSYJ#u3dO5jWL8a?lXB_aVLJ~Jqt}xBy4*e9= za3Bj=*4jU&*pPyI)zmfW2F|=av1?aTGhTN%B(uQBBiPU2JVLHQx+5I@Lgby=HltE9 zwUhtN5hn_~Iu16i5w!mmDUqN+Aovlxn|Jb{PRtNTHyt6uN;vJ-WTfS=8hdDZ5M3>r zm!q-+7ddhFS+LID$mCeHad`6JsOm2Fj4wtiI%G~{ki@sbACJ?m6A>sToTA3pMdJZ$ zWlD6%x#?xS6@hZ1Dyobf#AhH^U8M#~dDmPv;4Y9!2Kqh=qVXnFY}Rv5g$|NfJ4&*# zb+buBr6n#z;9_@jxI@Ffz#6%#vH3n9_dmo%$@MyRN!WV2vJnXh<9TM`M-fLl0{R3# zMw*?!wG1S)F`=Bqqfa~o;YXAF-vVgL6Hz67n6ypg(Gu%2{1-Sasr-MCyui&MH6prQ zcZo0OcpHe-`dfFjDo3JfS^48hBMJ}c_l{j$AB~QE`dAr7oRoXjI@wn4}FpM*6GBU=WjN`&QYp=Cy@$5?qD z+8#3v?47Uz{{}%ze#)N1!;@(-SUTkP+xz*3fiuhPjlK3QPU0!#Q}xtg@g^JgR{(7V z(M#ndsaS6vu5s-{QPn&pCt+KbxgYZ>XO4B1Xn4WT6Z8p)gKb%Nc=C^&T*Ue5+C@EmN#9Y zlETm3reh`6+nbh^6(A(S3&4MwQB%8HVo=QWs8_(XG~(_Rfd2kvGb}@A!)0EZt#U}e zXn6XH+V-O>S*M2IT?j)+S_Z{GCi{bU;SXwSefjzD2Yv||$;1NHvPJ1dh(bFxH#YD` z>>AtF?D}mG`t~IzNMutBar6?k^0LK1)w_#ICD2kr_2Z>@1;>K(?_{Xfr7F;9%Sc$d zYW5bsak1fwtR^mOe9&XB%4k9?U*iMG{ZHa2=HFSX>9FNmpCzZW*<2HE=fv%f&t#PqP& zz~rFQZSQg7mWD#I$iDL?1Pu1^_&xSOto2Z>qaEN!Pq! zB>F^ZheJ29NTIG+Nso+(49QZb!ovh)bG*_xYpwu+!=PWrANRTzJkA11pzK-1DVCIN z<-{&?)`5h+muOBlZ^;Hm)%d~s$MZuVA+CTy{t62L442@84L=C0L|(B86SRkRz)U2j z)CYg!7i{p4N;0C%ZV;n^1G19(3Rpt7Q*C$JQiXMOb>6uic7{zHX#V(A$2loxdZN(P z_727ZxFL!3xv)gn9)dU-924;x+@WT5iYK|k+#2p!U2Fr5)#oNlK1DW1+8WxW3j9{6 zJqFH{cJmlt33vGYFxWuvdC1sEb|L4S&NuXOHESbJPYbEiOziqkjwOb1!v!|4Sptf= z@x+#dlq;c57P${FhHW+wkzUV#`jXiJlpivpm`bo9>-_q6wffB6jA^p-Ywn*U*Iu9n zGn1Kvz!6pvg`pTL{FnXV9yI2%`&(O*{IsRWkvcu9no#0e!h#bR#BwoA7klxRESV2w za4iw&XL~Y$gK^xCfBUH+LIvb20Tj0y;`<}r;Be}&PxBW6@>m|~)%Us-{d`eO7OMJd zUWUq*gq%~EFH9acd(9vMIa5vs<=(NEYD0qtazpQW#BYpGz%gSJPBOvoC~0s>SLh>( z%gljOo9n{qeaH7_JuuOwM;_=nh3Yt~sIH*e(yVRoLlzsT;NF`>P(fgBC9nr|le#pS zWU^LfZpQFL@fJYkQ~)WJh9WgG6?hVo#)Xn$sd zr7^k4hc{=8yG>n%#U!9AA9th7SgkiE=uxpn9;0H6C%CnZeQqoX!964SrfZD5U9PG%)8A(bBlQAH9$3c>BYiCF1 z^7`4yC*c)CVPYc?Z{+bT#!jOBfI_88cs&(=H79FzF8;*HW$heni62tyojBQN?WZ;Y zRkPV!JHqf4f(!j4+K|AC4aY3c5|g$7tP+-C_5!x{9}C_`Ya@Sp8@gSvK4sqfLh{V& zmsj3*~cf;?BQ9_RqY4$dJ0fJWr$NoXnU&osp zPfxSx6>XyolNlRXdW!DqaQBh?0tMo*N%mMrt?W#H^73BR{|>pVQx?4^PIGdFha~x5uAilv4@<1@ zjKms`V{4~+6N%}oo_QIHLJ~j!H6px#=6qDB?EE5i_Y1kjzzOPR68kK>3fb9s44)fk zc-%aBn;vN=j6)tCro>|Ro{T2Oi7O*1(KPHEvg0<{`hz5p`N-^#bA?3>U1x=rtsv4J{7C>w25=qU7lZ zF5MF}n(4dzivwPPE!N)N#6$AXJ)AGple%4J*Euq)n>Cx!I`Qa~PrX~kH}X?DzF_g7 ztx#fLwk*8@MndG@h-pYNe(bC*nCx66!U~iyRdXtdm8a5>{isPE2{NFwYw>kKx^X0G zFlNaAw*EanULSvS5W4{FE*E{SGV28bZUKn9dVrc^CZgBqI-yK^l)>Kt6p;efEY5>~vF9ERDCHAjwk{{mIFJ3n!BW00Xt&Y1-3F8Ib%BQt^a+(XN) zfZi{*}N0g8s8PJ8J7}t>PJH4d@p9oOTpm3kq#Tpta zzZJ6N+vqle>M<(tK1QNpg_8fjo_t0bz8TNOH9o{-rKEVBt4nrf{D$zAv6TRuv3?l; z#pF(+s{*Emq*SUN^gz1#tiRglx#cN?Ub|{iWk)`2=x_|UNPplZTk}+_DdyS|FWAQK z4X=KGI~Esk{cY)Kk*sg4L6jt685`poOb#DBji5J!XRmWc{-BZ<{!?Q@zNpixy2#4F z3P{UAF#JwT#H^iTY>s1#l=ri=}8ANlEaUSk$)ZK8I_lpxpIzxxx0jH@WCyKNG|Z$>z3HmnfH7 zmM7HAr0eJoePdIuo+vk*tzRi~L&b5euW%N3+aR}C_i>?e%JxAc_~GKgVUzrpz1+E; ze;0?xr!x=@1(MhH-os~}ynUC$Ky0~n=#(&nbHUq$&=D0!m3_!FgJ}uu)SnbtxOpmvfKVU0P1Tl^JQT0Ot(f z1R$oMdS$1%MDA?{o=yj_INC3Lf#-hjlv)b9TFxLVW_DfoO$*7Bpi=*oolNyLRC!JCA=*I32zC#j z;|Ajz%jS^38}(a4XzU)UxYZdTYcAZLCeB=O$?+gf0l`Y)1tIbFI<49LvE$`^jhPY3 z<&izt&KhXDJVk9~Zb}PXnZ$GU=pu6_r0gqo{S*49F~ph)z`Ch>t&BL&?5QGNv3Zby zDVg0u;>WbqlZShEHrEXtw(PApni4eevGFf^&*o+k1UA%_?rWAYsLb5f)D>ZiwD$?M zIaC9UJuNqPU$Q_dbOMaCVaDG}s0ZU)if-8 zC!)Sx{?##4A`>Q_hvA#yH8%L1gI0-u$;Sg#XU zH@O*7IPWaKH&cqd=$V$r+SLp=E_OWrhnOVi1sa;2z{V`sdmd=LSQXT_o$C2{$La zvDa~S0?79tT9GNqo*X!a!8K>%J57I9y=v2Z-iaj4zN||}k9G2byP)UG1}1E|r-kno zASy5$)ypwopomxO?LguE&7LW;C~D~7=yKn{#n5)f=jyEB<}Ih=-^sHJBD)^HjMGc` zU>K;SANl$WZ4CUazCO(ardYX(KKd<@PA{vK6ZmEu#YQXQ~bA8aJtEnC&CUuG;xfxwh zL8+fKcbc|N7fUyUwka3A%rqF-rs);<1a#S;a_(TG8(flZEcglPxT_-)Oxg)*yT0lA z2;2~Mk!-_p#sm+|YC?I%)aSb_YCpzCLeycdt9ZmhpP7l<}DR=>555HT4}3!wYypPF35tPaY#i z4vKphjA;DrRrtQQR~$K|E?EazS0R;Y9MZU=)^5RXGnO{#Qx+G$X(&iTIlMD~+2m4T zaQ`FdC_xIddWJ4o&_GNd^U#xIZ`XBX#ge7Q@BP=!$w02{rj{QRZx;gRW7zqbH0E#` zB#CM>fnx0qMS4Dn>lQGNnFJ~%r1p6na>{#3unrHb2)Fu>;s(;}D9r%3Owsz6*ox)p zd3V%e8Gwmbo7g~8NgSEv>;-vU+#JxE&5rN3(AYbdxK5$EQhH;WV_sS&0+}?Jd6O}! z3wfBg=V(>dn?TX$z^`3o&40_>!{+D5NjZMSu8ZPAjdiTis^9Biz6sEO8;Sc0U}5L@3k{Ptas_#h2iVrH%Zjk2z(C(NPdp zH)iyWM3X|OL2V6gpQQV|FTQ`E-?#Uix}QR?X!j|$v?7m2@~=;0rzS~T`LsG^!K#^)trjUJr@_`5-P0Vj;(LjMM()ZyeTw%W0|8h6HMT3wTvs>wL=@ z9Ib<}JR^tObTilWJ3Ql4vGd|IlT^o1dh?0;Ve^*Bnz{auYItoES%shljpf=Pqok~) zWTFkbW6oL4sqO+x<(;hJ@^G=}O#4Olnlcu#>LpTK5hdddLr$W=BmSx`DCk*usV$pp zlLZE4>`bb96Lh8UplOfUP_+mid&ud&$lK4Ag~U?i!JcK|_WRyN?$B@+MpF~(ByEGm zb)S|ClAo59x4k2N^Ck>3XN7;;nxTo44#;-_eVCcekrphrV)QBF0yk7Tf4c{+tc61I zEVt`JRr`AvPn)5teg#LpegSz>bp&HMml;Itb?fc0Bnps`D-3XU`*zc1Q_-xVr6u)@ zPENy95HDWl6S*3nBW5@IpoD#umU%$!~2H=ukB zn?k!7(v2fU{x;_lgZnrKq7oAJ{fXrzIQ37z90OIKRKpaB36Kj#ba@&_%DV!uIZ)6j z{wN0M%fNlG({(y|8|Ug%wV1l0RmOe{Np!k6CR?OVz5ZkH7Rfqd&G6l|UjW)t4S(Y9 zd0^qjCbe^(pA&I-oqBu`&(IhO?iXE09J)@Uh~P3(L-RU?#wOS;qUXbUus+O`r}d7q zBey~&8K&5w<5*t$J*P2ikVwt8!2A)VmqVxY6>tNCMr&^O-Z~J_`NgrXG5IFg-goJ7 zd}?CQWs)b;=Tt)o)Lh3~6l56o_8Dg+C@VQ`-8z{t3JJr!TP>C6H}C_oTe@1`WiU8@ z=PeDD-BD+!vQzhZ>iJkMt_zK5$g{`S+=W=zTbPYCDq8TBEQ%P|f2lhifIC@CWnLH} z1#T^@KTms5bXDdwy7nX90Vu;P5snoSj+Cl~m;u()2)@lU4Qb?1KPp#(r{41Wfv5)lU)ek^C7G~upsc0Xe9TRzi53U>~p zth3wii{`+Q2VlFtaqKGNt9tV<6 zQxnaS@<~0BSTg^IL~`dg)4R}HF3iq{ZCEqkumi+3%S%MJSP9i3kBDAxil6WT{qMY< z7$k@Y{PVUx2E92uwq?4gJ`Y{Wz+?0d?*^E*A4&u+g!oKG{S}ITVP<< zj)y~+dUNG@d%Y7XA_H%vSreg|o{=La`~B$H5x8fXS?zI`&^tT{xr;N|DICw(O&wJw zc)%3PuOnCd*?+$<2vZ60k~@C@J4BmYSW6p@iPE0NUxS>T3!mbLXhXxG_RD_du=sJryJ-pml1mjzoG4EH5{IJ zWFPEsXskdwY54mNk-89w0O&^b&J6lzqRTwrncIC60&h~}ZXiGrYRCnv@t8W&8rI7# z>MyYUj@&vDw0!(JkK?#C00B)*`olm_jZT}6=I#Vwn0>Sj@bfQ4oGWqXk1Bfw=V#T8ucgknlvx~CF66f-JYi>- zL|CYu*1^30;oW>r{G$Fj8Z}}ue~B!^&W&=!knI^wXdy^*>s&I@Vn)q!C(61#kzaZ` zZ_!RqM3G^CUMMgb0 zS!9J9RQ}yKD>Omj$C8({!S_s^V@%>Wy-^hsSR|z3Mu%FkYOt41_$q1k_kP>m1NzoF zmrX7UDPyLnfvH%eeJo#>zmo=v$_34*R@3y(S{2LlLI0Q)n>~k1w$@Ds+`Y3nu9ar; z$^*1P_@{ZZJR6e$2>>@$Mfx!_S1uHiUHjWeb?#EK=qz8>XK16MlfPkaTPn#9d{YVc z*l2-2LN+Vv&*;857hq%0vts$6&;U71I|YNk6EC-R!M7q954sC zP2grm{K{=i=8M=dT+IKxh?Rhl2%-@6;* zSWlad*TUrOdpA4857SUCW%A%&_&ibOge0eu-ZDEW%V0X$d!psKCke;*>a4t3J=s)} zBb#=q4kq+TDxc*S=T#~doXl$_5M^{Kq{lGwu8rHXlsCU5!aYZbu3l~n=;6bLJ7(nM z-@m{()=UTf+t2*2N6A)rP_p!L#x}(^%M5iB&eEKwkRur@wzV=BZthUpjHvmqbOmZL z8PMf(I@bmR^%pcU`xu{GF8uj87cNhn4KGV9X}{1Vr3O)hF$+9UoRFu?kVy zbqnW0A);@Im4Wa4VZR~p$3M4VyWaMVc>{b(bQK8M7{^6~SjG&<1Y%XM;Mi_pSazP4 zW|F}Fr(e=BjvI+)KxFfqVs@CJO_o&lP_7jEi@hdI=>afW5KuB-O2ej6M9kr z37&!9&g$8jteWJI9NVKf#B4O_lFrAkVgE$xceAy6d~XL&{uAZ3o;z|I%lKYe6g6}a z=YtYR$1sYm=EV`>Kkl5AMJQ>lGj{Vbw`Mps`Gsf-FSL>uVhPg_14+7I51Qd9MkFRL ziur%eU!ToqyJ0snqRb;M13n(nR>!$>RO{Wh2~+$1yH*>CP^BRyb}%{!#UC^0)>9U< zE@5n~l#N9BWqEdq${*qkxxwA$7Vi2=C_H&z_~uw zg>mE0v_X-~LId?ipTa+y^_Iiuy&D=_&L|bg#9<#4(fPB#AB>ItF93ExiND~|v~Df% z@F$=9i|+^!_4fU$;F@VM$8iuGYg*4Gd@xDbZUa^BAkF^(aozhSu?42j03($Ay~{W> z9wz5W&)I^KLIg?m>*`}wzj3#SoWD-e5-Cze1FPrZg|yQdRaPr@e)Rf*wQXI+CBEms zUPjk;ay`AEYiJ=(lgFc=nzcyHCbmD7SBl_Lec{?WfZJzi_h#^=w*9!=#LLa9;?^}O zBKY;CkCm0a%v+&K$_rO%_P>92w)JhkZPr_AZZ>Nm5MJHGGSXPuGBn!E?^-d^SN;=R z6?6jf%Y^%n)qBm-*K^%3HpNY|T(&kA;OJYXic4cG@-!^pn<|~}8~Q`8 zI*-Y`Zr1km%NE;gyN%VnD|hno#S+hOiso75Dk3aol2+n)Q!3WDvdcDOX&_d~aeW8R zN!)vCuHR}WlwZ?xMiPSHftp!GNpAZGPZ+dTc0_Y5Z6>N1$) zbLbmWJdw9nB>+m{YW38p#A>B7sTqIZ4uw}L!%3;{etv^4NKZlQhbap)(Ll^G1%Rgl zY2Yz^Lw{|x9=%XBQsB-$uI_DZ+t4b-)rwisSkCO0)2WT$LiA*f`n63UW2kt?prpITk*cja1CiN})u$C^TkPrkbiwTB=tIR$nu=mX8SU-D(^A znL624Um{YQ^z92+^Y8D7OCrCLHfEuru1Ct4ZW^fAZ&!GFuwzFXvHf}z#+5kPR?dC6 zjWR-5ugx%_;3L)U{A+@i1XSjow^{j_ZW3}H~$Kg7lV0O7^9`-+7E?4h%|+%4?l zSqyg<5yoYbLCYI34D1p%pitV5M=IdmTSj|XBiG_NE<)RF?Es5P1(30MG$O>tJdJvC<>J@_|JD`3{l(XFYHS$e87VpdGP z1$Cc-*WZc6r(;F!l~uReZP%XW3!UL6kVZh4UrnRaB?6HcEUH$3smKjknnE3oBWS&X zJ*+jvi4;*JVWg3kIKg75qf-z$nW!piPqzrAr4Dt5S$5lqr-nC;s*bf^Dq;9knX}cV zN8ER7NMM}2npOpsrmC{(NK~|pa^Mtk@$ka$k~S+IHo%hI+THDNU0X=6Z!@b$ENmV( z1xZ~jGGN0xt`x7x)5aJxakhqd#BWDdT_&K=Y83FH`!MS0iQXLUcLG!@Lsyt*T)aNq z1>elY;rdMTzHXWx)!wJ-G-a%&jFdV_GO3SFp~F_HV)|{%%zXa<+ZRUHxHkL3vXfY5T$>+8?_`x!g^-&aq1^h2(2JyEMf?g>|6x zM<%b-XI@<^kr?MJ)qO-Z=eKWB(*FQj%ctiHkH3FOhHjUASK-*;>Dj(xLri)>hK zCAhe^vACF(SXq`e2%@HT3mWJ6wQv|24h=5XVBdFVHu3g|Rzf9Sw%lwMhk~qwDFHn{X*fjSL14puwF!r{^x(+ zq;-aU<8qO}RtY8b%Mb@L(ON_0hNuCyM0xIJki4@=JCV%txpqGmB;agXQa3D>QBgsN ztt51ThI5%5FiF86L28VSD$Ywtyms%^p6cywlHa#>50kcrDP);uHvLKU%tJ9IMtJy) zxiBPXug_(=&H9qqT}VN%Q{~@HTQWfk7e8hx*tJqhp6lCHOfqi!bUby!*H-ZyhvGu1 zibQOFKKAa2SVX(E%S##NNo4D>{-G|vELucYfQo%b?xo`_7cNL7%LNajMSfBXM0~ zNb}E^oGu075AMmmjh}k9+!2p=J+|8(U$-r_mZ}3^^X5q;&w<6!*1s<_pQ=SWJ8Q;$ zzW`@;-R+i}ZT08M34sY$GbYl(z|M-Tk56JEbm^ zOzH3U_V9z|tLP%OmRMcPHvyfIY{urcABAq%P55w-ncPV2-k8OJH8|9-1g($#%fUYMfD~Rc`yQn74o+g3w9?b9@ zhQtpsuF!xq|5F$IDhSXTaIFr_7TyW!&ED zvKr5Dxk}sKDY1$-h;y z;OLSUk+%emu4p2>hI_llF-0Po`Qi|mW5i3I=($YkXl+OTZvwz zDbNCu!nCC|&ONnQhTPLHx+RM^QPEKeItctn#c z$Q|WYxOZIJ^&Q%^9L0`pwRN?Ms)!@pbPe6Tt>a7ju)$bOy!0{7#15mXX+{fHb!H5! zn^Ox-kLBPW?xsCtB<2@~vklDQnehE4Vf7Y5y`8($H885b{{WdHAmd!MFvP(1FD-O1 zJ++}p^~fsfju4&|WGi2VYCMi(JZ`HqG-!5~KQY)cGP5%qJKKZ?gd@PyFg!eZd_Lc2 zE#Z!!sH@y8UAX$subms@g$J{37> z*Pjx$M9y|Rtx_3u?SHLz8=d?cS-oqk`^j`g6xQ@As=D33N+@GtrCTnhtgvp@NCv3h zh}o_0C6NS-QI)t;A?%^4!m;%Zbq4s7*z}u`iYVsjG^;2y5i*vj6{k6jd-LO#4fQE0 zjr(~Nu}gq{>)u;N(KB1jvrjrGW=IuD8i8P^TOg?v`$inHrVra?cX0ZJ+k2EBExGpn z<>VoQtXEN3%^tKTLr~mEE97ZWiuZ+KihT4ZT9t;XFtk7;s|4^+() z^u;_2(@lOjk3uIQ&63*Mdq{1Klvh_1O-P?jCufp~v8;%eITt)gEs&+U>Kd`*z+%CJW9_cb?VFC>+U_ql$SyDEwM&Vv3pryX z>#r~+ktl(J==9~xx~qV*avf?Dle9ZU<0&S~XaO8zR)K-@)dNIH_tzBMg~kE1{W$GO zd7y%8R&NOAXH)Q(sZWJ4RZJK+iT1lxj5)cvW2ZHQMtrm9MgpGGjCa9}IoH+N2Gc#> zkiBFRvhDWL8;Li~#xbRWLS(mwm0cy1L@6n13h}|aRyVh}9(Hf(UjG2xa=~_6TS=j? zGTgo6hq$?r$kI(C04maHr=(YuY4P)_zU3WTuWxY0TdOJ351sq^!u$8@i#v;JCYhx& zG-f#M?$=U=3dSy$q|!NCn!HIQl39&>sj>I63kNz&1I22s z{{Rud=r*<=w>wd)%-4u$C}K$E9^O`Xf8xOCG0x&*lG;Y;yw3z8ptDDp*o^1yz}Z1` zX|eSd5e7?Z@t~S=$1td_L57S{PIVc0jUVbdixQ}nFZqUGDom_CAuE6hR#7tAR-Q$g zOX(+(PLz3?Sr}Ky$}3C`OY5lKp9Gq8Hn(>lOePC#l1FX43zuh|OUDU;U|C~ME2>s2 zrKm`&3WHo{R-6`xwUd9MG`>IXrN@lZiu3;fDW2Ybd8OrqEbPrGN3WnIN%+)Y@jaM5 zQia738rsoKAE@;?@{0ce#fN>v9dMnJ-pJ(`kx+2WLHN`*EoCLJ!yD*C^j?ixnN-x* z;1<7TDdd2@$s#yj4YKLvwhOPN4XpZDkt0YXeAu#)(~Xxg_<^oeW>+ALxL{(E%M(KK zqCAJ_FmBy{e-)X<+3%h z$dOtyN;DM;=1D(Ex}n1b(s_Jo;X%G8g;yeIA-Z2k*PBdm(V*OJHx5{pqY9?wN(bvT zK$1DeK%9j#W2XeMKHa-{i&+%82y8d;-)whx3S>bdt3?}1uCa|r@W#@jGtk#R4{^n1 zq*}hRsbH?aBDLA9=Sl^U?=?xNnloSBNb8@I$k=MEqN`|ncF_pFcajd_B!UvhbV#Wq zN3W7ZKtfzbt`4DJMJO7nOe+mlc~`e@P4`@Z zMW|e6qWPz{r{nP*-R&&B(_fkZeo$$K=3149=`#i+ndLKR<0S%oPVC z@jiW1&Is^0)nx)r53xjBg^k9~EXx;GS-O=f2!>byr~{d_^q(T9!;N&~7g1NuSka3A z04zk8&8xMxJ*TzLZO^?&yFq&GcXK3m6F?DVjUi~H63MCR6qTS>UNqrWZ5WV z&)VIm+uQY)@g3B0C7g{cq%G8qq2qJ|<>_$k%EW0Ne~T<5?>ji|8C-Crw%Ye^)UM_3 zHnZ4MZ2thK)vg-K?)z1vT|@~ntnoKe>0MQnQ6i5p#j@oZb7FQAjn=xfUnjA=6QKRE zu0*AyySuiWTWS%+?ysR{jfiQKN2RJpph=)786Qad*YoUa-W$uN{lB7v+%KZ&CrVp% z5yhe#hi9D=q`CyHBRlKULfV*`@i-=$WUZtX1>{!8sVGKX6naw^1=J65x^1z@!R`@% zl|xBSA4&&mU|uN=-U(Y)!PRHM%SI ze?$AK=4U$xbL`h}1l01`Tuaq*1!&Pmr3f#VG_=DwmB>T=B>JncvE3uOv5pmNq}q0Sy~YJblG-xrtO-)w05qqG#8nFKK@D}fSl?M%?oeBWytSHk zlG0~%62MV(AeJ~>ipY)UM)nhmwVNx7jTGUcUI;pG7(*t;4Pw($!ubUxFcihs#V~Q&nuN>AV zm0Y5XtE?=CI#g5>?mP`Vw|YVvu;WfsaofnE{afwZt=DF^+%BY!UDCu!rUVLEp)_fl zHxaCbQh<=zaP*&0WX)=&$kT&`n^jtN^Y8d~_k8&wXeDS;d80)+3Y7r+fx{J~XA$+f zx{U&xs9rZUWOhPp?rv|h5u$QKu*ZKG1uUVRB{b?ZDpWB1CDbSf-f?`iEA77#*(SL$ z2)}ByLI#$SC}vVf%TsjZRQmw_I0l9(kKk_BYj~84jkzuh3W6oMh>!kh`|&MZNslB} z0v5M(iA0cEBr&~c7pTAkl{GUEPBQ1BY7Un}men*1S!O+=(MdUh#rs?cWN4#^#$k>k zYFT7=5=Tur`?46ol^r9N&uH=fj;2g^u6Ex15}0kRW;_zr>+kL;n~O}B4IGOCviL|@ z582dOXN|b)rpGGxYj_(MNhL&f*6}4Y)uihZa^?oK7-Gg{=N;oL>R7Cj#3@iniCh!$ zT>k)u7^n@hrXpMU& zdqtk(dvk3)y~MED!!+|v11_ZzykS|DhMhE$j0-RIaicD@Gbt}!wQnZdw#bUJ2rZ;S z(oW2}`kh?CaU~SU{A=nzXjw}xk51ksJLs9Nc59%n%Vx4oAwUagouQLUwJXWd{9b2) zt{Z&Nkh)`;32c`45bc*%@Y>Bjk@{&AGF&lDRsA$WGv;Z;du!q|JSprlxfz0F~H1z zqBT1+dk#4yg55#O)$`WGara{IRvCFMDEazmjl8;QvJi4)H3u7{%v?U68(U80x4plW zZSA3gXOZ85423B|GxgSe=lsA zVWUdeFE&)Q+w88a?WMWAL`ImxHA$#s?;uVzJf!?F!>!#acWpMbmYWTew-QP2HpLLW zriN#|ecXj%8w_fh?vt90Pw@W;-F_j(^anlNtRudxz z)XXa=HClRjD%5ideIsjR>f4P9rs4F2S=`%2d)*dD*Rymj7Vg`nBRMq@#HQqRovGFU#_zlM~^Wst({ zUO_9t%Q&>l1WQ6XiCpPHs8r$nVv;JZY*s#?$Q!$FsB1*E8CHUpu?;!?Ur)ms=>Adg zS4lc+;&BlMnvA-5V#_-b2sN6Ok2a=c;)u_qc2LQ)*hI<^qPB)27PPEr7suU#da!F< z0KZTk-8*XM=A6X@jFCwoQ2Kg~61q)m>ct?Q#Jg_g+`ET;G49q@mb-*IY_}6E0%etq z*AUWDt_p;XFtJmpSxHqsnqgMPi>rf|KU@3KWzwT=wdykjdwW?LK!%_X)}BPK4D|dl zQLK?;s3Py{7kWET?~QAuFlC0;&q?;Nk_iY4>?0A;y$edIg2#V%MeK~@xOX{749t*E zBvdSQQ>O`x4^N2m_Ts*f(&elk+BXjT+ywGj*l&B~z0D(coL^o?QJK>#^)$AF>i(Tt zxM6q1t`^=)jKSmmQ!M0x6~5r!oNJ2o<~!FMdjnffXzvo-#RPjf>rUsOvR~UUsI6DOjt--yHg;YiPOlM+&mLOU*4Eqnbl2DKI#ug% z+1G+`X?C?%lA<|js@0`CHoTdiTx}Q923tEATu^G)Du5__!isG=!K>QK(41r^o9-G;PgZH!(?1=*fwSkmE^lx7WHl8q=tdKq-v{a(M$`*2QyH#?hTzFE4fA`w54Qp(O#pXC zWWDV}^3Z?%@9oU5byAw0APnUrQW2Y4>hySiTx90nkLot`m}cAjzqwI^y!Y*y#46C- zTa^l$kSo_b*|;Ap4qT5PncC^U9b2huUfyN0%j%SDK38$KXte1Lv9OVKGJ~j8CRyU~ zbt#zopToL6JA1}Qc*lP&#ir2&a?2{pUO1FOy))`*q&$V2QRNkL<(lJ9DLFYRhQwQ} zyLUmi2%uM%Nu;$)cor_8RbHE1OZuiy(H#2JSC>r&FYU;?N2EWuY_eb6Y+J?Xp5EZw z?Nu&{gWL`v z4#zOvcf>{%j??9A+IW;L8KzfFEI%u*tz5oeo0|`5@_|wEmR+tP9^V3?8Vq5OFAXis zxp-u8>@g?A@SRoD$k`@&7+W$i;wWj1{c4O)w|S@WjXdkOainYkTW#4Yj^;ZnV$Vv5 z^g^1+@*1(@-c2C)JVRD{7Qk&SK1SZl<+kZwO}T8*V7GSDUIT_39JUe29W3%IQl_BbXO7`jau%?e0Fq--#4Z(Im~EU6 z8ZRirI6#MKB-sF$P?RTFb+5436D$7!tacsBcY8;xexBf(n~e<3=1~?u{TRoIe5R{z z1rZd7Q;fVYNEf*o{>&U`S`jfTZy^~SB#L-oQC2>llGs_xC8~O0c26%!rXWd`{_TbC ztgWpgQ1jnjL1hwh=OxSON|{xo39Hmvm^$bMszlp%d(ESMxVGF-t>vw%#3o>xBz9DsL~Z{hgzErztYadw3#Iv6f=m|Nb5~B;kbI?m6{`88vg(Tfp5hN z=KM+1?H^8icHLgn^VQRzVjZR{I}p4{u~H zE_y1=VzjpDEmV}yrgLu@Jh@QPlr^Cky4tB|i0Nm$EBmL{n@HpXYud@TDl2h)aImm% zWHGGD!-xFU{{U>Y{0pux5~scWYufR=k&lYdyoGWj^8J`yro%DC z9F2DDdtU9e9n#i4sy*Cp4IBLG$8ByceqBsq0>pXRP|B)Cbtf6G^7ifKY{z$Z8<4k- z^4hy{@$AyYrrhC<4Q(1l3C3LXOlQZn*GLY}gywq_7TVDSC`u%pTzSe6) zG&9K@linn zT(im}k1}-BYDP#r~G05LC+H2G52ydtzNhAZOH5i?J6HPbUPu$V? zTiIF?)4#{P{{ZVN*4i~B&utN$mOVtW9k4dgXihbC&fL1+hmY18GNx9vw)A};W4fV^I zktK}@iCKcVDI==H^ip)Or!_Ej0_4m7>)UR2xnoJpTZV^-+bT%%L|g?8tS?3Mvwjym zJg_vxkXbTl5VG77O{!xRwakFp&1?n2!EaM{3&T_$X=KPbIj1gFz+}L;AJXKV9QQwC zlCO04*Qi_NmIsYPfMqOAGCE4efFB>Wn9Sna9p#@Td$`8mH2fr8&5Z4LG*ex5bm#!0 zfE+S|n=4m`FCYB!xS7=n|r^R?d|pU<9)T*!n<{Kw3-)9Sl84OK+UxPczJuU z{0&*0FUU7d7jE7{t@j@5u-xO(ZzPSSSXjR*w^u<7jpFoWJhdf$(a_1otxhFXyNwsJ zcOR&+_VtSG`<}}Cd=OiUX!iuTcllEpW2>UEjcN*j8c#4v(-+Uw2NcmSblm&?=S9Zn zx7jQ)tMw#U?+J0+kB@$avrh_kkutcnoO8fLAR zA3OzY9h{X-!(-fTcLIwtI=5DR60IYQy9zBA@aI*_DtmLgT0pH}Yhj7@jsEv^+@+4y zEi4k+TZLy9z}`xU3~bU0jcXi@??OkWxYFtZYBekty+({C*K(7@iW~F_O+?_NfmT&M z+GBG3t+1n+f5YR<x!WpAhk3uepv@*V>eSF%MgaP=0)5GC5)g8~f#c1^mL))9I zJ-IH^+Z&*;xfj2wc`z3SNCEvsR*1wSayfPAsHXvw4K!`7JWST&tf#;OXAjhiITp^% zZmxFZCMhm8K~Tl2#oG9#!>ayd4dk_^6^Y+dR%eeyT<4xwo*N;#NoPH*(a&`dg(PWU z2Pk!r%8KTJ+cu{SH4TETT~@o9TsJ9bV*dbCn91F4;8dhB$5G;>8A1O5=HqS;pB$q$ zZ7x+r#UO1Qcz`jYEGAGWG>swB0djH&h5@D$XqrU;0;ZV?;gzbY1CwV6JWtz!tJIKB z*`qIi3%ZkS8|t1)(NR=a)5QK9@Vc(%(HgGi-qPoyd-Q;L<|XCq$QTHf2d z7jUJ;w1b*CQDj3;HDFZyaH}=Ab(GJPeyr{5vv;NBHjEQ*xr*Lvc$AtgAy}lXd`$5< z_~NUPGfKwbr8BhwcF1Lmkd+|p43e=BFqDs2Q&P12-dMMwDlBOIk7UEQ_kWv?c%_DG zNux=kaJmOw1IAnMv7UtR%ldGfxcM4-YpSsB+qT(m`Suc@`EP^>iYuQ2%F zs^l1fh3ziqzPXj{Zf3EAQE_mp?5MP&BP(>`LE&9oJUri8fo8*9^(dMxi)%OOecTpe z{?_u|6BI1=@mrsiplq0b(p|q$C{$HuGa7zs z)fSY5EO!X*qpF<}A<;BviZ_wPS+*7{LnKiZ!uGQKO+|0S zaGZSvJye&~es0+-8*8tc@Ywdx2I%v^)5X+%zS=aF>7+6|mrjFO498hI4yV=ln0aG$ z@Y$n{Tcq_Yy{`&()uGzqW_u32lE5U=rX-h1=z^eCQLCrliI+%md$Z= z_IrmlWLD8oUzK$@bXtlE3KQ?eW>w>>)T(n&MJNn+7rfDNe++Wm$r2%HktC8nUUgYl znuUq3mZybC#RpV@+k))2^WR?GL2zvvGdl}<$Y6rr0AD9`K($FECZd{;kgF*~!L{I* z4X4q);RU7SN(IcxanjtL;6!c$EK!zOovrmMfQqNvLCk8(rB$elCd*FYw)TeAYP+qy zzTImr&|7$~;({)uiX#fKiDO2Q$4U^i70G;t5!I!_NAzjC#1u~HySJHb+9!rZm#NhZ z6`zO;%0cDz*Bne4I*RS^JBtn(e8v;IE+CTHdF^d?8SP_|C}o)n%@WL_IeJd4p&8La z>eF66XB)6*>KoM=dXAT;X+wRzI)rvFs5>Ubw0RAQd zlrFPYsgc#!$i_SJ39oye?Ayfhr6CB>uA%-)YV?oITo+B3hB%(m9$XIB0zL9?VFreQM@xx6mvy!ECm%yC!jQmAx$}{img*hWscq)ysDYV&4-Hu zjk-IVQ)OY?#!E*a{G`&fXwCVIC6Tq7{{Z${v2B2=37fWeC?yP4ZWmAmBWf{I=crI0 zb#M;p2CBlH*V{WSp3z~vcI?*!Q&TU-u*`D!!93Y8 zgcI$hS^JA;jg1uUHhPI12VF5}s6L5~HB|~lbmfW;qGq(VmWEi>=xVhUrYnRL^&uE7 zY^0u8W9hxi^(SQ%F4ZSfpA%L*aV7$-0A0KF8@cubcxOKdzsbKXp3i&R?;JyKyttYd*9!%azfI;)->&lMV5~HeObT$uCo5gbTvrPFyY2Ro z?3*3Uh-PcQkhL&tB~jO`w%0~RZmH6_#?(KDJRapt6K=OH!tT|g+&hrBOwh#?GCCb6 zLX=b%tLYRPbnsg8;1xr57>~K;+L}P}-S;F{QKhu7OLZl)F-cN%lT?d!N_5#Yny)^u z!VU|FJmX?T_iJ~l~9X92^kSg_+nY=TZWD4+-EXvpR(>3`&o7p+1*cXG>Go1&lHS=iV^u~i=9eU z&_&-)ODSNvSG#VT2!yjuO?oJYDHLS%Y8<{8upq43E$VGiD@MVV_Dw;Qth!ku(nzQI zbyJ@V6GbL%{{Tt*muuNAw@9~kXclN~QWcICkT*n2sYyV<5*&J{F;#K7cTL0*d){H) zJLR_Hvq2)+#kX7ABFY4BsTxU2n&rvW)9NClEVDNyD!?iomhoxxaU?+1OGx!ap!G3_ zTWS8B@Qjp@)<_<*+P~BM&Xl)X=X=J4wz4|(c+@oqD2G5$JTuIbfOQ+l?Z(?>rKIZz zxr#oT#i>ALRxT2x627CP_!H$&O0{eT#M-9ODjA}Jg$|YyYt#zorPaA>L-=YvrvzJL zx}l*lM@;eykDom__XDRqq8$djHm%_N5 zxfR9`cene)c#0*qOV!-kDh`aXt2nJFtJ3MaQ-ZTRyJSEX^~XKw%Blj6VUXYf+sETvjCe$n3U6tB9qOYuuxAsZQ6ptn3DQTFPKZ zT^wI_(>dCU&5)C3neL2%I_0Pc4gEPl3LC-r#7u*(>wtKzy z-?ZJ_wY81Q+VyUNGnawPa{mC95lZV;1C(LZPz$@Vk_2(@1od9;Y-?(TRl1#Rr6km7 zK*BVCJc~9i>PoE1YTGvnEa3S&`!|N--lBO?`q5;^GF25*-BZJkIGs}B&gV&!Ldym1 zr0{g&xj?EplrT~?WHkdyV=BG;4J#EB(61Ydc;nOsP*4FKICxXT3Cga=JESo->VQ!e zfPQW`EPW#tZlxkL+pu+_)KkmH8)?usWy#m2u^#@{b~cH3G%p>L>#8}1O3UULd+^LX zGE7c>rQ1L{>^n1Cy}Z`R(vn4)hNsu9E2s5!b#m0-i^aUndiAl)bd!+w{=oyZk=R7e z#-O=r0QUzhF=!S@lcx~r!itIpK|latEl}%W0>w8@*uB}(I3t?paBHW#2&Q$UFvhW0 zDK!3@HytNe^5M0xmWKan9mgGzM`7LDF@QZ$}{&9gX9ZDU6XK&Y2Iu%Ns!AO zmDCqZfC`?Bf=UYIUR+u#B-kxJsP>K8>XK`V230KL?gB|ZOY+9zzZzoXB*Rv{&9H6+ zEvDmbD|jH4RnPYrhWoKhxg<D>n z7h7dRg<9b%i<;i@#gs=`l0aEPg2s#afZ@j#TOwH!HqFhf>k0&c9!{&o@vM-%hl@!Y zDvl;RacWG-AdoJ0+f7Hz8fDg=z& zs~$rDE=c~6HGS9)K`lsHg2*+3q#VH%BL=KfpzYAM7c{b+uA(tpMy1u(MQGrS9UoZD zmDG+#Wv2!>C6KFPfwkprAxS>mse+jWA^5&xNp!*LUSb!B9%h-0Om~aQCF#i)J==5c zZtJ>&<8_S7J)N>ZlL!PM(OHNuAsVD}sGc+&KwUvmv`lGt0d*4=3m%F+Z0ATkCTTAJAmZHq(#L%jDz(`&d1 z3Z%%!zvE9g+c2V# zjgg64s4A&Jo^`-vqhza0bH2M7=#-0h@#JEzAb$>gLvICF-jc%O6v$8}Qj>@Kw>0(O8%36aufkj@>XvJA2-}eGVcOr9O?j@0DMHfa$Dao!@wsv$B&sI08OG=)VOwIV1|()#D5(Kl{{@y!uBD74fD8;h^Q7#J$qS3rm)c)<%{r zs9aF+&lhYer7X>m85u(`8nl8y^QI3S(iL>sq4%F~4$6zS_Dg<^-Nbed*Os$-m?udf zc`2=zTSq7a)MRor!d@s!5bESsc9)|41ZV}fHy7?Gz+OG+HPIdwKl zxQ>sw7fUZePv84%yt{3hTdQMnVXBF9Z~C6#%g0~xt#I0lvVyw1#LrP&%|X?X2L#Y4 zUfsQ3SlpMWp4HqtTee)_Q&P5Ma35gBL16vxPr0myBr%C(GI6d%lbN8w)dOOyF5hz* zYPxdqqYvO{aL5-VR*jU>qb@k0T z#9#sX`gBB1XMOtPT9i}r?bdJSP_=6PidT7J&DfnS1b4Ysj8+NO^H^Ay^jI;%Y0)<$>h|s(}*Ua)8f0vq&Jd zl0=$4bWT%7GDaIsYLw4Ew*;<$wP2fNx~T)JRC0Z5!|cVuYe1}4wj0b%6p+SdPfg5x zt`|Ir86G}3r;N36`1?m}RtAzWT)-?x{5o(|s2rOKL)%s_%q-(_zm3sZhq;I?gJ_Tt zi)S%`9C56148n|;>=+h{L+fP7X+Ud&ss*Aw*4`rA_Saj5L317Kb4S6n#IVY_{#=^A z&@mAjJ#^c3s95hVmIitR*5WY4@Bp<2C7cl;70+^Z^{aHv$aym*Rb9AyX{YbSNg#ti zdu^)13wv9MT*h2G1dR{Gv3h66MkJIL+v1OKE^b&PdI2J|{pQZVYt*GS8@U06SHKG%!Qng|}KLQsnjL_hIm4e5t z$iZe^(&gf_FJpOgw%^>W7WWa`+!&>u%P-4ADG;QNts7;=qsq7&QN4;zG-R2!+nuX) z-1d#$%1ce!0|~i_6ZM+FxX(#!5S3YW-hH#Q;-$b{4zi-)W{{V4#Y~$RoF76WQc~S&xqL%ieB-cb{ zewj2z)KB5{VC#6&1zxkSdA1R6cNGXp;BNy*LqlI^G$RHtR0`fUeVsO|*%r>$2aXA( zS%fiyj3Z&Lx_k%SioYjVMbWNFzY{%XR7>DMp|p`kd~)Sc{cT0~<2|bDNM@R3s=UFR zy-A_V4R$KE10lUxnP-SISC(NWS~;q{9T_p?I=Y@?NvJC-q^8Z6=$bS3{^0W}CY|a) zid34?%GlQ9*PK)D%-_v_Xt$3 zyL8hm%KCt4i&2$Zz|dh%@sdpyqqa7OcadXzx!qjt77Mm*=uky!E}HU`MFouP&#PMk zN7=@>$;xh^>Hh$%-V0g1Yx{%A3z5e`*yAde&cH!)d2v)|o-1~wq;jxn6CiLL&|E`% zz*M?u(u@+BgX|Ui@D@_TPeZ+}WS70mw^}ix&2XfO`jsmrgZv0^cBG(Hqf_5hcO~3A z`tILHF4YaaiW;d8$@472oH?#a5PvQ@KaZr|QG=?yV=7lxYjHALh?}Q5V<}qcq;a&Y z2_r~t5w?(hRK@;J4uO6*KBbFf?>^<(tyCiZ{jvo`JAL-GxI~h=kr}$2v1toVVN;Un zsmGpoNuBpe+i>N#YW7dnSf;qyBAa2}Y|4Dv7};Jfp=~;|2_nhTV851!m2m$6xZcJq zSA*2a`?F)a?B3$M?fGt!@=FsCX`@{#;#iVl$V(L?sZKa*@D}w_hAsCPhYg*DJzDH8SSgJ-0s!wm8Dy0 zVmA@gh0^U+CpD!?dH7=aKrXUsxAyC_Sq>@TI z^68BUA#1}*i~6KzhHK8#`9rVV^M1j?hg*-8GNek@}Gwo8f3YE9!)SZ!BhyaN6!=wZ4osB)5^Fl|v~EQb^eyPLh2lxLXM#)3x_5+V&B|@(5ZnpeB|% z_)%4V^1xXPp>Km@-sMQqv_oq;i4mlbIy`!rYMm~p;$IqLHbtmZhUBd7*WV$y$+pR_ zUA(Ua;EV=i8j~E+#tk{xR4qvV0As6IEwf15?t4mjq`%+ZNH;jG#CsOqw?!yTmfbY8 zOKCiaw&wNND@!AKh|!#CYIe+p*A1sn?9Ml7cPrlL+;8d5PVS=GqQ)SCI%8mQ$=>}Afl+6vy^^t$fXxNUYT zUDn3$cap_7u>{auSs}GDh(RQ+$3CIy&xT@$3(bypWBNX!V{>IayLM&%>fZkVwxhI$ z>u|AszGBhaS_t7upQjOd`aLLvyA3oVt(ID~5sY-FQCmc=KB-4vFJIB;tF$!yX4YQG zetRwd0LBKdP{y~gvW6W4T?Vyfb)W}_9BiezS}btf(^QP^P%7R>4ZUC*+DH&tTwPq< z8*Zh`$DlMyDzQ{@>(VNryxR-q1-hFT(WG_ltHA0j`=p@h^s@_iIjiHA2K<`P8>Ec3 z^^L~!xXz3UOM7uDQ-D{Mm{ZxFG+LU2&Hx+GwYl6Pwz-tSZFXUTN!p?)S*BV`BdB7L zpv|(vDUX&&+}CcQR~z#=+&fji$-P}|)_3vlme9H)UBIOg{{T)oN|mdC$<+YSkHat& z@uhM90Cl`g1KfFKjZGZn%a37oA`_d5HLHLkL<#~Wd9_eflG)V%0J9nGxb>6n&)0Cxb46ybfUzesjWN)t*jYn>e5;D(^9#4q%-?ppmixJP z4aJqBj`s5A>NSx9IEranNQ13CLN`!<@Nw7tb4CxySG&mZFDp#T16?Et7DZkJHLC}ix*k1 zrx7z`dNH|<5lWsBs*E2gaxvBtMSPzhJQ`ryrwJg|zMNZNqTT;?o7xQeWJ! z2jS)`i%h16*L0q`nupISVAB@EQ~g9e0c~fsq+|us(_A!(qydqMBZ-HP{LEL|L4DRj zlfV0tq}!WoRs%}(ZIV?3&X<2PM)zXYG!Hfny6%qXnT(R|m#PT_tWvZQvG&CePCp;Z zf+!KpU8N~m7}cdBu50Gpab-d+-J5%^8El-5NP3ZZm9ICxhM1%hEZx7i+gR?GQb1q* zdL)e>fRkI70Qh=_k>!E12^wd1t4-P4$YH6!YrM$tKk45(efS4eRu<^_hi-@FZlbh? zHo3T!#BswKmv?i)RYqmKm~)5}lVO?rdwQ`(y13f!VU92z#MaYBRSuqZS5Q@XhGzjJ zb$gN_nt70$cwR+~L1v9h1qPY523&AbQE=^@zWZ(7cI((WH;>h^GV~;Vnyu@sF9hz2 z3Oe<4H92a1Txhn2q`;DFm*PaVpHLCT@XYyuWL`%jA^6n$u?W!u&uD@-m9C_XrGdhW zM{uH4V9I2#8p7IJO*xjlz<{n|(hM(i-leM9%<(O>arD+Tg~6gwN2Ii#jRQ{#jy}u- z4Hq8Yk!0E~3XsaS&It!B#;@$a)oK-(4{#t@%1A5*j#c#zF;Ent`z^xO7Lt3S-C(Vx(@-x% z{7zNEl|!?Toc+UfZ46LuakMjCMIF_rMr$&oiHnG27*1t64nW>dyK4Q58!#pn~p)9<=OVnl1L+x1$K}V zRdy}JGYXGjI&v7>)AbQp5!7_5?n?!~)OcO=+*pW^9Q04rZ8`8$8VdU>@y4I_)a+$_ z1M&9^Zyo2`8-!Y?+oYQI*(GZ!?jeDU61p;mM(Q)itAQ)vefTGutpiS)^>R{oF6Y@F zS+luWp?KpG+(=}y5wUeIANr`{_H$C7VWm|j8<;BF%YqHw5x0=cnzqq-YTYHYAci*J}h%`_t z=>w~7b!*YVwA50rb(!{=EYx~5Gf5n@(mM?$iOU)wrbXQ$g3E7;{$&k4-NQu~xOQ7^ zjju{q6pQr)jAr#|ZVDEh+L4b}Gb>PIM-iU2Oj349dPhV;W|7V@#M}zjl0VyJ|~SG0UqUv^tg83ZERX4UDQ0 zb4I+F3R77J?ZI(eikAiNv#YXpUvY1!sFzZMZ*0;ADDIKc-n1#n*SXU{?dI?UXZ12; z#_f8fWp_8SOeK4m)Tcmg-a1z>6R#MDJ95~a79BIax z9ATpP{TmqUNr4}#z`Yd8^J{B=bo+$U&+`+@CaW2eH8Pns5>)kL0Bd4Gj#J7294ZLe zUcc@Odw{RE*o$e)J53~#^^}Iwr4e*S%c6~&Gis>x>E@!scl&Mm+idS7xsH2#t9WIG zJsmcStb#(iMyipknZ1Jo*l{SmcAcWdv(CE1R?e()8l4n#W%zjgxHN^ZRm5USK*WPA zu|^2|CFNBF`!mlCsJ9YRwXyt8B5HbI%P2RGWMj%$={#^R7)tI!TaDD0 zi)V1-nhRL%c-Eo{svq!E3sf6ynil@pxp|;N>g%sg?N!+>h4XZF7v0Vd}@xj1T~BU1i$8&Ejx+ zi*`X>zqxMG-R#Z0&}~kCUhdT+YxYA$b+u`jM;|R$RSy$d(@q+)DrhaIe&D|CeY9>i za7DK7_q#Muf{|^TwUj0)2{Hp9x`rtrid9D-NNU;-t2Q{gbvbLY{X2XCzfW62x2#{& z?bhSB=gLp0OYiTt{yvl+3=1sKETE@om7lEsR<8&=GVcgK$e* zxvflhGtQRs+c+#^g^Gu08QwszpwX&)y;)+Y_VX_46JmRvcKeNz+i#XmlUUqM(w1f& zNln8wc+~|7sW~!be{;+DHsQp+-)|8!ZFiPEuW@fGbZu<6JAR=#vAIZY zqA?L%n@j4*cyoRn9xKL8du~=ruePJM#b!XpssXMZ*@_J>wACcoZDEXly}BN?3pR2{ z?3E`W$~X*XyB$PCn%%x?Y5+^kAp8{bX9Vv0UOeK+IaS;eZ z+yn)fuCU!S(_Hi6;Jnmumy4fpc&z!i;GoU+n|QAG?XK4CW#$BibgGRQ z9;FX%xQDnBI4t6t>dyAzY*O>(<`&Cz#A%PJ;w2IVVITp6Q-*ZMKkVMFS*7(gwuY(o z&v0F<>Cf%J0(U#BTM$>2D2$t`f#v?2O+C2lw5wP-82)RwvGW_bVn>!1*`-zniyw|6 zBNEI{3>1RVM{0VfWgX><6J@=G#TO$O$!`C#m2QD zH>C0RVyudjLhPNzZfq7QGiyWA9(rrZ_AXxxQViC2Sy1-&;TwSKF5h<%@CT^_r~a%b zfXi6<%FF{8QN_3nGl;HY?qGXENdc+On59tN$y+2BbM6tg+jkbaB9@foTuZ0 zY!Gby)3@sX081Rwy93~=KJqbYz`em+*5mVx6rfWHATaU3wiUJo$|?aKp~nzfJvG-- z1-Eq+jf*4rja1Y=G#HW-*p;yD5N}(GZO?P^FL1ILr)26VXya))lrkP|8hz&q%aPSc z+*s9zx2>JFOUZWUl-vXg@lB*BQa2v00iRBF9DFc1^77O?xK%=ZpM4BdErg8Sd7t!# znvYF*{4t-ZP^5QHc`prQE@L6-rLr~g$2PZ?Z?XrxGwpH(+g#nF8oh-YLW&eFwQS65 z%lM5mH1=$wWcuA@pmc@Stp18q{Fn$~wL zt9`JWCv@B`RpQjL31ksTsy|TaOQJ%5yPt+IX2jc+ZKh4Zn9>+r+x=3kc0o;v`B*Cs1{x$0r<@r%yWK>z5-?$!cR;7KH4+ z=HB$i)&w4(Rj)TKJ9#%D-9hQ7mS;^42yTG5po29!dz#6bIrNW}I zDA3GJS!w6sDW422WbQOu3bN3X-5u2(>(P|m`ak5V@a%^_OJDW|qSev-xwAK>RSPFc+S3fSS$R!dOz^bch z^B8sJ)=Rj{Dz$IaFaG2=RhM+xHYKPwmh#_AUrT($^kO60pD!hUs~WOBL}9LyVn(@+ zI7y^Salrz{Mj|T0nB{{L18z>7Mp`3lC~zU;OhTmSjJ8;!4kSjJ zMp&sN4##cc{4pdJ!~L(KJJ*dyl#wXs;xH^an{><8+L{oiPOJt!Ds++^bki?>Eimkl zn<12tu1`0!5x4*eZdUB$^B| z%tcnASZ&_Nb+odUMTT3MV~s^du6Ctqia{}s>o)N3*H+NVWQN{X5tlV#89LA(?l8du z(H_gCEYX<-VqDxG74{sk7h&T?ve#&}zOlF5EuOa^vtC3FlvzWu3XS8cJuFH5N6RxQ zNiNoc?b{?6%L=IJkQ9UG<$|jy&b#g~z13DBQcEP(cz?{7FX4h{23OKIhH#QDVwynD z0N7Ph&fKKBi*YPYm0ZqV%new17KD7+Yxd!SMFmGd4s0umEMeMMipvuekq9b5p#)-C zD+NaA62H2>yf9CFE6pOu(ph9Wn5bg(EOTyWCj(llg=@QvSzS$Idu@LeI<}If#9;6* z69-Uzgp5{da!_6y!!}l5pS);ovsP9^vCo7D{kCc=aW+ zJ-l5M^i-M-6+BJ?oktSw({cpd?dY>C*3N9?C3?g!QCUG1)gGh-m8i&r?Z&Eg5@)wh z#1QLsxyn;{wih-wRSMSKCwW|j(yS+FSRw*NGNG@knA1j@64OUw+iW^ZEvL-w&fMfc zvqssD6wt}=(@YIMXAHej8c8lBxs^`jRNIvjMJy@SJR@Z&1WzNAnLK|tiNh@n3f>6I zcfGW%R?Z`7T56OSf3~~PYt4g}c z0U741PA?m^H#KC#T#G`xal78j7e`+rh8h=?QJYa7-2CvzNJT7_usRFP#>(zH%NvO; zZXq>}NX-}=78GaAFGE4wd!uFVD^%J0x;Sm`03s`-R#?lBMM10RZWXBu_E#D$j0|~Q zb_H%9Pp8<6#L|g2sTj@kt_kTWRTPy}S58&btEW8usI-oE=jkqgNxOE=&gyO67p(1R z1-qh-MCOu-sAOppL}~bVaTw9lIl5*JuFY0EcNq`XJ=gbNs5`kv*9DWuWeKMN1WK}} z!hWVU-RdtDVyL0#UuFRfXyF;wxHKt^NC5Qbiv&O|i$Jr^C_VU$qSC` zI5fawmLfpU;frjfK4MTZr+Dw&sahE2m_LEOyO%qIG0VCsZpNDSlj=R-$$8DOdvq*4?-Zo7|O`a>rY~>f$?B->;0pn_wMF_~?0IQ^$ z(zUM;MF(QurSrA!CX&%-Vy<2g%*t{)fIb)^(LnFm`n&9xI+(g!LJD|k*5Dt!{g{I7 zxGh`!wp~1^23&l;;eg7ZZV_u6vl&Y2RSXVUF>eFsfU6G5SUO^%w5bh2nNkR?e8w1< zn;{UeR;kPbo-PVZTSk*c#0>nsm>7V$NjC3z7q$x&@R4;3CryJI7}PLj)54jJK2-MN z$VR_`Ew1OchCw_jBP>#n#T|JXfS*#(@x@iiFJ3G$+dKB{wkz4%O3xVQSY<5n16q@# zQq$pt<-o4wejcNVqU~MEw~8aZgP}b&a<;I*d({BPBR}If>~!&dPsA@2yMEyhUfyWs zT5|5AqlqH1@7OWQ$hlGPrYH-zHU`+@)kB%ua$Dw^bsu6&; zrJ>0lHbS(og)uSLT|%>z2)f!f`Lk*7HPCD4Ri-vqG#PJ>MyA`O4$Wsf z?CUM2^KjYkAX{lQ-pOsPR!G(~2-Jj<=s!<@%Dg?gdSs}anR2ULqpK%&E2Q2A} z0iiMgMn@J7!V+x`73u|ej2Z#zNpvGGjw(q9VN+V-^;9?qEDXKT6Ui0z$tJV zu=QfSJOnr<3ZG0CWB%Bb}6G{snzgF)@NSxZK0W6;G<$138& zMRUE{&f;{6DAg}i7M^fqtF1X>VCO*5N{t_zKV>#mkx#K@p%0r900-Ys?ZWc0m{C4; zE20o+u4>f#uoW%6L03`q?g}pfha@J?QIaUcdE(>|P|2V$;&CQQgOW=~d6PQjCMOV+ z2yPc9@olGNqb0mH?BAGMpotH^LMX*00b0^8W@sS14RI)jCybcf)`fu_r|dYKidf{m zEwv$zT4vNJe8-r;T4z$7d(d;MQ?B#DQ~I&kC}*qYn%-kfyiIf6*GxHzmQgfgm z%}h5P zc#=rWAn8~YWYwmfT};HS3Cl|;&&HyZ0Fkvwh<6stI*M|#9ZG$;9CszCL%I2g;C6U@ zDo(Y`3}}fI?EuI+Gbmc{6=HG&1}!mi{HkTFFdRS`;9CtMe#v38-mc?udvIWi!ouS2 z6%p$)+(w_H39BZM?UtvnW2I_c6|-lk8hjm$w5{U4nN}D(%R_G6@3^DbTIjZK=8M zv0BY?ELKRT0(K0*F@i;m5Wkto9{dohtw7h84{EntV>PAM`h_O$043BQ>LT|*{#*QKm}-|mI>2HauzvabfPoKseuIf5mSo2bQzBD z!ClAP_npx?bs>V*+T0NZy_{w^lzEX>EXT!|mOdonLO@K1EG(w%`0wqvg%Mru8**s0 zAUuKIK=Eqgi6T5gqPl{U=2r`Mw{XhVt8&%aZe)aOBsh+(P;09y{jCIbG8y%KULx&%`~ymkr?$kqE$i4b2*%_eZ+549j@8GMrLR>=`1W+dWFDf z7o$_nTbDAjar{lI;4m}bN)qDjP1VBBJt4PaP|UBYhlWK$(Ek8d1U3WTZ2m)YFoq5a z2B_c(CzShXLxV!@3!P}d_+W*h5alC|IafSTQYPka(J|AOwZRb&MI*hU$65etFbxEl zo12Lm2*Wcpkg?LIfK~#u{{RjtBHIY|_VYE7WrvYlnUspDKI{|}nR(my&6jaacYxX~ zBVEuSv~H1r@1cfz>N!(3CV_Pv59FP#Vi)=1lE>{%*HkfL#oQ})oPJI%yoWa#Pp^8?g2FrsjpS{YBU>%0qh3wcWYX)3{Pl zn$wm=8clt)#~U|6lCMLrhp(mg(6Y${R{>Z7Byy;9VVzX)VeG~y$k?4hmwO$%0#c~o zb}eLpZHYaB7aK*yM^IgAJVrHFFC#zNf&}q0kfc!g99z%_p%Pi`tyIZ!`XWr-g0oZL zp^x2%VyF*NMW=Rb1``wA2%ewwW04p9WpsV`(y_EH_F{q>N^| zXf)9t^2y_`0zUi=ax_i}fr>SZt|aAw+N)a75(Zx$IMeqziTiv7GON+ngm@e-AT;?N zGB{lWDtLP^)WPC6aK&1Q%)ZQDFnA0Ar;b&@awVuZVv1DJtCxii30wt09p`V5#!F_P zKBcWjA6({p;DSFy+tXd9qvFY(f4hb|rV6VhJh1$Q%rPm6-tV!C=;Ghcod7@-(BgNQ?#YCN$BndJwDC}LxTnt(BE8tPBk z8HVEv6Ghdh-&{pvty5vz*`(Wocu=YF!LpH=?C@?e?p?tIQM20J?otWbfJU-f%WxL7 z{9meT?;d!fgMTX}#@zA)M2&)wL9I=Fl*Kd>*mGv=yIJo=)9#8T5Il$sCr?mm(E})I z^XK}SsI=3iwN*yUx-Cr3(`*+4EfKq~NnL;hk*nXH3i)*^xbz8HY%t9%{aBdW+aNOn zO8)?u7$5{wK2qNdRq|*|c;)I5+pM75HtU;9x*K6>BhN!T^~|5uB>Xu zOBKt&=Ujr~w(lOi8;0nSUv45xi|D8ts)ztk11x|9I{yF_aI1B-S~FEY8+)lijhg7P zt+lx*^QjCe!D&{lcxCtD)kY>6PVl$cH(55zn;#Q zc!DZ$^+7gad#>S+ZMFUPW86`eB8wc5u<8k96%oq=(bqK~>RmZ_)B-N7I_gv|d)s!r zf=giVrhuxD^!-Sb6%F9o4_2z0oj;T~gzQrLhUiVW769c+YoTg;NCOl`F(hd%y2~34 zTp4_@Xj?=er6tU9%MgbiNMj_;0G)VgELNV-M~}P_?8ViR1lvul3Jy__TtYNBLrEY= zz}DFScwksnBu8ZSvTK?}AdJxkP9&Bl5%C_3YC3K32wx$6A1KXyi}%c>IB340v(@?4XCuJYUoP`I4R)n@AIsCJ=4kY~158+^M!L_#FQL>dO-z9wlVI zARkF1bAHK+#! z>993C&sPwl>2H*0#WhJ+eEQ0DtO}ZA!vBZ_s!nLC?LHxcZTC!a)0_L|( zg`vCKq-NZVYuBBP0hB3Yp)*}7{+?d}s1tzCjX3fj(dM-+@{Mi#)$ZmmsH;N4cL30J zC)DJRX{R^09PX#3QkmQQe_6>i{Jrz!7VTg%owJeSs~;-UFLpZk)E;K;LEYJ*x5Ns% zTv$X&uM%dPo8wA+J*OHZ&d?w=b74|3a>_{|3o{FffIO+}#c>!z+(JI5&sM5u(!N3wq7x0y$M^-#pjZP#HI3rJYHN_S~%ASxoVnG`imB6KQCltt#(UHYf zJtx#L5o4CdGpG}g6)JpjWioA~%oyCgO$LBGLjM4>0?7n53;UFEwxx~asbv))dog{) z3f_<&RANa$D^W^@0f*diXr@Wotv4Ge?Kghjy^*ikUFC|#Nn*K+OpKaNQL|N4Hzuw) z44C;K@#ThH_K&Jt%p^g+M7OAiLpf1zGSiNri=mJANA}~Bo1jl)uY<2kam~ZIHqGa_ zY%liOy0yQ{&KVtAGpXN0)76>$I+yvsE=;{UrZcJ2Wmzu>qu$#aTiKunqBklSc!kQi z)r$P8E2^&yhmmyvads6~Csh?1t5Du}84al)5+#4sAebHueM94mRt4isatWq|i`ZPe z9&xd%*N+3rej^s{Dd1s?4(`6&Wr|CiTYHX+2aahi;+cc5;!dhMgZ(taaiv|1*)g`D z^jogWC5^?ZT3Q)q;VN1|6ws*wX%R@*L8sze>s)<;s#q5wPO2pQ;_Dge1Wf> zPAd6laB<^RJ3ap7>YI5#D;vlqtcH;Z3X!RgH{n0{rVAY^$&&2!TZD#FY|)7%QJIK_ zQ|(d>2*6te2D3>c=9Z;oMnZBVih=fF7RJ!kkn-F_Pa}_xSdSYcu%^2zC>RiMN{qVM zA&bqv8;Y(%(G00U zNn=w@d2{$SG~1_*%$4eAV3x}W4p$3<(W7sRJ#dEC5oue$^Z9ye-`aRX>kX0CXJ z9FUBMjwpz2$;1gBQJ$b^a3l@b?hs2FTlAJIxaby9Q>b|y3oRl`e5JAdWmRSXlu{a% z%oQt9h{1$2b0ilEUNNOx@Yb~V92y0lwA$|N_T-&n>qjdYD<7#yC3Sh_rxiMqNr}4M z$@+J?FDMGu?JFwtYbUCOBxWb`mcs5@CT$-p){bj%@yukaM(nHxbzLr@D?$9Cn8*19 zl@7Pz^0k;&^&M{7Rs30;bGmDMr*o~ZD}&>%tZ8h-s{)B6)|@aQNU}koI24PM%A?_k z(Q3ouGJPk9hAWdr@KzGmwz8kqLlbZ^C;KqkXEWsKNiYr0-9fgW=i(IP&r;X$g(p@m z=Bx5E{{XFIKMwI{Kj~c|g}j(#*e)!sAmqC}jOVLtbehx^H5q#_&%DKbl!&t2%VjS@ zwD2sXXT#grOg-k7;OJogEo&jBqc$nVZcGJI3Q%t=_EOnAOQhRXj z9m301g{{0oF;mx7W|B=*^=c(KeT#~*6`0o1WBbVYay0I@5@~@{8WZ_K*8^u%wFyb4 zMf4K?07Fkdaa+Z1g0C^MEgC31>|5r5lEQBG#%%)zb|w zn$zrb`YT6_#ZcTV+?Zmp&i(-d$X(8^t&UTSUUG?d$T$e<0 z2==vG1Cn5v(vYRYrJmTbUBYhS6G9%}S&Xs=4ItE^ifW89hM}RMq2N}j1D&Tsv+YJ0 zcJ#Klfn-bBq&G4ve9a*=MQ2eOx-zc1nDr^83g)GYCvc}wgKJUF)vtizi)gtb++HoJ z>yfgrDDyZvqy?lmxA*(Q40g9stJ+4LFaV)d^IB54V`&S9`-;DHO|>x;cp&uWN*y^9kj2pfmFhEgjJ9oG05Dj06Q0vhP}9@ZUT-XPPE6`TXPqCj$N5l}25dd0bG{=Pyib+GQgnO01~W60x)DeUZ2aMgU=frGRM6rCHfTLKs4Vd;GX{s*h(b-HtFVsY z^r^@mIH)5YL8eZMzfhK-0~Faw7mcpk@(3f1R+_Bs3X1ZI6h0Io7ZfOK2BR^E%c9hi0UEP7PEnO)jS5awrWb1rL$>h*ZAx^taw7wh#Z;Rl za9pN~*b;I0WB>;q&0KLYXMi%{kb7;j+di$bW^u!%GcKy1ZzAJma;t7)wiYng=`sj6 z7))|km5Pu@+lM6vdsOPj2~n&DkMPW%q3^{#L2M;!WP&>D>k4uI0O5&U0(zpmx>>EQ zx6MZ6IBbCMt~$#bId38ZHT}t6XC#k2@v9$5*q4x(v|Y@blrjb@BASXHFmO+%w}2MN zk}m6Yb2JYpSe+wFw!G*#3fBStwSuN)vj$jhHUM-C>l=n6MRAIbO*u3tz1^HT0=H5} z6#!?E@xz@3EQqk|(+5JMGBKO|g(KB;00p;`xbNbNLfkS5WSNBrr4>PHc~k7c=E{;Q z9d#3J0~@I(x&m&Kay3IC6a+H({i7KlTU%^59y!z{xOWYN(Lrrey(SV$K;pntm)p#6 z{8m;5`Y>h2`D${vhE_feB@({?TLAJ7t|edUYbh7FvYRa<)GMg}01Pt*;&a#KE2=eRj*RGXqfxmMnSqG}LliZ| zggUyFfH?z-u|&(D3oC<@A`FE+Di`}OD3%~dKT!K{L_#$CGB|~Yu4k}r(6h6KCKt)L z%{1~ES(T8&i5(#!f)%54DHX1D%T0g)H3JkG;47KIHbE@j09OK7y(f9Jyb&Bda zUb<9*Y3#)`Ni@x;b&g0?+JrFWE5Lilh7|#(9*Kmg9E`a8F*z3lTbxm~q`C=HrnK;` z6`6ddo|(mfRw_(7j|Si|=Q8PI)YDp$8+o;lX}#HREKSDcaXb^*Ze~^uY_Mcu5mQN4 zN04}Wv#ymRtxGTU=eFk?JsRrLT7ObkH`pSATPY$AuW%(Y*V01h(Wm|GUko2krwoIg zy5>laD?v7a^sMA_hUxp*9chco0xAD1r`CM#1=%$s3D)Uthis file should be in the contentstore

    diff --git a/common/test/data/test_import_course/vertical/vertical_test.xml b/common/test/data/test_import_course/vertical/vertical_test.xml new file mode 100644 index 0000000000..68c5745f37 --- /dev/null +++ b/common/test/data/test_import_course/vertical/vertical_test.xml @@ -0,0 +1,9 @@ + + diff --git a/common/test/data/test_import_course/video/separate_file_video.xml b/common/test/data/test_import_course/video/separate_file_video.xml new file mode 100644 index 0000000000..b90ea9d8c4 --- /dev/null +++ b/common/test/data/test_import_course/video/separate_file_video.xml @@ -0,0 +1 @@ +A4+Rg1+@9R{~I)gih7~~prpVka6<9qbrQI-MTzImOd@k>_wpiqno zFVob$py{{5=<6NkG4I~xfN}b>HUXC~5c|a1;2cM}c&v{KVhxsEHYjPtfB`YuQ$}ko z>(3=oj1JG)RHi0%k&Ee$zj8=(Pr2aMa6x3ZCJLdF()^Z&ag!mY86yn5ehczq)0_no zQ7jaUKWk?fcg#!A5<)rA1lQ1Ly3?eD?W6YUM+AQSq@cri6A`?s$#$%Ab6` z*Z%C!{Y##xpSkCW3O*vq#o|Y(5zTl`z87Qc|L}`K@$^fJ8DW z%?9Pf;@CV z(`fjFhpAFAl9<#MYo6Z| zddLQgQ{vm-Jz-E!SkHLgyuu;suYQTqUu)mGd7FI*58Ge;-GlbYXOB4za-sdrFI@U~ z)}Hb4wIT{#|73%!o+X!Z8%7|%dI&fSSh%``Nw=c3WFP{xn#9-F58EGnueE>i`?uQL z?|;}j`Rs|Kqlcq)2q+fIEA-_D5K)!jmR3kiGfDA zwHYV9!W*UnrNNe`HW$zVl4l7X$(eO!lAoq!cBH9y(`KAy(6(69hlrnwzB!$`YftMs3lK`tXD{wgs077*|Nzcr}E z6L?36hXJHm#m|}hp@inO&Fj>{#CHF#gK#%*a4gO#gKqR04<0>W(FddS3_Mu~Ub}U* zUBAUg#Axk!~q4oeRv1vBg^mQo1$ z$y=UfjOKG)71V3z*A!@0FDt&W9Dv$hJ8u8tH!s@1`lsJv4*y%MPJ0`X(R8O}HGlTF z9X|cM{qdjgw!i+wroOQ%z7!?lTNP`#v0&d7zgNJ^#U0NcKjFCRb>OQ=rm3cU?CldN zMl&!5Sux>=@3_pIk=__3K_W7mM266cgzptorPc2>LnhJ~CWrb=a*Z?1fWly0Tp*ls zBi#X~uUP6XRNy)Ti8^A!sPU{>-@MCNPDpiohlzCtnmnz+1@f*N{^XaRaw_4=#;P%z z%nUT4TvkdhaIm1+VEW?54NfJbsk)4!Uttmc63yHNKKJ43p-WdYQ8T%N#b&Z=hrLW` zz7pT11j z7%2lyvs?N|LW9p>7suk@E&u>P07*naRPY&XMp6oQ0UB!73ew6+pS|a4kx4E?4w;s-+%h!#J_7X)Z0Seo zg;K!T;EaXbdOJM4L=74Rr7I~o5=i&zpVk?|+RDu@gIG=Zk zzT4^U*py5_h0TJ$=B}DiASlV06uBJ!zodjU;htvT=$sSR?%Yr@%b?51!Xzll=n_5X z)PN{8Ucno7OnfD`lbl2tg+%w@QJFDGLJufG_#!5_!DF3OIJdVin1f}VFa0k!fZO~{ zUzUvl`z*3>i<+yUJYCR5``U_Yjc;*)+x2T~*;`-A6weJtJui=5(8^>AhNkf)QwP#C zQ=FD@@Tt;M>BvBmLhu(O196_n%80BnZfn%GW}JOrm5I_lwXDhTvL9@%n`M^Z8P;Qb zYjcIb{3kEK@m%4=35`vBUKRpg_Nh|~%MbBbsKIV}3Iu9?t8e{)weYqTUP@2;c+st& zJeO*g0_H4ayrD+nw#ZZtpFUwaX#N4GJ92{K`7+aD+t(T3dz_VFt~A?Xhmbh#-~WQa zKDPR?j`ZGbPW)gB?ni(3D|Xn}Wh#9uovI|7Tx;iCy2)YlH=zm3t-|${SNdOcQYdw1=5`-6Y9(cXIReZ==h(lU3#Hq}?3wB3h4YJd9Y`|aQT z$wmA0Ir0HXNJ2aKeHW>0!JBgMN|pl{Zn^2oI0pfva!iy!qZ7OsTWp<8)~?`bNg%di ze3Mip;`Amn!UQR|Fc+jmN8#pHndaDB;xKNfI-EM-+coT!e1#z#?fO{@gbpc}Ab<2#@l=mP-Z+RZJANj7$oo7 zp#2zYbZh7EC9@_#=6%Lc0hCd--w{}|nDd;NL&DjcstUnTT>HKnq&27r;3a$Uo!5=5 zG(-vGFMiPpFMW;GTGN1Guo}Gn;xqoCi$wV6`?w5`_ga?t82cr=eJWoloQ3fQEtM)p zLZ8VzcT`yg42*Jzrr*20b(_ufc%D4sBVX6~${&M$PCq#Z?`p7R(nd7@8;Ya-s0O=u&th#V)!RFfljEjQ4EA@nb^ClO~V@BK=C#gPH{5z2j@!CU! zjZlW-K}Zueic=fc?rpVu@7-v3*n7E7sbwNO#c!Vxzz1JEYM+0`ahs1B;Gln#z!^qL z8wO*REb0*1lwv$;rVsvv7}&3#wt6!N53wY%K{s#?kmA|ZKJVYz;`1CHMOw#+I%j){ zf;%5;^H2614fW$y^1JUgI?!xT6Rk6Kah*fH*=mcQgDuIgKc z_=53WySl=Wun(E`!4rP&yzvgH)J;Q6Eefo<>?ce`z|EbM!V88_j z(S3Mxp?&@l1@h7(&cIq~A3d0FfAz^+`{hGUkZ}2ym;5NBBs9b&TwybJy@o+1SXOXo zXLg|~To@}0?F#^Ou(|^UTj;VPN&*VKRD9N0jTi!xL=P^62Pd!dEK6Izwbs7-{kPf= zKKP)$b(O&wns^HHgnp7I-dp`&Xa4i{O-?XC@qcvnVf*x>XYIk~PrzW}Z9ejV;gpgJ z0sNtn#1+%lS0NhU3YYhI3e45t;wPBG1Y5!+qM*)D!cF>1HjM!wy#8EM?1A4~EUd65 zu}SCG)-~`8YNRDf;kVy@BMT_D=nr3N3@DjRVAB{SAvsxOP!#bOx0iiDYcOGLQo)l3 zCM7jmr!}0ltj0|t5ZOc{F;1JtJ&k3!!t)b{&0m|xnHG`(@Y(FuXf%cnq zD=rN)1qZOgv(epESmJZEI6Sd}%9BfcNgo#f@D5oA?sDO_z@m623|UzScqIdHqf{D& zm-D@IELyq7!jsP~93wAzjXDQ^(TQ{+34$j-zM(%uf*Fp&X_jjMQyVvI1FIW%$ z^tlJxb@s_S=S9^S4F|_WMB%Y07QbTl=3gE9e}!o_vdDk zYHbF;3JXqmbP_mG^aRu`iR_3IAMd3r^X)qvIQ1Lvzu#`tj9FvoJ|aYbZu7dJ zCeYYiFT2D^3EzJAJyyaTx5tn6QX4NA4s5PYSA7z{KlMuflE(1nYrz{H{w?K48uXf6 zBIvlgpK;ttv(1;!ukumge6|8U{ZN!=72UaWn{K7zqfZL3dsRKvd^sWZWL1%aQ zsMaQfS952aq0#@at`YMjo zyp0fKybRJ#tX_e|;oPc5t6^Kqx-f%p$fmi?9C3uG=3Qcxh9B}{u9)2mHQ=N%GtHGL zBx_`0896oym(;$O;5g^h3MY9|Q_G%(3Cug1tBh5u`eC{lxg+Ii@(E}D@H?YMw(n}8 zzD~DeosGT^IWX|a{m)|ZM+eMv)AHG3#n~F;1()ZXXnFlQaXI$r;1PuZX;GHYQfI`O zf+Vy{T!xLSoWo+)VMkIp=G58W~ z(wUcpf)i}`KrkT;&+jlQ_`~1)UiFz5P~u>&-XXEv6=%!(BqDDF-oH4TSz0=}e7F z`s}+Ecj`FljbbBs48{c&^BSW4SL9%Z2y0E9chf`R7UOcIlhZCfhrw!KCfM54%bYfj zSCVIqMwsEobwEj(SqMnapEZ~@Dz#{g-I3~jeoXI#SEq`o5ifz!U@qBZl%iVuB;Z#~5y+6f{~Ji2Etd_Fash}xOR+nd z9zk@Kyn0xjSJRouBGMZwqMD~EbNdblb-NOXC40_qR#UNXIyE9gRv_-FR3X-x?7w~e z&Gu^_oU~`GE&V$(!84OWXY|*Sh@vGu%^tOg(0!M7rPKv-{Xz*)jqqr=bdzo!-OznE z4HhCh3?ePj^l)z4<#^ldC3X2BXwpPG;Bc}H=4L(R!G#qY7?Q^7VdU14ul70VL4{^O zYMGI8Ymi*v^TW`SzMr+AoI}^1?E-u3GRlcxMhN8(K4Qcgi*9Qt6Kt($zt%+*8ormg z9%IMt9Bt?-58iE-x`CfV*OGYqsN!+?VsIMHPrJq-6eH))P_?k>0Yi36p~gPPoN)C| zi)MyC=G_wLIa173cRn*`I;rt2l#n~=oTpXg*nq}6Z=ajXFup+pn`Lks$2KQ{+_}N& zj*N9YV{Nx*Cf~ltQc%uZ_6Q-rz~_gu{kHDmD-K>?kY|mk3{ughhSOnM$Y)&fE01{M zSMUiez;X&K{0OgC5hT1~i%C71!uekD0vg>Negnnqb5hTAyoRZFu_MysU$Np{JwZ2c zPAa;IyEyWwqKce|MiQ8t&NxI<4D*P87{t?rtS+;|g;g%D!F8ZPL$#kI(M%kJ$eK0y zXcE~*-zF9BGW`Ge{af^FA2V0y7Bu6jTOq965=D}D2X{U~Eev5MvqYPh39vjFxx$TE z@%5JB!i(Y1A3Zx|K|dgy7^fAJyf0uVF0i;sje3Pc!(CszK%@(5ogXLQ)YKp|TD+RskwNMoXj z&l1NAPlJ>eErRsJqjm5tJ}#t@Xvu-dJKmsUdmXiRwI-mY5y5BW6EM;TuLi74>{@Iy zn0IA=EgR;K7(G7bn3{zZw(hZZ*_CG+zB_BI1~teoGkfh5^&@fzND#N@gupg zmv>(3t%pyzihlJd!DZ~=2VTROq%-k(4_;ynym5BePJFh4BhkfBMd&z-pBQe(s}1DP z0_39|6b|!&|G^-bvLdRO>b_D zR2GRwsZ5oLhG;x%Mh(_H)58Qrql}L{FNtd*0*`5O5q|06-~>~$0AqY`A+QuKq#<9E zXUsvr6uvyHQEfVAg_R`aQ$F~mNh~NR;{{I(9!_PU#h(R%Z$kKP!71Q8X<48^bOhO{ zvqh%SJWKi|%NOTX=v%Tbd6B}z&yl#>%KX@&#k08Jw|BuYd%=%BHMoOY{@}kxxQk!G z!$*hhZ^q)UI5ght7d*e_B^(|Tzlt~F%*LN}SNN2#349y_XCVzQU8f~fX|Q>YjlrjX zG>--D5##ES^YF%x|5+nQCX*Q!_Q#a!Zf3U0X-8dD>W+$JHjzWokp=q&{JFHt5_XL} zs@FN${#zfu+n#>;5vLe(N??Rd6fT7&%@QdY5G)Dd1_5D(<1g<*t7wTQ9KHtS0uMn3 zd#RwO*;o#DDiDUNf_TcH3;perx~JAbhjj*=oQkkEHAQ}{ZvK=*yls|hNGQF>(#*4T z(e<{q-xXMJCI-ToW7=g5m}J!$Ollmp2ARxYv{F6Pu;;UU(LSGs$dwUPo-!{GgH0I( zO8QBC;$fIEYMQ~y!Hlu#e8xIr&STct&etC$C%ur_d%m3L1Doq!%9IN#C@w{*|VgXCe`% zA~{d%9#~I^@O)qgpq3UnW`-3EK=uU2^s}w*kQIrPcxY>lai5(g6 zZr{y*zN(eM6AUjT^-&(kOk32bZKZNx3-&}IVbj?Z(0q+?J0_ro0GCu?n5o6UZ=tD# zt%x&_B5rF^l@a_DlUdE=e5xmF%z4rRMxI`_%>ol525Phv2)K=FQ(0;r9V%zm@Y#n; zv)REX$01}g!98~X6ji?=chgRWDi4pkkq3w{9qbni*s{kq$gA^bd?th~kbL<11v`Lt zNWaIizWvU7v}RV?ox5+Q*?otv@GUKJM6KzNZWIN5DmHi@e$s#aD%jWkI-~KdaOF@q zPvLXm>Xt4~22g3CwUN+>9%$ut!zlBY?mGeYSa)>o`_ zQd#ru9h78)^~MmQQaj6r>qu6Uh~}*MW88BV zVR(FvHDdC&i#%vVWgf^FJcJLO$o+J*90>4hfsWieo#OdhyG*V5F|iE}Uw5i)mwn4S zoH2dv+I8}XuOl3P#zuOkrihy~gH!Q^RGt=ImFtFknHN*zD?h`^-yG_I6IZ>@#<7OW zWxVbj4+q}FVXTA!9Q>0=;c$R|!&7)9-tmZ~c$XM%W2hwjD?bM@Gpb=Gh3tfKF)*Q9 zQG>W(K>$MRzP)0=;0Xs$@6#vUWcLUSFRAbw$&+c^{xgy&a|?`alK=E0{R|{)mDwO} zEq6A@2xhnxhnF-;B=wAE-2zv>QPTM0srHBQ5~KWVhS#6W_Yf7H*0U4<_r8feOKSz z!4ovjbEXdlUgLxh=-9MPJm~`C=(B~T>2e2=XK$UbF1=Xcs27^Zn>)Ai*$@|4c*e44 zM?Zh@Qw-OZ4F#d;QrfhQCeI?p4qJGKH3+IvL8OZ-np+ zD?A5|fx)2H(E4J$iZj(u@QN?M1rzi*^MQ5JYztEuOs5z~1meh&*GNcSa_jzbXGzDO zmOpM)DqZ1cvG|!3=@}4MZeO%K_Ar>I#FC;U4rPRE8NL*z^VC8?jNJbST@V) zn9dOfBetfA1t371KgK^~QTqk!tMNh}rd7%KXY&(&RbE~4Doz3M@m*KeC}RLghpi1v z!o>`iG);vgRw+G}rD0w93(IrrtXT}vqgVWF*GdN~k z!I5mIbXNF&lD6$84ZqINmZ)v{;Oz;0a}Or48@9a0$Hg%C3)143392~>-kug7b;%po zuph(gGN4|D-_Y}aJdRew_hXoi{%$8t(iWU1Ux#E_u|FcNC~8QCgNPwP@PXp z{}2;SjzOSHiXjGFPYd+eldGK7WQi|57lTmtM~VDqj~fI|84xku%S`sWw#N5ET`VJtf!c3ZL9LZyoUtV%@773Gq^2ufLgWWx;&AM#O>2z`o;;*Y!&!*ps1l@J*}D^nQGUNgBkg$x zqAoB{MRn;iMN1!3*b5Z%U&D<2tg#_3nt13YAg3GRnS7E6{;nUkrd3w4KvU3b*$EC3 zX11z?kqK#0#uoN#$oYV;#=CNApQbT~(SV7;KMot%W)9br7#+kie>yY4C2OFppUAmN z`Sq;#c;=Q=;XU<_90Dq}9xki_KD^aKM?XUJw8{LLZ%oT38b?l<#_En}RBw3mjImJxQYcBd82;sElavs7b{$ z_!%6PY^GvXSt=Ep+_(q_Py_J!ieoOH)1+Tzud1Jr)c9QgusFv#Pb?7efbazjcZJgw zRX|6?E$#jO>ydoJuY*+cwuO;TnyEZ(rY?EzQp!PPN191v4FuUGpcZw#vatC3G(1ga zYDedAGrbCpIZiF~pm5Vp^`2&}Ct#Rk^5dav8ilQw7zxi>X-1lfkBCWY8GPn}Nne`L z&~vSBM*Gn;>kZXZGR7;7sXVY*3MtaZR>MqJ$xjuw=1=k!CD0f}AU&1PKK{8a1@;pk z(x3K_cMm8ZgJx+d&|goVn9=N0(sv@yu&~OCy31XbR*Dk7{HmUZ?I|n2`?Il13xoGC zCQahVI$UuN8{Eq`Y0BGx3Phz?j9;3O^9IyJ)xb>z+9lm?JvPnqAIXL`xOA5(O~Xlo5vQ6P{u`-$@BI&Tdwl5wC3 zU(;dZEMEa%{#m@y1DkTNj9J`oO$jJvQqf9FhJjF*AT9abDNu%%9_|O)&Y4GyB zK3hV~OI&MG^GQA`-`v_)-}%|cNHy+PG=CTPV7L2b_xOI02d|%S#LO<;56|N|M$T90 zwpfeVD-fNW!FK6YWw_+I@b7;HPcDJ=Fs(ien8YhTy(U*+dzA1`$Ur!Wr%Y%WJi^1w zv#BJ~gm(nx^BUUmGC@1=%DbLX1o4m(!YC3GMYQJr>x}fsh0K=%7{)UA$zldeeS*)9 zFF1JXxc%MMBfgZr&b%8Rf8rb+@{Xn&FeW7ZC9zS`BXtL6++4oP3Z4D^LrPE-IE>1t zbwD`n8A5!*wj&gz6uyrWp~=yZ=wSU0FE;OBeC?H3du#E0XHnh9a z$phQ0`Te++ie&BRmOLBFTZ|~5Gq-xiT<$70@jiSn*wj@37^2@Uwbndi$)WR$?k|o) zkQ3tAYNmnFN+aX%z|X`uj&@^t=n zffhscFO{@HfqP`2^c;9ghAB4LPGLzHhG0cY<0h+Zl)oSj5{bcu81f#Y?CFJt7^(ou z*`jP*bBum3Z_bDjAm1N`=)VaxCh44_`osPa<3Fj6)32gZXnYYC00SW}alExHnL>p`H z4G~|J3D7JiCNT%E1!wE#^{X`dbwA(Il%~sKd~f!8GS8_2IJ^QhndQ+lHEOE@9n$=Hv3I~XOQ}szx*;4@ z3~0C}UO29VmRjHkFEM}_0So49*z~3c4@-IzYCp)2COel*#YXARsw|c&I)LE%Vhir$ zA2f1A-p+Kg3;g!inE$1(!=3{dlxvwq7TEkv-ExD9ZZA&752@N8jy;L`ye{>xcwR zN*9mhC{m}esDbVGE>QcVrownif$aZPvz7q(gJbYmAf-?uy=Ux!jRC+zeU^hFs1k+0 zHE7AkFSe&|%)1JnZ_C60amN^I0Z0~-kZ~5A7AS6^bF+U2qLe?p+A47#*3%@dr8Tg~ zm0Et;Wt(hAG@i1sAgHZ~$XdooLJSjr?PJTI{%VPT0q_Y;TEiTpYQg4V?pu5)+inK% zG|`{G+{X|3vx;knO$CmByyEKxo5=Q^x9%{-c%LS;ecj4)MAk2JyWoMQan3SmRx?Z;PTnfAVEJq%d;V&zx;Lny%GXrPk6&NJl=m zL4S0Sxn28Vrzr0{hU?(h1uKB8p+cAR+G%VXriB3KckA6Ij3knMI1iu9e z-}7@eSLg|yobSXUi=0jg+fIF0m^ig{?m(n}@NU|yaVfbe990?nxVF0ZwhteUrsBjX z`AjGVOOaB>i+rwdEbS3{oUc&xS{UrIF8uM+JyuRtGrtPIh_T-^(w*<2^q+hLw z(EuGNSQ=NEPQ6XW9pt$n2Uy;ULfs36Ze668$6&!oqqhD3D0{OX%aY_y&tu=0jEIcf zDzmaGt9sw2H#Mh84mlcPfPt2QfB`hni$Dkh^djg@ulxtZR|ycHd65JO5ClOGKn@#Aj3nYo#po0|(K1H_AEDdxsVvVn#u-OH?mas! zADp7D$Cb>}=NsjfYrEz8{tf1oDtz#L3vq8V2Y!G{&5!QhFAqL>j0b-$sw_gSn82;z zEL*Yetn|vi_4&o}^)K8gw{CFOE5~%4jV&@#kC(mYAG62xae3uqVE*gPa`(BF6ICpg zg|g(nrFuLuhvgur*RSiJxN<<}u8V92*uhlML!c);w+Bw*sRGV#6$!^V>d^&m=m72j z7Zwr8M~&tv7E%XfGo_M9u@-w?o6(ckMiHCM+xeOX8FKp^Jz`tQf#?7pY8iR9{YQXP z304BO-1H4Vgp;KN;+%qob?l{XM6ztgg=u9luw!eDxl;}Y5FMwRcVGj}_71Lrh-#6i>tQ1_8qbgoLA6e?GRWd+jv9qjhW zukFu1*a_np~Y&Eg(c+L{=;dFQHiv{CT;-HpAx4HI=Fz*%26{r z|4QGEqC;7{iFC*F0p^_$ZOt@y;S6dg#Gwjxc?=2$@Ruv^2z#^*0WuN%0P*GN!=3Wp z2U}(B$|FvlTZqUxjVJzH<}&a6@`G~k9mJeXJodw_9Nm(I`Gbpd1ZrPEIa~mZBhl=+3`Ac6Dpz2&Ckr3wl%add!{Db&WE`qyTpMSA(}`!8^(K7q zUg737ZC1b3_w!%jq}{rpn#XWjKbW?a!}soc7nF7;VT0u$q&`5WcKa|z9VK?0R-*B< zAT4Z1Kz7t{K`!K13qD9=3A@aW@T=r;4?DP*93S5;pFF_J9)xlUr{Bwtm@Kv2zxxDd z_8fPJ2x!hhhOQ1{h@o>BK+G+zm&w^1I00iG{P01UI>B5JqT1NPbaJa#-s4=CA3s2I zy@OH)y^6RuxoXNl&BAYPV6;HJmSowULc=}s8Q<&282dZlo_ zcz~irql*I&`xG;}X~auU!<$6mrlCl7pO{C|Bfcm;s+xhoY1Y(_01J^ZR?aAA)3$en zMOBR&)Up~gn&xmj!iirAka7SUj0hpnTluEwPZXY;cGtOT<@nauz8goh6tPXOaw~^8 zW5AI7zARP>Q-II&gnQ3kr>K9 zKw3Y`<&|4F?9h9@{QvuL8jx1$;8$oig>=y_TW$DigFp?hWovO-D(~&Ni^seFgq%E$ z(beZ1I^ST1yZ4`_?dAtIG5JItMCHJxKrQxu(I9(fEx%27wzqk2NQ*)vea(Ln1a#d)uLX;n0xa>kB? zCUPq7x|Pg9u!q*y8Cw)IB5ejMi9v-8_0If~cL>Mrdr%dzLGV<7l!jSJ@^+a?#13bR z$O|#x^J$c_WIZ>vF`>~_MgfU-k4-d0rSZtI9w!y^?E~7t_K=W8ifk@`pxp184egZ0 ztVKqDqG`^Zxi@X5@(Umttc)W^Ha}rUN+Gd5E1x#LCpDO-hAcTYg@Mn0a09r5rhdhu z8=y@O&{IqUPaVmlk6xe)q5cfC;@QQk;$02%n}uzc&$R>>lMTnIDa3u-D}!Fr*fKSB zgI4b^=ih~<&%3s|P=<@C8;s2L#nqRt#<5#XGIa&b9X}%;XR@3g8BTd#R0*rTQGr%g z0@6&iErt`O;|*v8X$PI6={-T&VAE81wIc@!&4A8P?Am2-?FpNm4_Lo_z@qiyBF6a; z|2~9z(u3i z#d3+!XAcFLqOoOE=`@EaBRQcBpJBw;oRM;abQHCmVw-G(R8iG=9#yaoa7x-2C7R_T zqR}jxp}vC5P9?GNxo%4J`Y*5fd9;B{(C4J2BgB^C#YzL-Op#EZN@ zCCP&r&T;KOs4-<|3Z0ZUEx-Uyy`2uV0b6C+DdP1uMil#pv@XYPcn0ku3btuP;zQ!j zaQcAj?2kCP@RX?q+1QL1uiC%~qq_Cp!YrK61LAl&br+Gow10a}U2@g%mt%iQ?p^d` zlFybQ_?MD)@QGE20Pp5R)D*UbRxtjCMgZWy`dq&mJ@Q*Vbn(x@8(?SE3Z&p&Yt(1I zTQcw5cx-JlD2NgBos7luTpi-&*__FPPl|lj(&)z=((6CriEIZf2WT?s(H-Gjl*3Bb zH9!)Ot_s0cR=~ATfgTQsXb7C61005N(3XY}3s^+$?QDTCM<6KcuBjG@0?^+)K&a!Btd8UyF?Ue3;n>#S*$iSiAd3)BLS^w?p z^qee#lDrKMm!J!r)>n>2LAcU2#-&Yc3K#F}Nv9Q=S}>-+K+HaH?Ur)!fMU>nW3I^x z0izubbgpe&#eYMsb71?-L)xjLfSSabNyst~F7YQEr4K zxQ10aAloiV;`3awye6-oUW&k{%6QJ}BY~ODnBAO~05eNkek`E`)V=4G&k|8>o{AiT zNZ24Pkp@@Ske^bC3f@B0I4KVC5|v-`O(XFm7dv)z7cM^*X&^czk^z46C&kXaJ|}iY z<|9oN*B}Ov*7JJwaeNAbOh6nW!~shSW-7NBIn&K2NScv|B@zhNHAKZXxAcv|nS6Kj zukR+MjEHq|4$CjHo>1-y5WEd3Dq|6GDkY^ljyNlhpUG86c zmUgloj9o9E#44`(Snqj1kL7EtFHmhR=dIq?q*Ex$PBGw0ggLTOja zqkFgTULiQ^Qt`bDb2#jBNS2xyBSh(TuODcgM8luK-6IAU%D%Ez~{ zcjUYv%+!o0jn8n{@w4pdRWzpMAvPBMLGE$NIxfd$O?RFJfA@7xvJ9f3f{Gx`6^)w? z1FS?51fWcoLB){mc#22*36^uh;Xqgxf`|cp=GSU~duT<|{SZ@M?y8YK#v6*iV7Zil zuP4gJHKO4I?Pe3E?;w~7klUj(Ci2T}AHi69+_SW&f>E`&*fF$MtFx+N?{{Yl7i z1nadiIJ|`9_PJ3+(wuQi8Amvgz)b`F6CBPFS0{IqM=XOKa!Ng7?6HFd17O5*20_K{ z6Ewc!mUXMUx-0{O2JiMsy#r3E01ob8;OIEQ%7a>cH_J~Fch6%+kxEVtN3^HV#BYdg z-}C2Bx*oW+fAe;kkM#o)YFzI|ZMaPlyQ|`sedlFe+VjPfH9zYRVzSJpL9?6wAZ_3W zN`;y7S$pI8k_hKmo2aJiKDYOF?|RP|{DE^0BuZ{6YX?lgwNeBOKGaBt*J$UN``-Qr zMagFxuvU#5l9wM&_}f_H=R%N{0i&WkE#ecoQgaCM8l3 zLk`L#+^i}tDzS=#DrQigAU=Grv_47x*9+ z?G+nyI|QQC>@$4_S2jp1G{f`aAZA0e=I!nxwd%srjgig)^qb}1F5)uTr4{|2ya1No z=59HurKRiseD^u6lgGp{N*B$t_`!SHhrH`YLWNl)7>O&92T$hM$IwFz^y)A%&*yP# z8pYk@F$ANFIJnikfD&-ji=@#J7=tjsTShRzthww}A54%4KGcLlNvxVb$WX6%#ehlnzAD%08N9bNo zyc>gK;ED)`aw08>>7e7x*I1eY$@ZALPQg{<6AGjpgWI?RXR_n8p020zN=~b>i0iV7 zpw0;!(Z)rKBC#T+c!^vSu1}p)GwxA5IWHu_sExv$HH2xI4U_mlasq>-Dt4U7I8Plx zgoLtqr)476ng$ra)t^$@So>t)IJJ-~#gK!$z346(BHCACY#@>r;r^ABp>S$|H(dE=ZqF28v`WEb#m>@^Zn+aV@d{w z3FgtGDzE^UqES7igo1RgiRmJz6kc7!mlVfy?6dL^qE88^24E_RP&y06RS}+WpS$0D z@F#I(Rb6CaB)BCeyb^Hxp3lGfW4@F#X0!B_@IqKG#{}+qIf%teyp>*vSIbag)V8x; z^%^|z8ltViwd1=u-7Q5<{F-U|Y9q97fEQl|SKth#4*~H#9ZGmBeg!}gNZit-G9*yE zsh6p&q3<=oz z`+m4$+oTCg3cO`)S=Y;1)dC74IO2+x6s5=7}gBO|tMMLfaD} zOy0!Je3pSju9s&y6!piQ7dJsOG%VL;)8lZBK8bUF$F$#jdLtp+*r%1ym8ReIDHX&PxN*;*a z8XAm_*bG5Plo~Zy@yhYQ$4*3)3#P48UI!Qmk$7?utE}aerj;)HoJm9=Mnp#lwg-1o z@pOtt++`ib+&;z!S^o{vS9rQib;H7>b6%EmG>DMAEQ2Dc+G2MOjcB03SvhC4(HR`^ z%w;h47CZly$Rtu*)nh<4Lv+nrrBXAEv~I4iveQ5DAVb=qlL)A%#?omA0TF|O@+Xfb ziFFkg`5dthufsRFtb?lB^<9)~S6}gHmaCN8;f_>kv!oW<-+tMc#0utyxOn1zAzi{7 zmh}CfN51fl}@?`+OrV*!ns`#wTW#S>g=T)hh z`P+)t>Q(2O0q@VRTo$GkwHAyF?W-K2u-(;&9Hh@49u!C=N+ zasc@xR5@@?QD_9pUHTwG#L^Hl#O#t%${?~6$2Cz~#7-S|E-Gy;5f*bZxyo%jKq_L|Fi*wDDm^%`7|^3 zlwXQBPZPI+RQz@@s#1nh3++O?oj5NhPbHIu@c7ua zFhf_Z=THETElk{{s)p41Q`h=j?>-N%#H()&=Np_soIJFMrftuD7=BK~{3I3th1J@j z#jPnCob&WPwY<|`z#fmrI3@FQ`!kQ4a8MOeguH9b1!bgM4QPF4KZ@mv90*SJPc!(ZR5$IM>X;f5qGH)~w z&2s6&9Xx%to17x2*sJk?TH_EAdAkhco>~!hZW^0c=73r~ITS?Pm0QqeRU0T`0^GG! zD($WriB|DlrHaTn&h8#fCSCWOU{J`~X*eA)WlmH}g2>W{QZL|6A(#=ANT$IgA%v|6 z>bL6GiHNS}QnypqkWUp~&sZ)xbqk=1DWLLAxN;{sv@PyjHJ&Q?<}RKVR>f`F0lRD0 zMo$q-okA#5+m}iVLZFc-0AakyS)sdd$=U&|4f8D@ZFBxI=nyCUl=e{%z`8h`RC{MW zQ4R+wo`D(V6$9BUUMiL^tsuUSqmVkLe>lkkU!M*zGZ3nnU+tzhGXU%^{?|^<6VTDH`=Qv5G_qi6l2K!q_f%ErNo=8b5^jubZ@Y01zhTWBXTSW*ns$~kCJS1W(jxR4mO9Vm( zV~TOmU{2%3U>uQGv9FJ+y#Jg;gsM2H4RY2z$v}mT#)YliRg9!8%yqMI9puikj!21CT25hjmWEhb z8u2s&)VZOx_6s}^sI5n_NmQ#CZx#8#-+>r2aEfhI@w69~B zQCjs7A%zU0GoQ3wdcckrmnEd^=E2ZJMH5muY30t`A&Q=LWUx>HG{KtimAX0S+W*SkQxv!?4Dx$oW za7-}D0DX8+-O{*zzPo{e3w^cC2KiW2QGyz~^#%!p_~PBgtKqcm=OE-|T3Rc=J{dfs zZ!pW{T5$rbTFs@!AzH#J%%Qxy(nu*Cxf#>w0hOOdxHV(|yD|V_wY<)s8Z3)p^_T?l zP-VQKf+U~(Sm0LS3eSdWfB^@PL7lo}gEU?17&}$-fyj zT6VX}5_K=rvuKW;+cK}y2!onKeFy(} zbCr0_tHKElm?t2Nn|Gdl2XhS-XiCawp4%gDHoos69Nz0in(k_GE1?DyUn3gG_Ig6cfOlm8kS3-nFKkEK986+j$4?O zg~>7lGTif6#r^4Wdz85vp0yKt5?-@YZfdh2+EL(T$T5WlmJH|4HFs1;L%+=+{CrS{^6>Qrm zdMpFtukR5t$+Pa>0Vrj+Sh@q6n4(+`idrht&MJz`OYwW0EYfHX6UYu&1da$a8*Ot#&qPU}vcsyOGizBo>0V435vtsW+ti%VzaE4Qw} z?dQt(e*Tp6X<4%)uMoP3QIUkO^ho#Hy>d%<8>=ir7viofEE`T)b}hd``!;1nq9)*0M|`)&O(N!uAufLBwSM`hzxHNXzqF=+R{s2FACxCsI1qC%f?JtX*D}W- zi(GY=eYtwlSXqR#8V4m%a}4Ktrrb)RO#47c;V2@lg(AL&7_Nib-+K_<5!%!dJU6-q z{OInx<>sj-0pb9@g-_(Cm1B; zbP|uEobBeNX7`Y?#D$E6Ea4GPu~>^GIlTkI4kyzcI(K!K6J@qR`yh{mtNT;u?bdad zc_X1b)0S{u2Zjlx2Ywj{m`-qfhd!=D<)k_F&(Q@0&ESc2p|QsfCWtW2C<3Qx8;Lv` zbfgj2$4@+dlUm4udnX(;&Ol>57fJNR8&}HbF5`{{Y2il)PZ_w-YHKZ)J}0jZPg)z; z4(XgBd@-nfH<{NkKGl%(Uz5rV>OGNbemqK8pPsf7*Qb9Y9e656*Q=I7=|st=(pvpq zrGzeCR}OlhOIg#^t$AIXnplENTXY4i1-05~2hu4$#^2DO5Va9PWFjCDTik&5C%_Ho zh3A@mN0y0HLyTq=yWTtv&F8$=I0@)rX;ls|gwE+QvlDh;A%Mfe0uI@BGm=jnL`qSf8%9J_l&?1;D^Xpmg4 zKp_pwUQ<##kYyh&VVdwtX!oCPl_$@)Ix!gyG+b!!-bZjs4I13wjX$a5#~>)0QcgZ# zt%Kby3@yxkClx{A_AY(&=x|RIa9^daVh-i7%q>NNx&%@=V9v>*Y@So(pm74Cw`DSE z>CE+A{vE(&b$^5WNmp2N`jiM2VeFnd&$aRQQK8G7CpZR0DdMoliSZEs5%W()W&3k( zZXOYKE=Pk})-&R5Jb%XSoa*xQ1`hdI0fQSfh;5_fn>{VaTfB zkzaW-s1>tPz6qkz2Z$YO7MUw0qZ-G+q_VI+bRg=DUxE0)g&WV86qvi=N$7}D!!v&v za8c39%ZQf141KO|rb#GawY(!`WoESe?mzl+S%lkeJYj=7oC9YVWp3?8`RbRxLi(}t zy|>;j8|=;9+So=UK1K98!hOvZ5QBAF8f*$J>NxIL%+VBJiWa(C< z-)FrNPWuAiz#}+RV{NA_E#Uk+h7iy#L@8I)K00QPl&>0gkPSqsS*Ap*38t6gt$TqF z8Pvw<%ZYGlMtcUBXl?nfX4fMp4=`wOo%R_=P#!QqOib@#^E|~jTAFemVt;(V0Mo

    gG6rIoyo6k4l>d(sMD_3bQ@Pzii|Majd&5f7&rK=o1zs;^8men{` z#!V8@AB?L_?z086?&W>swObgnjIkwev%LNO({j+8${9<2{7PloMx8WJBh7$_6r>S&jh|oo ztCrCvt?_Hi<@4Y9d)Z*|!T0`(El_n?vLjj&beWkXq-z0f?5@Ur6gAV=mdY$UNcQmW zbjF&W@rGL{jYd!{41H@kYAYq4kF}aLxPGcIlB*vHv?xAG*xH|4l+`A5uw7z z&J~1xeQmP5iV56f_F&ooTK@FmY_Ebryhz zr65k}Bm@;K4s8C}?1PH<4esGMcnsAZux7e}Vqtf0x9mW82m4M+5nIu6KY4~&4s6fB z)dBj_l~>_dBPb#$7o1hi>8^Itej^Zl=$c&vP!2&NQ*+aAvuj{5g;%E%w**JSx7diAOl^i1;xNho_*p6a7-1rd= z>|FlQ0;#``m&60sxSeN1-1|BOz$DQ;k1+DMys`vwPT)MC+G&r?uSb-9%z$`w@T6Sr zzs4zeoG^bnU!Jn2ytnzZtY%3EPD}eu=-hsi$up%qWUxEqaJ;{G>lbC?GR_FtSG`NV z2iT5hxh%^=^u?!M4P##VNbs+I`PGn@Qq?p>b6IMXeVS(s7#W>|0H*g(?5r5t@Uqd- z%kYfeQq)19rb|5f&Hix4-UHp%-ndbI{WrgfbGz-b^Y(k?DeWgsuudxov#fU!wUP>V zIwu!##xMo^*6;jwnK~RPfBt{`VcFkie5b`Q2tUtfcw3w)y=RZg2k*XD&KSv8-@H@i)>q5v(?=kp zTizzi?dw-Fr}$*^pgh~9F&4+*7Ko0nxgws)z2vOUEh8d#8JSmXb$M63%vLl8xJ@>1 zyEX1$`)PT`#sqg3dB~I^t@}gea{4@S9_qldyZx-(zyBc0G99VQHMeme`T~ys=+Ra* z+iu*qOb7g3=46DYqq}HPIcAeN`#5t#*PIWLtQRoz)3N#y&dOZ_ouGXtf#;!GSPy2t zEV1>7A5%gk2nvfN;C48cz98ZQMPaO}w&NL5g&A>ahdsW-NZAQR#esX6xX$+xw;8-Y zdHAqAe)b$GV708URO4Xsd~=JtQw|3bm%kpCd0en69XxpO0O^2oKiR^g$Q%RAKV)ewTaVo#)0=@cq*V9c4{R@4R&n zmr8&2Bc^fB9x}-71m`T2(ixY8(Fnm;3)GPwi3M`DdsyE2{*ORp6eDmG3ltI7=n@ux z5|(iTD-n42{_+(HGiJR)e7s&DLO#9xejeYaPXSD3HkJR}z&>{RtnA-^Sa42J?tk>S zeC9X4RTfrp%gNE49C$Ik~3A&hy}DS^F$wtEi4kRFJAdO}w- zz|3K)sHCprwi(jcyCaCN<8WR5IE_H0YLnyV8ICsQT%UnODZsfjj2$0L_+moLH9rT} zJvizvguS)LcL+v~tcdIq*cx2$&KpnK)>Sqt}4=`z9yj7w@B`W#~D0xDeL_-CZ^d&`T!!3dT?!T{?4Tu1D6 z6iIAtPg3mtOAj}#@BGPMmOJ16VwqxXEH=4_ejX6D03x1Eu`GdB^uR%h`QF`+nB#(| zYnV4KU?36uUL;+WF^=?>EBipBQ64GJP`vEMi=)djb)Gtd@xwuHygWy&ajr6t_d$fy z@)XY;vxs_n(|227-;)P8Mx~GcsfnpY=H2~r^>YjYh=?x`wU61fKSA4_plOtd53rTq zW_|;mg7EsUVo-qSj`5|XMv}3GJK{3jUr|^?ifI&Tik`R_YgM^IbKqQnMA9m zTW#^-8Pf=)5S3j=a7wwV68>H zIC(-*e?sjyBK$TmPN-{^`9uV|!M-0{_R9c4hQbGN&cn{RBm`?3h(z=hXkrcHCtyz* zY)?52!^;Vh9C46U@H3x=eqv?g`qYZEi?hX?fVW4-8=F*$u4fv?4qcCoWA42D#4*-B^7WKi^wXy8@Fw#8pe&rR_@1EwGY>VN@v3 zrnMzo?dsifdYv~yea}e6oXocc#L)iPE}|d}&btSKojR}l=##Sc>P_@Oa31DTXw{-T zS!CYV19==#rzW;p@|h`jKiMu|`(9 zyi>mQ-OrUD{ormH6s~!kKSIizB;8e#S}L7bfWz(}s6cO`}wipPk-2PZ1c*xiKIcZoKF{)x|khG^fzA=@eQ z&W$H${Oy+a-@cCsOuI0rwY!jRAzbI~Q)OitU&d&b*HEC1Db6eMG6=n36Tm~<=Pt12 zZf+JL03m0HzqY(wcFALTWeo-4Q>F+9Xws`W=nj6y9E3bZb=a3u1`A%; zG^04q|7`iBQ8R|{VGexMN`a9h-;-e-9a^Dnj1HVP2{j-I2Y|p$FYtU$fg1!r*2@ry z{yYugBuMl`N$L(@O;a1~;&bXD5KEEF+js78S2<4_PlP416?X)W#TUq}-{L-VJbmhp zf{dq7xe6MWb*51?u~{!$^5j-)^Qe|aXUh-X-YUz>m&*;b(Ml3kJOVD5smOVP;eZ5p z#s-8QoJ1mAoLMQ~`FH*~1IARj{D?W-^b^G14HQbx*s!o&Ub%IHbyEnAN}P~RuNIy@ zc=UqPpUYs+>(>AZQkY}027;QV`v9aS@^>PIkw+1O?t%Ag^)cQ5kV z|^A!ixEcp*ZSE1=mNFC7XRj2tR)Q>(|DX^x#;^R=^fd=7X+3LLt$0mf#T8U)Ia@18SZO?4 z4aJ8`4RS;5=u01NJEh_SYnx*bSf^>|hv@rp9JgVJzJ9fAF#iJQVQpjOwX4(2p~lMh zfAT^3r~mM4WgY|mGnQ^*5d=cXd7XE?=~QFPEq9{;X`Uws`RP9`0jR zDj^Dk(4G_Y_`ws>F;{avn7lMJIDvcZ;^2Mzg?7O#?X1Yx;B0Wf132gb3aN1{sWi9L zvxdHt7W5mnw$A(;<<9PAxjK*72`AIy$ho-^fq1PI)e-qS5LnkmltnKP_cqWAd(1abiY?{VWq}2G6iYn8*a!v^P?E2mjosQnYZ;j-!YRM=#U(BXof%j*;{Z^Y?@K5tiUDqC{vCG;RJ>q2*AU=w7XS&@4;=A2u>-Z zc|&y&<)YNaG6)=>umia4F_X*L3Cu2!Bwp5%0~1XvODB6vm&(JfC*{=z1{RKlJmtW! zsi{@wh6}WpJ7`2Wqz&ervv9l>6gnE-@320+ht<{r$UDg#YV(lO=@1n^`s8_eaW-8Z zp&)z0Ne(VCDUmn|9HJ3dOkF{#=Vl5CS1Y9*oX~G#OtM2|cAt+#gTKN-XP)9fC8!P( z0fXftoOXVGqx|p}_o-vQeC75^`8n&bU;ErOd|L062t)C=3U|J#I6VmhUtU=*Km7SeATbEX^VG^UmO-fFk%k9{jyT)TAdo$jR(mLvUN8vgZfy$fHjEX; znsw`EE~wVKngOECrhF@@r-E z`kgYee6yU)Lg*7XyqjJv`^@E4Zs~$Y*+r3gb8DA%G z3XV{A^%(3HH0q~)$0&yeOR~o!=E}OgRpaSkA&1xY_{tTQ-5~g5M9pLBeDBF2b6)0Z zh|6F8hySG9`PT20pZ@&q^58K(j*%KZe`TUvTbVBpS^D|jFSb~(W$EAo%3be%gdYL^-qb6b|hT3k*keNzC?+^-6oLES_5PZ>IzlIqxQvAjvC&%x_fRm zKprf?;8Tq^h-^{3glc3Xc3~EpFs-r@;!ofW|vhtJ>1MZeRKj>(+Z0CM<{}@iqcr&3g@v-vhB_TtTh32spQa2@rtMXX=SBI zr(#Qm$|qY$8BbZl>iu<@JKBK3Ar3FEluIbIc3y0AQXvcV*dD+C!M*a6kGING#Q$kD zk=N%K>=0$uLZ7Nlr~Qv(6HFUm6O3SAo?^4DCOI3{!6>4Z9h}Htq3v`i?_A?JR#CJu zioHH_{Ax!HvY{wbVKzs751qTCJ@3!6g!A;Ey#L5k^PZO9|NZ~5tY3STHR|_S!`>>N z!&}9xuU_Mz^2xIO_6EiUo)Evl^abwD01-^-@F6G>4B;s_&j+=v^#h3CaKr@ll1N3f z*4x25D7bOGB;_aC>cDPk+Z{Nj5AI1^yaMo)DbOj0l~1g##w+E>lg%=&lZB3Ul6&x! zCo>`3*WtNr%^@H2%`}Dyf;C)G)7%IlPD6g9=Q1pic{4={=wnTj)Lo$m33y2Q!1U+S zu6*o1406l6h+8k?G!p7y`e6XFrfZNKAH3A4e(-p=+}*pLRi}Hy2xMlS&BJ$!Tc3XB^<~#M2#4hv3vbzzy5mp;l~^0CqI3+ zymII34N{@x+>sdpGXk|}v z7jtL6l=TJhP+ zD%`lifm<9g`WTUWZ}bvMAO^PHv$EBjF6#^cw1NRh#=-Aau<>qgG1A%9~++~Bx2}haGkb~$oz3|UL7#3Qpl^||| zZ=NYtO~$X=qw9+zLa@GjA4C~?piu2<#O-?vtto4B8s3P=(k4#Q6Dz%I46l~j{E0V| zNBt%N*En{N#XpBNoaVgT4z!ybA@coqH_GR}zF3y8q0ZmIqaSl3t&V2kNbYH!L=1ic zR}%4iEW^w*kKE(PkU8eP<7^9aeHZ2vEwLl}wM!%Aw^)<>@q16p!$13$k$) zC3a=k7)4yWT)zJIzf>Onug@TIcKgH!GMrluOSyAakfIpH^+2t9QXnS`!abpL#8XUI z75#{y6_!N#@T2?GlR<{VveeLfOl2Q}mMgkZVG#w_%^T}cQmK%7cz+u$KEeSaeGh-i zlh|Y5yfTkL%L1ni;D?%}ttFfmjB@sp?YPC@=kXzA9DJ~Bl(ynh@Iox}75wP7CmQDh zI`O?CkDD5hAnv>#Vmi9}L5N)3szB(wiew}Z>iN~+2|JODmyCGRP93YMZ~x=JU*7oa z?egKH{qm0D2m@CJ0u$GSx!0vu(+kU+4*{5RXSs)<5N(S?dQE&1AU-)BQKvkC;7C`?(aKf7wN$N|mEe<}bbZcE z{qa4F8@_eD%rd%bJ!jVm_F7)U^gQw~2(tVG% z+EcVywig0xVdjL57?;o%uS8P#@X>C0gr?f1B>h5Ny{6?E4)=NvsvLc=bX9 z2`_68N=&NJHBnT?YhMjnEZk2o~`~VmqxBfcU;!xld63xZ~!kmOMD- z*4g+T8p$1;yKQ5h!ro^-sJSFY@gRrD>>N|0444zf11zJ!LFVDC zUtJq3i~X7M94nwc3a@$QaTE)~0y$~tEdujNiG?vI9Bhq~0FTisOK>ozD8+29afa}D zt&0$AX&?v{F_64n0xOdMWdIuAdtN^Kh3k}cwrs)4pz74YB`FOtynDKygd+?O4V$Aa z)|cJ9-}$_fgP@tVURpot$UQEAh&x936a$sc^lsg{g3n^~2@v@qT=2mLbAGudgGirs zZPZtk35_^w;p13o^?Q3*LZKYIcNbQCo3+X5^7U`tDnCU5_Tk;99oE%;78yd2K5(7m zi1>W|-UznF>LSw5BMdU5OG}j7a~5np6Zzek4Oe~kN?`CSuS2M9zGKw&^RHhkYYgl= ztSWdI^y+1F2^zN0;bB(6Rm(0o;hh5!i~Tzf1FO3VS^oUdY}JgB57LU_Y$HkU!;K-V z5Qe$&KcGAKX6v7?#%XDJts#EM)g@@J!}pF@Thyw7K93Oig9?yO_iT)mGp%5i`Nuco zZ|se6cI{cYhDFXa8`0gxa)2^scjGyP3&Efv-9!1b%iM7i)5cMjOXkp~F3^e37bdBOi6~WgaY`CG~aOPNWE_=&5j={}! zz^ws+b3;g_EaPo(5BBWl0*~exVY%oGPIJm!yN_j4#AXP`wNe#nF0;(CdEhyO|NMwi zA0k?W(@URo3(F9l=9dtyBQu1L7(xCJ2;z2ONo!?kmQ53@#)0j6>tYF#z z59pNOok(m0^PX>Zmhb5`?>wbq1S7hSF`qrV`ypkfjSM%waRQ(|5C68bhE)_aO~pAk zqAV@1Wk2*|2Bu%Uf4^+J_Yr$xnKOf5dbJ!AAHbV>b(ytH2lwu)py`Xmq7LHF9P{-~ zdrWXwQKw>F`YkfD?-?Pw?^zTGfq{I)$;G#D#xAXd<7EXKZ3!!(9dYuthICoC*TLr& z-&6!f5b~T#MP!4R+%~5pw-Xwz&!{gak^8Fr*W7?2SWPHm~;))||xqi`X93@i`Lo8ySz>NjTPznXlNK`(cK5I}g_gRnf(6YrVg^kEqOb$0j35GH6KU^`GpGOT zoy+A<-hPyWw)XLOs9$F{M5wURu30gCOfJE|Gf6wl&_)yED9g~o=g=_3XS*O{0}_LS z!x)3lENj!tOY>!Y9j!X+va=X=>%L{I;6~xZXTSV8HUM^J zgXO)#PKb|w1-?|FT#SStclK|#F}PJbf0(Kzo4oCS#im#r4s3FEB!K^*#iKp1_vW>5;WCt(-TE- z%^InZ%O#%W>ReU>3H!PS(Yx8fxYH1j8_xH(*(xSy#t)Ng%%gBEcgbj{tmE<8`!*w) zL(mi!KUFUqh7-9cx7#kbZ}0`3?r zy@y_(Evaa;EUcyPa}YUNFu9HGF80)ayyTs_1;;E$3$yo(+g(TpRF$SAP7ur_#-O!r z8~zgCXG8fGT*~_eNXaUZ_;L6!;V9bm5B~Ts%g3xPplUB?&sd@gffDIC>sm+(mN|(B zg{pZ1f#8mqQzjyGS$0JPkNOyxunEH~YZ;AE|$_k_1%;#bn(H=WvIF-9d z)WA9)mD3*XYPb#;0wF|3mtLl93WlF$B@0|1@P${5@kXt28}|)ZiwOvk{b0*cOkTz%kiOz;bn9 zdW4HDtt6s|uR(zpXKI?=YA5HN7-t}J5TTr#tkG^`_&@G+1ZBVG59weX)%!Jwdk^0OXya>?|=2g+exb+?6pJXL{>l zK27Wpk{DC>t!YiMqj9O1;sCEhOY~+ zgU>Z~N@w2Azb;&M%6V9;SYY|XpN-J{A9P;A_|YJ6D+E` zW+_JiDUUSrr~LYDl8fL21`vqD0EE{=P}MiEM0^UY9Mec8wYua0g$x4Dolg+W0SN){ z1H5rYjh?tEcWP z6G46b>EaLIbXf#eSL$;RmbWq=6`x+#-K&CO3n-g5x;ZU@gTmAY z-&^C$Fh(Delya{7wYYX^7(G?Ly-v)F0B?PAwkkl55Q=H<>n5$R}5_U%aXGjdwjJ`8$!xOQbudZN#p-&}E z2t9UEZXBf#0OfFFgxQ|(B9IFr1B+f^G5xKd;l1!^0aZW5#kWNi651uZN!@`iheacz zA7J9BOH)l5br>*a$7)xP-B;xwFE zF}!dIiVbrCIYN*tEHEk2HU9lgLa)-!w~B$0ek4#GJ9#=4fO&|{t0e%?mxwK z4iu{<`OQ~m$`@X{Uf%jQ-!EUeig{z|&%lJm*wtHC5e1P~p$? zzK2tCuVm*NFNs!hBQFjt1m_U53|>aa|9n3G5o%2zY%)PR($k> zSIQdFy%=6URy^*06EyfSBVklHGA*%6sq6U$YxqI>WKv5sT(J6Fo1+16iN&iCOlvN~ zDuMdo+t3A|Hqs0}4b`-jVUT_(XX73)S94?dB1)GJ zFtY#J8>_gtVJT=A?-ke_%Z=T;tCX_4y&0qWeRjH7R-01T*4sbVkmDs$&WeV$M;RqL zHOZP^%C#0j_SZtf8IVv8(IX;$I+Q{+) ziXqo$&Hw89rE-gwyKH4YUT6JF_{X%Pvs!ysN_5b%pU(9m6!E2NKzoMs5;CH_^2*Pz zipJ?3a;Zof-1U)sM6RSafBq*%jV5ArDTHiR_G=ynune#Go+p1QEpd0SH?INBQ@3_C z`Z;FW5KbFnF=GqSQnc79hMAlD`S<#s5)ER${E!qDysJv9+spBMZ}8hU%TdcUG-6u7 zPr1y&EAVS8CBzX5Txb4|M+&y0DQYNoX>HVF^qz$KxTD3*+VdP6A_vlq>KTR^?mJb} zdy3e=jyyWRaxf1@tac9MatEvu#TJ%R5W<%}bESOmuOF7rvQR&c8RWtKCJ2Ww4C3u= zJYz=*{uOUY}5i>vItQ9P>}dB@wDO z!k}^aMtSn*Kf-j8J*sfuWeo4-d^SKIMO2@u09u_Gj<_xwjGzq2xi&h8ID*1xw%kN3 zd4vS;FOSs zL1>SCzflC9c`V8*O0wtWKl*on3k4nusI3R(H^2BQcHf(kge2NYmeZCmtzy~4G8lIj zeOVS#5(0}O##M|Gr)41(b%%e&2CEv3psZp@!yOyOXa6An1OtygeHjOZVZ6*SjYmIZ zuwX?ZR|UaiCA1DyJ|$-Y1_OqJ1^Kozna0asFw3UORpU4;wHU)`L_~175r_5Y($&;| z!|$h6tZSPKDbC{w72x&%g*Y|cr{_9IZ@Gvvcprk0w(s7%T#}FRZ3vY`hO11HYm9;n zGuX5qVQtgLLEQw-v}fm*AueJw-_-5s6vT?21i}DOU2}Aa0)Qfjh#F%K-gFoj5tK?tH{1{KRpp@R%! zjs)w8@288{<}yHG6w;@Twvp%VUdH&rT}SWYtdLqJ`R3u*qXONFXx|VNmBFv)bFO_uZxi+aw7|W#N5y&7ZK2P!YM@BIh0ND zxx>EG5C$@@ZaQ`2DN)#rS)YU>M|@?TR)I6hfsk@+mwZIz9K8Xu@4nb9-+E)Y{KdPQ z zb*x+3>@}v~T7fC*9WmdQ5YIH7#HH#YW|{x-Z`>}sPac)`|Kxk+H$Jz_2Vo7kbA==@{!EaJhC=C6 z20BfwM<|onmn+eErrkqtDFa_Ah!(LG7B{QfB}uyaQ!~nMS9lBMb%A(!Z%tKUG)NZ< znS_I%3ftG#FqLEy!cY5N3A4vq$R#(G%F9Fau2K`vYc}d(N5x?XN~sT zuU{&E{r->2A3eEIzWntsv+aXqjxEV613u{iOw1DTC~LPUgU}liamCgB@t~c9uJqK!G0Of z?F02L66~qY5CC9UmpggaI&=k0>1Zu0QZuoTS~;0r&6Ia5Y!p9ofy2C?hZ#2!Qd*wO zgpj1`zNG+N`Y0|2MQVY9u+vCq8h%>-dF`Slbf_#Bzxl>xT;jMcJI2z*mL3oDtmg#G z>n>epIbyMlJ$hL7S#mhV1aKVVdw0kj>tGFcDRa|o8-vS^p%|ez5MkvgG$myWv5|=e z1jktGcm}OyU4UTS{W67>({F$EdU=NVPs-2mQh4p!db!08mjw`UYWbi{Vaj)m zk%x}X6v=h6rr32z$J@N_$5c{CJ6ZzPnI~q@2(xj0lfAYdfAA5D_4mtb?6o~)NpK1U zl_&IB2Psz5NXw)#!;uzY3#EB^8NK&0A%Uv z>vvYzB6m>k-g{c^%`UR7ZndnfETUjq3`f;!%%!b3jw4UodWt#xF$0myY}+W?wl^`B z|L8uOGTue0^sHQ2JT2e4z5o%Pl^0u&Gv7U?jAKqkY+KK}r2(cevL7F3DuLc>ZwG%- zSh7uwEr+w(!Qx^FRYLbcd`!nXePEDk+>lT&+d>=^H#Jtdwl-h>v)}#gvQIz!{(t?) z?DVQzGi&Vj>nP_{^UI?hl6>+8)O@Wk`L!(hpIBs<7}1Axu^OWrpfoYhkn)V$zL_p` zQ~~%Ush#i%xDu^FM&u@)hsbI?<8(=k#MOoSXguR5sD*QH1X}0hegNYpR!iO=|=Z03HRgZem-!#Yt;dK!Cr21CC>$|Fv*(w&whC0`4O6D4OcHTm{w&9QL=r zd=+mLW91VzDE#GLJOI(A*?EJDROV^3xPqBt&OCyv#Sz4GMP)fathAgvLwFvexeu%( zkoo{a`)zzUPaJKQ8`JoUS{lJ-8XwANeOE7EhKbIHYeqv(TPwyYsyTPoP~wdJoTDmn z##oPD;Otj>fPq3zt|r=bmBV65l$gU#h{={=ov~Nul+_9^NE`q5a?Z}$d3D#_VoE=zC0LB<9-)(tt*bAxz$7goChd ztnmFJ;nq0LN+8;y%a~nBD#7`!hDn_Caw1EnX|zF7-bk0^&YifDo`Ae{KSB9KG_`@i z^h$%a=Ni-sVuN4IuZu^tl!CM^M3FZzJJ5c4xOr0EX5;#;FM>2ZJR;uz5M{w`S-E<( ztlqp)i4pU^6EwcZbhMkjYhwY)fsJ?9XI&|afr1j6h{xQ^WuX!6j9IG`S^BKo$~9tV z3|H@+?lM1HF5i6PYWWhdw@^?#XQ#@Od(X-y%Nd?NaKNF-q^#n$e7-DMfST=rt!AJ$5s+H z2Koi=?`C=Dqz2_8Z@v0|)HF$MseAxiZd0p$7`+J9&gT?-p-5)&QZ`SZEM#o%}i7U$2eid!|^ zd1#UwJ^0(gsT1$@PuPCvhsW`ZLI#+UfZX{FS(&-Fd1^nEz<5=qzT63(%SlvL(0^I&-&&J5roy=X7_FBe@}R*5wVsGw+aF!I|B(Z3LHrs!1RO z0C&vthaxHzKc)#G7%BqVol9^X&%F6OVQNBMbK8di<0_T1>l0YT4y(0T3E?q?{t2!l=f%v+&J* z!IF>pHssIet}=rhc`&>+SbY|)&kK<%`=PsPg?(YXgB#f}rk5KyIHV%^9frdfVAOmX zr1oCFwNM#RYp|LvB_J#vz!&q;hWD?Tmu(+ksgAeHDuxd%#o#Y^1}@?Nt+`yzxN&|3 zft(?>*0!)lmfg;;rB&Qw)|C091m>|DsSlskQhPS7#I5$$d9h`z8etVbkRu#UgLO>| zT~D3EAVOctv5sPyL>pNZQw4ARa&woKJXTV>V%?}p1hcH!&La+wW60pi2S+N_AUX{k z%vZSX0nXGS+*Shx_6tLl6Qk_cMXc^KPZWoAZ%US_om}zx#wMOP9>$W3uQ9`vpoQ1N z1oY&H@-kjIw?D;tOY5@J90=wbv}?$2K0ryQ(>KayTa0oxQd=I<*82dlbss|uiBl=X zmes1szB8@6r8e2;>=7XAH(sNRBjtTG+p$>>a#VaUoycOkI%a@m*Tm18H6>p76nTT!~8czzx6dCftLB@2-92IWE z*>+Ef1=wY4xOnJ$Ho;qPt$@Z5d9pSKJmVU1Ks-FhxmpLrhH_MwlL$L0Qo@(h@lN=# z2+|7HwHIk=(h8*O0D;#l`4}-*GObVbLlZ2i_A(o> z*`_wuiHv6;NFN2t2?*Rnw4HH|1`=zuet;dXz@phrUczDqNQEs$L`5D{>P&$3Cm^9- z2E%cUL@@~f06+jqL_t(&OB*F+Aq+t>@#1<#L?xsR7G%X78p8`%)F@)Du#+)}djL1g zH{u_|nT`=B-BT-4YhC1EGr`)kMCwTZdX31ArG(?u8fie$(>ghNjF^_AEqZV|@h2PH zDX0D3Bfxd!Cvd38c(B}lE~i40X8A_%4rkM7gCWXnX=GC6juSZm(@>nysa(`HV?8z_P6}kEKlwFhGY&Jo&uFj4rH2Yh4v8;oj z5$#$3<#B+c4J9%F@YU^wA=vIihn}f>npp?}A>Y9}57oHE(zMH`+MkJ@;PbB?kST&*Z~5+r|<^R^UB_@dmDqpCOjazHzfG z+`7*36Z>WBhd*a2QA2QZ>>^uf{!;AoQYVi_Y+bIoRYEttSJ%L#Gw+L+zzno;E$lGx zWLcvpT}_;>;xQ?Qqjz(hnwur9u&u_U7ya_DetN%r_IKFS4uY!z6@iYKbGc#tWCw3~ zgio-hIRzIRf#cvFKbcsUhRi2|Ao8;vQvDOU(RH zg-bRQSZ4^xeY|e9JJ#I|h#Ybzn^W>v5hVBRIWI(PcFpq$g4dswM^x(TNlUJ2T)D(1 znK|<@1{QM=zH?i(+S8b2j<6A4t#Mx>>O}t@+x48Ms}*-A5dbo1>pfD$Hr@S)7=SQ^ z(B;l7>&R|qn8kadjglp0+Pla4uhv`+S}uQ$GKf#QG~-@g%6@`&eN3n6l+~j~J@6~i z4|Sbp{;Vcfw?8y6#Hy7RT$=ID`S3BD2K3eCCWDU|K;L^n8#w@xwsyM=#nRL_pL20{ z5D>5OO{8ld+uhHJU-6_RZMazRv|*5u`7#8ihWzTUxDtpRqT5=P-I5)H)G-o~jtNFa z*zUt+zM&r@^4CP-MpKp`1nhSrw?Sw8>RqBWrUNSvKGPmpppBIe-uh)Z z+uSH8kDhX38JYwVMnp)0I(j5`)BlZ^ku7q)%%e6J56CP2lPHpTqSdgPg+;FgY#kt! zj9f%8PEC?ndCzx$xB|&|4X$?zP+>C%fI#m)FXey#%a6-{@$Y{P?#7N2khBL!>xH|3 zOs)~a#dg?*qe~i3Kc8N?!njdAP{>t9O0Ue%5W78a+0vPh+<07y=86>+jay4CM}jCV z4}X3`vV_16K}6sxqVr5eVcR(-n)E-^lBJkQ&WTyMd>t{GGjZ`= z`SHhS)Je04k-spVubxq7H!dy28PF?4&wa=-Vwc}5GZO=8tfDml` z;Ky8veU4BSM*<^)C~*R)i+!Gk5vU7uFn;+8rgecl*CO#=``jW#py7i9nGwVV5!RGZ z7*uE}n;6N<3)-r4Ji=8t zHX>anFfZZr(%w@Eg9$gSS1rR(K?n$88pjAUq;X7}G|Iw$;IG0kt!0KYus-=e|C@*9 z-~P(g^6k%FOXbyS&MvVhm4Bzj}_{ZH6R$F7}GG~GMhUSM0)fyh2MgQ9y1P&L zgtCuQPQ~vjOe|H9Ik;#5;&N2PY5R+C=iDLqw4+*P=78?*p?`NdKINSUc!5BCR%O+2 zgKWA!;lXhGC{Oi6i>KrwqWdm33@!<#B3J-bF0D0}Yqr9p9)rmBp+acC!{3KvTShR3 z!~xHzl#KjaEc4BG^N_O+zSJ}=tzU9=1BU5rGPbhN@P6?-fdZ~C)}_8B$R7$*j~E<) z%cnF*2fqg7xBeu&!l~eZ(KTQy(S6S|DZHn8l%?g8)Yim{yJKE!0>{TRLRUd+kobvf zMBezHgRgc+%762J{<3`L)@6>u^f->{A4*S&wu>T}rg$uTvJ(Y&Ge~hFXIdB-x@U3f zoTBjK99AwWS&L{AqGeW5WNAZ8ih{&FL1a|Zs%V(`E(olP7U#p3QEubbH;pzVsEEU8 zxjkswV=&GjSj#3%#q>pXB`xqbiwR+ES8J`-T5bh`bC1(>=C-aMAG6G*U9b6ACsEHi zqG=^8jUM`#44&Z$(f+}bu-v(GsocB&VGKP^h|}+5M55FV@w)x)6p{E8(Kp9`F!z;! zt(&&Y_QHs50|85{!8Y1KrI`e-3md&+$VtW-xME-dA-PsOr9UZ*ab14;ym_qF9#Lu+6L zf`BHJf{82snJ^GCSQzG;ml4k6Imm?b`bCWz2@{uSzNNi{SSw-WYXm?0OiT0Eu3EI3 zQ4Once)ZWx0;x+_8`X?!eBmYsek;rb7*;=vltY_WBdjQ-n8Rs1s|Sly#Z1I#r5TrVG9p7bYK{%pr;{8<(K+HU zOCugfDMysMy2O)Cqik-0b5plvcNs-47ni9F#jY1?5HD9fMWJ;@UYaxtJ7>#^@QCuG z7(`UAOW8<20+AfTjom~cmwTQ0?O)8kO&rQcQ}!6K+?cNovc@J8%vU{YcZ_8(vdg+M zpC}i-pTP(?)C~<9Wmh2ukrHtnzfh{CC!BhKn2>(j&a}FvaYs}Cl(Ua8g8A{gPYR9= zskfqG#e;OOeJT^NtNA&n;nwl#ii#{gj2D|9ucJq#oIthJ6n4C9DV zV}P&iq5)|$($Hi)8OR(y*Txyxsk1=AVP3CSIJGn0Z$c02`+oX27kzDSGR$9&qvARAi(JGP?iysIt z?{G?57Lp~1XGG(1+C=AabL`RejGj$g&gN8kl3C*;)@MhhQeZjFw;tw~lk9*y&VZsA zXL~q*b(!oK;+?_wm1rt~sgUZAs6U~Cv~6|yLfQs+{{7>?Z#sWzA~YGbrY8Mi(AwY^(N-|Ho!>31pGLxUK}`)mrwgBaRjIu-6vHaV@1 zjx||4?%U1OJEP{e}1GBPg6G{T zD%N8#+20Enoy9azQ$e@i>9B1QSE&vXhn$8tI?AD8m=(G!$vRjU^N8{ZIBK7(k_Te3 z4b2s)!YfGR_+tB69uU|82o-ol^+To$^RqZ*-KoYAwgGdBXuU_+xS;n(iE~63IsT&! z>WTqCS?01mLyW?voook>z3juC`=pf~561>0y12!-g>!lmC!j8rR8>np!4!!BKIftJajfx3nX!1E|6$pDfMHeVNM5MF^* zP$_neq9~CPcb!0Is%;dx&X|k#S^J9?lg9R(SjABHb7~Ev_%&3tj^noM$q$eth>v%% zu(@-2s@z)DTF9$5jL(P60e8`+zhK`dXu{m9x{Hx$MTg4a;EZxH{=&f!KRr=EPGI;U zOCK)bG%lJoNjYh_ZRLz{DFd+{p>%SSforyM+!NsfXA*a%daTg1p>s&Nt;!t<#fo@y znfq1@(e9xAaU;K9NNR7@85rnA;5MW zSd<|6Gam<>5UYJedP`Ed2;adtxv?5*xw?I!q~YLXfz8wHfcAC<5c)M`=DTMsogflZ z*z(~gFUo)S|2!yuA-*UH4}ub0#97v+q$&SeGy^`4t|q1P)C7vCIAa#7wX zwBVmxJSxBTTEG0-*9u3J;@$@1k%OInd5r5G_&-;L=aX%94$+YFtL2lB#2fnNdm>O* z>NkyX&#T}1YT3nI>@jAV4*r%)1*QX*aWkLwA^)qGUds|dDGNlq{Ch*$oIifrQ%zUD zRJgTjL*eaF5hl+xSeh{}?P~=wJX#I1Yw=QWDyx4bv?4u3LeEM$193yRe3x^?L=aJH z3?dm}S!4uc@pAlb4K1i#T|pek4%jyGQ#|Y6Ugi8*?GGS2mq3oOj@rdN%>Mt&*n2fw zmSp#RdA%uf%e%JIJsx8)07(I$1b`43dQb!;0(ubT+g)c>Z5Qx5%U;yEv`ZukJe^(ad%b7Zqg%aQn;pPZ3Xk{qtg|cWvj-0`C0vKT2qk#Z-Xx#&7%~|Cal*9wQ)>C2 z@)2OZmQhYLPp2~u1tZ{=pX9>r9fRQx%UAENwNE}TZzpdA7+TEa+t{5&D~YWQyi4GE+KG5ph}a5m42ZXhI1~P9@r8-YuRlT52Z9N6 zoOoUkOB-6lEU?Jwsq}}~rrK+GX7K?gckXC*$YgN@R0S2PxbhWZ6G)hGTyy=1yIjBf zf4tlN$3ME!{`6}rTyN<*COscVvFu_c^~3j$+h@$D{--w;l8aY#A1?P;} zG$3sL0U~)GBG4*?)Dp|=(Havdd1P4UunKG);%1{kLIlD?!O!uR5d?TTEcSxyIhLs4 zTKj(Dv2Blr>KW(kF0&C~g-gCn=WT$}-(kA=IcMHu?U~F;LND#`9!5E0NbmN=QgIbC5erI$Lyc|MI;xj=W*03d(61QlY{D5_jvK&_h|3;W2?* zM)NDC443(-Ph9w6>|xqDB3vTxYp;E=E!_EAG;%bMch))E>LJq)j6#w)O${JMkSqw_ z4CynL2=veW6hBjsBWD`^d@!myq?Z-UNby8fnup~{Dam}2cY};+1ArIk!Q@7yZ1E~! zL;yc^!P!7STAEPIfT|F~O&ch_sQH7Tr(~KSGDaq;l936jm96{?8M5LT0W+R|Fxd>t z8t2$b=XH^nI6!QH1GRTHSWhL7Q-t^)mQ6=PUJ8ehhFk}zRxCb%11SG=ZnS2v>&|rh zpa0co?QcGK-u}}!IlX~t@+R}j51&o9KY#bAUA}hO{s+ql6M{_2X>XEsU4+bK6i=LB zR2ha;31x3n2{U*1F@*VZ|KgEg+_SRSpRHNSn0v!EH_xEI$gY!{&)O=Y7*2(YwJjDVqV^68fX z>G;YzkturvXK!to<+tIrA)ZCrRkqDhpR&p#bMnu&X4}8M&vMr(=Ui!lBEOka9`)`> z%6uV!A^a;BhWAS$ub>3@S_qO#MEwt9V8Jw~>d_I#E_3;u6>TpoZ%zWKEbxJL7=TEL zIAQT84+l1$#m{4`Dk6B|6&A{m;r^MJ{{ZP+X?)kTYk6ykV@zLlB!SiV6W>}^;M4E z`~sST%P3~N^mvOQ;K%nLw>5g(=L;O-Z0-9uSsr2FV^-!}EnG54F*VIQo;uh5^25XS z{SQ~U#F?HJmF))i!k$cB;D$UlVs9R_JKX3duU3y)!@LYPsbo;e&{ENKZfZH~Ma|Dm z@$!*^5=|it;}Vd9?MUGOVR8r5MXZ~a*bAEZA{A@-?1kJXtH8R)mF}4w=+XG+ywP>r z>zunZMI*M&ZF~1QknIttO?Z#bC9dRfXidg64B$sEy3ZP{FlP>-yMBwiZaJFg7+hn%In5d_7l=57a~Uj51+BBuK6EJ}J0@`-uuD+#g3|^S zJvJf?NJz(qd!{^|uVv5gklqo6hX6d^@X$LAF-T`whdS$AA_sOcmA3vRQnK!`^y=>w z+xia=4v!Pog;%k!fAqlzjMUhN%;NL<-Mj7SQ)p$wGVpiOnRamIJ~icPg5V6TeCpju zJ>PLykK5yn4sZ+1${}Zh@trT=PTBE6cQxjEj-ifL>UFc#r8OjsB>kA zLtL-1_y}#t>tDo(p%PS&ALOg>uDVu|rzV zj*3x-v4|@J`?r90RRx=Cv~#Qldt9qRGa4F_m&|{`TM>wNj-F!|A#hupmQ!FuTb)#hmQ~K69*XYfnv97KA^w;lgl) zQDt_BY-3|-ZKSOH8J-k1d|)r(vXyJK2P|oMjF0a+8!me~%)KzASY>I5v}B`jPu7*A zo9^_V3@iUI8*rqw=_|gqFTULgcb7cgeU6UcZ=SWcKmBpLNFCh&?4bSZ>1^9z_-oS6 zHRCR>Ly<3qH^7u=`8Ee}=&rwy%Ml;qR>;`jejf^s^3@!3ej+?I8T90$8kR&KbG-`W5VV(V*Uco+X$>pZrPmB7#tgz`T)9DAvQN z^%D-gvUl{#jjZ*tF6+*Zy3q}@JHC-@hUCoAP*_++f)S_W0Ib4J_yxY_Qx)0~tFI(A zh5za154bD#mmfZFfBI#5Gyr!&G@cDyDVJ=W5nPA6w2lsmHSvp5CJopCmGY=+H9x^g{z#Tis zj$B4;O73A=8Jyz!m!G1iU*!_eKPs99i8~u+dXGlP+P%+K!@zxL&|AOc zHKZQ_5rhQ`Vpfs^K?K3M-Nog!;E%Z2=-nsJ8`q<^H#pJX`(5@Js%@}8aoXh`wV1fx zbN5CjSph%(z&9%iWOkAySr5Oa7bk~E`n$3H-@pH;{r5X}+Ibi)Mgia^gNm!Yw89z} zNpgr>1{s#Yctci1p-kZDTIjWFS2-s1a?;lEFn}Zd3~Qe=tR;KnfkNy2bis?f0CGb9 zE~8pE+1pFL3ay3$^Yok!W9JZ1g?@^3TL|v6$E(1@8p+*7Gzy2VsUma^ulMLFcd<>j z6HP_J zde5;`rBK#b!*%jO_eofdNMzla7;LPw%6TFaCu~Qv0r7XB=W$KL2dAI!?x&%&WdCoq5;+ zB`@g{ei^XS6IfN&eBhB3d4?Ji21o7IOF_aXh$k#Se5<$uAL8;B`_F+KNkfq0u!$n$ z7ytfO?df};I`+W_%!bT_HFC*kfGM(p?ROO_<qO9NNax21=^_qg2Z z<44==o42}VkyyZyX$xMUBV_beEYH|>xF+g+&UvXM*6)}FeV2kRU%A|_-MA9;%DxnM z=0$JnQKa@j?)-6RJj!^vAvcy+}zG7`qyr9>mI{dvrI2aUNgXRi0c_ni_4B8imqOK zee4b?8xTBgfV6K8eaW-;q~wx@M$<({CpH3(EQI24_z9}xhifz-oakXgXnMo);J4SQ zVV`%FOA=P>dVeuig^hjrCXE~#VI&|p2S&k52_qlH7p4uL=5vHud<`okd@-!2>F?ub z52*~vpFCBJ(dPl8LaQv|VUE;|Q27y^QbdVp;#Oer)C2E~1d_r`(;f$eEuP*Xt3fz- zux!5jxUF+Z@eCDW4A&eB^bjdA<5Q>%i0UH&#kmW@ENilt-ng0Jk|*HrK6=8TXwOlW zV1jn(8Ydnuwhy?x_lPAQwURmu>z^?C6Q~9xJQ;AWt5tZzkXDYC$#c@9!(tUwEK!)3^Ff`xuFZCinW1bt@89i(a2`D6kR8D~qV*6T zX%hTHnkvgY4V1^vAs}=8S&)Pl5QcFb}2sYInj#?{q+xDg<(|3k_vy*4C#i9;}<;6W*8X3ViFZ$1h!z3bCtogm1R&jNf*Wh zr1!Aig1NEB>G4Y<>LalEW^9lsVr|SJLOd(30P^V%;dvB7`bQ~3NFVQej;{)gAe}-~ z5~57F4$EhE1vA7GuHI`KZR^AL+dP*_eTe<**4uC$Li5bga>SKIyH+~v$8 z3@eD

    ={;$SNp35G_5{ z?Ty^@Zc_7}VLm#~J+LPzvt(oi+MY3rCwj6N{u4LrvLq6(2r2>_O)JVEH`UirN*D+V zFkB{bomAY+Q^8dT+1G9qvNbhcQ(|NoIalzImKrzUZ3rHq#LI0Xrj51Fq&*z|oEa++I9+fZrVD*OJSj zzW8Y!=8#)u)ih~L8OHew`oJmZwm$?F=fSz@eFedQ}TE>nbnQGw60McKS20RE&`~hEM8=>Rr zS80!ZtCV?5?V6HIlBiMw&30|lp+rc{`K!xZz{jg?33*$4O9_NScKN9&(P<4isUKg1)CX)KK37LHH>Hrg1O zE9H^l#K0{130@>sc%z;tnmxp?9vpD8BU{#3Udei+jDX9WHB12vTSL56=%~Cor90op z##ZA2mv%he;S!1=uY+`MY^DwqJ&?_%6PHCil@M-*xxorxC&QA9imiR#GY_Pk8uq6m z#S?3>DKx}8+bFo0D&*iag_lFY_gSYp1~y0$(Lb$jd{9yw8(`U^JMZ*Hd$q^v31q+; zIh2xV5BhJhtfYd=M#3J}dUHf!&1F;poy;PFOk?F;y7t;LDD=D=N$Qd%iKq|fw!m<% zD~_K&MGxRVfaQ;9I^WBUUic&vojAV48!$NVvr0x$jSB{*$v>ggd&M3iCULkx?*V?# z#5nz3C_PQ0j(#RmfUS#8X=K{;je1 z&fWXm2lJ#&vEOtWs~{0|;GPz~BgA*d$?!vt9_@wp+b|i|Vd{lfu0fRjw(;Nzb7N1V zNQ5BFTbS_v>QQ_D(Qf_ zpX8%RU$|EME|j@OgzHQ;m@A8K;#ekJdPHE911O=^X)@5$u=4inbIu?mm?F3g^>2L$ zyFL?|nE4sNS6Rd%Z5Q-ccnf0Iv|#{8QFcQ5dQIxW^=obJ5;rY!``|tY@0HL702vN| z%|zkKvmT>z^nztr9O4<7|HkXzWM`2&g2IW9@Dl=J6tJ?G$du5~GG11K!!3t_`h%YW zqT8eO#@c9_;aXbmjw3-;)Bz^`zQtK_Aw)0>Q4q&AclD_K{r~3MoaKA2{l!23Uc38q zmZ^jiV)VoqA<%IA=)Gqsqi%tVM1{KGWKS85GK?;hI=pphpeo8M+4+*N2l)OoF;R0rmoEYdFb8m-bshBy^Q!|R$ zLZA-m4Ly=)mxjS*Cw`o6$N0nRJfnmYVe+y~5${ECP?IgaYW$mR!zCe~^aP4ZFI}L4fRm=-S(F!mITQx&9t#pzIrJQdI1KEzF>3wt1*xEc3# z?%DZMwoAPVVHP;9<$t^X{kHjhT)|1g&lCm$Kl*5+{lmX?tzATGZNFZF8JH9T>!VX} z?J3+`f5PyXU1%s=VA34A22&1Yvyv6kD3lOF*Isi91kA0_y6!2?DtJ^dIj^<#^>#TG zKgaZV_K4{%=_N^xA6OA*9xXI0Vi^{@4R8-VzyU&+lNW;qy!az;el-j z(A&HmVtWeYDZ}pVgX#8g{kYxZfHk-4U06JAx317Ipa|5M!=6Enwili$j4G4VG&=-# zKFh!ZnbFvv`l8&93x4 zgp&{j4hxEC@7Yq2j=;;F69^G3I0+NYV%{MGpcNph?PUNy?-!gpVx$8=$Z2xnh+m!( z06|Hv6hBFbImD|uA|G&v1Qt8z5e`J{?XO*0Y_~4Vx9b?)|NfV6x4(RCp406$(@WI` zs`)BBo}K)wdzHgna(N)co<6`&$Yjc*R|CXT&Y zHWr}q-Q+5a96E{ zTxZT{6~sYaL6BZjtu1w{w%>WP>%1zscAvPl5F{y<%S38(AFc1VyH9u8>ehCabS^S` z*dof45E@fQ_W4db$K{`gnLFHF;Oa(&RjsiC%l|yx?W8rPJF{q)!d_3dPTRYfUruq! znDsJ9(@;-5d=tP6&ie1(R%;hP{d||T_c11E?9b(+onGY9HNsBZJdrr^f`5~41aagU zSc&G3;V-7a2l_eU`Puu=xueKl*24fLwv7Xv@wo!%NAQHR9uOFk&@i*)_S)CqZdYgs zu3@q}HGiISvL3d_?|nqYaA8~lHa?k5;3R4ao#AH!h*Zpv?^9;FJ55fRL)z<{*yFLt>$W_{78>w- zZN~|Mes^QP{rWyyXyRaY%|;3oRSdhAIej8DAdbT=@;}Bfknw!;D{j4cx{@O>L|@ zYufVgy!i^#DFkYRZGGTaZuHVUM$gW7diY+fWc)Et^vL?|acli=K4dzA;91v00%8xi z4lt@SAiY}JPexWVtFUV6Bn47AbTt_g^UQfrDBL{6O<~>HXE^$Fv;E}mPP@A; z6Q8v62+b`nANtyjbM5Nl47c1N2pq767=m*tgJe+oC3pMar&ii|uQtYq_B!Ax(|DAO z!pY#6&YAftqk>~6qTJ21$tH|FEQH>FOz+Ro`5Hocj>hIZz30j$%p2L5?{PQ;kvBav zf(SUY`q)Y%f&Rm8KGo<5CMkBAj2mv&Z_uJy-#xGb%GQs7>Tx`hr(*?IrP<%wq^oe#4L9 z70=}Lem1=FF;e9@QHDv_Aet#0n_spV3jg}Af7EWgcB9>=D63D`+8RCW6g;Aig!WCx z?-=Lq!&6UwgOzW@tN(dFTcC<-tVt*QmxuINMcn>1ewq?DS+IC?X!We%A?^^p3v&0( z)9Cz3vJxA4h+FlZ9JKpS589V*W32>p@20m)P?uE59y!cZtSncw~2zkc4ne&-Iu zMYoT`pmuw_#l($>fT{DK@Hfo)sRBNQB1gC(Cy}&Da_w^uMUuzMIGo!;3ADPp&01({ zH_xHCqFMcn(-fY=^jj!6b~xhGVbdRc6BEc21dR0FP3utC*5iQQS|f>h%oXP}r(_NL z82f3j{j?Q#T5t#gduUeo=;`*4?Ws|qxFZPvM~~UF3Y<&ypo`?Qgfi=#8}gY#EYjV3 z{OLSrJ6Wb`^EIsC)h~KbhsWAatDLjfcMGE@>#NPShc&%cVJ?Y1p!4_?3%VI^sGq23 zh1FV`boufp^;luOp5tkUBMoq4So&EX;IduA@df9PR6f% z1!moL*VA-;ia~&ks}c@^jHgo`8}kLWRyvGdd$xv>^m5zddePla?&ko2@D2Q^x+{PL zHU>C+3<&hh9d0CkFZ9_IBzn=o(f}=5cId|GC6gewP9RV4=Ls@d%okzz?+&Yfli!dP zBrH!KothL{WsNx5D?NO^)&7bLn_hkWc6++E-5zk)4db~edom{{uE{(0Ds6u70jD1P z&T0GN72y(Qhr&~(P`0cgptU@sF#)|9OzN^n6ihI)LtK|`_BB+XM>=GejmG@jjElF^ znoe;E_09GgilQB^#PGJb_qmesLzF9P2>3SNUt$U7wHxQ#$L~L6K8{)B&6V6n=h?;@ z<2!Wrvf@Le{($XwTrQBgvL=hiNZAQO>M<^B&$vNQ!xM++>%0Tsok4T#?OkAWu!Py- z#p~V`gt3VA4{IG`9+?B^D1%BXK+?NHGs^D}*mYNL?o(l=ZL+6fNT`f%h6Yfr_Al>p zoh7!v%pgWiI(L36s>>VtT4&ZocjXr!>mU_WewA*BHwbgm5>LHKK`FlL(E9Ui-W{&F z7u4d{p_LlAqVt9=_)_dcl-=1-}+S!O5hK%K{(BbKVXdAF zil~$~U?9v0Uia0b+*zUO;T@$OtH<>@AjYE?OdpZ&-xXU}6;j{*SHh0dcHgtljz{^9 z>$NJ|!k%d~%*MPxz+Uu(&ENm-dq3uqIu!^9?a8A@@TH14Vgan+fkzOg5rPTU&;8m@ zr|i3PX?fweUAe*eRcL8<*lPFo!9sgNz8}&HKVWa{6L#=S?a#M+_cypKdyhHx8N!Ql|#;1T4d|tGAB3&YD3Tij$hq@nR1c^Ws#w*R|PW`Pc&TokI=?c5g~RC zTWgV+yJDsFo2RTfPhnURVoSucby?o22q1T7-T7bMFf5;WcT$sx)^SgeCvhjho2;BK zh7dh@_ki*n0~x=`6XUNWec@*uF7N3&RP)mCL=C!AqiO(pUW6KvsoJR)aWvS^-~XT; zdzKXR4i7_H8B(jUMD8}mV-RLnt_!P~RN=b6M3Q_ja|5J2j8TL$W&~*>Rp`ZcJ;xXn zflTr891QuKBWlIAQU_Rg$Wl6e+;TSs1Ld%6?}&Y{+5XKx`ymV2Xj&h`NJbr14j(}* zDFa;1?{iNKJaj7;!@_hGhV#d;*MFMmY3%QDFWNXe*IPCW!e@8=@QBKv-k4&;6ZiJeTOYss8QNY9EP%Pj zl^#>vl;8zjNA$A$Xv24yhfgsVou6m9jQlh=MX$yASRVPp{Fh#IiHob}SzGnQ#ASNc zrEBc4Vu)@pwGA`bzZ{|Xa)ZBj@7kO0u*EIL7?dlgQn-tV@B`q>Xf|op%Q@w+B49ci zhmW4^w?`Zvudz&#&6uBeRz**jC)+cf}n({9K+hOs{ zDu6J@!Z>6YumY~IqDf^YGJU908S6157#c_rPLQL(?GDqFwc3%Gb}Yt(?A>o zW0$$H_w&g7!I%8K>sE!|BFeQR8ZnhT9x~??>Im9pS|l?$!K z8DGrrV{1&j?mFMp_fQ#7-QlBrBRm_O1vWM;P`_KxwnFQnb}Jd+x|X*0#_AhJ;s%Vf z`Sjd-t^(kFV1^NF(Fv;lWEku=7a=-@8)X*GG|+~gj?+%=1Hh0kn6#KKhpi-4BH!gu_EQTR(#{~IpA?w^Rm zcO@pI;2LC3x*LMa>Brn!1+jijEdV-XA-UWzQ)_4JVw_JIfGy4q=SJJ-9oMU1pB!+{cTy ztY}Rsoo^zdnTroQT|hZCi=aB<;PgBiF?%zVNISsRqDZ_c zQ&ppsDrM6&y|47tVu}LdP5shWY@|6Oa__yayBkjiJ0AD_ipv$@2m$>+DnUfly25cGt(kGSW%^MHstY6v4Ni^mt zzxzjD;X>by_Q8LA039I&-XYNMeu%}R zDl^0+L-W)bHFcS<`WfXPq{hHfP*QXls72cmMggwP6{DrC3J^DXuiag3|LhVz{_Xaj#<)^qe)coO6Vrd1kO@|JqB&2@?U5mz5Ql zJhU{jw^v}T1cmn;LVup2st2h#TviBFCb=Kio?Gpr`)xS^4NYRrm8X~w9x;+|=)H*Y z!reHB2izG;uX~K39daa;OF9xg^g+;F{_rRrFMnP@ST%V(e}QG1K{4e~tlRESX794S z_7;X8+glun4s7v>B^SMgG|Uc@4xF z@`VqHJOJ%}^<4l`dfQ9AU zOQU@uAJ8+3i#?GIK+a|Iu337%gA*_6;^w77I9-P1B1dMr>xng)n=_z zgZ~)>+~MGyb5oRDt`+Yg*qHXF*K;F6H4pf~VcGTv<4?KwmY(&98wxTKfcd>wXdjrz z;IZq_oA2ByY~7hwictP+0+2Rokg6PW;NOk&BZQO<36Ap0lGsWWy( z-|;tL{4XBh6`}!-dYx{+_&4uy(KfB7i^L>LN-HKbA4AY9L+f}=<~5E}K?C3TVYC2D z$dLa_(n^Rkz0ix_mHu^4pRS`Be?Wya9%Mj`R+6ay?fIB4CA?VPj}o8_wxT$zz{O zppTe8dh((dJiCmdzx0XF$sqKEHPjS$HYq45gnQ9dt;NE82=Ns4G0VIUI|i1OP*9oF z`4j-Xkq$jD3MB@o?5sIpbAxunj&cvEdpFAO(c^nEqe?puOJ88=Cuk3q;E>3U}b1$B#8`T8zXgV;BpnE551*IE4?@4Uzt& zOs~KB0a||I#+W5z7+i;Fya@c*W5i3NxRwj}$=~Nh+%a5BJmCg`)i=p4aCZS! z@ZvqZ_Tb_pJSk8urB>RWm(dF-{`J4w2LhNiRhTXK0mp^zvjh;PAh1}}U)(^DQ^nlM zPi6*4L@M7pAVrWLGD*5oK}4g7XUr%R@QONnh*R+m)fvbi05d1z)x*CMB77r28MH53)t*=~yvDH36C>$;;gu4i#*K}BhG$i0O%k<#@ zTj2wC56v;3Jz zh9lvjA!YJP&ENuV=P%qu2}i>dN{AB)@}oFb1(bk|-+PQap`&fQH0{I5^iHW9se9Jx zo!9|4g!8UH#mLhb^6+nT-ueQ~%eCnp=Y%SI_z7*?C#yw~_NG4VMeL2`e&ah#&GtMC zg>Yr52YSRbkf+=F6k4s1T=Vrx62(J7Es^vIIACV!Nq@?;&(W+7q4~^Gjq_kFm$HGK zvJnMm=+KT*hV1FZ%cmOwY+1Cko(DGlD)4JBZmtp~-2wb!Lvx;yoR@}Krho-*JqJEW zF62gebz^}Ue`Sz9jEuob;bCV=RUgUQp>R1u#lLcv@hFgr2x(Pl?9_ zQO~04SC8}gkd7E|gkc2WCEtnPQ-)RcES{R6Kuo$KVEHH%TTl1eyZaxqF`nHhG9^I; z&liSmq`!EZ!_IGBX%9cX%Xv*TY^{o`n8I|#arMSR`v-saO1pabdVBPAxBcShPun{` z{W#|a%kVlz}i!hz#;V!b7FS@Sum?Y#&Yzb)lqKU(VOvW*&q^z*9DRF%(6N7Nm#0m*d;5002M$Nklt^~D6kba#XT{%S#up>v^<^kNaWZkez3jIHG#Qha*n@_+ z;tKJsD_-d^4gWmwic<~CDF>J*)-%cgB88=|XVZmuse8zgGJ7@5*qx1Qo|PQPXHYQS z*rVj(#WIO-f@Ni~xB565@f-106&hsZ|t$`28!sP)Xp%YF*Wafp?J-vAE(0gibG zZ@&vM=c31Gc16^-@r0`>ihLEP2$)1kGv!8Scb&k%fjWTdD7Ys1(NErQtDNMoOxbHD z-ta3Vb8xBU$|cTjW?A8_OH1urZ@s~my^Z!y+jjfG4;~H7Rro$$$IS0f52yo{3{OJ? zVh%|{)5Na$e$UcafBW}e#V-7?y^8|3;0}@UFyFy-)Wjt~dsu(=cz*lWaV>xI(}#6h z+xOJvfH)O6f?*U3ULKKj873+CwWbZ@|CoRY{M=8{;@h9&2!&V;55~ToQpu~LBp+Kn z@vFu)p+x2Bq?)xv**|Vb)05g$?LFVc)|iW;jUdrRfqavUMl-GEO-u*RBecuY+=`Cr z;Z-b?{^)nlw@*I)tg+c5#g?(3F>Kyo20K=h6|Jf$j>fjRE}ta>!B&cq)kg*{g>30CG528|Vk21y<&l zx71_h(z0iYD{-?#Ka#U25Gh$^hc;qoI zbomx@7_S4Kj4eLpAL2}a9H$t+Rn##YVMl8`_DTUQYlt)gYM#Q8K$X8tHp|(=m$|9$ z;ss9sr?c_yovYl9bL*Am_Ss`Mip!^58M06B&6OcvVySHZfcwgc>j@AiCum(|W*FV| zSeZJs5+52m^U$EfURc7LwL{McBtunuLw_8crW|68fJL-O%i2-s6Z63cDhebFJC3pE z)f&lpv^KuIP)S01t1SvHg3|N)P5h8aBKpUdZLv!_T5T}UK#EyBH3##pz(vT#pp+3kpL`eRAeBM>iP)Va;IaOD` z1!NRByhyj9jH956A84bbepT6vCe$0Q_|!=LRsJ29@Dp@>=O>?{d}PghP?Vak7bW0_ zkAjs@e@0m6WB8e5&2f&CF<`4n;3gPkI4@l`Ekp-)N^3c{;tmq5Wcbsn$ZI;DKW8r) z!fU}QzJdV2o&g~#z(oEjbUY=7e-*=Y!qC-YfJ4tD^CsE|uIY3Yzg~wp`IrbJs;3r? zqM9_x>6v`tzb6np!whhiyKIi<7)k@mjQ~q4x7(M#eB6Hh&b#cy;gC3c8U8-xyxtRz zR#kE2aC64EXp*&*l94rzaQ|w}*~lPh`U8EGYs#uIzDKBP!6f}1T84B)cOBOe09mCc zWj2|fIOjYAKl54z#dNN(Iuu2yEQdh+m-C~{k%l!aY&!BtPe?+0dTDOM;jIl9R=8@D zrK?4hM$3#cra3le`P_xrMZ0X{us#214YIuu7=XkMnf<^Hc;JI6jBPv@$}-tIFQ4Pg zD74hBI0Fr2IB3Ae3A|I4lEgnBly&Unh4Ghfex8YvuK~adft`&;OArKw0Bg>9FM!fBj&-pfAPC*{r>&-)9=0C{_sz}-2V30zuNwn|Mf4phVw7%_oksthdI>h|wy{GMOe)FXL{x`1EL#(vz>7BNwT`hv;g*q~~e=3XK z{cO8!?q6y1(5L>{gVu)dDS8=rRMKIr;vsZaq(gA6fmG&H@0a;C0^rlVp9<3~XF+8j zD14*I;wi(_IJiDB*E_M^{PzR3!y>dOV}dA z@+EpuZ^>J^a*?fg=eg#xth0`>iS=5NB{nWNU-dLbb<(a6+jt#$|-bqQ|RU)G8Vnz_$*acKo@JC<8q;4RH1zjV>{XaDfK*Jb?pF@F6jRO1wZGXbv) zi4|b1N@vmWu* zbYGu?kS0#`xJFBi9u1aUd$iI1#lQS%`;^@>Z@uvfrh?n-HC=7LdFL~hN7%2bQbxf= zz*pS^%M#f9yhAGlgl14-b9gxeu38u^Vt}FadNM^7xdh2weH!(rEZE`Nt2#$IRr5+j z=e2R+Y;Xs@npa@>oRme3H!sT}>c=0nN8G3Ltv~n{ zYlH8$Cm%k5{sNK71VB0w7$H1{s5e%$2_F+f-?L6Ib{7Xv(GUY#xE-7}xgBNwd!>{w zA!(-1!aLg_5G4ShxKD)Q6ou02y}kC=|Ki8(N9TSKqy2g2Xd92nKXW$X^51P`&$jv7 zIc-x?&lX*6iy}I+lV@~LEH;yuEy z!|Bd(_?zpsa~Nr`m`jx$u!{*D!3~a6`3%h-&OEIL>sq>mOZ>=pDk$-WW-RY#eo~1; zd>#Vp^LEQ><%Yz<#6_HWt-9~<>sfw@HvIJP?pHAUDg`QaP93MS8=_X8J^t{N7nAfw zcojbG#R&&b^%-#;=76uwFRuGeyau>GkPiTboVTjYw1>a^r2VUhY{F(pG($!0tZi|j z)YW#0;~t-U^w~fn;hJ~l)VD?jtp3a`OP(NRUMP47S~02s0Wk`X-vGcQC7=~YA@)0< zhT>F6hw&26GZCDl(6g}JYvvNCEwah{h@(`GxWi{x0-Em#guN->cy41@@F~aDeEB-_ zSbI4I7Jr3O`SR&)@rWODN-v)}!2o}kr5ThuL{N9(jqF;0&LSu-8_h8%wk)QN7MaY1 zHAT-hO~arv$a$k{pY!vcm5ktu9wa=+sgm~irgbQK$niJ|tx6^7=Qg|r?40ef=dj#b zTC%rg?Unqq{tA4BRd~XQmYOdc39YkExZK;G-HrRZY}{Ytv_@|p5T7X6sDN71vB9DD zriXQH*15g{x64+>|MH*yn|8SFa!L_>OlBFr z2-6FpfWfD6f;M-Z@QFI%>Z5S#0rgbCCS8QMdys*7jB%)dj>qs-j|t-1>ub<)%t2cA zn&M)bp04LCy7A*h;r-2LyX`;!I=9;)2rlEmd0{$fp_!pV;~JGqBp%@HQj3D?oOS{D zDur@X4MSzEq!3%S%bkZVuFZlbxOj4+jP3?}EsLmx2$jR}S&STdnbhp6h?17mG$I~l znvDktfqBoN8J|Ib6;uWL6w^r0ri#$8&}+{)x;#sukl9|)-AlVPP;N)mfMXYP&CMN- z$l<6}zg;4dKG9A)dP0fg5Pq3nRyxKEl5(Cf{CA5>*9z%<)F1`=9R*- zm&m*bCQX@*h!b_Ox5D+><>*B<%L<}aOBD{IbKJ}l0%p(V@HD$)$ZwkYqzywD0>+cp z8uI5bHHM$2w`WXwK+JQQ7a98cZNssMpk@e+@kaO~1OyTP6ITW{FRiphR4uWZY?VtM zEwfFJzU{U@aP=gM=WEzna|BKAJ#2)ej|w#bEVGU9(h7O9{ZBmgWf;+Z{dkWpuq;pI zoMe}j=Gv1d=n7tcvwiKq{d;Zs%7ymq-H%j5%M^nv$_GZM*!`vFK-bD49x-r{R?v3w z)$8rTYqziv)6fGtQ@^z&NFi~Pko}r;RreKd(mzGi<4jP;j;F7N@l3t)Q?M$Id6sMv ziOQI{e3`N$+|YOgo4SZ83)LAx zFgj_1uE_J9?0D+(?K2VLHU1d_3q3!|C8|GTua4FTUixCz9e&#@!8q<-+TvD+9hQ64 z9y=W0@vt}=9Ic;pp`kd*U^tZh+$Zd?oMUWIbh#Y0=-Msw;W=pIhJ#s#+TMI{nR|#| zxw+FGAFj2>@BXIkesquR+2{hMP}NHnVAWAvLxm-W@sBi12w9uE_2)?+vz&Kzjtj}Z z`g>o2HVf?+Kl}w=jK(k$VV8%lcDK} z4zI%#%;k^`hl(i+X&jO+U^NvpKZNfbTME67(jJSwHKbi?o7{-5CDjs&A*I_Mvo;nW ztfW^c6>xXrxYXosAr(w6?$4pcHV#}`xX_BYeB_~OUejSZ%XUD|Y1d;pyt&>X;}Y{> zY2dXTyM!yO=2ctu)DZIro*Nv?T8bxxJ>{)N&m|peMD4%=je|XG%s3H{Mdr~<%jen} zs}Fk&hfhz>F^H}qwAZ10`HjnM`#0~m2Y>#M4I&Ut(-M#ge@b@i65Ph_%m>~~CY(3Y zYkU6JJXUSD-n!nt`o%X`0eBUf?zOwW;xIdw3|Fquh(L2M7JH7#;-QRl2Ha$3lL6&l zvLShUn9mNwIJ)Aa{#AbC-=vVIEFpEY^T9{xE6{zU-teVSd+YCfy?e@$6>j!Q7 zV~(PMnS>gZqM3Ig6>Mcxnf7cP`ix!0u+@(@pJ&5+Ou}u@`X|qV(tZ3YAAj`?E5dg+Uq7)9tPRh zEkMMtzxjI1_coc6O?e0!|LTCVGL+CKb6Tu(kB>R|(8LhMIc(;iFnV^CAaI`Qu*H%R z^2)FnYhH$j^Zduk`u}rQvB&XvZ0qtYWndnihK3&75yBklfB(%p?R!7_sO>>-=~I;U z(JO=ZGmiYM7JnFuPhDoYirrS1R_5C4cb3|l+|%?HJanED?3NfMeDjr+w$Ae1YZvy~ zH{M|L!pw#Cy&pYkfAJ2q;S>;gXPlF~J^mVJYlhTv_@Q??L1Ktf4NXt!b279d4L%Aj z5zKd*HE&48pMJn6>GBQimw_j29CMs&y^Ry}a_T>( z$*8RdbV2dgUhSF|ZHrRjhwae{muij{HT*DL<{XniZLGiWl z=6rh!%r`EdZ-4lQ-)xsyKVCTfUi%3rSw1*+&J8Yuo8*P=S5IZbx2ofM>M?vzyiUfN zgeU4Llfxd~rGGoe2*Cutvw}Ke+Ma;`UvM4w zQQQ~9DP0MAhy6l{UKSs0EYtx?ny1`ByNK

    yf7jNf^~a4i%Wfb{H>KYWE*KLz2;# zFD6OdpYg2^^C;ZMfquKJVT1Vk)r;+|+gI8S#_w~q@*i+h!FOJnZ5Qo>8QwXh6t={+ z=Mr8NJv=kqMewj|3R6z!l>e$c)5v5S+Z^yj+BxWOkptb73=G-y&=O!P3Sc{c2C<&M0@aZd8+T!hN?aI!h_LWQY zsCUk{xB0$&aU~|UhYW#tFsocX=4Qmh=WGOFm`}sDwt2z#rRXc5;v>udNGeup4W;7#Mb@;k3H`e3`~&)$bN$^h}eQC=IMjovVQ zl@awWv@_&Q#vL%&&^r_!yw?ly$F2*}*vDw&tjyMyrTduY%iMql`LT>d<^Jeje7}9; zcfQyzV?y_Qmt~kUFh37LsAma0#4*1i^HCo8?ih7gLYVaPeT=J1DHqVjzQT1G-*}a^ zT$T|I9Qx6_-s8~oC+rNlw32};$&?a=j0|Pw^t3|rn?HlS|3;>EmU2vQ>+sYyQYobp zXnhmG#kmCIcD-)wpDQ7pzOfmyL2)`DcIQ?uL=&#k z$-KOkVBdxZl7{cZpEA#~l%=+Ji#uxf89L7+%x|x>_Qs`4?JmOmt0#cr3Id6Rh14#0U-RMRr-$NSWawGH$GVr|`fjIv3y zPx;3P`JKq2zcM%bHj58E{KYl{@qP=|KFj?Mk$?5`1wndd`l-xfz8|>`Qwx@dxWN8FW>G2$GoU*me zoAoyL=yAb`KAlF(A@@;xogwopOhI;z*kMK5AAhpe?yhozCi85eGJ#;Y2#q(NvlDBo z-MEU8&k8-NN8~UqkPnI4rZDi>=VCLD@_B$g!H<7*h|M@R#UI#U7Oz{BDoOnUKD6># zWf=SGBOP^}cShf-=>ZZCdAaygKMEG6G9~rk13o$WjI+&rCoFN3ykO`plya2k@7y?K z9>5M25`Kmu{)b!hDU2)9B4PWiGaoB{<<^ZX(m(n1o>&J|dX5gWq8l`c8w=+|pw-uS zVOqwTK+2)d*vTkzodBmWb9?3^t`jGeP2nu|KyYINb0oZcm`uSF-Y9K>GWO_g{2~Am zld1I$k_mnli?8dL3C?ZaWsBa;c5jzqB+8Wqns9rdeHy)c&tUd1Qx?)nJGZA*H zj?zFp!b&_19Z}A2|Dd&Hvlgw&MeYysRs?Sg^!OV$TDTwAMun@B+9ua`s>wc8c?ZsP z(ox2paESRXcl&5Ex<;>Sg)TE1A&bEJ%H`?y_O;vGCitxV_}&hCnXC4vJd>G`%Ixe# z(ztnD3ol18(g~#aDJ2^{@4`h!NawhgWSySdwQ1>DCAMIe&HO&^x~I<(15h((3eRrtqK3p}<&Z|{` z(WubFyD?ywhT%RpBplNrOksz8Ox)x{VsX*3i+kqL3m}9TmfYK7$?Sy91`MCMUJSEY zZUkib{Kt%)zkcUZ`{~`S_AV!utZsn!3b!zvEVEvI1s0?1G-g_4iI$C;BmG1Cx9HWk zrmizY=Z>5y_C`xc%Nv5GSSbfTzs04XBRW+i6H6OftPW^0^#D6wR&>3X0{jL()z7pE zt~gl@z)KoU#+&`UKl>AWVTP^M7=tfg+v4W$N9}JuQUTC2uv{eLZ(m~o{)!v|H0!Z5 zP8ZyfFJPuvi<{=8#~OBxLIfOPwQ_YmZHLqIO+LwLjFY*S6Pt-S3ErlAk$@fckf`E} z@&tx72a-bI+Ug2>N^f1iMrEJ2Pd@sjeTlj3?VC5-uQs?_hsa*GtWxHPBT5{uefH$I zJ>d?WH#lQTp%5dFHkG3_lF+g!N4$WaJ&*WF4@4$IIU9^@H0Mf5uG~RUyqqLj`ySTj%VRH3#rvx{7chtmGdpGxJDUCUpWX zoj!g?U`Mopp1dbVUx=H$c{RO5P!W{|^H^2b_(*@x%5u3U2e{Ltp|WdFP_|4H$K@i+ z?U6LcA_jDoLkBci9zt+|BYZfZwmoN9?1A3vM|15q#u_WEWgc?A_C;>>`v+fLX}`D2 zErx85{1vwFzXXSy*RHqQSKv`j(P&f5Rl&_h!#Vmo+VT63@3#*>{D|pHDF-c!|8%_m z|8~V+)gVZWTHYG?F0jVHI{W~aRuk?_82&HEfea>+%`V*f4h3VZe?&pi6ux!kLHqrC z`|W4HB|b0UhOMbsN-(W<5*}KJfT8GNEjDTLq3ntKgkuE;!5Gunu$KeR>%F7^x78AvBv9=yenncib39vKCBwEz%q0Qj%1u zgWY^8)CeiWEyWnL<6AnER$RqWC>&yS`lK9zuea11C@9T%wXU4QYfv$Vu;jdE8Y-=& z+^CSUaXZ4})oHHkJZ@Zd%Z@O5;Iz~3eiCLuUZ-paoN#e@g)26>A^sWm&C+Cxp81pA z*>-zjw_TrOXw2H>G9!m;*f#%{Z(nKOV5iad?`^jq|NMh?ab~lfym6h|Uzi zTM6%z<~??5J!k9XA$Halsq>d~8h^>x(gm;2xp68r?^iCf-+F74olcBEso#S0Vj_uT zP=0tRz@c5ezL+52(!Y49aP1h*PIEegZf2I<9&>O96@BZKSKFnF2W@SCD@92r18@P& zvcz%YtsCtbljr^CZe;G@0&1eL`jh%h>C3OujKfBZVLqcF1jxb>GKoUypfB7XcxOvn zP)XZS8R3i9L|{P@E18?#fiRZH+6~Tmx{cD~5|{cO&0lMKwDKFAw)o)T6Dnl0eFFx# zbA73O$f*_wRE{lcgaAR{jiW=W<^Bmb#sA%J-Nv{81cE~i5%bQs_uGoT_y5=f*Ab)k z3RDm~O=nxI5G|_}mfmAW1@NI6o(pTGARhp#>5 zZ0ccz5k9K&pXJftCa@ApSHt;pR~FlU_mBP-I3Bfs`hWiWcK`nKjFaRXd&rs|%z_1|Z7dYemQaaDB_O(mxGB*UA!&dbOrr4ZX;-K%D zwz0O}o<4il)>fag{cx+jedijps+qP8qsXfB^ej1GoZj#<$FVFRAgd^f-v4y1t)kGo zhH}rcEwcS>XJUXPPN#8I7g~w7fQZHZawz5>EL;7ic{i2GbD4k>19mYIC5!d6OnC zi0~eKh-YI%8sX$JYs%uc8tVmyxd&*c)viB9nC^4w7FY5CyTF~ID04m?#Ttps*Ih&$ zr$>3W(Iar?HSu#$9*wtiWOo8R+(XfHyvWs`D4!_kHn!W|tz;ek-nHfS;pSBP^{=0| zkMC`@ORqd>=dN9Bdo1;+oxgZ}rEP9-;s!gjm{Ei_V}TVj3-&Une-ERq5PIY3di&{b z-f7?Z_LthNS8ufk_g7O-UD!e@Qdyt6Y-aZv9~*`aFToQ(6@MZ-6ALH~@vDT+V^mgb zZfvwq?mcMF)~4E(l@;tncVG<1F;*bI2+=L^ua1JSv}jo5v6^ zA_{)On1eB2;aJqyuAXZb+;@uMx@SN=r}uia@2vnX`ZK@hkT&;y?l9~*z%s}U?$^$* zFfX!aV59+~DjaHcmzPkEF#C6&c$o{Wes%v*dyi`-uYCPFxNB3)FqW_g0#9*LknE9M z7CCU93sXlCg91nTD!`VX|1tERr7>|nyrgv#nq>uj0l_^+aX8H}C#NvF<&#_)DkFdC z!D;VOb$r{4S?`@DsT&1Oe4v+U6;!prqq@W|g!cJ@OM3jokrXAg^tR5cb&_;B+c48nd*jth*|D-?FH0{>jfEx-RcH9iLOI@xYy|K&xF5f_+P?YLdFG)E z1JMG@jkRVdUJe|DQ;g#78?~e5_X{3HM;&-Rd^8Qok z!h!IdD|NKTol)F}ME{x|_ZdUyee%3bW4DCf*-Ehi*m$zrUcbUI^0%hj{q@84WR*=0 z(DpIAjpjK{XL0@#jm$$7b4Xn;Rthxn!RyPM<<)heJcW-o=(Xc7{REL_umg~`zFV^T zEh^(@-4^KynRGo3!Mp}`j|aVGvvvog9v`f=?WZ5NPd|Cq?yq8`%#dt-V;wrEq-Ss-nf0SJ??$G;l6dDW${ul}{gbkj_BL%VK9b1dcu#h3phwt1C9u7?^ z2_a@P&?lrZ*#P$Bcsq;pfAIPx)}rUzDvSE7z&T)<;|Kcg*7$7QV;L@=rL;_z$69-Q7X>XNyBltaa z&y#oxG|YVt?e6c~SZ;s&%dfYGPdD59&tZ~<xr1gxSnV6Z{cN9YstA-@FHID6{>ZEoTi?5ShOtp zpnP)8-?Nvu(UR}cfILTex8vy=i~=YPuow;pw(#vBJ8fKDW<3>6@g9QgoYw0twEdy0_eJ5^r&u9+w9Akg3epHcBdn=|}MQP3rp6 z$!>e~_#D^0TyDQa&>yU#NTp&UbVU-<;v|WPz0^Su%bsw5dgRW}eve&32VR#qtJUwo zc}h@S_o}Zm--$8)X|tbv{PWD+-uc!2_HTdmsJ-(5D;&4@L)z;0ks+;c+ zP=?J*o<)X{qd?KsikFr#a!_l8 zo5$+O-d2ILx7AtE-je4D1}E+SAe9Eztt)AZbxt96E;;Fq4< zKx3Fce+l5T=NG&p)QMPeOkqC8$2q*OP(DDznlQblgWK4QJCD`k$@!^U`JOPPSzNwE zSuH!{r*Vsd5g=hF3@Oy&yPN5R@_L$OdU<+YwbHT4VN0Rw=X)DZ+w28yPG83Mn{9T} z440+RAwBL6h8WLX7e|wRec5e|SSqt5w1nyDCcCf}7)jk=H~DRpir>0(qy7Bhx%N{G zVjgfQxR;KpOba6qcafhS*(C{$*$>>B#<1UB_=I7yLVXj()$L0hvP}_=Fu&XKR0oFF^9buK z1G%YrH<^1H16EAg+IG+$(6gUIQFMzXfdv``ZHD835dl(IDN~k>c-qq_d@{1jJPNIf zs+5Z|Nh^gyQE`KN4jq@I#L?d#Gsnds*uuBB)umB@_PP5982qn4X-9ti4$i#!PMjXE z8V^#`f5RjK&XndECgInSgmZHhnXVx_bvS~LJf8X^6i|=%=Gbv_fgLvXo-AF#Un&4k z5j1;U3WAj-h9LB&Hi{|(FV7-mj3m$=v~|{0caCU+l0A4aR2J8HdfbdsNOulFKhGT1 z^Pe}l*nA(@R7rZ{C5$pOH{E4ww8S~qbJWSy=96}tA@!Tw?KHo1v8}mL0>$EpFNd;% z9FB}T4+n`VyY%y|(=q53k_qYli6y=8I2(s(ldb~s8NH0&<^S$xo@`$D?xSZ84F|7@ zR<31A&~;9yu#-5%NMiR8%K(JjC7+oYhE}tjm(Nm9&PHZB!GM_!)_C6j_3`Vgj|9y!6o%LXzh zBho;Plu^*T_u!*664SJWRlKWOir%SjUiC{_e_Q5A9-0%3Eoka!w7i|+L_tJxgb5VOSQwM29kz^==fWI@8Ez=x-Hj~l zB1GpP%3uv4<5lkeBJzzWUltwr1JeZN+fGhCzW{Tt1)qxy(O9_>^m$^tO+!IC=$UM=Twn z@IbH@XyIp=CoRzud$E>t#IL`4v7N`(mux5o%)l^BUH<~>uQSYP-TCIQ&A;^K^7p& z+~mOdMb4%2wgZh^G&XTlKPD{gDs$dfubyY8&;=S9c3i<5>Js9lhh^qtz`&NP!s6$< z_0(hZAiukth@WVaad{sHliTz;cp z{rNkkN1Ea+VGqbojSwMF#5r#jZ``^JOogU)jE!-}F6sS&tIngwf9v(vax2^Z7UpR# zoj`0`?D=ySv2bF8G{a!nS@lF)WBB`gbu;G>U1hR#fg$b*hj6={&S9z*MIXxESo#M# zfw%8mM)8A!gNk#IyN3a_AWGkYW0v{rJUc~VafHyH$c2P_he{t4tEp93X`8}` zF1196i34j#@e>?ntw^IHiVF^e_+fn^7~&LIAYvX7XTpsVe62e^)Kn`xX^c=bseIaG zgyELJHMS}~#|-uWrP5`#%f((=ULcLuP%dSu+&enP&;%F`k53O&A|VfyL!n*t9!<)> z4;`N|L`J^TkR%d6(q^8rEJN3&rStT>2%7cCp9nf-Sb_(HKeNBVu`R3$a4+#bjouPx zX{}tKMrV#tn9)G1kfbi{k*}<9&CB_V46*B!kId1@DX)BTR-DDKzP%v3_I! zp*>8w#xaL*A0xS_(A3K~V%}!vFI@g^&p#EGbYumRJ+t2?oW*cwA$Ih78c&*=;?zOL zqH;js$_Cx}HbTsoGf49xzyWB*rNSMW!C2>)mfM@Jze-QW{E61xjovxZWuALbxj@TH zqZkC!+s-3I&tnOrK%S#jzre8R-0>#I*s#W?ki+dZEXQb%RZ2W%LblBE#n*1JM$7j_ z$gx1Lw#`z+)I3J)uCH<(=HZd^EA3+$%EBz-s64Kd-UHBXa|F$Tix|wIy{JqEkl9HD zcMc5{<*uQx!{rJRjs;g}R3jOlB?7)Bn1XF&KkEYed7JhU0&_7r*gBT+x+4Cpa^ z<^3h`X6Dad`mX*~P{k)=5aT;eATLRYpD@p4;+JVFAU2=e9fGzHKjw~UWs;HU24{A@ z=BND9whISdASpSKzw!JdioM|y0`>MAZ!o81O^+QMM;y0#I=93(!!%@K20@x(j&wpL zauEVOK3lwivq0;sMac{dvdp@k@hrUaLWj`8_H>1ZoRsh?7gBv2>zYMi*jsAexCN9G z^30t!^y)`yFJ1bfHMci8pw`{Za^Ov+R500#I-K1$4Ij=ept_ywr3vZ=| z^eRq!ygd*RZff$7p*6@QAFR`knU^{o*CcdhhB>M_D)Kp`&eqsc=h3Ht2p@QQc^Ud^mF5;nIdWie-UKCA`d7Gh{@2HFUE=VOSiQR)7Ir|+Qj7v*y>yr8RF?Ji+g%4*@kWIpk-fCN)+r|{PP2LZbaN|0VecqV3nhVd!QaGD z7~l)j^+FGrWoRh#IM0*iuOUpYz4bOd=nA_%m%MCkw`-G|(+_F+Pv{vB+$0WYJKqS1A4Qbl zBQ2a;+k0jsfz=KUGPlCzeAcjC6e!sc0OM&O@BGm-np8?<*AM9wO5vq`psH1X*H9Cc zzYWG{h>YOJi4y5d4`uSS(NWQ5;oUkXF8k!==JhA5|4&)x*4xOD9pDmgB1KXpMO{Xk znH@W0XV*61tbuIoV1Wb(ke2{KHV;9NhXDC0`_BU8KP1R{FYwM}JTtnCB;F)ayyW{% zbxT^iM;tc0tGcVsy-uAvMII<9|M(-;w!%!#A@2}2s)m*DWnLqRT)1y^U4yzV$RBf0 z;aRQTfM=I6>X*=L<-Op+X|LG9f#Hfl^E)^K8=AxRi}qC50DKDvB9SvhKJBM53+fj) zV=SC3aKD@Fr02LNx~u{MSkyz~ZnYVM^E_~V*sY|){x(VgPcj0z$vF48cE>V6R`alm z>#+L~@zYEZy+4I{rL%%vflsE@2UvPf;NH{seF;titWhb>Ju7kJw}<3bK;OF}Ag=o# zoSRL#qua>q-i$R;CYDT6Tr zA!mf@BznpC@Xvp8oT|NNX;dYLExyJ3C}o>J&9n5g-nF`tKM+G zof-}oH&MPIvg(xmZm`^#f?BfI&LXs}y|#^}kBJNt5O=|k0UwcWO3 zR)KNsOT=`~X4@#K?2uccNu%A)chgq!)}ZgMXRhImo;#MMJz6`ayQvTfR|A|kD=eZ` z*QY>#EW*bN{fZ!rr%cmz(Bv}&+>|l528Wi=Ych}QG+E_Fw#ETDtjEkktch9Z;k;@y zL(r-+7r%Xw_K0jcJwH$H`N+J&Ab=!Y31{FUyq$C2&_1M9^et%IjzqcQyWmB?N;;$} z_{i|}JReahPBD)Mzb-EdP~In8{^Ss;Hl#crPV=?L_j02tNugLA5DaVmxRn3%1pqfc zhjRXlF;rPDdqg*kH~S1)z9jbdAHLX2fAQ(_bh~qiiP}u#i_>&7zQ*xHLNexLZG(;n zO-gg-8Zzi+VPTG{d2U#(#CxHYAQo4Lv`BqwS0y@Lk7rmyCr(up31a>7*?=6qS))1m zzkq4Sm0i};5(EH|x@oeVaK4w#^9W?m!4U%A1Z^$K&)H>pO}^uO#!X_@OnZ%pzyhJP zN5)ih>wu~evul7t)K?Sm4nf6BqCHL<#rr~-$zZ$(V%22U@uVf98S2=U)9gl|etv|w z5()A5fMf6T5}A{~IQ%ZnWpNbaX)E#pLzFK}Z2Jz)$b*{2ug+%a1M6Dd&-UQtDcz<2 zz*7NZu!ZZ0g}>GB&}K^(21Fm}kHIV(0$(b?v`e~E+z@!Bu}YwmHP5-i3mMSU1pojR zS4l)cRE3VFT~y|?1AH9M8q%sSL8cAZfJo<%W!^R3Xcu>E%^zN?h>o(Tf55gH*Du(Nc*aA6QzLq_^oMo}aH?^qF#LrU5&< zT@rrwKR1K)ehQ&Zi2@-sZA1D}5ol@?+XAk583ck;gJ^RI7|DsJ*NJ|mI+U5;Lt(wV zGK|a^vCI(Hig3GNVD6jA&t&A9Q4(zn6RFYIg)n6br*9ZT(?2(#5?wb?`VJ<3#SU;D zOh_~iBLspimg!`$O2{6x5RpR&f&q;+AVZI`8bS}XjiZUt50iUKWUlB@>e_zIV4XSE z%Z!aP1u4FO81%}@Oors?#g&L0W6OYE^Phk5jsP`J;JGDp#@LtwdAf&21fx2R z3dR=DfqP2i-R3bRfycYi8cPBp1N?`F2jB#!8=YaW!}}eZGklPNu?Sq5WIkmD1&_nr zn#GL&bHo_g3Q4ECMTSNDUZ-rGzShXHxD=;$4NENJm-Yc$haHA{ zuF=ACqNujQ;Yaz|3a5~#g1DqfI8pA49bqS{pnpgUCJbZR#pvWp6AG#$c`w+A6 zKp#PedHL4%0@J&Z+Lawl=cn91U>WCmXkRxEk6~QzAbt9`-=uGTdW=N7PXF1uOW*$2 zw+!EOv+=UWhaLFQkxK>21d@I9KaS9}UdtHu`-eyQz=s!QDN!Qt;rb72#_zIvanqPx z4^1wQ_y*BRP|c%Hy6g0-&v( zxBxC^^iUQ4<-!1!n#WOKBuJJ>sxjoAsF>AZEU#idQI4<29^zi1(B zu|^^r2BvWXjZ0k z=5bjne~YAy-17PZ_xaNMCl&839~gIXfhd!u0~N})S)e$)2l>YQ+Q;ox`sdI4>0iF; zrO$}j)to&YZPK@B*Y8$)LHH~y!rUa)B!SJWF^Nq6WkAH?!9hA0 zKEW0vNgC#tW4tMjs}YW|)$EEmCXV#kG z46Sa-yrAR5sf#<1t2wp8JT~?>sGh#bU@nb*U|M%!H-$_H z4PmND7fhH5Hl|kSoz5GzrZAN0Wh~>QQL-?PTE(o5<}S|Wy;Br2mCO}*De(bwXC}=} z4aXN{cw8YABwU3+)QonOxbo#O-lYvPtIwxJF@J<0|YX6|^&P(07>P_Gcmt!DSm9I_`DqPhBV- zD0uWC%;t`7Nn=ZTnE^PIx0eVJmjm`l4(G@Bp@T7*(agg^B=;Nr@%!}ZzkieZ1a18F zS6`$ze|VoR-jm(JHWf$a4}O$5DzD%FrbKk1tb?zkto*}oX(*)YedR|bWWZqDhikw+ zUsTnX=Uj=?C}xFFK~x3lR4|9pL32rVy>9yZPj}N^n+=y)P=C()$2BIDi>BYK$i2i| z=mHa`x}M^Kck4u^dB~Pw?p&W0jtQiB10X`@{0I)Tvsz9KB|+}>F{NN+rL7WHtk!FyK+5s+LEn7&*Sr%We9ukbt>EvI?m z9ZU_aG3R=OR=y+s39;#TkMU?@8PqW)>-8IcWA5=hrMz0qHru`ocAlrEz|5-ySA!M= zs2bQj)#ix`hGX?nWYdH()B;`;)4{ep$f1#k!bCz;pjgiFf8JYG?U5NaYkG-1moEj1 zaLSL7VY7y(N(c@a)^YUl6Bhn6CclPxj0vvtKIf(tA`xb3TOc{*n?8X$0V?4<#0eLX zRlK1YyAIfI!#;Tdf10e0QRl$MX|olZQrpD^WvV!mCV0*}z)>@Gf+miQ$%K*rXsi8Q zQ2z!v&zRF)V^(jNYhRMgP&0l+-mfKVtA_+ywBZPGA$^7#=Bo}v&zT;2_u-bHj1-9! zYCCu7{2YfFc)=b_PtnFL1nW7^nc97FupVCWC&LMeIz& zj2ZY(Cwfg;krOFhC1lwVWa?=|irf5|-al9NK*f%q;HWTb1)S!YPL#*36#*7AEYe)i z?ujGPeU{%bhNE{9zF=7ptGZ@70hS3z1}qatBV(TGTxB4Xd09D1Kgfh zNbaVHjyOmHFEGFL(1dc?;p> z8PBvmGQS^9Rhcu!ix8-NmPoDR;n;W;;f47bGOSGEGZZSkfwDN?(Ck+T9|IM-1km(| z-#zE9?!8irH+0S+qyV>xJm?oS;|Iw7JOz$|Rr*=s2|r~$?D4RkPFVW+mIe3DHdI(Wv|qb~u331` zSojLSjQc zh+PuBU@VkZ_oxN^5Jdx!MUI5(M<1BRFZrv#nJ>JtvB-78;{+P$t#ayFkEM|9{>2(?ral!@=RJh<4mcm1CF$23`bRmZ!NjL>IYDVM;M2Mi_wgepGL>aP@Ia$;k zZ{xt}Bq`3_mt7+b#8Hl+_nu-?V7vXg>vyeO4$MJhP(1jFwqwz$`J^*x86|W!Vz= zoS+Y~UQr8kd8+#nQ@&Rt3n#S4vjai+R`f6*q4lS@r)|%M`QkRyMbAlcKg3k7I@JXI zx+P1%NO1TD{v;c}<7dFnAO7}!{=l?(%BR3JcO}!}cvLVdl-GasO^xDh0^zfEG%V{@f{{A@^80_M#aSGWyDwFQI14Cf2YVLZfJn`?z0CUmO$FGl4mf(1-#gF>|hFJ=k=aBi0}i$&Q(jn6v@PqOXk5ArcXw_tuv= zf4X4W2fr8XMKCdfY8FfN+)&vRqegIfDW-I2gMC&Kdqn=IAT)nvOCU5Azk(Uj4BhYSl-_yo+RYZyfQPA!14hq9^pH39SIW%L2z!&MGEUkXg?-5 z^DZe|dPE=1Fn4b$uke&X&a)i)xUy)w^~+u z#WkDS0 zjB(I4%iPZ9Fe+&=$4#=?%~-b8AQuoJhwhK+(ZTF;3r2SBV zNmy;7Zu)HcBoWS8rJ*Fd1z-ytRHp3=2Y_yUOz0No`8I(g_t~L=Y)c3G=Lt)g0L> zh%ullhXbiGMw@7or~&SN~FGa{NUn3Hu( z_=1S9Enq)_al#1}?~>sZI`qR%p~+B<2skMNk2xl7jdG4$o!+T|-`4yx^;nYEA#||C z?(k}2Gf(t;%=c2K!PMUlK^i7Rx1T5+1U~Cy(X?i4+}iHYK^$_7Rc)_=ugz550EgnB zhruQ10cdu!@}C|d_*{Hr7Mar;3}idn8P)< zSeGG>I~yDx9;K_Rt2Da)fOf@s1+(keTJnrF775^>iR82*y!9Dz07DWMI>a${UYvzb z#CC`iK<0mf4>08m{HWGDy#`Is19%%7`8GrgO&)2nGKy@u{wpeuvS;oOBjiGkzlxF6-M0( zGZ5xy5dkEJ5P?x)+!$X%EzaG>+%fM8{7_d$ESbue#=Is_cB)5n(7L&=N_+?-#=C0x z!i=?kd}s|1Ed^Z0BHD*KkO}40zG~QvYZ*2RIBtRa=#Te!h^FrxL3phr5KZ&5mxp*v zcSD%z7<8(|=h+MYIpp9nu#i|8Q*+168!kq7fq?de(_U&jP2)QL4Yi;js=!!{bhytx zNpH_$HO?GiE&lbCUxT<2n^QY0pp~K)+itd*6`1l~ZJ~q{uosI4`hB9D`efl`Pg+-b z>Buv#*r}sUs~;8>16J@Q^=1_NP09nzc7rY0Nf6CD*E$MknHtv?;yGvKP```$%T^_C z-d@GfO<_hA2WP>&O_+Cy)paNHid!Jz(nR+>diMMf!9r{^v==F!Y(oLGU_Dxt-vmUt z0L%g<#l>ht{AYvn2^BFxt_dzi#A2Z>3uz-gd49Aie?O$Wk! z<6LJ(j1|LtV+@33H0$CT=Fka|1qtIQ&Z}reTpEl>yh{6R2SAb$T<6+s?~xBmGcz-g zOrjK4@5Lcdg{Ff_fy+mea|?uSF(si=aU2E@MS^ zgqr!{2tmrU6e0xcMDq?u5#?Nh=Y5IWalabLCfZJ)afbuvj(o*Odu_A}W+qG;Rs(&u zzALNjgF4Wx84V)n4iBCad@)Y1UcG_=#xT)4d?q((aD%6s7nyjjfq*1@CWa<=2#(eD zDcrf6s@+4p$oyt%(KI%i%ynp42N@WpjyoMNW~cxApr-|s>*Q)<6|f?kJm4ezA!I-? z)Qa*_jD0~huoux4_@1Y?t05_I+5~h!ebDC}K^pdLuz!;Fj|S=N{dM~7Z(pYm7x05f z<_L`&C!B*TNsONwaT!qF^9V%c(;!e8HzS`8bN={{P`oGNk66+2%by*jpFeBizDHt` ztol2=a&N|jgrWU9bTUjgpMhCX0oaQfZhAVA$4@Yu2x0NMt6C6ZXjse{S}x(c#t}#a z0%S6S@7+4m{7OnUwSdl~1w`x^CB}P{*F?(^FOZ@pGSc3y6h)uFjdGm1pj)9~dt{L4 zH`ik|3(pWrQH5)ukFrcTa6#la?r@iO3%Fd62H^3ujntBe0eHX1R0FG79P4TgxUY>) zuuFp0TMZZ0TR*=yoKDnHo91^aSdiGj*=rH7&kg?t;{ms9;s!$KC%C1#wmNH79?c=X zcx=X5^c`Wq^%(PmA9NwOpq5SHBW%3q+@%2$Q?%@7n06oSchdJb>n_=*b1uAfI8ja} z)Tw4#K-*x-nLCL zs?%y)wiWogWY0q%2w@(PHpupiyS(clo?`a(QCpe|tPayI@#JpEY@xHs_pfi6w_l(@ znq*AeJt8P(lp2e12ok|4Ai|2`$Ap|?YlQkiSQ^yOfI4+{_=xVyVM!QGuCz~JsdLLj)iyW8M1gF|o+Zi56(kO2k_zWHr! zZS9|XUsu~kdOfYn|}}B^%Q{r!N=O^3jhg#3IG7a z{}r79aAj@WtbhQU{(mJ%uP{J303Gdr>|c)l-y5_y|6yUEqhnyIu14tDIOjvIWYk-`Two>|9yGw2jF8M(I9<8 zLBaKt@7F zMMXh-^Dm*I{P%AxQ1DR!v;=6pbno8C>hOK`2qi>MDUs8qCu+9x{1KMQ&oEcoB2SEQ zu`?imD0{DG?e)F&!y=>LKOD$NC`hRPp&}#y``14JNchMow7dkU7qUA4dPynyLC5zF zt$A(-AxEf7ZzQ8n1AX3@4FaDq>vt52-c(jv1nl|Ib+Z4IS0%F!P)?j@yK53}bXo;H z)_KEne_Hpj(H1&D=OnVw5_`Q7E3Jtp1@ zKD3X_jcPe+}wj&p%zav$B5E2D}#Q*dm4iX}y2D5S7EuQrHR zeN9~6sQu{b#UnHFZEVSnT0$sCJpP{NkKu!-a+ral_CPuQ;C2GVT!BG6QUNYU$*(v~ zk{Vh#R?JvbC*Zfa3Y(U8ynYrspk~TyKiQ+ywhk$VQDw>0gHWV$fTDOlxZ6VkgRN~S zb(rlQxEHP6WQ;xjlh9Paa#BOc$B;(3xMjKfzn|P+MRUxJkM`P1mq}`K?RLKP`T=Uy z1YQB{^f(=4r^vtuy~>RuZ)s+dS?*& z!q@$f&IAd?Ncwn}olJ9e3(a{%nrh)NnMMa{y3%h8umt3pT|D)a^0B!wws;n$w>#lC zl#=|`{%4ht%@A3`nFbw;BByi<1;H>CI-xxZDCZSNUu6P)A1g-${!DW2*Fh!Hy6J-= zgzx2<;opa6`GObYz|m6^-tHN%QNUKk_wxo+)%AQ@ih=s~vx?RRZ;XHW^jK8EtFDBr zyxf;3aEu)o-+81;PIle0GAwFLDKuDD`@~h&BZ_Ig3p9?I2e&6E6;1c+>H=1hdS2#s zy{Uu~6S#>TSO5TPaAa&PET(O3#`WPfv8U7boD02K4 zZVwUDuzQiBZVhbQ@Y3Ky7OO3lQ%Vd))~7Kd{1on6dBIrZ>Qx;IkMCetqJQ(Ux1~JMj~TGn)-&GYn49 zfA3U0I6Nin!%>QkicV{aIcfuhO8r)>Qj%}CZC2Og;FHG!DWmNiegrUM3yUG{s@^Ou zRs9dC;l-Ny9*3O?$cvv%_^Q7$Whm}6Qa*kQ(PP+f6Bdu-?0g8Gg!AYBoE2` zo{zM1%~Gq@%UZAyL~FW?qV8+!TI~9 zxi(Uoa)8HUv7TaU0Tv7(dL6~7wAzy9Jj(SwHi0{}JO9AP1Xi_4YAvZhNpVG_Ula66 z>*IG4Q|s;(g_p*sdz4$2!y}a>R1kw{1hc~=SN{SH5lQYFfHN@MSL`$craiN~zIDz`2XTvs;F@}7L6nf#EWXikRgW5 zx5R+T$hg$U$OHw`kq2~Cul$cipFE=Abzs3nQF7S$;Fe%ckv=<)uTx)pQNloyRdKjk zfq>u>;(3NO(J*MS0_5aB7wDY&GBV$K&R>@*{gB0uMwYN>Ac{+zVvB*IJ&_-T7=Ma~ z(RCu01uvA3t?G9z4MN_j%Qz&-Mv)tca>l;N_Hh9e3{u8Qnk(4Ictheij@)n0Y!=Ep z)yHMY9Tn#5iz6BSO+3-}gTCpzS&jE2Fu$6dZJ(VM3ecgnJ6UrIE_mLD>=0LiF!_<1@(Eox>DbZdjA!fo zq2|rW=P$6wpKd=LysnUqI-J^4jSpm+TWUG*K>`)5jqlAJY0t%hD{+$N;+dW@|=!$z{By~gibf*+gH%~@3cB$(9w_$EC{hUmd{RfWLl zR8w{1kbm#8MzNFr0)9^Ry#6ML5D~z#wk7DT?w^U1H#gB0B?dR7n-Amq4B1x>EE{uL%W zh63q~Rx1Ap%3-|g51ft~yjWH$H3!$bc)A2{=xIui6|JSQF_g6hN!^EwoxikO<*+zf z6v#t|f<8D(oqAsNW&fEQR#QQ#(t5Y|T}{>pQkimHT4lSDy~w%&QVi6zT2vyI> zw-xEC;intnmP83PUJp^>XqvC=473jxZu5VB3?60za}z&KOHZh;+Um zlqf~wmWzie;BAp~hABYho&<@0KIxA#M|`>5snxkYKiR!SoZrZ-d2~!p+E@Am1MMr@ z!+By=SjZ$ly}^7iZm6}>6j@3&1Nl$g!_VLk6Q<01$>M7ERh4wUI*UfM!^%WIIpqgB zKI<-Xvg1?hOC9X=Mfrtx%jd3JDj7DAoD3S9tolg?^@{Sc z2p5^+!+F+nVoT);kpa#1Ki-`Zjw^>n3%fyk;%4tkDb>jKYaL{7KWAj}R+Abe(aaN> z{>9uCnAD){r`4Hc4~*V>^3cevmBbuS=F`+SEJv$(5yAVF-n_cPYReJiSZuVZ1s^zF zJ@uItZYy_VE0uEoA^Vl|Akj~Mrbu}wcht}2>^Z4fE%*zn;e|{SY}Be%4Q-UI-mV&X zl$^uSIFiE0fvMc;A;Jx+2I+a;rY1Ua^27YY)wedOV8rdaBY3=7lh9pZAMbhZVlXi> z9cAUEYZqlZE7cU{38jW`((TXuH0#a`67|_mXN*T% zY(1Gkbev$2%HBmKU9)Ccaw@J4&n}=ugp^(x>I-^4INbDW(!3-xBd_)i40cmKKdXrO z!onb6f0OG_S#szUqwJ>5aWh+WCpGP3T^iWALkpgPP@W}&ZfHJZVb|^m4uez z6(pItqt-8VI9#*reMMybA%Bc ziAn(PI70?|njV z#!&q7t~O;+A8g{tN5~*tgdY!f^gF*K%Zv-eKlf% z{AW}Dpv|7pPi4W1Uk@%r?h%(4SC`+JIyo4vnj;rdO7|?$Sq2xYTa{zs-lNJ1GJiuT z?9*+3t#0LkY|!Kf6IkO8Lmv$ZT^e&(7HStCd<3O8(ed|dynm$pN#e}kU)(^O{NWQq zDFQA)`I`9qtEKW7)1C?Sq!xVW%Sx!vANa2Ndcz^?j^vzktF`}#?~QhU3y+wIX-R0M zf5zCi-ZydRAufKNu*x0pQ|f^AvQ4ftDiIsIE`T{J--o&~eO1I?JA;J^oy5~GxV`~S zRepo=tR{j93>Skrh3JIulKb(zDn;ByB9y=5eb)B3BF@f8l0aZNf5b*$elPx>vKOt7 z#n%5_HP-!d7@ORO&GWjDBwVmz4gha@H+r!13ixK7ljr3k#KEXSYQrPp{a^0(Lc4Vm z6n`&PsNaogp>PX~CjOMzXoNq2dR zDjlcdg25;7On8IkgOwg-YLtUVZ{cR_hMi-9E2ml%0aecgF{&V$=q4%Kf76&pfBoHm zR5Z3hG->eV2T}h!e%l&Ji!11mQZhsfFlpU3+vrY=L}JBP5|7?LMzio#NOlpOC5r(0 z?=cAiU94XLw$IZ+-d8w1$Rx{vuo)cW?}!m6;P?bl@GGESg*Jw;4bjTaB4?(|MpP>> zSIfTlJKlFvt0y!y$^w697Z0pZw`>;s*u^!7uAsG@T1;< zTQFer&B@Z~2kQ4N%S{;|;Q-&F?QvJ}_g|xG=!xt(QWIj}l(zh%JF8fOe;hB0?2$*}yS|)?U4;f<3|l{bH==@Hw_k6N zaAclD&ke2If{@`$t~IfZ_k$*%(~6Lh%JsmsrE;aTfUA5K<7d5^4{TGTH@F~Ed-NKI z+T4uH#P;-`OQ#EN)Y5-`Cb5O2LK1?SkN$boF5Ne0A)0GK`^aPcvCX1Q^kh5?we}~! z^X_GBWK|cNsfY$|L}wF^zj3MT4CN!-OYE>W}QsLzs`B>NWg> zAvG+W=unj6KTm$X*#`KdUWkNlbgR)Gd;;l8tZQ@ud2r%rj?G?Zo zAECgW(qOU!Z4g^tmph*&TwPwG;9dWB{imS3?v#U@h_+v_3F9XAW!Q<~B>T57HzLI? zMkzQHQQ*r_aGA!8Eov_y1u%6zH^XMHIeF7PuJA{fxQ0ou4^)xSG^Fb|tksw|8>iNz z*bi>+q!)!DL&>ZObI3#U;RPSQ?;M2sDx~DeE5|Lf9mMDu5Ygb-W7O;=2D?m5#=QcV zw?Dq=;SRB1Vil&Y(S(1~e0Dz*=#D#LHUR7vKNs1Gitu(F7zNPREIA}%t&RmYA30!F zk>)Rhi)qq!_Cg=8*94UD->DSeC}uAHLb;cxWZrEX)?+L)!2Q_beTQaKCA+>e4-ivq ziEi`k9vWerrZz3c3)aAU*|3JF`=4A1zXD*t_zgw*2KhexV34lHWObkwD-(+>nNU9X zrGU%MNs!3e6XXnm|03d8*zQ{BP&z+(o}YARHw&B$bjoR)Uk+w#eAkYRZ=8BsE2--L z=pFCHcYXHw2b#11Jx%;6Z>rknvi&%E^i(8tMlkIm5yzn1Fw$U6s2g-SSXZjNVmC80 z#52s|?R0B(%?j~x-6+6%yB)WS()tdow}lipt*DjAo>YG{CzOWmm$s(DAH|^b@4FuA zY(2;@u>r0UhqM}i&#|J4g_9+4u_HF#`WUxS-b65m)Aa6WXu&k9t)*pSOBwD%s?8Do zU6fKjnamKdGMcAn7qPju$`~whbISrhO8n;cbu+uWN4{cZ!z+_Ee`MvfZ(3UEfiA&bF{Bn*DfXu7;qn;%aCL8>T zGxh`D$nWgnzkvp?31R{Jf zjoqrnthk{oltJLffSaE>QuF-b^Rv_;71-WR@+Ydid*T^!{9vF9_Ai;Ku~dhniSl+M zT$_Y~^mwoyrRu4NGx$xpqduP3Jjk8KeEU!OV>-1z*jrl5^B#& z)2r*AOCguteT*TxfS1g8^G-gKOMuNsaArNU48}|)>SEqE9x;qiAkD3nog7{I$W29r7<}l4RI;0ZKZ>3gDRuP$+ePMG=(~c5 zFJPfAFJ=-gxuPD6xr(Xsx>}?p2Rr@1$X$?=e$Dl zhy$&-y*}5?VbG6R@b!CyX;Y>XK5D9pAG=xgvCo_A2aj4wIVdHqgZhoNl-R*zoY4ry zPijh``bXLz&EG*b0@jM+M#WJgQGz4BTA zK#%c*BsjuXwdFu{du8~z_tY!kR7dJ7 zl^Lb%xn%YVV2WLt-Ml>H zJRNo8ABAit(5+I>kr`Ga+r}I}Nu6?ElJ+hby#mCZEQ_rxIMo6Z0)t_F9vz!d;@n-~ zHTbfbKj(o=%NQ-+pS0^Og()u8wS9-*tyrLXIJhqqXIS?vq{GE-!4;S?;egu8vXp$Q zy@K1|bGwwccmCHG!9UW(XLh}3*Ac8}sXIovSW-$f&zkw=cZ3~iCuD8-ay$0lA#rcL zMI;Ps|M}8v2~%oP0(O};y=4g$AG=mX4JF~rcm?p1t9-I&l@kqRR^_@h1cS1CU&I1? zrK^k(t7(RvUv(N2Unq+jf17};BHcfCszvK3R~UY198i#t71TLpu0&$64QeWPc9DV6 z3zvSMX%}Fc=CyBSmB84EJ~r(bHA~5N#1OSwk_C_I{-@}sp)?-)g1Y18F)8O0lVO9Y ztYf-B`Vdh9Eg#2T{c6Sfl3UUEa>#O?%y?qj#-nJKp3aprwl4}?CTwoZu!kx(A%d5; zyA1aB4&Y2!^+m~#8Rup5X0B3HaDl6_hN;x4)2Z0EJJy37RBWBqZ>x``4qzYNCm`AK zD;Dd1J7HK!5TO4&PRi-OxTE}IVQI?W>K9|?TW%R!6>IgZ2(3PR{j8tptnc?f1z*2R zeMHXmik%v&_x%;oc+5{07#mC7hgzL1bZpU<6mlU&+tuN}Jiq!M)rVLX+C(K^KAsPR zUrQKs!VonKdf4j2xx{CZJzaqUQWJ2g>vd=#*KTlZ;E5Uv$2QBA8SIAnNWg>MXJ4)a zXcHWuOj!M6r%jaaMz~J1wZ^db!EBox+*{&+u|aIoP|_?+oYLOb0c82l0ic^2BWlvE zmBCxtBbS{Fm0dm!3`HjA(RL=o%I&Vt-6i-lD?))a08B+47+|6M>c z&pnRow3M#;(ZsktA5tTvZ0ZVYcdz1gV|dSe8m7WCBe$ex>Eq6akSYJ#u3dO5jWL8a?lXB_aVLJ~Jqt}xBy4*e9= za3Bj=*4jU&*pPyI)zmfW2F|=av1?aTGhTN%B(uQBBiPU2JVLHQx+5I@Lgby=HltE9 zwUhtN5hn_~Iu16i5w!mmDUqN+Aovlxn|Jb{PRtNTHyt6uN;vJ-WTfS=8hdDZ5M3>r zm!q-+7ddhFS+LID$mCeHad`6JsOm2Fj4wtiI%G~{ki@sbACJ?m6A>sToTA3pMdJZ$ zWlD6%x#?xS6@hZ1Dyobf#AhH^U8M#~dDmPv;4Y9!2Kqh=qVXnFY}Rv5g$|NfJ4&*# zb+buBr6n#z;9_@jxI@Ffz#6%#vH3n9_dmo%$@MyRN!WV2vJnXh<9TM`M-fLl0{R3# zMw*?!wG1S)F`=Bqqfa~o;YXAF-vVgL6Hz67n6ypg(Gu%2{1-Sasr-MCyui&MH6prQ zcZo0OcpHe-`dfFjDo3JfS^48hBMJ}c_l{j$AB~QE`dAr7oRoXjI@wn4}FpM*6GBU=WjN`&QYp=Cy@$5?qD z+8#3v?47Uz{{}%ze#)N1!;@(-SUTkP+xz*3fiuhPjlK3QPU0!#Q}xtg@g^JgR{(7V z(M#ndsaS6vu5s-{QPn&pCt+KbxgYZ>XO4B1Xn4WT6Z8p)gKb%Nc=C^&T*Ue5+C@EmN#9Y zlETm3reh`6+nbh^6(A(S3&4MwQB%8HVo=QWs8_(XG~(_Rfd2kvGb}@A!)0EZt#U}e zXn6XH+V-O>S*M2IT?j)+S_Z{GCi{bU;SXwSefjzD2Yv||$;1NHvPJ1dh(bFxH#YD` z>>AtF?D}mG`t~IzNMutBar6?k^0LK1)w_#ICD2kr_2Z>@1;>K(?_{Xfr7F;9%Sc$d zYW5bsak1fwtR^mOe9&XB%4k9?U*iMG{ZHa2=HFSX>9FNmpCzZW*<2HE=fv%f&t#PqP& zz~rFQZSQg7mWD#I$iDL?1Pu1^_&xSOto2Z>qaEN!Pq! zB>F^ZheJ29NTIG+Nso+(49QZb!ovh)bG*_xYpwu+!=PWrANRTzJkA11pzK-1DVCIN z<-{&?)`5h+muOBlZ^;Hm)%d~s$MZuVA+CTy{t62L442@84L=C0L|(B86SRkRz)U2j z)CYg!7i{p4N;0C%ZV;n^1G19(3Rpt7Q*C$JQiXMOb>6uic7{zHX#V(A$2loxdZN(P z_727ZxFL!3xv)gn9)dU-924;x+@WT5iYK|k+#2p!U2Fr5)#oNlK1DW1+8WxW3j9{6 zJqFH{cJmlt33vGYFxWuvdC1sEb|L4S&NuXOHESbJPYbEiOziqkjwOb1!v!|4Sptf= z@x+#dlq;c57P${FhHW+wkzUV#`jXiJlpivpm`bo9>-_q6wffB6jA^p-Ywn*U*Iu9n zGn1Kvz!6pvg`pTL{FnXV9yI2%`&(O*{IsRWkvcu9no#0e!h#bR#BwoA7klxRESV2w za4iw&XL~Y$gK^xCfBUH+LIvb20Tj0y;`<}r;Be}&PxBW6@>m|~)%Us-{d`eO7OMJd zUWUq*gq%~EFH9acd(9vMIa5vs<=(NEYD0qtazpQW#BYpGz%gSJPBOvoC~0s>SLh>( z%gljOo9n{qeaH7_JuuOwM;_=nh3Yt~sIH*e(yVRoLlzsT;NF`>P(fgBC9nr|le#pS zWU^LfZpQFL@fJYkQ~)WJh9WgG6?hVo#)Xn$sd zr7^k4hc{=8yG>n%#U!9AA9th7SgkiE=uxpn9;0H6C%CnZeQqoX!964SrfZD5U9PG%)8A(bBlQAH9$3c>BYiCF1 z^7`4yC*c)CVPYc?Z{+bT#!jOBfI_88cs&(=H79FzF8;*HW$heni62tyojBQN?WZ;Y zRkPV!JHqf4f(!j4+K|AC4aY3c5|g$7tP+-C_5!x{9}C_`Ya@Sp8@gSvK4sqfLh{V& zmsj3*~cf;?BQ9_RqY4$dJ0fJWr$NoXnU&osp zPfxSx6>XyolNlRXdW!DqaQBh?0tMo*N%mMrt?W#H^73BR{|>pVQx?4^PIGdFha~x5uAilv4@<1@ zjKms`V{4~+6N%}oo_QIHLJ~j!H6px#=6qDB?EE5i_Y1kjzzOPR68kK>3fb9s44)fk zc-%aBn;vN=j6)tCro>|Ro{T2Oi7O*1(KPHEvg0<{`hz5p`N-^#bA?3>U1x=rtsv4J{7C>w25=qU7lZ zF5MF}n(4dzivwPPE!N)N#6$AXJ)AGple%4J*Euq)n>Cx!I`Qa~PrX~kH}X?DzF_g7 ztx#fLwk*8@MndG@h-pYNe(bC*nCx66!U~iyRdXtdm8a5>{isPE2{NFwYw>kKx^X0G zFlNaAw*EanULSvS5W4{FE*E{SGV28bZUKn9dVrc^CZgBqI-yK^l)>Kt6p;efEY5>~vF9ERDCHAjwk{{mIFJ3n!BW00Xt&Y1-3F8Ib%BQt^a+(XN) zfZi{*}N0g8s8PJ8J7}t>PJH4d@p9oOTpm3kq#Tpta zzZJ6N+vqle>M<(tK1QNpg_8fjo_t0bz8TNOH9o{-rKEVBt4nrf{D$zAv6TRuv3?l; z#pF(+s{*Emq*SUN^gz1#tiRglx#cN?Ub|{iWk)`2=x_|UNPplZTk}+_DdyS|FWAQK z4X=KGI~Esk{cY)Kk*sg4L6jt685`poOb#DBji5J!XRmWc{-BZ<{!?Q@zNpixy2#4F z3P{UAF#JwT#H^iTY>s1#l=ri=}8ANlEaUSk$)ZK8I_lpxpIzxxx0jH@WCyKNG|Z$>z3HmnfH7 zmM7HAr0eJoePdIuo+vk*tzRi~L&b5euW%N3+aR}C_i>?e%JxAc_~GKgVUzrpz1+E; ze;0?xr!x=@1(MhH-os~}ynUC$Ky0~n=#(&nbHUq$&=D0!m3_!FgJ}uu)SnbtxOpmvfKVU0P1Tl^JQT0Ot(f z1R$oMdS$1%MDA?{o=yj_INC3Lf#-hjlv)b9TFxLVW_DfoO$*7Bpi=*oolNyLRC!JCA=*I32zC#j z;|Ajz%jS^38}(a4XzU)UxYZdTYcAZLCeB=O$?+gf0l`Y)1tIbFI<49LvE$`^jhPY3 z<&izt&KhXDJVk9~Zb}PXnZ$GU=pu6_r0gqo{S*49F~ph)z`Ch>t&BL&?5QGNv3Zby zDVg0u;>WbqlZShEHrEXtw(PApni4eevGFf^&*o+k1UA%_?rWAYsLb5f)D>ZiwD$?M zIaC9UJuNqPU$Q_dbOMaCVaDG}s0ZU)if-8 zC!)Sx{?##4A`>Q_hvA#yH8%L1gI0-u$;Sg#XU zH@O*7IPWaKH&cqd=$V$r+SLp=E_OWrhnOVi1sa;2z{V`sdmd=LSQXT_o$C2{$La zvDa~S0?79tT9GNqo*X!a!8K>%J57I9y=v2Z-iaj4zN||}k9G2byP)UG1}1E|r-kno zASy5$)ypwopomxO?LguE&7LW;C~D~7=yKn{#n5)f=jyEB<}Ih=-^sHJBD)^HjMGc` zU>K;SANl$WZ4CUazCO(ardYX(KKd<@PA{vK6ZmEu#YQXQ~bA8aJtEnC&CUuG;xfxwh zL8+fKcbc|N7fUyUwka3A%rqF-rs);<1a#S;a_(TG8(flZEcglPxT_-)Oxg)*yT0lA z2;2~Mk!-_p#sm+|YC?I%)aSb_YCpzCLeycdt9ZmhpP7l<}DR=>555HT4}3!wYypPF35tPaY#i z4vKphjA;DrRrtQQR~$K|E?EazS0R;Y9MZU=)^5RXGnO{#Qx+G$X(&iTIlMD~+2m4T zaQ`FdC_xIddWJ4o&_GNd^U#xIZ`XBX#ge7Q@BP=!$w02{rj{QRZx;gRW7zqbH0E#` zB#CM>fnx0qMS4Dn>lQGNnFJ~%r1p6na>{#3unrHb2)Fu>;s(;}D9r%3Owsz6*ox)p zd3V%e8Gwmbo7g~8NgSEv>;-vU+#JxE&5rN3(AYbdxK5$EQhH;WV_sS&0+}?Jd6O}! z3wfBg=V(>dn?TX$z^`3o&40_>!{+D5NjZMSu8ZPAjdiTis^9Biz6sEO8;Sc0U}5L@3k{Ptas_#h2iVrH%Zjk2z(C(NPdp zH)iyWM3X|OL2V6gpQQV|FTQ`E-?#Uix}QR?X!j|$v?7m2@~=;0rzS~T`LsG^!K#^)trjUJr@_`5-P0Vj;(LjMM()ZyeTw%W0|8h6HMT3wTvs>wL=@ z9Ib<}JR^tObTilWJ3Ql4vGd|IlT^o1dh?0;Ve^*Bnz{auYItoES%shljpf=Pqok~) zWTFkbW6oL4sqO+x<(;hJ@^G=}O#4Olnlcu#>LpTK5hdddLr$W=BmSx`DCk*usV$pp zlLZE4>`bb96Lh8UplOfUP_+mid&ud&$lK4Ag~U?i!JcK|_WRyN?$B@+MpF~(ByEGm zb)S|ClAo59x4k2N^Ck>3XN7;;nxTo44#;-_eVCcekrphrV)QBF0yk7Tf4c{+tc61I zEVt`JRr`AvPn)5teg#LpegSz>bp&HMml;Itb?fc0Bnps`D-3XU`*zc1Q_-xVr6u)@ zPENy95HDWl6S*3nBW5@IpoD#umU%$!~2H=ukB zn?k!7(v2fU{x;_lgZnrKq7oAJ{fXrzIQ37z90OIKRKpaB36Kj#ba@&_%DV!uIZ)6j z{wN0M%fNlG({(y|8|Ug%wV1l0RmOe{Np!k6CR?OVz5ZkH7Rfqd&G6l|UjW)t4S(Y9 zd0^qjCbe^(pA&I-oqBu`&(IhO?iXE09J)@Uh~P3(L-RU?#wOS;qUXbUus+O`r}d7q zBey~&8K&5w<5*t$J*P2ikVwt8!2A)VmqVxY6>tNCMr&^O-Z~J_`NgrXG5IFg-goJ7 zd}?CQWs)b;=Tt)o)Lh3~6l56o_8Dg+C@VQ`-8z{t3JJr!TP>C6H}C_oTe@1`WiU8@ z=PeDD-BD+!vQzhZ>iJkMt_zK5$g{`S+=W=zTbPYCDq8TBEQ%P|f2lhifIC@CWnLH} z1#T^@KTms5bXDdwy7nX90Vu;P5snoSj+Cl~m;u()2)@lU4Qb?1KPp#(r{41Wfv5)lU)ek^C7G~upsc0Xe9TRzi53U>~p zth3wii{`+Q2VlFtaqKGNt9tV<6 zQxnaS@<~0BSTg^IL~`dg)4R}HF3iq{ZCEqkumi+3%S%MJSP9i3kBDAxil6WT{qMY< z7$k@Y{PVUx2E92uwq?4gJ`Y{Wz+?0d?*^E*A4&u+g!oKG{S}ITVP<< zj)y~+dUNG@d%Y7XA_H%vSreg|o{=La`~B$H5x8fXS?zI`&^tT{xr;N|DICw(O&wJw zc)%3PuOnCd*?+$<2vZ60k~@C@J4BmYSW6p@iPE0NUxS>T3!mbLXhXxG_RD_du=sJryJ-pml1mjzoG4EH5{IJ zWFPEsXskdwY54mNk-89w0O&^b&J6lzqRTwrncIC60&h~}ZXiGrYRCnv@t8W&8rI7# z>MyYUj@&vDw0!(JkK?#C00B)*`olm_jZT}6=I#Vwn0>Sj@bfQ4oGWqXk1Bfw=V#T8ucgknlvx~CF66f-JYi>- zL|CYu*1^30;oW>r{G$Fj8Z}}ue~B!^&W&=!knI^wXdy^*>s&I@Vn)q!C(61#kzaZ` zZ_!RqM3G^CUMMgb0 zS!9J9RQ}yKD>Omj$C8({!S_s^V@%>Wy-^hsSR|z3Mu%FkYOt41_$q1k_kP>m1NzoF zmrX7UDPyLnfvH%eeJo#>zmo=v$_34*R@3y(S{2LlLI0Q)n>~k1w$@Ds+`Y3nu9ar; z$^*1P_@{ZZJR6e$2>>@$Mfx!_S1uHiUHjWeb?#EK=qz8>XK16MlfPkaTPn#9d{YVc z*l2-2LN+Vv&*;857hq%0vts$6&;U71I|YNk6EC-R!M7q954sC zP2grm{K{=i=8M=dT+IKxh?Rhl2%-@6;* zSWlad*TUrOdpA4857SUCW%A%&_&ibOge0eu-ZDEW%V0X$d!psKCke;*>a4t3J=s)} zBb#=q4kq+TDxc*S=T#~doXl$_5M^{Kq{lGwu8rHXlsCU5!aYZbu3l~n=;6bLJ7(nM z-@m{()=UTf+t2*2N6A)rP_p!L#x}(^%M5iB&eEKwkRur@wzV=BZthUpjHvmqbOmZL z8PMf(I@bmR^%pcU`xu{GF8uj87cNhn4KGV9X}{1Vr3O)hF$+9UoRFu?kVy zbqnW0A);@Im4Wa4VZR~p$3M4VyWaMVc>{b(bQK8M7{^6~SjG&<1Y%XM;Mi_pSazP4 zW|F}Fr(e=BjvI+)KxFfqVs@CJO_o&lP_7jEi@hdI=>afW5KuB-O2ej6M9kr z37&!9&g$8jteWJI9NVKf#B4O_lFrAkVgE$xceAy6d~XL&{uAZ3o;z|I%lKYe6g6}a z=YtYR$1sYm=EV`>Kkl5AMJQ>lGj{Vbw`Mps`Gsf-FSL>uVhPg_14+7I51Qd9MkFRL ziur%eU!ToqyJ0snqRb;M13n(nR>!$>RO{Wh2~+$1yH*>CP^BRyb}%{!#UC^0)>9U< zE@5n~l#N9BWqEdq${*qkxxwA$7Vi2=C_H&z_~uw zg>mE0v_X-~LId?ipTa+y^_Iiuy&D=_&L|bg#9<#4(fPB#AB>ItF93ExiND~|v~Df% z@F$=9i|+^!_4fU$;F@VM$8iuGYg*4Gd@xDbZUa^BAkF^(aozhSu?42j03($Ay~{W> z9wz5W&)I^KLIg?m>*`}wzj3#SoWD-e5-Cze1FPrZg|yQdRaPr@e)Rf*wQXI+CBEms zUPjk;ay`AEYiJ=(lgFc=nzcyHCbmD7SBl_Lec{?WfZJzi_h#^=w*9!=#LLa9;?^}O zBKY;CkCm0a%v+&K$_rO%_P>92w)JhkZPr_AZZ>Nm5MJHGGSXPuGBn!E?^-d^SN;=R z6?6jf%Y^%n)qBm-*K^%3HpNY|T(&kA;OJYXic4cG@-!^pn<|~}8~Q`8 zI*-Y`Zr1km%NE;gyN%VnD|hno#S+hOiso75Dk3aol2+n)Q!3WDvdcDOX&_d~aeW8R zN!)vCuHR}WlwZ?xMiPSHftp!GNpAZGPZ+dTc0_Y5Z6>N1$) zbLbmWJdw9nB>+m{YW38p#A>B7sTqIZ4uw}L!%3;{etv^4NKZlQhbap)(Ll^G1%Rgl zY2Yz^Lw{|x9=%XBQsB-$uI_DZ+t4b-)rwisSkCO0)2WT$LiA*f`n63UW2kt?prpITk*cja1CiN})u$C^TkPrkbiwTB=tIR$nu=mX8SU-D(^A znL624Um{YQ^z92+^Y8D7OCrCLHfEuru1Ct4ZW^fAZ&!GFuwzFXvHf}z#+5kPR?dC6 zjWR-5ugx%_;3L)U{A+@i1XSjow^{j_ZW3}H~$Kg7lV0O7^9`-+7E?4h%|+%4?l zSqyg<5yoYbLCYI34D1p%pitV5M=IdmTSj|XBiG_NE<)RF?Es5P1(30MG$O>tJdJvC<>J@_|JD`3{l(XFYHS$e87VpdGP z1$Cc-*WZc6r(;F!l~uReZP%XW3!UL6kVZh4UrnRaB?6HcEUH$3smKjknnE3oBWS&X zJ*+jvi4;*JVWg3kIKg75qf-z$nW!piPqzrAr4Dt5S$5lqr-nC;s*bf^Dq;9knX}cV zN8ER7NMM}2npOpsrmC{(NK~|pa^Mtk@$ka$k~S+IHo%hI+THDNU0X=6Z!@b$ENmV( z1xZ~jGGN0xt`x7x)5aJxakhqd#BWDdT_&K=Y83FH`!MS0iQXLUcLG!@Lsyt*T)aNq z1>elY;rdMTzHXWx)!wJ-G-a%&jFdV_GO3SFp~F_HV)|{%%zXa<+ZRUHxHkL3vXfY5T$>+8?_`x!g^-&aq1^h2(2JyEMf?g>|6x zM<%b-XI@<^kr?MJ)qO-Z=eKWB(*FQj%ctiHkH3FOhHjUASK-*;>Dj(xLri)>hK zCAhe^vACF(SXq`e2%@HT3mWJ6wQv|24h=5XVBdFVHu3g|Rzf9Sw%lwMhk~qwDFHn{X*fjSL14puwF!r{^x(+ zq;-aU<8qO}RtY8b%Mb@L(ON_0hNuCyM0xIJki4@=JCV%txpqGmB;agXQa3D>QBgsN ztt51ThI5%5FiF86L28VSD$Ywtyms%^p6cywlHa#>50kcrDP);uHvLKU%tJ9IMtJy) zxiBPXug_(=&H9qqT}VN%Q{~@HTQWfk7e8hx*tJqhp6lCHOfqi!bUby!*H-ZyhvGu1 zibQOFKKAa2SVX(E%S##NNo4D>{-G|vELucYfQo%b?xo`_7cNL7%LNajMSfBXM0~ zNb}E^oGu075AMmmjh}k9+!2p=J+|8(U$-r_mZ}3^^X5q;&w<6!*1s<_pQ=SWJ8Q;$ zzW`@;-R+i}ZT08M34sY$GbYl(z|M-Tk56JEbm^ zOzH3U_V9z|tLP%OmRMcPHvyfIY{urcABAq%P55w-ncPV2-k8OJH8|9-1g($#%fUYMfD~Rc`yQn74o+g3w9?b9@ zhQtpsuF!xq|5F$IDhSXTaIFr_7TyW!&ED zvKr5Dxk}sKDY1$-h;y z;OLSUk+%emu4p2>hI_llF-0Po`Qi|mW5i3I=($YkXl+OTZvwz zDbNCu!nCC|&ONnQhTPLHx+RM^QPEKeItctn#c z$Q|WYxOZIJ^&Q%^9L0`pwRN?Ms)!@pbPe6Tt>a7ju)$bOy!0{7#15mXX+{fHb!H5! zn^Ox-kLBPW?xsCtB<2@~vklDQnehE4Vf7Y5y`8($H885b{{WdHAmd!MFvP(1FD-O1 zJ++}p^~fsfju4&|WGi2VYCMi(JZ`HqG-!5~KQY)cGP5%qJKKZ?gd@PyFg!eZd_Lc2 zE#Z!!sH@y8UAX$subms@g$J{37> z*Pjx$M9y|Rtx_3u?SHLz8=d?cS-oqk`^j`g6xQ@As=D33N+@GtrCTnhtgvp@NCv3h zh}o_0C6NS-QI)t;A?%^4!m;%Zbq4s7*z}u`iYVsjG^;2y5i*vj6{k6jd-LO#4fQE0 zjr(~Nu}gq{>)u;N(KB1jvrjrGW=IuD8i8P^TOg?v`$inHrVra?cX0ZJ+k2EBExGpn z<>VoQtXEN3%^tKTLr~mEE97ZWiuZ+KihT4ZT9t;XFtk7;s|4^+() z^u;_2(@lOjk3uIQ&63*Mdq{1Klvh_1O-P?jCufp~v8;%eITt)gEs&+U>Kd`*z+%CJW9_cb?VFC>+U_ql$SyDEwM&Vv3pryX z>#r~+ktl(J==9~xx~qV*avf?Dle9ZU<0&S~XaO8zR)K-@)dNIH_tzBMg~kE1{W$GO zd7y%8R&NOAXH)Q(sZWJ4RZJK+iT1lxj5)cvW2ZHQMtrm9MgpGGjCa9}IoH+N2Gc#> zkiBFRvhDWL8;Li~#xbRWLS(mwm0cy1L@6n13h}|aRyVh}9(Hf(UjG2xa=~_6TS=j? zGTgo6hq$?r$kI(C04maHr=(YuY4P)_zU3WTuWxY0TdOJ351sq^!u$8@i#v;JCYhx& zG-f#M?$=U=3dSy$q|!NCn!HIQl39&>sj>I63kNz&1I22s z{{Rud=r*<=w>wd)%-4u$C}K$E9^O`Xf8xOCG0x&*lG;Y;yw3z8ptDDp*o^1yz}Z1` zX|eSd5e7?Z@t~S=$1td_L57S{PIVc0jUVbdixQ}nFZqUGDom_CAuE6hR#7tAR-Q$g zOX(+(PLz3?Sr}Ky$}3C`OY5lKp9Gq8Hn(>lOePC#l1FX43zuh|OUDU;U|C~ME2>s2 zrKm`&3WHo{R-6`xwUd9MG`>IXrN@lZiu3;fDW2Ybd8OrqEbPrGN3WnIN%+)Y@jaM5 zQia738rsoKAE@;?@{0ce#fN>v9dMnJ-pJ(`kx+2WLHN`*EoCLJ!yD*C^j?ixnN-x* z;1<7TDdd2@$s#yj4YKLvwhOPN4XpZDkt0YXeAu#)(~Xxg_<^oeW>+ALxL{(E%M(KK zqCAJ_FmBy{e-)X<+3%h z$dOtyN;DM;=1D(Ex}n1b(s_Jo;X%G8g;yeIA-Z2k*PBdm(V*OJHx5{pqY9?wN(bvT zK$1DeK%9j#W2XeMKHa-{i&+%82y8d;-)whx3S>bdt3?}1uCa|r@W#@jGtk#R4{^n1 zq*}hRsbH?aBDLA9=Sl^U?=?xNnloSBNb8@I$k=MEqN`|ncF_pFcajd_B!UvhbV#Wq zN3W7ZKtfzbt`4DJMJO7nOe+mlc~`e@P4`@Z zMW|e6qWPz{r{nP*-R&&B(_fkZeo$$K=3149=`#i+ndLKR<0S%oPVC z@jiW1&Is^0)nx)r53xjBg^k9~EXx;GS-O=f2!>byr~{d_^q(T9!;N&~7g1NuSka3A z04zk8&8xMxJ*TzLZO^?&yFq&GcXK3m6F?DVjUi~H63MCR6qTS>UNqrWZ5WV z&)VIm+uQY)@g3B0C7g{cq%G8qq2qJ|<>_$k%EW0Ne~T<5?>ji|8C-Crw%Ye^)UM_3 zHnZ4MZ2thK)vg-K?)z1vT|@~ntnoKe>0MQnQ6i5p#j@oZb7FQAjn=xfUnjA=6QKRE zu0*AyySuiWTWS%+?ysR{jfiQKN2RJpph=)786Qad*YoUa-W$uN{lB7v+%KZ&CrVp% z5yhe#hi9D=q`CyHBRlKULfV*`@i-=$WUZtX1>{!8sVGKX6naw^1=J65x^1z@!R`@% zl|xBSA4&&mU|uN=-U(Y)!PRHM%SI ze?$AK=4U$xbL`h}1l01`Tuaq*1!&Pmr3f#VG_=DwmB>T=B>JncvE3uOv5pmNq}q0Sy~YJblG-xrtO-)w05qqG#8nFKK@D}fSl?M%?oeBWytSHk zlG0~%62MV(AeJ~>ipY)UM)nhmwVNx7jTGUcUI;pG7(*t;4Pw($!ubUxFcihs#V~Q&nuN>AV zm0Y5XtE?=CI#g5>?mP`Vw|YVvu;WfsaofnE{afwZt=DF^+%BY!UDCu!rUVLEp)_fl zHxaCbQh<=zaP*&0WX)=&$kT&`n^jtN^Y8d~_k8&wXeDS;d80)+3Y7r+fx{J~XA$+f zx{U&xs9rZUWOhPp?rv|h5u$QKu*ZKG1uUVRB{b?ZDpWB1CDbSf-f?`iEA77#*(SL$ z2)}ByLI#$SC}vVf%TsjZRQmw_I0l9(kKk_BYj~84jkzuh3W6oMh>!kh`|&MZNslB} z0v5M(iA0cEBr&~c7pTAkl{GUEPBQ1BY7Un}men*1S!O+=(MdUh#rs?cWN4#^#$k>k zYFT7=5=Tur`?46ol^r9N&uH=fj;2g^u6Ex15}0kRW;_zr>+kL;n~O}B4IGOCviL|@ z582dOXN|b)rpGGxYj_(MNhL&f*6}4Y)uihZa^?oK7-Gg{=N;oL>R7Cj#3@iniCh!$ zT>k)u7^n@hrXpMU& zdqtk(dvk3)y~MED!!+|v11_ZzykS|DhMhE$j0-RIaicD@Gbt}!wQnZdw#bUJ2rZ;S z(oW2}`kh?CaU~SU{A=nzXjw}xk51ksJLs9Nc59%n%Vx4oAwUagouQLUwJXWd{9b2) zt{Z&Nkh)`;32c`45bc*%@Y>Bjk@{&AGF&lDRsA$WGv;Z;du!q|JSprlxfz0F~H1z zqBT1+dk#4yg55#O)$`WGara{IRvCFMDEazmjl8;QvJi4)H3u7{%v?U68(U80x4plW zZSA3gXOZ85423B|GxgSe=lsA zVWUdeFE&)Q+w88a?WMWAL`ImxHA$#s?;uVzJf!?F!>!#acWpMbmYWTew-QP2HpLLW zriN#|ecXj%8w_fh?vt90Pw@W;-F_j(^anlNtRudxz z)XXa=HClRjD%5ideIsjR>f4P9rs4F2S=`%2d)*dD*Rymj7Vg`nBRMq@#HQqRovGFU#_zlM~^Wst({ zUO_9t%Q&>l1WQ6XiCpPHs8r$nVv;JZY*s#?$Q!$FsB1*E8CHUpu?;!?Ur)ms=>Adg zS4lc+;&BlMnvA-5V#_-b2sN6Ok2a=c;)u_qc2LQ)*hI<^qPB)27PPEr7suU#da!F< z0KZTk-8*XM=A6X@jFCwoQ2Kg~61q)m>ct?Q#Jg_g+`ET;G49q@mb-*IY_}6E0%etq z*AUWDt_p;XFtJmpSxHqsnqgMPi>rf|KU@3KWzwT=wdykjdwW?LK!%_X)}BPK4D|dl zQLK?;s3Py{7kWET?~QAuFlC0;&q?;Nk_iY4>?0A;y$edIg2#V%MeK~@xOX{749t*E zBvdSQQ>O`x4^N2m_Ts*f(&elk+BXjT+ywGj*l&B~z0D(coL^o?QJK>#^)$AF>i(Tt zxM6q1t`^=)jKSmmQ!M0x6~5r!oNJ2o<~!FMdjnffXzvo-#RPjf>rUsOvR~UUsI6DOjt--yHg;YiPOlM+&mLOU*4Eqnbl2DKI#ug% z+1G+`X?C?%lA<|js@0`CHoTdiTx}Q923tEATu^G)Du5__!isG=!K>QK(41r^o9-G;PgZH!(?1=*fwSkmE^lx7WHl8q=tdKq-v{a(M$`*2QyH#?hTzFE4fA`w54Qp(O#pXC zWWDV}^3Z?%@9oU5byAw0APnUrQW2Y4>hySiTx90nkLot`m}cAjzqwI^y!Y*y#46C- zTa^l$kSo_b*|;Ap4qT5PncC^U9b2huUfyN0%j%SDK38$KXte1Lv9OVKGJ~j8CRyU~ zbt#zopToL6JA1}Qc*lP&#ir2&a?2{pUO1FOy))`*q&$V2QRNkL<(lJ9DLFYRhQwQ} zyLUmi2%uM%Nu;$)cor_8RbHE1OZuiy(H#2JSC>r&FYU;?N2EWuY_eb6Y+J?Xp5EZw z?Nu&{gWL`v z4#zOvcf>{%j??9A+IW;L8KzfFEI%u*tz5oeo0|`5@_|wEmR+tP9^V3?8Vq5OFAXis zxp-u8>@g?A@SRoD$k`@&7+W$i;wWj1{c4O)w|S@WjXdkOainYkTW#4Yj^;ZnV$Vv5 z^g^1+@*1(@-c2C)JVRD{7Qk&SK1SZl<+kZwO}T8*V7GSDUIT_39JUe29W3%IQl_BbXO7`jau%?e0Fq--#4Z(Im~EU6 z8ZRirI6#MKB-sF$P?RTFb+5436D$7!tacsBcY8;xexBf(n~e<3=1~?u{TRoIe5R{z z1rZd7Q;fVYNEf*o{>&U`S`jfTZy^~SB#L-oQC2>llGs_xC8~O0c26%!rXWd`{_TbC ztgWpgQ1jnjL1hwh=OxSON|{xo39Hmvm^$bMszlp%d(ESMxVGF-t>vw%#3o>xBz9DsL~Z{hgzErztYadw3#Iv6f=m|Nb5~B;kbI?m6{`88vg(Tfp5hN z=KM+1?H^8icHLgn^VQRzVjZR{I}p4{u~H zE_y1=VzjpDEmV}yrgLu@Jh@QPlr^Cky4tB|i0Nm$EBmL{n@HpXYud@TDl2h)aImm% zWHGGD!-xFU{{U>Y{0pux5~scWYufR=k&lYdyoGWj^8J`yro%DC z9F2DDdtU9e9n#i4sy*Cp4IBLG$8ByceqBsq0>pXRP|B)Cbtf6G^7ifKY{z$Z8<4k- z^4hy{@$AyYrrhC<4Q(1l3C3LXOlQZn*GLY}gywq_7TVDSC`u%pTzSe6) zG&9K@linn zT(im}k1}-BYDP#r~G05LC+H2G52ydtzNhAZOH5i?J6HPbUPu$V? zTiIF?)4#{P{{ZVN*4i~B&utN$mOVtW9k4dgXihbC&fL1+hmY18GNx9vw)A};W4fV^I zktK}@iCKcVDI==H^ip)Or!_Ej0_4m7>)UR2xnoJpTZV^-+bT%%L|g?8tS?3Mvwjym zJg_vxkXbTl5VG77O{!xRwakFp&1?n2!EaM{3&T_$X=KPbIj1gFz+}L;AJXKV9QQwC zlCO04*Qi_NmIsYPfMqOAGCE4efFB>Wn9Sna9p#@Td$`8mH2fr8&5Z4LG*ex5bm#!0 zfE+S|n=4m`FCYB!xS7=n|r^R?d|pU<9)T*!n<{Kw3-)9Sl84OK+UxPczJuU z{0&*0FUU7d7jE7{t@j@5u-xO(ZzPSSSXjR*w^u<7jpFoWJhdf$(a_1otxhFXyNwsJ zcOR&+_VtSG`<}}Cd=OiUX!iuTcllEpW2>UEjcN*j8c#4v(-+Uw2NcmSblm&?=S9Zn zx7jQ)tMw#U?+J0+kB@$avrh_kkutcnoO8fLAR zA3OzY9h{X-!(-fTcLIwtI=5DR60IYQy9zBA@aI*_DtmLgT0pH}Yhj7@jsEv^+@+4y zEi4k+TZLy9z}`xU3~bU0jcXi@??OkWxYFtZYBekty+({C*K(7@iW~F_O+?_NfmT&M z+GBG3t+1n+f5YR<x!WpAhk3uepv@*V>eSF%MgaP=0)5GC5)g8~f#c1^mL))9I zJ-IH^+Z&*;xfj2wc`z3SNCEvsR*1wSayfPAsHXvw4K!`7JWST&tf#;OXAjhiITp^% zZmxFZCMhm8K~Tl2#oG9#!>ayd4dk_^6^Y+dR%eeyT<4xwo*N;#NoPH*(a&`dg(PWU z2Pk!r%8KTJ+cu{SH4TETT~@o9TsJ9bV*dbCn91F4;8dhB$5G;>8A1O5=HqS;pB$q$ zZ7x+r#UO1Qcz`jYEGAGWG>swB0djH&h5@D$XqrU;0;ZV?;gzbY1CwV6JWtz!tJIKB z*`qIi3%ZkS8|t1)(NR=a)5QK9@Vc(%(HgGi-qPoyd-Q;L<|XCq$QTHf2d z7jUJ;w1b*CQDj3;HDFZyaH}=Ab(GJPeyr{5vv;NBHjEQ*xr*Lvc$AtgAy}lXd`$5< z_~NUPGfKwbr8BhwcF1Lmkd+|p43e=BFqDs2Q&P12-dMMwDlBOIk7UEQ_kWv?c%_DG zNux=kaJmOw1IAnMv7UtR%ldGfxcM4-YpSsB+qT(m`Suc@`EP^>iYuQ2%F zs^l1fh3ziqzPXj{Zf3EAQE_mp?5MP&BP(>`LE&9oJUri8fo8*9^(dMxi)%OOecTpe z{?_u|6BI1=@mrsiplq0b(p|q$C{$HuGa7zs z)fSY5EO!X*qpF<}A<;BviZ_wPS+*7{LnKiZ!uGQKO+|0S zaGZSvJye&~es0+-8*8tc@Ywdx2I%v^)5X+%zS=aF>7+6|mrjFO498hI4yV=ln0aG$ z@Y$n{Tcq_Yy{`&()uGzqW_u32lE5U=rX-h1=z^eCQLCrliI+%md$Z= z_IrmlWLD8oUzK$@bXtlE3KQ?eW>w>>)T(n&MJNn+7rfDNe++Wm$r2%HktC8nUUgYl znuUq3mZybC#RpV@+k))2^WR?GL2zvvGdl}<$Y6rr0AD9`K($FECZd{;kgF*~!L{I* z4X4q);RU7SN(IcxanjtL;6!c$EK!zOovrmMfQqNvLCk8(rB$elCd*FYw)TeAYP+qy zzTImr&|7$~;({)uiX#fKiDO2Q$4U^i70G;t5!I!_NAzjC#1u~HySJHb+9!rZm#NhZ z6`zO;%0cDz*Bne4I*RS^JBtn(e8v;IE+CTHdF^d?8SP_|C}o)n%@WL_IeJd4p&8La z>eF66XB)6*>KoM=dXAT;X+wRzI)rvFs5>Ubw0RAQd zlrFPYsgc#!$i_SJ39oye?Ayfhr6CB>uA%-)YV?oITo+B3hB%(m9$XIB0zL9?VFreQM@xx6mvy!ECm%yC!jQmAx$}{img*hWscq)ysDYV&4-Hu zjk-IVQ)OY?#!E*a{G`&fXwCVIC6Tq7{{Z${v2B2=37fWeC?yP4ZWmAmBWf{I=crI0 zb#M;p2CBlH*V{WSp3z~vcI?*!Q&TU-u*`D!!93Y8 zgcI$hS^JA;jg1uUHhPI12VF5}s6L5~HB|~lbmfW;qGq(VmWEi>=xVhUrYnRL^&uE7 zY^0u8W9hxi^(SQ%F4ZSfpA%L*aV7$-0A0KF8@cubcxOKdzsbKXp3i&R?;JyKyttYd*9!%azfI;)->&lMV5~HeObT$uCo5gbTvrPFyY2Ro z?3*3Uh-PcQkhL&tB~jO`w%0~RZmH6_#?(KDJRapt6K=OH!tT|g+&hrBOwh#?GCCb6 zLX=b%tLYRPbnsg8;1xr57>~K;+L}P}-S;F{QKhu7OLZl)F-cN%lT?d!N_5#Yny)^u z!VU|FJmX?T_iJ~l~9X92^kSg_+nY=TZWD4+-EXvpR(>3`&o7p+1*cXG>Go1&lHS=iV^u~i=9eU z&_&-)ODSNvSG#VT2!yjuO?oJYDHLS%Y8<{8upq43E$VGiD@MVV_Dw;Qth!ku(nzQI zbyJ@V6GbL%{{Tt*muuNAw@9~kXclN~QWcICkT*n2sYyV<5*&J{F;#K7cTL0*d){H) zJLR_Hvq2)+#kX7ABFY4BsTxU2n&rvW)9NClEVDNyD!?iomhoxxaU?+1OGx!ap!G3_ zTWS8B@Qjp@)<_<*+P~BM&Xl)X=X=J4wz4|(c+@oqD2G5$JTuIbfOQ+l?Z(?>rKIZz zxr#oT#i>ALRxT2x627CP_!H$&O0{eT#M-9ODjA}Jg$|YyYt#zorPaA>L-=YvrvzJL zx}l*lM@;eykDom__XDRqq8$djHm%_N5 zxfR9`cene)c#0*qOV!-kDh`aXt2nJFtJ3MaQ-ZTRyJSEX^~XKw%Blj6VUXYf+sETvjCe$n3U6tB9qOYuuxAsZQ6ptn3DQTFPKZ zT^wI_(>dCU&5)C3neL2%I_0Pc4gEPl3LC-r#7u*(>wtKzy z-?ZJ_wY81Q+VyUNGnawPa{mC95lZV;1C(LZPz$@Vk_2(@1od9;Y-?(TRl1#Rr6km7 zK*BVCJc~9i>PoE1YTGvnEa3S&`!|N--lBO?`q5;^GF25*-BZJkIGs}B&gV&!Ldym1 zr0{g&xj?EplrT~?WHkdyV=BG;4J#EB(61Ydc;nOsP*4FKICxXT3Cga=JESo->VQ!e zfPQW`EPW#tZlxkL+pu+_)KkmH8)?usWy#m2u^#@{b~cH3G%p>L>#8}1O3UULd+^LX zGE7c>rQ1L{>^n1Cy}Z`R(vn4)hNsu9E2s5!b#m0-i^aUndiAl)bd!+w{=oyZk=R7e z#-O=r0QUzhF=!S@lcx~r!itIpK|latEl}%W0>w8@*uB}(I3t?paBHW#2&Q$UFvhW0 zDK!3@HytNe^5M0xmWKan9mgGzM`7LDF@QZ$}{&9gX9ZDU6XK&Y2Iu%Ns!AO zmDCqZfC`?Bf=UYIUR+u#B-kxJsP>K8>XK`V230KL?gB|ZOY+9zzZzoXB*Rv{&9H6+ zEvDmbD|jH4RnPYrhWoKhxg<D>n z7h7dRg<9b%i<;i@#gs=`l0aEPg2s#afZ@j#TOwH!HqFhf>k0&c9!{&o@vM-%hl@!Y zDvl;RacWG-AdoJ0+f7Hz8fDg=z& zs~$rDE=c~6HGS9)K`lsHg2*+3q#VH%BL=KfpzYAM7c{b+uA(tpMy1u(MQGrS9UoZD zmDG+#Wv2!>C6KFPfwkprAxS>mse+jWA^5&xNp!*LUSb!B9%h-0Om~aQCF#i)J==5c zZtJ>&<8_S7J)N>ZlL!PM(OHNuAsVD}sGc+&KwUvmv`lGt0d*4=3m%F+Z0ATkCTTAJAmZHq(#L%jDz(`&d1 z3Z%%!zvE9g+c2V# zjgg64s4A&Jo^`-vqhza0bH2M7=#-0h@#JEzAb$>gLvICF-jc%O6v$8}Qj>@Kw>0(O8%36aufkj@>XvJA2-}eGVcOr9O?j@0DMHfa$Dao!@wsv$B&sI08OG=)VOwIV1|()#D5(Kl{{@y!uBD74fD8;h^Q7#J$qS3rm)c)<%{r zs9aF+&lhYer7X>m85u(`8nl8y^QI3S(iL>sq4%F~4$6zS_Dg<^-Nbed*Os$-m?udf zc`2=zTSq7a)MRor!d@s!5bESsc9)|41ZV}fHy7?Gz+OG+HPIdwKl zxQ>sw7fUZePv84%yt{3hTdQMnVXBF9Z~C6#%g0~xt#I0lvVyw1#LrP&%|X?X2L#Y4 zUfsQ3SlpMWp4HqtTee)_Q&P5Ma35gBL16vxPr0myBr%C(GI6d%lbN8w)dOOyF5hz* zYPxdqqYvO{aL5-VR*jU>qb@k0T z#9#sX`gBB1XMOtPT9i}r?bdJSP_=6PidT7J&DfnS1b4Ysj8+NO^H^Ay^jI;%Y0)<$>h|s(}*Ua)8f0vq&Jd zl0=$4bWT%7GDaIsYLw4Ew*;<$wP2fNx~T)JRC0Z5!|cVuYe1}4wj0b%6p+SdPfg5x zt`|Ir86G}3r;N36`1?m}RtAzWT)-?x{5o(|s2rOKL)%s_%q-(_zm3sZhq;I?gJ_Tt zi)S%`9C56148n|;>=+h{L+fP7X+Ud&ss*Aw*4`rA_Saj5L317Kb4S6n#IVY_{#=^A z&@mAjJ#^c3s95hVmIitR*5WY4@Bp<2C7cl;70+^Z^{aHv$aym*Rb9AyX{YbSNg#ti zdu^)13wv9MT*h2G1dR{Gv3h66MkJIL+v1OKE^b&PdI2J|{pQZVYt*GS8@U06SHKG%!Qng|}KLQsnjL_hIm4e5t z$iZe^(&gf_FJpOgw%^>W7WWa`+!&>u%P-4ADG;QNts7;=qsq7&QN4;zG-R2!+nuX) z-1d#$%1ce!0|~i_6ZM+FxX(#!5S3YW-hH#Q;-$b{4zi-)W{{V4#Y~$RoF76WQc~S&xqL%ieB-cb{ zewj2z)KB5{VC#6&1zxkSdA1R6cNGXp;BNy*LqlI^G$RHtR0`fUeVsO|*%r>$2aXA( zS%fiyj3Z&Lx_k%SioYjVMbWNFzY{%XR7>DMp|p`kd~)Sc{cT0~<2|bDNM@R3s=UFR zy-A_V4R$KE10lUxnP-SISC(NWS~;q{9T_p?I=Y@?NvJC-q^8Z6=$bS3{^0W}CY|a) zid34?%GlQ9*PK)D%-_v_Xt$3 zyL8hm%KCt4i&2$Zz|dh%@sdpyqqa7OcadXzx!qjt77Mm*=uky!E}HU`MFouP&#PMk zN7=@>$;xh^>Hh$%-V0g1Yx{%A3z5e`*yAde&cH!)d2v)|o-1~wq;jxn6CiLL&|E`% zz*M?u(u@+BgX|Ui@D@_TPeZ+}WS70mw^}ix&2XfO`jsmrgZv0^cBG(Hqf_5hcO~3A z`tILHF4YaaiW;d8$@472oH?#a5PvQ@KaZr|QG=?yV=7lxYjHALh?}Q5V<}qcq;a&Y z2_r~t5w?(hRK@;J4uO6*KBbFf?>^<(tyCiZ{jvo`JAL-GxI~h=kr}$2v1toVVN;Un zsmGpoNuBpe+i>N#YW7dnSf;qyBAa2}Y|4Dv7};Jfp=~;|2_nhTV851!m2m$6xZcJq zSA*2a`?F)a?B3$M?fGt!@=FsCX`@{#;#iVl$V(L?sZKa*@D}w_hAsCPhYg*DJzDH8SSgJ-0s!wm8Dy0 zVmA@gh0^U+CpD!?dH7=aKrXUsxAyC_Sq>@TI z^68BUA#1}*i~6KzhHK8#`9rVV^M1j?hg*-8GNek@}Gwo8f3YE9!)SZ!BhyaN6!=wZ4osB)5^Fl|v~EQb^eyPLh2lxLXM#)3x_5+V&B|@(5ZnpeB|% z_)%4V^1xXPp>Km@-sMQqv_oq;i4mlbIy`!rYMm~p;$IqLHbtmZhUBd7*WV$y$+pR_ zUA(Ua;EV=i8j~E+#tk{xR4qvV0As6IEwf15?t4mjq`%+ZNH;jG#CsOqw?!yTmfbY8 zOKCiaw&wNND@!AKh|!#CYIe+p*A1sn?9Ml7cPrlL+;8d5PVS=GqQ)SCI%8mQ$=>}Afl+6vy^^t$fXxNUYT zUDn3$cap_7u>{auSs}GDh(RQ+$3CIy&xT@$3(bypWBNX!V{>IayLM&%>fZkVwxhI$ z>u|AszGBhaS_t7upQjOd`aLLvyA3oVt(ID~5sY-FQCmc=KB-4vFJIB;tF$!yX4YQG zetRwd0LBKdP{y~gvW6W4T?Vyfb)W}_9BiezS}btf(^QP^P%7R>4ZUC*+DH&tTwPq< z8*Zh`$DlMyDzQ{@>(VNryxR-q1-hFT(WG_ltHA0j`=p@h^s@_iIjiHA2K<`P8>Ec3 z^^L~!xXz3UOM7uDQ-D{Mm{ZxFG+LU2&Hx+GwYl6Pwz-tSZFXUTN!p?)S*BV`BdB7L zpv|(vDUX&&+}CcQR~z#=+&fji$-P}|)_3vlme9H)UBIOg{{T)oN|mdC$<+YSkHat& z@uhM90Cl`g1KfFKjZGZn%a37oA`_d5HLHLkL<#~Wd9_eflG)V%0J9nGxb>6n&)0Cxb46ybfUzesjWN)t*jYn>e5;D(^9#4q%-?ppmixJP z4aJqBj`s5A>NSx9IEranNQ13CLN`!<@Nw7tb4CxySG&mZFDp#T16?Et7DZkJHLC}ix*k1 zrx7z`dNH|<5lWsBs*E2gaxvBtMSPzhJQ`ryrwJg|zMNZNqTT;?o7xQeWJ! z2jS)`i%h16*L0q`nupISVAB@EQ~g9e0c~fsq+|us(_A!(qydqMBZ-HP{LEL|L4DRj zlfV0tq}!WoRs%}(ZIV?3&X<2PM)zXYG!Hfny6%qXnT(R|m#PT_tWvZQvG&CePCp;Z zf+!KpU8N~m7}cdBu50Gpab-d+-J5%^8El-5NP3ZZm9ICxhM1%hEZx7i+gR?GQb1q* zdL)e>fRkI70Qh=_k>!E12^wd1t4-P4$YH6!YrM$tKk45(efS4eRu<^_hi-@FZlbh? zHo3T!#BswKmv?i)RYqmKm~)5}lVO?rdwQ`(y13f!VU92z#MaYBRSuqZS5Q@XhGzjJ zb$gN_nt70$cwR+~L1v9h1qPY523&AbQE=^@zWZ(7cI((WH;>h^GV~;Vnyu@sF9hz2 z3Oe<4H92a1Txhn2q`;DFm*PaVpHLCT@XYyuWL`%jA^6n$u?W!u&uD@-m9C_XrGdhW zM{uH4V9I2#8p7IJO*xjlz<{n|(hM(i-leM9%<(O>arD+Tg~6gwN2Ii#jRQ{#jy}u- z4Hq8Yk!0E~3XsaS&It!B#;@$a)oK-(4{#t@%1A5*j#c#zF;Ent`z^xO7Lt3S-C(Vx(@-x% z{7zNEl|!?Toc+UfZ46LuakMjCMIF_rMr$&oiHnG27*1t64nW>dyK4Q58!#pn~p)9<=OVnl1L+x1$K}V zRdy}JGYXGjI&v7>)AbQp5!7_5?n?!~)OcO=+*pW^9Q04rZ8`8$8VdU>@y4I_)a+$_ z1M&9^Zyo2`8-!Y?+oYQI*(GZ!?jeDU61p;mM(Q)itAQ)vefTGutpiS)^>R{oF6Y@F zS+luWp?KpG+(=}y5wUeIANr`{_H$C7VWm|j8<;BF%YqHw5x0=cnzqq-YTYHYAci*J}h%`_t z=>w~7b!*YVwA50rb(!{=EYx~5Gf5n@(mM?$iOU)wrbXQ$g3E7;{$&k4-NQu~xOQ7^ zjju{q6pQr)jAr#|ZVDEh+L4b}Gb>PIM-iU2Oj349dPhV;W|7V@#M}zjl0VyJ|~SG0UqUv^tg83ZERX4UDQ0 zb4I+F3R77J?ZI(eikAiNv#YXpUvY1!sFzZMZ*0;ADDIKc-n1#n*SXU{?dI?UXZ12; z#_f8fWp_8SOeK4m)Tcmg-a1z>6R#MDJ95~a79BIax z9ATpP{TmqUNr4}#z`Yd8^J{B=bo+$U&+`+@CaW2eH8Pns5>)kL0Bd4Gj#J7294ZLe zUcc@Odw{RE*o$e)J53~#^^}Iwr4e*S%c6~&Gis>x>E@!scl&Mm+idS7xsH2#t9WIG zJsmcStb#(iMyipknZ1Jo*l{SmcAcWdv(CE1R?e()8l4n#W%zjgxHN^ZRm5USK*WPA zu|^2|CFNBF`!mlCsJ9YRwXyt8B5HbI%P2RGWMj%$={#^R7)tI!TaDD0 zi)V1-nhRL%c-Eo{svq!E3sf6ynil@pxp|;N>g%sg?N!+>h4XZF7v0Vd}@xj1T~BU1i$8&Ejx+ zi*`X>zqxMG-R#Z0&}~kCUhdT+YxYA$b+u`jM;|R$RSy$d(@q+)DrhaIe&D|CeY9>i za7DK7_q#Muf{|^TwUj0)2{Hp9x`rtrid9D-NNU;-t2Q{gbvbLY{X2XCzfW62x2#{& z?bhSB=gLp0OYiTt{yvl+3=1sKETE@om7lEsR<8&=GVcgK$e* zxvflhGtQRs+c+#^g^Gu08QwszpwX&)y;)+Y_VX_46JmRvcKeNz+i#XmlUUqM(w1f& zNln8wc+~|7sW~!be{;+DHsQp+-)|8!ZFiPEuW@fGbZu<6JAR=#vAIZY zqA?L%n@j4*cyoRn9xKL8du~=ruePJM#b!XpssXMZ*@_J>wACcoZDEXly}BN?3pR2{ z?3E`W$~X*XyB$PCn%%x?Y5+^kAp8{bX9Vv0UOeK+IaS;eZ z+yn)fuCU!S(_Hi6;Jnmumy4fpc&z!i;GoU+n|QAG?XK4CW#$BibgGRQ z9;FX%xQDnBI4t6t>dyAzY*O>(<`&Cz#A%PJ;w2IVVITp6Q-*ZMKkVMFS*7(gwuY(o z&v0F<>Cf%J0(U#BTM$>2D2$t`f#v?2O+C2lw5wP-82)RwvGW_bVn>!1*`-zniyw|6 zBNEI{3>1RVM{0VfWgX><6J@=G#TO$O$!`C#m2QD zH>C0RVyudjLhPNzZfq7QGiyWA9(rrZ_AXxxQViC2Sy1-&;TwSKF5h<%@CT^_r~a%b zfXi6<%FF{8QN_3nGl;HY?qGXENdc+On59tN$y+2BbM6tg+jkbaB9@foTuZ0 zY!Gby)3@sX081Rwy93~=KJqbYz`em+*5mVx6rfWHATaU3wiUJo$|?aKp~nzfJvG-- z1-Eq+jf*4rja1Y=G#HW-*p;yD5N}(GZO?P^FL1ILr)26VXya))lrkP|8hz&q%aPSc z+*s9zx2>JFOUZWUl-vXg@lB*BQa2v00iRBF9DFc1^77O?xK%=ZpM4BdErg8Sd7t!# znvYF*{4t-ZP^5QHc`prQE@L6-rLr~g$2PZ?Z?XrxGwpH(+g#nF8oh-YLW&eFwQS65 z%lM5mH1=$wWcuA@pmc@Stp18q{Fn$~wL zt9`JWCv@B`RpQjL31ksTsy|TaOQJ%5yPt+IX2jc+ZKh4Zn9>+r+x=3kc0o;v`B*Cs1{x$0r<@r%yWK>z5-?$!cR;7KH4+ z=HB$i)&w4(Rj)TKJ9#%D-9hQ7mS;^42yTG5po29!dz#6bIrNW}I zDA3GJS!w6sDW422WbQOu3bN3X-5u2(>(P|m`ak5V@a%^_OJDW|qSev-xwAK>RSPFc+S3fSS$R!dOz^bch z^B8sJ)=Rj{Dz$IaFaG2=RhM+xHYKPwmh#_AUrT($^kO60pD!hUs~WOBL}9LyVn(@+ zI7y^Salrz{Mj|T0nB{{L18z>7Mp`3lC~zU;OhTmSjJ8;!4kSjJ zMp&sN4##cc{4pdJ!~L(KJJ*dyl#wXs;xH^an{><8+L{oiPOJt!Ds++^bki?>Eimkl zn<12tu1`0!5x4*eZdUB$^B| z%tcnASZ&_Nb+odUMTT3MV~s^du6Ctqia{}s>o)N3*H+NVWQN{X5tlV#89LA(?l8du z(H_gCEYX<-VqDxG74{sk7h&T?ve#&}zOlF5EuOa^vtC3FlvzWu3XS8cJuFH5N6RxQ zNiNoc?b{?6%L=IJkQ9UG<$|jy&b#g~z13DBQcEP(cz?{7FX4h{23OKIhH#QDVwynD z0N7Ph&fKKBi*YPYm0ZqV%new17KD7+Yxd!SMFmGd4s0umEMeMMipvuekq9b5p#)-C zD+NaA62H2>yf9CFE6pOu(ph9Wn5bg(EOTyWCj(llg=@QvSzS$Idu@LeI<}If#9;6* z69-Uzgp5{da!_6y!!}l5pS);ovsP9^vCo7D{kCc=aW+ zJ-l5M^i-M-6+BJ?oktSw({cpd?dY>C*3N9?C3?g!QCUG1)gGh-m8i&r?Z&Eg5@)wh z#1QLsxyn;{wih-wRSMSKCwW|j(yS+FSRw*NGNG@knA1j@64OUw+iW^ZEvL-w&fMfc zvqssD6wt}=(@YIMXAHej8c8lBxs^`jRNIvjMJy@SJR@Z&1WzNAnLK|tiNh@n3f>6I zcfGW%R?Z`7T56OSf3~~PYt4g}c z0U741PA?m^H#KC#T#G`xal78j7e`+rh8h=?QJYa7-2CvzNJT7_usRFP#>(zH%NvO; zZXq>}NX-}=78GaAFGE4wd!uFVD^%J0x;Sm`03s`-R#?lBMM10RZWXBu_E#D$j0|~Q zb_H%9Pp8<6#L|g2sTj@kt_kTWRTPy}S58&btEW8usI-oE=jkqgNxOE=&gyO67p(1R z1-qh-MCOu-sAOppL}~bVaTw9lIl5*JuFY0EcNq`XJ=gbNs5`kv*9DWuWeKMN1WK}} z!hWVU-RdtDVyL0#UuFRfXyF;wxHKt^NC5Qbiv&O|i$Jr^C_VU$qSC` zI5fawmLfpU;frjfK4MTZr+Dw&sahE2m_LEOyO%qIG0VCsZpNDSlj=R-$$8DOdvq*4?-Zo7|O`a>rY~>f$?B->;0pn_wMF_~?0IQ^$ z(zUM;MF(QurSrA!CX&%-Vy<2g%*t{)fIb)^(LnFm`n&9xI+(g!LJD|k*5Dt!{g{I7 zxGh`!wp~1^23&l;;eg7ZZV_u6vl&Y2RSXVUF>eFsfU6G5SUO^%w5bh2nNkR?e8w1< zn;{UeR;kPbo-PVZTSk*c#0>nsm>7V$NjC3z7q$x&@R4;3CryJI7}PLj)54jJK2-MN z$VR_`Ew1OchCw_jBP>#n#T|JXfS*#(@x@iiFJ3G$+dKB{wkz4%O3xVQSY<5n16q@# zQq$pt<-o4wejcNVqU~MEw~8aZgP}b&a<;I*d({BPBR}If>~!&dPsA@2yMEyhUfyWs zT5|5AqlqH1@7OWQ$hlGPrYH-zHU`+@)kB%ua$Dw^bsu6&; zrJ>0lHbS(og)uSLT|%>z2)f!f`Lk*7HPCD4Ri-vqG#PJ>MyA`O4$Wsf z?CUM2^KjYkAX{lQ-pOsPR!G(~2-Jj<=s!<@%Dg?gdSs}anR2ULqpK%&E2Q2A} z0iiMgMn@J7!V+x`73u|ej2Z#zNpvGGjw(q9VN+V-^;9?qEDXKT6Ui0z$tJV zu=QfSJOnr<3ZG0CWB%Bb}6G{snzgF)@NSxZK0W6;G<$138& zMRUE{&f;{6DAg}i7M^fqtF1X>VCO*5N{t_zKV>#mkx#K@p%0r900-Ys?ZWc0m{C4; zE20o+u4>f#uoW%6L03`q?g}pfha@J?QIaUcdE(>|P|2V$;&CQQgOW=~d6PQjCMOV+ z2yPc9@olGNqb0mH?BAGMpotH^LMX*00b0^8W@sS14RI)jCybcf)`fu_r|dYKidf{m zEwv$zT4vNJe8-r;T4z$7d(d;MQ?B#DQ~I&kC}*qYn%-kfyiIf6*GxHzmQgfgm z%}h5P zc#=rWAn8~YWYwmfT};HS3Cl|;&&HyZ0Fkvwh<6stI*M|#9ZG$;9CszCL%I2g;C6U@ zDo(Y`3}}fI?EuI+Gbmc{6=HG&1}!mi{HkTFFdRS`;9CtMe#v38-mc?udvIWi!ouS2 z6%p$)+(w_H39BZM?UtvnW2I_c6|-lk8hjm$w5{U4nN}D(%R_G6@3^DbTIjZK=8M zv0BY?ELKRT0(K0*F@i;m5Wkto9{dohtw7h84{EntV>PAM`h_O$043BQ>LT|*{#*QKm}-|mI>2HauzvabfPoKseuIf5mSo2bQzBD z!ClAP_npx?bs>V*+T0NZy_{w^lzEX>EXT!|mOdonLO@K1EG(w%`0wqvg%Mru8**s0 zAUuKIK=Eqgi6T5gqPl{U=2r`Mw{XhVt8&%aZe)aOBsh+(P;09y{jCIbG8y%KULx&%`~ymkr?$kqE$i4b2*%_eZ+549j@8GMrLR>=`1W+dWFDf z7o$_nTbDAjar{lI;4m}bN)qDjP1VBBJt4PaP|UBYhlWK$(Ek8d1U3WTZ2m)YFoq5a z2B_c(CzShXLxV!@3!P}d_+W*h5alC|IafSTQYPka(J|AOwZRb&MI*hU$65etFbxEl zo12Lm2*Wcpkg?LIfK~#u{{RjtBHIY|_VYE7WrvYlnUspDKI{|}nR(my&6jaacYxX~ zBVEuSv~H1r@1cfz>N!(3CV_Pv59FP#Vi)=1lE>{%*HkfL#oQ})oPJI%yoWa#Pp^8?g2FrsjpS{YBU>%0qh3wcWYX)3{Pl zn$wm=8clt)#~U|6lCMLrhp(mg(6Y${R{>Z7Byy;9VVzX)VeG~y$k?4hmwO$%0#c~o zb}eLpZHYaB7aK*yM^IgAJVrHFFC#zNf&}q0kfc!g99z%_p%Pi`tyIZ!`XWr-g0oZL zp^x2%VyF*NMW=Rb1``wA2%ewwW04p9WpsV`(y_EH_F{q>N^| zXf)9t^2y_`0zUi=ax_i}fr>SZt|aAw+N)a75(Zx$IMeqziTiv7GON+ngm@e-AT;?N zGB{lWDtLP^)WPC6aK&1Q%)ZQDFnA0Ar;b&@awVuZVv1DJtCxii30wt09p`V5#!F_P zKBcWjA6({p;DSFy+tXd9qvFY(f4hb|rV6VhJh1$Q%rPm6-tV!C=;Ghcod7@-(BgNQ?#YCN$BndJwDC}LxTnt(BE8tPBk z8HVEv6Ghdh-&{pvty5vz*`(Wocu=YF!LpH=?C@?e?p?tIQM20J?otWbfJU-f%WxL7 z{9meT?;d!fgMTX}#@zA)M2&)wL9I=Fl*Kd>*mGv=yIJo=)9#8T5Il$sCr?mm(E})I z^XK}SsI=3iwN*yUx-Cr3(`*+4EfKq~NnL;hk*nXH3i)*^xbz8HY%t9%{aBdW+aNOn zO8)?u7$5{wK2qNdRq|*|c;)I5+pM75HtU;9x*K6>BhN!T^~|5uB>Xu zOBKt&=Ujr~w(lOi8;0nSUv45xi|D8ts)ztk11x|9I{yF_aI1B-S~FEY8+)lijhg7P zt+lx*^QjCe!D&{lcxCtD)kY>6PVl$cH(55zn;#Q zc!DZ$^+7gad#>S+ZMFUPW86`eB8wc5u<8k96%oq=(bqK~>RmZ_)B-N7I_gv|d)s!r zf=giVrhuxD^!-Sb6%F9o4_2z0oj;T~gzQrLhUiVW769c+YoTg;NCOl`F(hd%y2~34 zTp4_@Xj?=er6tU9%MgbiNMj_;0G)VgELNV-M~}P_?8ViR1lvul3Jy__TtYNBLrEY= zz}DFScwksnBu8ZSvTK?}AdJxkP9&Bl5%C_3YC3K32wx$6A1KXyi}%c>IB340v(@?4XCuJYUoP`I4R)n@AIsCJ=4kY~158+^M!L_#FQL>dO-z9wlVI zARkF1bAHK+#! z>993C&sPwl>2H*0#WhJ+eEQ0DtO}ZA!vBZ_s!nLC?LHxcZTC!a)0_L|( zg`vCKq-NZVYuBBP0hB3Yp)*}7{+?d}s1tzCjX3fj(dM-+@{Mi#)$ZmmsH;N4cL30J zC)DJRX{R^09PX#3QkmQQe_6>i{Jrz!7VTg%owJeSs~;-UFLpZk)E;K;LEYJ*x5Ns% zTv$X&uM%dPo8wA+J*OHZ&d?w=b74|3a>_{|3o{FffIO+}#c>!z+(JI5&sM5u(!N3wq7x0y$M^-#pjZP#HI3rJYHN_S~%ASxoVnG`imB6KQCltt#(UHYf zJtx#L5o4CdGpG}g6)JpjWioA~%oyCgO$LBGLjM4>0?7n53;UFEwxx~asbv))dog{) z3f_<&RANa$D^W^@0f*diXr@Wotv4Ge?Kghjy^*ikUFC|#Nn*K+OpKaNQL|N4Hzuw) z44C;K@#ThH_K&Jt%p^g+M7OAiLpf1zGSiNri=mJANA}~Bo1jl)uY<2kam~ZIHqGa_ zY%liOy0yQ{&KVtAGpXN0)76>$I+yvsE=;{UrZcJ2Wmzu>qu$#aTiKunqBklSc!kQi z)r$P8E2^&yhmmyvads6~Csh?1t5Du}84al)5+#4sAebHueM94mRt4isatWq|i`ZPe z9&xd%*N+3rej^s{Dd1s?4(`6&Wr|CiTYHX+2aahi;+cc5;!dhMgZ(taaiv|1*)g`D z^jogWC5^?ZT3Q)q;VN1|6ws*wX%R@*L8sze>s)<;s#q5wPO2pQ;_Dge1Wf> zPAd6laB<^RJ3ap7>YI5#D;vlqtcH;Z3X!RgH{n0{rVAY^$&&2!TZD#FY|)7%QJIK_ zQ|(d>2*6te2D3>c=9Z;oMnZBVih=fF7RJ!kkn-F_Pa}_xSdSYcu%^2zC>RiMN{qVM zA&bqv8;Y(%(G00U zNn=w@d2{$SG~1_*%$4eAV3x}W4p$3<(W7sRJ#dEC5oue$^Z9ye-`aRX>kX0CXJ z9FUBMjwpz2$;1gBQJ$b^a3l@b?hs2FTlAJIxaby9Q>b|y3oRl`e5JAdWmRSXlu{a% z%oQt9h{1$2b0ilEUNNOx@Yb~V92y0lwA$|N_T-&n>qjdYD<7#yC3Sh_rxiMqNr}4M z$@+J?FDMGu?JFwtYbUCOBxWb`mcs5@CT$-p){bj%@yukaM(nHxbzLr@D?$9Cn8*19 zl@7Pz^0k;&^&M{7Rs30;bGmDMr*o~ZD}&>%tZ8h-s{)B6)|@aQNU}koI24PM%A?_k z(Q3ouGJPk9hAWdr@KzGmwz8kqLlbZ^C;KqkXEWsKNiYr0-9fgW=i(IP&r;X$g(p@m z=Bx5E{{XFIKMwI{Kj~c|g}j(#*e)!sAmqC}jOVLtbehx^H5q#_&%DKbl!&t2%VjS@ zwD2sXXT#grOg-k7;OJogEo&jBqc$nVZcGJI3Q%t=_EOnAOQhRXj z9m301g{{0oF;mx7W|B=*^=c(KeT#~*6`0o1WBbVYay0I@5@~@{8WZ_K*8^u%wFyb4 zMf4K?07Fkdaa+Z1g0C^MEgC31>|5r5lEQBG#%%)zb|w zn$zrb`YT6_#ZcTV+?Zmp&i(-d$X(8^t&UTSUUG?d$T$e<0 z2==vG1Cn5v(vYRYrJmTbUBYhS6G9%}S&Xs=4ItE^ifW89hM}RMq2N}j1D&Tsv+YJ0 zcJ#Klfn-bBq&G4ve9a*=MQ2eOx-zc1nDr^83g)GYCvc}wgKJUF)vtizi)gtb++HoJ z>yfgrDDyZvqy?lmxA*(Q40g9stJ+4LFaV)d^IB54V`&S9`-;DHO|>x;cp&uWN*y^9kj2pfmFhEgjJ9oG05Dj06Q0vhP}9@ZUT-XPPE6`TXPqCj$N5l}25dd0bG{=Pyib+GQgnO01~W60x)DeUZ2aMgU=frGRM6rCHfTLKs4Vd;GX{s*h(b-HtFVsY z^r^@mIH)5YL8eZMzfhK-0~Faw7mcpk@(3f1R+_Bs3X1ZI6h0Io7ZfOK2BR^E%c9hi0UEP7PEnO)jS5awrWb1rL$>h*ZAx^taw7wh#Z;Rl za9pN~*b;I0WB>;q&0KLYXMi%{kb7;j+di$bW^u!%GcKy1ZzAJma;t7)wiYng=`sj6 z7))|km5Pu@+lM6vdsOPj2~n&DkMPW%q3^{#L2M;!WP&>D>k4uI0O5&U0(zpmx>>EQ zx6MZ6IBbCMt~$#bId38ZHT}t6XC#k2@v9$5*q4x(v|Y@blrjb@BASXHFmO+%w}2MN zk}m6Yb2JYpSe+wFw!G*#3fBStwSuN)vj$jhHUM-C>l=n6MRAIbO*u3tz1^HT0=H5} z6#!?E@xz@3EQqk|(+5JMGBKO|g(KB;00p;`xbNbNLfkS5WSNBrr4>PHc~k7c=E{;Q z9d#3J0~@I(x&m&Kay3IC6a+H({i7KlTU%^59y!z{xOWYN(Lrrey(SV$K;pntm)p#6 z{8m;5`Y>h2`D${vhE_feB@({?TLAJ7t|edUYbh7FvYRa<)GMg}01Pt*;&a#KE2=eRj*RGXqfxmMnSqG}LliZ| zggUyFfH?z-u|&(D3oC<@A`FE+Di`}OD3%~dKT!K{L_#$CGB|~Yu4k}r(6h6KCKt)L z%{1~ES(T8&i5(#!f)%54DHX1D%T0g)H3JkG;47KIHbE@j09OK7y(f9Jyb&Bda zUb<9*Y3#)`Ni@x;b&g0?+JrFWE5Lilh7|#(9*Kmg9E`a8F*z3lTbxm~q`C=HrnK;` z6`6ddo|(mfRw_(7j|Si|=Q8PI)YDp$8+o;lX}#HREKSDcaXb^*Ze~^uY_Mcu5mQN4 zN04}Wv#ymRtxGTU=eFk?JsRrLT7ObkH`pSATPY$AuW%(Y*V01h(Wm|GUko2krwoIg zy5>laD?v7a^sMA_hUxp*9chco0xAD1r`CM#1=%$s3D)U Date: Wed, 21 Aug 2013 07:14:16 -0400 Subject: [PATCH 093/179] dummy commit - trigger jenkins rebuild --- common/test/data/test_import_course/about/end_date.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/test/data/test_import_course/about/end_date.html b/common/test/data/test_import_course/about/end_date.html index 2fd9f95700..a0990367ef 100644 --- a/common/test/data/test_import_course/about/end_date.html +++ b/common/test/data/test_import_course/about/end_date.html @@ -1 +1 @@ -TBD \ No newline at end of file +TBD From 48c6daacb89ec7a3480b6070447f93043b590aa7 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 19 Aug 2013 09:45:18 -0400 Subject: [PATCH 094/179] Removed unnecessary settings wrangling from ModuleStoreTestCase. Modified navigation tests to use MixedModulestore Updated factories to find editable modulestore Updated test_submitting_problems Updated test_tabs.py Updated test_view_authentication Updated test_views Updated courseware/tests/tests.py Updated test_masquerade Updated test_module_render Pylint fixes Updated video and word cloud tests Updated course wiki tests Updated license and open_ended tests. One open_ended test still failing due to Mako initialization issues Updated staticbook Updated django_comment_client tests Updated instructor tests Updated instructor task tests Updated external_auth tests Updated course_groups --- .../djangoapps/course_groups/tests/tests.py | 13 +- .../external_auth/tests/test_shib.py | 8 +- .../lib/xmodule/xmodule/modulestore/django.py | 31 ++++ .../xmodule/modulestore/tests/django_utils.py | 65 +++++---- .../xmodule/modulestore/tests/factories.py | 10 +- lms/djangoapps/course_wiki/tests/tests.py | 15 +- lms/djangoapps/courseware/tests/__init__.py | 4 +- .../courseware/tests/modulestore_config.py | 16 ++- .../courseware/tests/test_masquerade.py | 22 ++- .../courseware/tests/test_module_render.py | 21 ++- .../courseware/tests/test_navigation.py | 4 +- .../tests/test_submitting_problems.py | 9 +- lms/djangoapps/courseware/tests/test_tabs.py | 4 +- .../tests/test_view_authentication.py | 4 +- lms/djangoapps/courseware/tests/test_views.py | 24 ++-- lms/djangoapps/courseware/tests/tests.py | 132 +++++++++--------- .../django_comment_client/base/tests.py | 4 +- .../django_comment_client/forum/tests.py | 4 +- .../instructor/tests/test_access.py | 10 +- lms/djangoapps/instructor/tests/test_api.py | 16 +-- .../instructor/tests/test_hint_manager.py | 4 +- .../tests/test_legacy_download_csv.py | 5 +- .../tests/test_legacy_enrollment.py | 4 +- .../tests/test_legacy_forum_admin.py | 4 +- .../instructor/tests/test_legacy_gradebook.py | 4 +- .../instructor/tests/test_legacy_xss.py | 4 +- .../instructor_task/tests/test_base.py | 8 +- lms/djangoapps/licenses/tests.py | 4 +- lms/djangoapps/open_ended_grading/tests.py | 19 ++- lms/djangoapps/staticbook/tests.py | 4 +- 30 files changed, 255 insertions(+), 221 deletions(-) diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 2e519edb30..debdc68c26 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup from course_groups.cohorts import (get_cohort, get_course_cohorts, is_commentable_cohorted, get_cohort_by_name) -from xmodule.modulestore.django import modulestore, _MODULESTORES +from xmodule.modulestore.django import modulestore, clear_existing_modulestores -from xmodule.modulestore.tests.django_utils import xml_store_config +from xmodule.modulestore.tests.django_utils import mixed_store_config # NOTE: running this with the lms.envs.test config works without # manually overriding the modulestore. However, running with # cms.envs.test doesn't. TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +TEST_MAPPING = { 'edX/toy/2012_Fall': 'xml' } +TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestCohorts(django.test.TestCase): @staticmethod @@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase): """ Make sure that course is reloaded every time--clear out the modulestore. """ - # don't like this, but don't know a better way to undo all changes made - # to course. We don't have a course.clone() method. - _MODULESTORES.clear() + clear_existing_modulestores() def test_get_cohort(self): """ diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index 6bb9c38e6f..187acdb595 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -16,9 +16,9 @@ from django.utils.importlib import import_module from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.inheritance import own_metadata -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import editable_modulestore -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from external_auth.models import ExternalAuthMap from external_auth.views import shib_login, course_specific_login, course_specific_register @@ -64,7 +64,7 @@ def gen_all_identities(): yield _build_identity_dict(mail, given_name, surname) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') class ShibSPTest(ModuleStoreTestCase): """ Tests for the Shibboleth SP, which communicates via request.META @@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase): request_factory = RequestFactory() def setUp(self): - self.store = modulestore() + self.store = editable_modulestore() @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_exception_shib_login(self): diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index cd0166e4b0..f1235668dc 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -54,6 +54,7 @@ def modulestore(name='default'): return _MODULESTORES[name] + def clear_existing_modulestores(): """ Clear the existing modulestore instances, causing @@ -62,3 +63,33 @@ def clear_existing_modulestores(): This is useful for flushing state between unit tests. """ _MODULESTORES.clear() + + +def editable_modulestore(name='default'): + """ + Retrieve a modulestore that we can modify. + This is useful for tests that need to insert test + data into the modulestore. + + Currently, only Mongo-backed modulestores can be modified. + Returns `None` if no editable modulestore is available. + """ + + # Try to retrieve the ModuleStore + # Depending on the settings, this may or may not + # be editable. + store = modulestore(name) + + # If this is a `MixedModuleStore`, then we will need + # to retrieve the actual Mongo instance. + # We assume that the default is Mongo. + if hasattr(store, 'modulestores'): + store = store.modulestores['default'] + + # At this point, we either have the ability to create + # items in the store, or we do not. + if hasattr(store, 'create_xmodule'): + return store + + else: + return None diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 8e4655cf3f..87156ec0dd 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -1,9 +1,11 @@ -import copy +""" +eoduleStore configuration for test cases. +""" + from uuid import uuid4 from django.test import TestCase - -from django.conf import settings -from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.django import editable_modulestore, \ + editable_modulestore, clear_existing_modulestores from unittest.util import safe_repr @@ -112,8 +114,24 @@ def xml_store_config(data_dir): class ModuleStoreTestCase(TestCase): """ Subclass for any test case that uses a ModuleStore. - Ensures that the ModuleStore is cleaned before/after each test. + + Usage: + + 1. Create a subclass of `ModuleStoreTestCase` + 2. Use Django's @override_settings decorator to use + the desired modulestore configuration. + + For example: + + MIXED_CONFIG = mixed_store_config(data_dir, mappings) + + @override_settings(MODULESTORE=MIXED_CONFIG) + class FooTest(ModuleStoreTestCase): + # ... + + 3. Use factories (e.g. `CourseFactory`, `ItemFactory`) to populate + the modulestore with test data. """ @staticmethod @@ -127,7 +145,7 @@ class ModuleStoreTestCase(TestCase): 'data' is a dictionary with an entry for each CourseField we want to update. """ - store = modulestore() + store = editable_modulestore('direct') store.update_metadata(course.location, data) updated_course = store.get_instance(course.id, course.location) return updated_course @@ -137,7 +155,10 @@ class ModuleStoreTestCase(TestCase): """ If using a Mongo-backed modulestore, drop the collection. """ - store = modulestore() + + # This will return the mongo-backed modulestore + # even if we're using a mixed modulestore + store = editable_modulestore() if hasattr(store, 'collection'): store.collection.drop() @@ -145,36 +166,30 @@ class ModuleStoreTestCase(TestCase): @classmethod def setUpClass(cls): """ - Flush the ModuleStore. + Delete the existing modulestores, causing them to be reloaded. """ - - # Use a uuid to differentiate - # the mongo collections on jenkins. - cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE) - if 'direct' not in settings.MODULESTORE: - settings.MODULESTORE['direct'] = settings.MODULESTORE['default'] - - settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex + # Clear out any existing modulestores, + # which will cause them to be re-created + # the next time they are accessed. clear_existing_modulestores() - - print settings.MODULESTORE - TestCase.setUpClass() @classmethod def tearDownClass(cls): """ - Revert to the old modulestore settings. + Drop the existing modulestores, causing them to be reloaded. + Clean up any data stored in Mongo. """ - - # Clean up by dropping the collection + # Clean up by flushing the Mongo modulestore cls.drop_mongo_collection() + # Clear out the existing modulestores, + # which will cause them to be re-created + # the next time they are accessed. + # We do this at *both* setup and teardown just to be safe. clear_existing_modulestores() - # Restore the original modulestore settings - settings.MODULESTORE = cls.orig_modulestore + TestCase.tearDownClass() def _pre_setup(self): """ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index f2e4017114..7913434086 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -5,11 +5,12 @@ from uuid import uuid4 from pytz import UTC from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import editable_modulestore from xmodule.course_module import CourseDescriptor from xblock.core import Scope from xmodule.x_module import XModuleDescriptor + class XModuleCourseFactory(Factory): """ Factory for XModule courses. @@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory): display_name = kwargs.pop('display_name', None) location = Location('i4x', org, number, 'course', Location.clean(display_name)) - try: - store = modulestore('direct') - except KeyError: - store = modulestore() + store = editable_modulestore('direct') # Write the data to the mongo datastore new_course = store.create_xmodule(location) @@ -117,7 +115,7 @@ class XModuleItemFactory(Factory): if not isinstance(data, basestring): data.update(template.get('data')) - store = modulestore('direct') + store = editable_modulestore('direct') # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 6bbd8011d6..93954dab61 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -3,21 +3,18 @@ from django.test.utils import override_settings import xmodule.modulestore.django -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE +from courseware.tests.tests import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class WikiRedirectTestCase(LoginEnrollmentTestCase): + def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(name): - """Assumes the course is present""" - return [c for c in courses if c.location.course == name][0] - - self.toy = find_course("toy") + # Load the toy course + self.toy = modulestore().get_course('edX/toy/2012_Fall') # Create two accounts self.student = 'view@test.com' diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 9d1b549b9f..4b93e804bf 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse from django.test.client import Client from student.tests.factories import UserFactory, CourseEnrollmentFactory -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.tests import get_test_system from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class BaseTestXmodule(ModuleStoreTestCase): """Base class for testing Xmodules with mongo store. diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py index 80a7b0a7c1..e4ee86878f 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -1,4 +1,6 @@ -from xmodule.modulestore.tests.django_utils import xml_store_config, mongo_store_config, draft_mongo_store_config +from xmodule.modulestore.tests.django_utils import xml_store_config, \ + mongo_store_config, draft_mongo_store_config,\ + mixed_store_config from django.conf import settings @@ -6,3 +8,15 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) + +# Map all XML course fixtures so they are accessible through +# the MixedModuleStore +MAPPINGS = { + 'edX/toy/2012_Fall': 'xml', + 'edX/toy/TT_2012_Fall': 'xml', + 'edX/test_end/2012_Fall': 'xml', + 'edX/test_about_blob_end_date/2012_Fall': 'xml', + 'edX/graded/2012_Fall': 'xml', + 'edX/open_ended/2012_Fall': 'xml', +} +TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, MAPPINGS) diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 0fc4eae242..3122dc6477 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -15,23 +15,18 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import Group, User from courseware.access import _course_staff_group_name from courseware.tests.helpers import LoginEnrollmentTestCase -from modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore -import xmodule.modulestore.django import json -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): - ''' - Check for staff being able to masquerade as student - ''' + """ + Check for staff being able to masquerade as student. + """ def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - #self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - #self.toy = modulestore().get_course("edX/toy/2012_Fall") self.graded_course = modulestore().get_course("edX/graded/2012_Fall") # Create staff account @@ -50,7 +45,6 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): self.logout() self.login(self.instructor, self.password) self.enroll(self.graded_course) - # self.factory = RequestFactory() def get_cw_section(self): url = reverse('courseware_section', @@ -70,9 +64,9 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): self.assertTrue(sdebug in resp.content) def toggle_masquerade(self): - ''' - Toggle masquerade state - ''' + """ + Toggle masquerade state. + """ masq_url = reverse('masquerade-switch', kwargs={'marg': 'toggle'}) print "masq_url ", masq_url resp = self.client.get(masq_url) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 25056ba100..9c11dfc617 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -15,22 +15,17 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase import courseware.module_render as render -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import LoginEnrollmentTestCase from courseware.model_data import ModelDataCache -from modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.courses import get_course_with_access from .factories import UserFactory -class Stub: - def __init__(self): - pass - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class ModuleRenderTestCase(LoginEnrollmentTestCase): +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): def setUp(self): self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview'] self.course_id = 'edX/toy/2012_Fall' @@ -96,7 +91,7 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): settings.MAX_FILEUPLOADS_PER_INPUT})) mock_request_2 = MagicMock() mock_request_2.FILES.keys.return_value = ['file_id'] - inputfile = Stub() + inputfile = MagicMock() inputfile.size = 1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE inputfile.name = 'name' filelist = [inputfile] @@ -109,7 +104,7 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): mock_request_3.POST.copy.return_value = {'position': 1} mock_request_3.FILES = False mock_request_3.user = self.mock_user - inputfile_2 = Stub() + inputfile_2 = MagicMock() inputfile_2.size = 1 inputfile_2.name = 'name' self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position', @@ -200,7 +195,7 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): self.assertEquals(403, response.status_code) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestTOC(TestCase): """Check the Table of Contents for a course""" def setUp(self): @@ -266,7 +261,7 @@ class TestTOC(TestCase): self.assertIn(toc_section, actual) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestHtmlModifiers(ModuleStoreTestCase): """ Tests to verify that standard modifications to the output of XModule/XBlock diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index dd1f00711c..2b416b16de 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -6,10 +6,10 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from helpers import LoginEnrollmentTestCase, check_for_get_code -from modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that navigation state is saved properly. diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 9081a910c9..f8cfaefd75 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -13,17 +13,17 @@ from django.test.utils import override_settings from courseware import grades from courseware.model_data import ModelDataCache -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import modulestore, editable_modulestore #import factories and parent testcase modules from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from capa.tests.response_xml_factory import OptionResponseXMLFactory, CustomResponseXMLFactory, SchematicResponseXMLFactory from courseware.tests.helpers import LoginEnrollmentTestCase -from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that a course gets graded properly. @@ -217,7 +217,8 @@ class TestCourseGrader(TestSubmittingProblems): """ course_data = {'grading_policy': grading_policy} - modulestore().update_item(self.course.location, course_data) + store = editable_modulestore('direct') + store.update_item(self.course.location, course_data) self.refresh_course() def get_grade_summary(self): diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 4435b5c951..5de7a39f63 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -7,9 +7,9 @@ import courseware.tabs as tabs from django.test.utils import override_settings from django.core.urlresolvers import reverse -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE class ProgressTestCase(TestCase): @@ -261,7 +261,7 @@ class ValidateTabsTestCase(TestCase): self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4]) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class DiscussionLinkTestCase(ModuleStoreTestCase): def setUp(self): diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 055c860fcc..849e5fdc45 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -16,10 +16,10 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from helpers import LoginEnrollmentTestCase, check_for_get_code -from modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that view authentication works properly. diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 6f665f7345..0c23b31f53 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -17,22 +17,20 @@ from xmodule.modulestore.django import modulestore import courseware.views as views from xmodule.modulestore import Location from pytz import UTC -from modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE -class Stub(): - pass - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestJumpTo(TestCase): - """Check the jumpto link for a course""" - def setUp(self): - self._MODULESTORES = {} + """ + Check the jumpto link for a course. + """ - # Toy courses should be loaded + def setUp(self): + + # Load toy course from XML self.course_name = 'edX/toy/2012_Fall' - self.toy_course = modulestore().get_course('edX/toy/2012_Fall') + self.toy_course = modulestore().get_course(self.course_name) def test_jumpto_invalid_location(self): location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None) @@ -71,7 +69,7 @@ class ViewsTestCase(TestCase): self.enrollment.created = self.date self.enrollment.save() self.location = ['tag', 'org', 'course', 'category', 'name'] - self._MODULESTORES = {} + # This is a CourseDescriptor object self.toy_course = modulestore().get_course('edX/toy/2012_Fall') self.request_factory = RequestFactory() @@ -85,7 +83,7 @@ class ViewsTestCase(TestCase): self.assertEquals(views.user_groups(mock_user), []) def test_get_current_child(self): - self.assertIsNone(views.get_current_child(Stub())) + self.assertIsNone(views.get_current_child(MagicMock())) mock_xmodule = MagicMock() mock_xmodule.position = -1 mock_xmodule.get_display_items.return_value = ['one', 'two'] diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index cd245d2610..995c7a352c 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,25 +1,21 @@ -''' -Test for lms courseware app -''' -import random - +""" +Test for LMS courseware app. +""" from django.test import TestCase from django.core.urlresolvers import reverse from django.test.utils import override_settings -import xmodule.modulestore.django from xmodule.error_module import ErrorDescriptor from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from helpers import LoginEnrollmentTestCase -from modulestore_config import TEST_DATA_DIR, \ - TEST_DATA_XML_MODULESTORE, \ +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_DIR, \ TEST_DATA_MONGO_MODULESTORE, \ - TEST_DATA_DRAFT_MONGO_MODULESTORE -import xmodule + TEST_DATA_DRAFT_MONGO_MODULESTORE, \ + TEST_DATA_MIXED_MODULESTORE class ActivateLoginTest(LoginEnrollmentTestCase): @@ -47,57 +43,60 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): Base class that adds a function to load all pages in a modulestore. """ - def check_random_page_loads(self, module_store): + def check_all_pages_load(self, course_id): """ - Choose a page in the course randomly, and assert that it loads. + Assert that all pages in the course load correctly. + `course_id` is the ID of the course to check. """ - # enroll in the course before trying to access pages - courses = module_store.get_courses() - self.assertEqual(len(courses), 1) - course = courses[0] + + store = modulestore() + + # Enroll in the course before trying to access pages + course = store.get_course(course_id) self.enroll(course, True) - course_id = course.id # Search for items in the course # None is treated as a wildcard course_loc = course.location - location_query = Location(course_loc.tag, course_loc.org, - course_loc.course, None, None, None) + location_query = Location( + course_loc.tag, course_loc.org, + course_loc.course, None, None, None + ) - items = module_store.get_items(location_query) + items = store.get_items( + location_query, + course_id=course_id, + depth=2 + ) if len(items) < 1: self.fail('Could not retrieve any items from course') - else: - descriptor = random.choice(items) - # We have ancillary course information now as modules - # and we can't simply use 'jump_to' to view them - if descriptor.location.category == 'about': - self._assert_loads('about_course', - {'course_id': course_id}, - descriptor) + # Try to load each item in the course + for descriptor in items: - elif descriptor.location.category == 'static_tab': - kwargs = {'course_id': course_id, - 'tab_slug': descriptor.location.name} - self._assert_loads('static_tab', kwargs, descriptor) + if descriptor.location.category == 'about': + self._assert_loads('about_course', + {'course_id': course_id}, + descriptor) - elif descriptor.location.category == 'course_info': - self._assert_loads('info', {'course_id': course_id}, - descriptor) + elif descriptor.location.category == 'static_tab': + kwargs = {'course_id': course_id, + 'tab_slug': descriptor.location.name} + self._assert_loads('static_tab', kwargs, descriptor) - elif descriptor.location.category == 'custom_tag_template': - pass + elif descriptor.location.category == 'course_info': + self._assert_loads('info', {'course_id': course_id}, + descriptor) - else: + else: - kwargs = {'course_id': course_id, - 'location': descriptor.location.url()} + kwargs = {'course_id': course_id, + 'location': descriptor.location.url()} - self._assert_loads('jump_to', kwargs, descriptor, - expect_redirect=True, - check_content=True) + self._assert_loads('jump_to', kwargs, descriptor, + expect_redirect=True, + check_content=True) def _assert_loads(self, django_url, kwargs, descriptor, expect_redirect=False, @@ -124,54 +123,51 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): self.assertNotIsInstance(descriptor, ErrorDescriptor) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class TestXmlCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase): """ Check that all pages in test courses load properly from XML. """ def setUp(self): - super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() + super(TestXmlCoursesLoad, self).setUp() self.setup_user() - xmodule.modulestore.django._MODULESTORES.clear() def test_toy_course_loads(self): - module_class = 'xmodule.hidden_module.HiddenDescriptor' - module_store = XMLModuleStore(TEST_DATA_DIR, - default_class=module_class, - course_dirs=['toy'], - load_error_modules=True) - self.check_random_page_loads(module_store) + # Load one of the XML based courses + # Our test mapping rules allow the MixedModuleStore + # to load this course from XML, not Mongo. + self.check_all_pages_load('edX/toy/2012_Fall') +# Importing XML courses isn't possible with MixedModuleStore, +# so we use a Mongo modulestore directly (as we would in Studio) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): +class TestMongoCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase): """ Check that all pages in test courses load properly from Mongo. """ def setUp(self): - super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() + super(TestMongoCoursesLoad, self).setUp() self.setup_user() - xmodule.modulestore.django._MODULESTORES.clear() - modulestore().collection.drop() + + # Import the toy course into a Mongo-backed modulestore + self.store = modulestore() + import_from_xml(self.store, TEST_DATA_DIR, ['toy']) def test_toy_course_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['toy']) - self.check_random_page_loads(module_store) + self.check_all_pages_load('edX/toy/2012_Fall') def test_toy_textbooks_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['toy']) - - course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None])) - + location = Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]) + course = self.store.get_item(location) self.assertGreater(len(course.textbooks), 0) + @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) -class TestDraftModuleStore(TestCase): +class TestDraftModuleStore(ModuleStoreTestCase, TestCase): def test_get_items_with_course_items(self): store = modulestore() diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 434d4d616b..e6ce3b2d25 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -10,14 +10,14 @@ from django.core.urlresolvers import reverse from django.core.management import call_command from util.testing import UrlResetMixin -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from nose.tools import assert_true, assert_equal from mock import patch log = logging.getLogger(__name__) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @patch('comment_client.utils.requests.request') class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase): diff --git a/lms/djangoapps/django_comment_client/forum/tests.py b/lms/djangoapps/django_comment_client/forum/tests.py index bd18ab80d6..2d889722a4 100644 --- a/lms/djangoapps/django_comment_client/forum/tests.py +++ b/lms/djangoapps/django_comment_client/forum/tests.py @@ -6,7 +6,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from django.core.urlresolvers import reverse from util.testing import UrlResetMixin -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from nose.tools import assert_true from mock import patch, Mock @@ -15,7 +15,7 @@ import logging log = logging.getLogger(__name__) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): @patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) diff --git a/lms/djangoapps/instructor/tests/test_access.py b/lms/djangoapps/instructor/tests/test_access.py index 688ed89dad..ee2e91f766 100644 --- a/lms/djangoapps/instructor/tests/test_access.py +++ b/lms/djangoapps/instructor/tests/test_access.py @@ -9,7 +9,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from django.test.utils import override_settings -from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.access import get_access_group_name from django_comment_common.models import (Role, @@ -20,7 +20,7 @@ from instructor.access import (allow_access, update_forum_role_membership) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAccessList(ModuleStoreTestCase): """ Test access listings. """ def setUp(self): @@ -42,7 +42,7 @@ class TestInstructorAccessList(ModuleStoreTestCase): self.assertEqual(set(beta_testers), set(self.beta_testers)) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAccessAllow(ModuleStoreTestCase): """ Test access allow. """ def setUp(self): @@ -85,7 +85,7 @@ class TestInstructorAccessAllow(ModuleStoreTestCase): group = Group.objects.get(name=get_access_group_name(self.course, 'staff')) self.assertIn(user, group.user_set.all()) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAccessRevoke(ModuleStoreTestCase): """ Test access revoke. """ def setUp(self): @@ -129,7 +129,7 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase): self.assertNotIn(user, group.user_set.all()) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAccessForum(ModuleStoreTestCase): """ Test forum access control. diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 155a8a2c9f..7d55b001d0 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -14,7 +14,7 @@ from django.core.urlresolvers import reverse from django.http import HttpRequest, HttpResponse from django.contrib.auth.models import User -from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from courseware.tests.helpers import LoginEnrollmentTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -90,7 +90,7 @@ class TestCommonExceptions400(unittest.TestCase): self.assertIn("Task is already running", result["error"]) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Ensure that users cannot access endpoints they shouldn't be able to. @@ -147,7 +147,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(response.status_code, 403) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test enrollment modification endpoint. @@ -270,7 +270,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(res_json, expected) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test endpoints whereby instructors can change permissions @@ -414,7 +414,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(res_json, expected) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test endpoints that show data without side effects. @@ -521,7 +521,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa self.assertEqual(response.status_code, 400) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test endpoints whereby instructors can change student grades. @@ -655,7 +655,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) self.assertTrue(act.called) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test instructor task list endpoint. @@ -745,7 +745,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(json.loads(response.content), expected_res) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/") @override_settings(ANALYTICS_API_KEY="robot_api_key") class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase): diff --git a/lms/djangoapps/instructor/tests/test_hint_manager.py b/lms/djangoapps/instructor/tests/test_hint_manager.py index 8f12572875..4513025aa5 100644 --- a/lms/djangoapps/instructor/tests/test_hint_manager.py +++ b/lms/djangoapps/instructor/tests/test_hint_manager.py @@ -5,14 +5,14 @@ from django.test.utils import override_settings from courseware.models import XModuleContentField from courseware.tests.factories import ContentFactory -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE import instructor.hint_manager as view from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class HintManagerTest(ModuleStoreTestCase): def setUp(self): diff --git a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py index b05746f015..b77626c8a1 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py @@ -17,12 +17,12 @@ from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name from courseware.tests.helpers import LoginEnrollmentTestCase -from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): ''' Check for download of csv @@ -31,7 +31,6 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts diff --git a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py index 1f5ea8ad56..4c1c252891 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py @@ -7,7 +7,7 @@ from django.test.utils import override_settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from courseware.tests.helpers import LoginEnrollmentTestCase -from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -18,7 +18,7 @@ from django.core import mail USER_COUNT = 4 -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check Enrollment/Unenrollment with/without auto-enrollment on activation and with/without email notification diff --git a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py index 90dadd569e..3b691aa708 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py @@ -15,7 +15,7 @@ from django_comment_client.utils import has_forum_access from courseware.access import _course_staff_group_name from courseware.tests.helpers import LoginEnrollmentTestCase -from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django @@ -32,7 +32,7 @@ def action_name(operation, rolename): return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): ''' Check for change in forum admin role memberships diff --git a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py index aaf03deb8c..fd285d2e3f 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py @@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from capa.tests.response_xml_factory import StringResponseXMLFactory from courseware.tests.factories import StudentModuleFactory from xmodule.modulestore import Location @@ -17,7 +17,7 @@ from xmodule.modulestore.django import modulestore USER_COUNT = 11 -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestGradebook(ModuleStoreTestCase): grading_policy = None diff --git a/lms/djangoapps/instructor/tests/test_legacy_xss.py b/lms/djangoapps/instructor/tests/test_legacy_xss.py index 7df6511b3c..784838fc4c 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_xss.py +++ b/lms/djangoapps/instructor/tests/test_legacy_xss.py @@ -7,14 +7,14 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from markupsafe import escape -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from student.tests.factories import UserFactory, CourseEnrollmentFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from instructor.views import legacy -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestXss(ModuleStoreTestCase): def setUp(self): self._request_factory = RequestFactory() diff --git a/lms/djangoapps/instructor_task/tests/test_base.py b/lms/djangoapps/instructor_task/tests/test_base.py index b67453e997..2c1fe02bd8 100644 --- a/lms/djangoapps/instructor_task/tests/test_base.py +++ b/lms/djangoapps/instructor_task/tests/test_base.py @@ -13,13 +13,13 @@ from django.contrib.auth.models import User from django.test.utils import override_settings from capa.tests.response_xml_factory import OptionResponseXMLFactory -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import editable_modulestore from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from student.tests.factories import CourseEnrollmentFactory, UserFactory from courseware.model_data import StudentModule -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MIXED_MODULESTORE from instructor_task.api_helper import encode_problem_and_student_input from instructor_task.models import PROGRESS, QUEUING @@ -95,7 +95,7 @@ class InstructorTaskTestCase(TestCase): return self._create_entry(task_state=task_state, task_output=progress, student=student) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): """ Base test class for InstructorTask-related tests that require @@ -106,7 +106,7 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase) def initialize_course(self): """Create a course in the store, with a chapter and section.""" - self.module_store = modulestore() + self.module_store = editable_modulestore() # Create the course self.course = CourseFactory.create(org=TEST_COURSE_ORG, diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py index 151a0faa9d..a853955c83 100644 --- a/lms/djangoapps/licenses/tests.py +++ b/lms/djangoapps/licenses/tests.py @@ -14,7 +14,7 @@ from django.core.management import call_command from django.core.urlresolvers import reverse from nose.tools import assert_true -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from licenses.models import CourseSoftware, UserLicense from student.tests.factories import UserFactory @@ -143,7 +143,7 @@ class LicenseTestCase(TestCase): self.assertEqual(302, response.status_code) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class CommandTest(ModuleStoreTestCase): '''Test management command for importing serial numbers''' def setUp(self): diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 262124d667..7ae5994dc1 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -27,14 +27,15 @@ log = logging.getLogger(__name__) from django.test.utils import override_settings from xmodule.tests import test_util_open_ended +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from courseware.tests import factories -from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code, check_for_post_code -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestStaffGradingService(LoginEnrollmentTestCase): +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): ''' Check that staff grading service proxy works. Basically just checking the access control and error handling logic -- all the actual work is on the @@ -42,8 +43,6 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ''' def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - self.student = 'view@test.com' self.instructor = 'view2@test.com' self.password = 'foo' @@ -138,8 +137,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase): self.assertIsNotNone(content['problem_list']) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestPeerGradingService(LoginEnrollmentTestCase): +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): ''' Check that staff grading service proxy works. Basically just checking the access control and error handling logic -- all the actual work is on the @@ -147,8 +146,6 @@ class TestPeerGradingService(LoginEnrollmentTestCase): ''' def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - self.student = 'view@test.com' self.instructor = 'view2@test.com' self.password = 'foo' @@ -293,8 +290,8 @@ class TestPeerGradingService(LoginEnrollmentTestCase): self.assertFalse('actual_score' in response) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestPanel(LoginEnrollmentTestCase): +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class TestPanel(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Run tests on the open ended panel """ diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py index deb13ffc9e..c18b3663e7 100644 --- a/lms/djangoapps/staticbook/tests.py +++ b/lms/djangoapps/staticbook/tests.py @@ -10,7 +10,7 @@ import requests from django.test.utils import override_settings from django.core.urlresolvers import reverse, NoReverseMatch -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from student.tests.factories import UserFactory, CourseEnrollmentFactory from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -36,7 +36,7 @@ HTML_BOOK = { ], } -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class StaticBookTest(ModuleStoreTestCase): """ Helpers for the static book tests. From 938b0946793a424df5bb3ce9ee2cf144cf32254f Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 10:32:54 -0400 Subject: [PATCH 095/179] disable the 'dont verify HTML modules' in test_export.py. This better traps the bug whereby XML-parseable HTML content was being dropped on export serialization --- common/lib/xmodule/xmodule/tests/test_export.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index d9b80422e9..5c5d8307af 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase): print("Checking module equality") for location in initial_import.modules[course_id].keys(): print("Checking", location) - if location.category == 'html': - print( - "Skipping html modules--they can't import in" - " final form without writing files..." - ) - continue self.assertEquals(initial_import.modules[course_id][location], second_import.modules[course_id][location]) From 42af561a171883fa722082ade4c457836bc469db Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 21 Aug 2013 11:23:00 -0400 Subject: [PATCH 096/179] pep8 and pylint for tests of nostatic import --- .../management/commands/import.py | 2 +- .../tests/test_import_nostatic.py | 37 +++++++++---------- .../xmodule/modulestore/xml_importer.py | 1 - .../courseware/tests/test_module_render.py | 11 ++---- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1d77e9cb54..520e36f4d2 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 4009fa230d..fc68975ebb 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -1,27 +1,21 @@ #pylint: disable=E1101 +''' +Tests for importing with no static +''' -import json -import shutil -import sys -import mock from django.test.client import Client from django.test.utils import override_settings from django.conf import settings -from django.core.urlresolvers import reverse from path import path import copy -from json import loads from django.contrib.auth.models import User -from auth.authz import add_user_to_creator_group - from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.modulestore import Location, mongo +from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.contentstore.django import contentstore, _CONTENTSTORE +from xmodule.contentstore.django import contentstore from xmodule.modulestore.xml_importer import import_from_xml from xmodule.contentstore.content import StaticContent @@ -36,11 +30,20 @@ TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex class MongoCollectionFindWrapper(object): + ''' + MongoCollectionFindWrapper for testing. + ''' def __init__(self, original): + """ + intit func + """ self.original = original self.counter = 0 def find(self, query, *args, **kwargs): + """ + find func + """ self.counter = self.counter + 1 return self.original(query, *args, **kwargs) @@ -76,7 +79,7 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): def load_test_import_course(self): ''' - Load the standard course used to test imports (for do_import_static=False behavior). + Load the standard course used to test imports (for do_import_static=False behavior). ''' content_store = contentstore() module_store = modulestore('direct') @@ -87,12 +90,11 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): return module_store, content_store, course, course_location - def test_static_import(self): ''' Stuff in static_import should always be imported into contentstore ''' - module_store, content_store, course, course_location = self.load_test_import_course() + _, content_store, course, course_location = self.load_test_import_course() # make sure we have ONE asset in our contentstore ("should_be_imported.html") all_assets = content_store.get_all_content_for_course(course_location) @@ -107,12 +109,11 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): pass self.assertIsNotNone(content) - + # make sure course.lms.static_asset_path is correct print "static_asset_path = {0}".format(course.lms.static_asset_path) self.assertEqual(course.lms.static_asset_path, 'test_import_course') - def test_asset_import_nostatic(self): ''' This test validates that an image asset is NOT imported when do_import_static=False @@ -123,14 +124,13 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True) course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') - course = module_store.get_item(course_location) + module_store.get_item(course_location) # make sure we have NO assets in our contentstore all_assets = content_store.get_all_content_for_course(course_location) print "len(all_assets)=%d" % len(all_assets) self.assertEqual(len(all_assets), 0) - def test_no_static_link_rewrites_on_import(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True) @@ -140,4 +140,3 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None])) self.assertIn('/static/', handouts.data) - diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 56c37fe04d..d20bf264aa 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -174,7 +174,6 @@ def import_from_xml(store, data_dir, course_dirs=None, import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, _namespace_rename, subpath=simport, verbose=verbose) - # finally loop through all the modules for module in xml_module_store.modules[course_id].itervalues(): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 85fa6d9be8..bf8d52da6f 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -83,7 +83,7 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): # See if the url got rewritten to the target link # note if the URL mapping changes then this assertion will break - self.assertIn('/courses/'+self.course_id+'/jump_to_id/vertical_test', html) + self.assertIn('/courses/' + self.course_id + '/jump_to_id/vertical_test', html) def test_modx_dispatch(self): self.assertRaises(Http404, render.modx_dispatch, 'dummy', 'dummy', @@ -139,7 +139,6 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): self.course_id ) - def test_xqueue_callback_success(self): """ Test for happy-path xqueue_callback @@ -356,10 +355,9 @@ class TestHtmlModifiers(ModuleStoreTestCase): result_fragment.content ) - def test_static_asset_path_use(self): ''' - when a course is loaded with do_import_static=False (see xml_importer.py), then + when a course is loaded with do_import_static=False (see xml_importer.py), then static_asset_path is set as an lms kv in course. That should make static paths not be mangled (ie not changed to c4x://). ''' @@ -374,7 +372,6 @@ class TestHtmlModifiers(ModuleStoreTestCase): result_fragment = module.runtime.render(module, None, 'student_view') self.assertIn('href="/static/toy_course_dir', result_fragment.content) - def test_course_image(self): url = course_image_url(self.course) self.assertTrue(url.startswith('/c4x/')) @@ -384,14 +381,12 @@ class TestHtmlModifiers(ModuleStoreTestCase): self.assertTrue(url.startswith('/static/toy_course_dir/')) self.course.lms.static_asset_path = "" - def test_get_course_info_section(self): self.course.lms.static_asset_path = "toy_course_dir" - handouts = get_course_info_section(self.request, self.course, "handouts") + get_course_info_section(self.request, self.course, "handouts") # TODO: check handouts output...right now test course seems to have no such content # at least this makes sure get_course_info_section returns without exception - def test_course_link_rewrite(self): module = render.get_module( self.user, From 21f4b058133934465e1d5d983a622eac23d67b25 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 14:46:38 -0400 Subject: [PATCH 097/179] fix some gaps which would allow the temporary xml attributes 'parent_sequential_url' and 'index_in_children_list' to get persisted in the database, whereas they are meant to be only scoped during export/import --- .../contentstore/tests/test_contentstore.py | 15 ++++++++++++++ .../xmodule/modulestore/xml_importer.py | 20 ++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..1cadcd69bf 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -945,8 +945,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 'vertical', 'vertical_test', None]), depth=1) self.assertTrue(getattr(vertical, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + if hasattr(vertical, 'data'): + self.assertNotIn('index_in_children_list', vertical.data) + self.assertNotIn('parent_sequential_url', vertical.xml_attributes) + if hasattr(vertical, 'data'): + self.assertNotIn('parent_sequential_url', vertical.data) + for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('index_in_children_list', child.data) + self.assertNotIn('parent_sequential_url', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('parent_sequential_url', child.data) + + # make sure that we don't have a sequential that is in draft mode sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy', diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 7bea0fdcac..109d759693 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -212,6 +212,15 @@ def import_module(module, store, course_data_path, static_content_store, # NOTE: It's important to use own_metadata here to avoid writing # inherited metadata everywhere. + + # remove any export/import only xml_attributes which are used to wire together draft imports + if 'parent_sequential_url' in module.xml_attributes: + del module.xml_attributes['parent_sequential_url'] + + if 'index_in_children_list' in module.xml_attributes: + del module.xml_attributes['index_in_children_list'] + module.save() + store.update_metadata(module.location, dict(own_metadata(module))) @@ -281,7 +290,7 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, # this is to make sure private only verticals show up in the list of children since # they would have been filtered out from the non-draft store export if module.location.category == 'vertical': - module.location = module.location._replace(revision=None) + non_draft_location = module.location._replace(revision=None) sequential_url = module.xml_attributes['parent_sequential_url'] index = int(module.xml_attributes['index_in_children_list']) @@ -291,15 +300,12 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, seq_location = seq_location._replace(org=target_location_namespace.org, course=target_location_namespace.course ) - sequential = store.get_item(seq_location) + sequential = store.get_item(seq_location, depth=0) - if module.location.url() not in sequential.children: - sequential.children.insert(index, module.location.url()) + if non_draft_location.url() not in sequential.children: + sequential.children.insert(index, non_draft_location.url()) store.update_children(sequential.location, sequential.children) - del module.xml_attributes['parent_sequential_url'] - del module.xml_attributes['index_in_children_list'] - import_module(module, draft_store, course_data_path, static_content_store, source_location_namespace, target_location_namespace, allow_not_found=True) for child in module.get_children(): From 81151ab67a0afc082a43a3faf6e997dc1bcb6717 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 8 Aug 2013 13:24:07 -0400 Subject: [PATCH 098/179] Prototype for running acceptance tests via Sauce Labs Connector Added in more desired capabilities and fixed the browser creation Sauce Labs is now updated if the test suite passes or fails --- common/djangoapps/terrain/browser.py | 52 +++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c2bf2bbbf3..5ae1ca007f 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -11,6 +11,7 @@ from logging import getLogger from django.core.management import call_command from django.conf import settings from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -41,13 +42,45 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 +# https://gist.github.com/santiycr/1644439 +import httplib +import base64 +try: + import json +except ImportError: + import simplejson as json + +config = {"username": "", +"access-key": ""} +desired_capabilities = DesiredCapabilities.CHROME +desired_capabilities['platform'] = "Linux" +desired_capabilities['version'] = "" +desired_capabilities['name'] = "Fail Test" +desired_capabilities['passed'] = True +desired_capabilities['video-upload-on-pass'] = False +desired_capabilities['record-screenshots'] = False +desired_capabilities['selenium-version'] = "2.33.0" +desired_capabilities['max-duration'] = 3600 +jobid='' + +base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] + +def set_job_status(jobid, passed=True): + body_content = json.dumps({"passed": passed}) + connection = httplib.HTTPConnection("saucelabs.com") + connection.request('PUT', '/rest/v1/%s/jobs/%s' % (config['username'], jobid), + body_content, + headers={"Authorization": "Basic %s" % base64string}) + result = connection.getresponse() + return result.status == 200 + @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ - browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + #browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. @@ -57,8 +90,15 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - world.browser = Browser(browser_driver) - + # world.browser = Browser(browser_driver) + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + **desired_capabilities + ) + world.browser.driver.implicitly_wait(30) + global jobid + jobid = world.browser.driver.session_id # Try to visit the main page # If the browser session is invalid, this will # raise a WebDriverException @@ -75,10 +115,10 @@ def initial_setup(server): # If we were unable to get a valid session within the limit of attempts, # then we cannot run the tests. if not success: - raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver)) + raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - world.browser.driver.set_window_size(1280, 1024) + # world.browser.driver.set_window_size(1280, 1024) @before.each_scenario @@ -128,4 +168,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ + if total.scenarios_ran != total.scenarios_passed: + set_job_status(jobid, False) world.browser.quit() From eb7fe7c92712f0457d8cd96da5b0d9e1443de347 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 8 Aug 2013 16:13:07 -0400 Subject: [PATCH 099/179] Added browser matrix (might not display yet) Added build information. Status image will now display properly --- README.md | 4 ++++ common/djangoapps/terrain/browser.py | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0261f87b46..9439d27799 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ + + Selenium Tests Status + + This is the main edX platform which consists of LMS and Studio. See [code.edx.org](http://code.edx.org/) for other parts of the edX code base. diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 5ae1ca007f..69c970fc15 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,17 +50,20 @@ try: except ImportError: import simplejson as json -config = {"username": "", -"access-key": ""} +config = {"username": "", +"access-key": ""} desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" -desired_capabilities['name'] = "Fail Test" +desired_capabilities['name'] = "LMS Lettuce Test" +desired_capabilities['build'] = "Alpha-Beta-123" desired_capabilities['passed'] = True +desired_capabilities['record-video'] = False desired_capabilities['video-upload-on-pass'] = False desired_capabilities['record-screenshots'] = False desired_capabilities['selenium-version'] = "2.33.0" desired_capabilities['max-duration'] = 3600 +desired_capabilities['public'] = 'public restricted' jobid='' base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] From 876651009e3fc98fd44b03923b04f6af9ea8fab9 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 9 Aug 2013 11:01:44 -0400 Subject: [PATCH 100/179] Logging out no longer uses cookies. Removed other cookie reference --- cms/djangoapps/contentstore/features/course-team.py | 3 ++- common/djangoapps/terrain/browser.py | 2 +- common/djangoapps/terrain/steps.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index db7b4d81f9..ab68050866 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,6 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step +from lettuce.django import django_url from common import create_studio_user from django.contrib.auth.models import Group from auth.authz import get_course_groupname_for_role, get_user_by_email @@ -91,7 +92,7 @@ def remove_course_team_admin(_step, outer_capture, name): @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): - world.browser.cookies.delete() + world.visit(django_url('logout')) world.visit('/') signin_css = 'a.action-signin' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 69c970fc15..680885fec4 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -83,7 +83,7 @@ def initial_setup(server): """ Launch the browser once before executing the tests. """ - #browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + # browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 9cf2aeda49..6e11ed19ea 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -99,7 +99,7 @@ def i_am_logged_in_user(step): @step('I am not logged in$') def i_am_not_logged_in(step): - world.browser.cookies.delete() + world.visit(django_url('logout')) @step('I am staff for course "([^"]*)"$') @@ -150,7 +150,7 @@ def i_am_logged_in(step): world.log_in(username='robot', password='test') world.browser.visit(django_url('/')) # You should not see the login link - assert_equals(world.browser.find_by_css('a#login'), []) + world.is_css_not_present('a#login') @step(u'I am an edX user$') From f4c19919cd0f8727f3d6c51c240a5cf13939f1a7 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:49:34 -0400 Subject: [PATCH 101/179] Beginnings of a feature flag Conflicts: common/djangoapps/terrain/browser.py Changed way feature flag was checked Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 2 ++ common/djangoapps/terrain/browser.py | 30 +++++++++++++++++----------- lms/envs/acceptance.py | 2 ++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 7debfe18d1..76a15daa65 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -81,6 +81,8 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +MITX_FEATURES['USE_SAUCE'] = False + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 680885fec4..7a1aff2637 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -20,6 +20,8 @@ from lms import one_time_startup # pylint: disable=W0611 from cms import one_time_startup # pylint: disable=W0611 from pymongo import MongoClient import xmodule.modulestore.django +import datetime +from pytz import UTC from xmodule.contentstore.django import _CONTENTSTORE # There is an import issue when using django-staticfiles with lettuce @@ -55,11 +57,11 @@ config = {"username": "", desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" -desired_capabilities['name'] = "LMS Lettuce Test" -desired_capabilities['build'] = "Alpha-Beta-123" +desired_capabilities['name'] = "CMS Lettuce Test" +desired_capabilities['build'] = datetime.datetime.now(UTC).isoformat(' ') desired_capabilities['passed'] = True -desired_capabilities['record-video'] = False desired_capabilities['video-upload-on-pass'] = False +desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False desired_capabilities['selenium-version'] = "2.33.0" desired_capabilities['max-duration'] = 3600 @@ -83,7 +85,7 @@ def initial_setup(server): """ Launch the browser once before executing the tests. """ - # browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. @@ -93,12 +95,15 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - # world.browser = Browser(browser_driver) - world.browser = Browser( - 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), - **desired_capabilities - ) + if settings.MITX_FEATURES.get('USE_SAUCE'): + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + **desired_capabilities + ) + else: + world.browser = Browser(browser_driver) + world.browser.driver.implicitly_wait(30) global jobid jobid = world.browser.driver.session_id @@ -121,7 +126,8 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - # world.browser.driver.set_window_size(1280, 1024) + if not settings.MITX_FEATURES.get('USE_SAUCE'): + world.browser.driver.set_window_size(1280, 1024) @before.each_scenario @@ -171,6 +177,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if total.scenarios_ran != total.scenarios_passed: + if settings.MITX_FEATURES.get('USE_SAUCE') and total.scenarios_ran != total.scenarios_passed: set_job_status(jobid, False) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 1e188d3b45..b86ec56aae 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -90,6 +90,8 @@ USE_I18N = True MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' +MITX_FEATURES['USE_SAUCE'] = False + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) From 28d9bbaff2d141412daaaa3bda5f191a46329810 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 14:13:11 -0400 Subject: [PATCH 102/179] Change build name generation Browser Matrix Data only applies to latest build information --- common/djangoapps/terrain/browser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 7a1aff2637..37fde93b55 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -20,8 +20,6 @@ from lms import one_time_startup # pylint: disable=W0611 from cms import one_time_startup # pylint: disable=W0611 from pymongo import MongoClient import xmodule.modulestore.django -import datetime -from pytz import UTC from xmodule.contentstore.django import _CONTENTSTORE # There is an import issue when using django-staticfiles with lettuce @@ -58,8 +56,7 @@ desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" desired_capabilities['name'] = "CMS Lettuce Test" -desired_capabilities['build'] = datetime.datetime.now(UTC).isoformat(' ') -desired_capabilities['passed'] = True +desired_capabilities['build'] = "Branch Test" desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False From 010fd5771d7114bd032c514ac053540cffb35084 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:50:28 -0400 Subject: [PATCH 103/179] Pass/Fail status updated properly Conflicts: common/djangoapps/terrain/browser.py --- common/djangoapps/terrain/browser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 37fde93b55..52ed8d1510 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -174,6 +174,9 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if settings.MITX_FEATURES.get('USE_SAUCE') and total.scenarios_ran != total.scenarios_passed: - set_job_status(jobid, False) + if settings.MITX_FEATURES.get('USE_SAUCE'): + if total.scenarios_ran != total.scenarios_passed: + set_job_status(jobid, False) + else: + set_job_status(jobid, True) world.browser.quit() From 1d7284b7cdd3beaed31309e864505d56a84c5671 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 15:13:30 -0400 Subject: [PATCH 104/179] Refactored out more for feature flags Refactored more for feature flags --- cms/envs/acceptance.py | 9 +++++++++ common/djangoapps/terrain/browser.py | 16 +++++++++------- lms/envs/acceptance.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 76a15daa65..0404d0005b 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -82,6 +83,14 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True MITX_FEATURES['USE_SAUCE'] = False +MITX_FEATURES['SAUCE_USERNAME'] = '' +MITX_FEATURES['SAUCE_ACCESS_ID'] = '' +MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME +MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' +MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' +MITX_FEATURES['SAUCE_TAGS'] = '' + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 52ed8d1510..22aefc90bd 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,13 +50,15 @@ try: except ImportError: import simplejson as json -config = {"username": "", -"access-key": ""} -desired_capabilities = DesiredCapabilities.CHROME -desired_capabilities['platform'] = "Linux" -desired_capabilities['version'] = "" -desired_capabilities['name'] = "CMS Lettuce Test" -desired_capabilities['build'] = "Branch Test" +config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), +"access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} + +desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) +desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') +desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') +desired_capabilities['name'] = "Lettuce Test" +desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') +desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index b86ec56aae..f30c22a486 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -9,6 +9,8 @@ so that we can run the lettuce acceptance tests. from .test import * +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly DEBUG = True @@ -91,6 +93,14 @@ MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' MITX_FEATURES['USE_SAUCE'] = False +MITX_FEATURES['SAUCE_USERNAME'] = '' +MITX_FEATURES['SAUCE_ACCESS_ID'] = '' +MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME +MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' +MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' +MITX_FEATURES['SAUCE_TAGS'] = '' + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) From 97df5aa997381c8996293f8a812f0d5fc924a3d0 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 16:20:09 -0400 Subject: [PATCH 105/179] Added feature flag for device type. Needed for android tablets --- cms/envs/acceptance.py | 1 + common/djangoapps/terrain/browser.py | 1 + lms/envs/acceptance.py | 1 + 3 files changed, 3 insertions(+) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 0404d0005b..7b91a7570e 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -88,6 +88,7 @@ MITX_FEATURES['SAUCE_ACCESS_ID'] = '' MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_DEVICE'] = '' MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' MITX_FEATURES['SAUCE_TAGS'] = '' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 22aefc90bd..204bd96b8a 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -56,6 +56,7 @@ config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') +desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') desired_capabilities['name'] = "Lettuce Test" desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index f30c22a486..17c769648a 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -98,6 +98,7 @@ MITX_FEATURES['SAUCE_ACCESS_ID'] = '' MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_DEVICE'] = '' MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' MITX_FEATURES['SAUCE_TAGS'] = '' From 3746c654d8e78577674ab56127b81cf53113bbd3 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 10:06:53 -0400 Subject: [PATCH 106/179] Added a tag for things that will not work on Firefox Added tags for tests that will not work on Sauce Changed build name Tightened up logic Conflicts: common/djangoapps/terrain/browser.py Added flag for session name --- .../contentstore/features/advanced-settings.feature | 8 ++++++++ cms/djangoapps/contentstore/features/checklists.feature | 2 ++ cms/djangoapps/contentstore/features/video-editor.feature | 4 ++++ cms/envs/acceptance.py | 3 ++- common/djangoapps/terrain/browser.py | 7 ++----- lms/djangoapps/courseware/features/login.feature | 1 + lms/djangoapps/courseware/features/signup.feature | 1 + lms/djangoapps/courseware/features/video.feature | 4 +++- lms/envs/acceptance.py | 3 ++- 9 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index a11a6cb869..2f0d396e63 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -2,6 +2,8 @@ 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 +#Sauce labs does not play nicely with CodeMirror + Scenario: A course author sees default advanced settings Given I have opened a new course in Studio When I select the Advanced Settings @@ -11,6 +13,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + @Sauce 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 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + @Sauce 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 +30,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + @Sauce 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 for "discussion_topics" @@ -33,6 +38,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + @Sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "display_name" @@ -41,6 +47,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window + @Sauce 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 @@ -48,6 +55,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string + @Sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index f13ce53fc2..72cff726f4 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -11,6 +11,7 @@ Feature: Course checklists And They are correctly selected after reloading the page # CHROME ONLY, due to issues getting link to be active in firefox + @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 @@ -19,6 +20,7 @@ Feature: Course checklists Then I am brought back to the course outline in the correct state # CHROME ONLY, due to issues getting link to be active in firefox + @Firefox Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index a53183e37c..d75f21e9c0 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -1,6 +1,8 @@ Feature: Video Component Editor As a course author, I want to be able to create video components. + #Sauce Labs cannot delete cookies + Scenario: User can view Video metadata Given I have created a Video component And I edit the component @@ -12,11 +14,13 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save + @Sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions + @Sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component And I have set "show captions" to True diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 7b91a7570e..c962ec5560 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -89,7 +89,8 @@ MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' +MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' +MITX_FEATURES['SAUCE_BUILD'] = 'CMS TESTS' MITX_FEATURES['SAUCE_TAGS'] = '' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 204bd96b8a..9aff0f9999 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -57,7 +57,7 @@ desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapab desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') -desired_capabilities['name'] = "Lettuce Test" +desired_capabilities['name'] = settings.MITX_FEATURES.get('SAUCE_SESSION', 'Lettuce Tests') desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') desired_capabilities['video-upload-on-pass'] = False @@ -178,8 +178,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if settings.MITX_FEATURES.get('USE_SAUCE'): - if total.scenarios_ran != total.scenarios_passed: - set_job_status(jobid, False) - else: - set_job_status(jobid, True) + set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 2b90c56f2d..28cba2e874 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -12,6 +12,7 @@ Feature: Login in as a registered user Then I should see the login error message "This account has not been activated" # CHROME ONLY, firefox will not redirect properly + @Firefox Scenario: Login to an activated account Given I am an edX user And I am an activated user diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index 19dfd74f1c..e723071fd5 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -4,6 +4,7 @@ Feature: Sign in I want to signup for a student account # CHROME ONLY, firefox will not redirect properly + @Firefox Scenario: Sign up from the homepage Given I visit the homepage When I click the link with the text "Register Now" diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 74cd9cbcbb..e68e8b1ada 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -11,6 +11,8 @@ Feature: Video component Given the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode + #Firefox doesn't have HTML5 + @Firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode - Then when I view the video it has autoplay enabled \ No newline at end of file + Then when I view the video it has autoplay enabled diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 17c769648a..e6cac76312 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -99,7 +99,8 @@ MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' +MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' +MITX_FEATURES['SAUCE_BUILD'] = 'LMS TESTS' MITX_FEATURES['SAUCE_TAGS'] = '' From 682d85c2f307e3f62b7e3cdb5ecb599a8a305cba Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:51:08 -0400 Subject: [PATCH 107/179] Changed feature to being one dictionary Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 24 ++++++++++++------------ common/djangoapps/terrain/browser.py | 21 +++++++++++---------- lms/envs/acceptance.py | 26 ++++++++++++-------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index c962ec5560..54407a8c2f 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -76,23 +76,23 @@ DATABASES = { # Use the auto_auth workflow for creating users and logging them in MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True - # HACK # Setting this flag to false causes imports to not load correctly in the lettuce python files # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -MITX_FEATURES['USE_SAUCE'] = False -MITX_FEATURES['SAUCE_USERNAME'] = '' -MITX_FEATURES['SAUCE_ACCESS_ID'] = '' -MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME -MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' -MITX_FEATURES['SAUCE_VERSION'] = '' -MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' -MITX_FEATURES['SAUCE_BUILD'] = 'CMS TESTS' -MITX_FEATURES['SAUCE_TAGS'] = '' - +MITX_FEATURES['SAUCE'] = { + 'USE' : False, + 'USERNAME' : '', + 'ACCESS_ID' : '', + 'BROWSER' : DesiredCapabilities.CHROME, + 'PLATFORM' : 'Linux', + 'VERSION' : '', + 'DEVICE' : '', + 'SESSION' : 'Lettuce Tests', + 'BUILD' : 'CMS TESTS', + 'TAGS' : '' +} # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 9aff0f9999..2757a2fa79 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -53,13 +53,14 @@ except ImportError: config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), "access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} -desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') -desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') -desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') -desired_capabilities['name'] = settings.MITX_FEATURES.get('SAUCE_SESSION', 'Lettuce Tests') -desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') -desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') +SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) +desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) +desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') +desired_capabilities['version'] = SAUCE.get('VERSION', '') +desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') +desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') +desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') +desired_capabilities['tags'] = SAUCE.get('TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -95,7 +96,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if settings.MITX_FEATURES.get('USE_SAUCE'): + if SAUCE.get('USE'): world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -126,7 +127,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not settings.MITX_FEATURES.get('USE_SAUCE'): + if not SAUCE.get('USE'): world.browser.driver.set_window_size(1280, 1024) @@ -177,6 +178,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if settings.MITX_FEATURES.get('USE_SAUCE'): + if SAUCE.get('USE'): set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e6cac76312..3689b0a18d 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -89,20 +89,18 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True -FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' - -MITX_FEATURES['USE_SAUCE'] = False -MITX_FEATURES['SAUCE_USERNAME'] = '' -MITX_FEATURES['SAUCE_ACCESS_ID'] = '' -MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME -MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' -MITX_FEATURES['SAUCE_VERSION'] = '' -MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' -MITX_FEATURES['SAUCE_BUILD'] = 'LMS TESTS' -MITX_FEATURES['SAUCE_TAGS'] = '' - +MITX_FEATURES['SAUCE'] = { + 'USE' : False, + 'USERNAME' : '', + 'ACCESS_ID' : '', + 'BROWSER' : DesiredCapabilities.CHROME, + 'PLATFORM' : 'Linux', + 'VERSION' : '', + 'DEVICE' : '', + 'SESSION' : 'Lettuce Tests', + 'BUILD' : 'CMS TESTS', + 'TAGS' : '' +} # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) From 2812b9cd5c5ad2834518f72a76b40ea63d542044 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 11:36:45 -0400 Subject: [PATCH 108/179] Fixed how config was obtained Added comment about the feature --- cms/envs/acceptance.py | 1 + common/djangoapps/terrain/browser.py | 6 ++++-- lms/envs/acceptance.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 54407a8c2f..94d6d7697a 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -81,6 +81,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +# Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { 'USE' : False, 'USERNAME' : '', diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 2757a2fa79..b92354e755 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,10 +50,12 @@ try: except ImportError: import simplejson as json -config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), -"access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) + +config = {"username": SAUCE.get('USERNAME'), +"access-key": SAUCE.get('ACCESS_ID')} + desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') desired_capabilities['version'] = SAUCE.get('VERSION', '') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 3689b0a18d..e9b7163d24 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -89,6 +89,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +# Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { 'USE' : False, 'USERNAME' : '', @@ -98,7 +99,7 @@ MITX_FEATURES['SAUCE'] = { 'VERSION' : '', 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'BUILD' : 'LMS TESTS', 'TAGS' : '' } From 3cf8083717419746de31a5960c52eb572cc0dc9b Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 11:53:26 -0400 Subject: [PATCH 109/179] Upgraded selenium version for sauce --- common/djangoapps/terrain/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index b92354e755..9b4885e160 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -66,7 +66,7 @@ desired_capabilities['tags'] = SAUCE.get('TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False -desired_capabilities['selenium-version'] = "2.33.0" +desired_capabilities['selenium-version'] = "2.34.0" desired_capabilities['max-duration'] = 3600 desired_capabilities['public'] = 'public restricted' jobid='' From 9fb0529036058a2ef1370cdf8b6e82e5e928d4bc Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 14:03:31 -0400 Subject: [PATCH 110/179] Sauce can only connect on certain ports --- cms/envs/acceptance.py | 11 ++++++++--- common/djangoapps/terrain/browser.py | 7 ++++--- lms/envs/acceptance.py | 16 ++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 94d6d7697a..24a5a02533 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -18,7 +18,12 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -import random +from random import choice +PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] def seed(): @@ -92,11 +97,11 @@ MITX_FEATURES['SAUCE'] = { 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', 'BUILD' : 'CMS TESTS', - 'TAGS' : '' + 'CUSTOM_TAGS' : {} } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) -LETTUCE_SERVER_PORT = random.randint(1024, 65535) +LETTUCE_SERVER_PORT = choice(PORTS) LETTUCE_BROWSER = 'chrome' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 9b4885e160..62e9cde9c0 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -62,7 +62,7 @@ desired_capabilities['version'] = SAUCE.get('VERSION', '') desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') -desired_capabilities['tags'] = SAUCE.get('TAGS', '') +desired_capabilities['custom-data'] = SAUCE.get('CUSTOM_TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -104,12 +104,13 @@ def initial_setup(server): url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), **desired_capabilities ) + global jobid + jobid = world.browser.driver.session_id else: world.browser = Browser(browser_driver) world.browser.driver.implicitly_wait(30) - global jobid - jobid = world.browser.driver.session_id + # Try to visit the main page # If the browser session is invalid, this will # raise a WebDriverException diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e9b7163d24..f908c31bdc 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -19,8 +19,12 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -import random - +from random import choice +PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] def seed(): return os.getppid() @@ -67,7 +71,7 @@ DATABASES = { # Set up XQueue information so that the lms will send # requests to a mock XQueue server running locally -XQUEUE_PORT = random.randint(1024, 65535) +XQUEUE_PORT = choice(PORTS) XQUEUE_INTERFACE = { "url": "http://127.0.0.1:%d" % XQUEUE_PORT, "django_auth": { @@ -99,12 +103,12 @@ MITX_FEATURES['SAUCE'] = { 'VERSION' : '', 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'LMS TESTS', - 'TAGS' : '' + 'BUILD' : 'CMS TESTS', + 'CUSTOM_TAGS' : {} } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) -LETTUCE_SERVER_PORT = random.randint(1024, 65535) +LETTUCE_SERVER_PORT = choice(PORTS) LETTUCE_BROWSER = 'chrome' From ee23b9d16180b1b56564b67c592563f7486c1e41 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:51:24 -0400 Subject: [PATCH 111/179] Changed method of obtaining feature flags to environment variables Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 28 ++++++++++++++++++--------- common/djangoapps/terrain/browser.py | 6 +++--- lms/envs/acceptance.py | 29 +++++++++++++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 24a5a02533..f503bbd043 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -25,6 +25,16 @@ PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] +DESIRED_CAPABILITIES={ + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} def seed(): return os.getppid() @@ -88,15 +98,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'USE' : False, - 'USERNAME' : '', - 'ACCESS_ID' : '', - 'BROWSER' : DesiredCapabilities.CHROME, - 'PLATFORM' : 'Linux', - 'VERSION' : '', - 'DEVICE' : '', - 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'ENABLED' : os.environ.get('ENABLED'), + 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), + 'VERSION' : os.environ.get('SAUCE_VERSION'), + 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'SESSION' : 'Jenkins Acceptance Tests', + 'BUILD' : os.environ.get('JOB_NAME'), 'CUSTOM_TAGS' : {} } diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 62e9cde9c0..223418972d 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -98,7 +98,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if SAUCE.get('USE'): + if SAUCE.get('ENABLED'): world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -130,7 +130,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not SAUCE.get('USE'): + if not SAUCE.get('ENABLED'): world.browser.driver.set_window_size(1280, 1024) @@ -181,6 +181,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if SAUCE.get('USE'): + if SAUCE.get('ENABLED'): set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index f908c31bdc..951ddf8418 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -26,6 +26,17 @@ PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] +DESIRED_CAPABILITIES={ + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} + def seed(): return os.getppid() @@ -95,15 +106,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'USE' : False, - 'USERNAME' : '', - 'ACCESS_ID' : '', - 'BROWSER' : DesiredCapabilities.CHROME, - 'PLATFORM' : 'Linux', - 'VERSION' : '', - 'DEVICE' : '', - 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'ENABLED' : os.environ.get('ENABLED'), + 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), + 'VERSION' : os.environ.get('SAUCE_VERSION'), + 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'SESSION' : 'Jenkins Acceptance Tests', + 'BUILD' : os.environ.get('JOB_NAME'), 'CUSTOM_TAGS' : {} } From d422cb9206c007bd012324fcd6e18f23a53cad6c Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 14:47:30 -0400 Subject: [PATCH 112/179] Fixed desired_capabilities issues Also, now lettuce browser can be specified by jenkins --- cms/envs/acceptance.py | 4 ++-- lms/envs/acceptance.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index f503bbd043..37107c7f70 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -101,7 +101,7 @@ MITX_FEATURES['SAUCE'] = { 'ENABLED' : os.environ.get('ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), 'VERSION' : os.environ.get('SAUCE_VERSION'), 'DEVICE' : os.environ.get('SAUCE_DEVICE'), @@ -114,4 +114,4 @@ MITX_FEATURES['SAUCE'] = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) LETTUCE_SERVER_PORT = choice(PORTS) -LETTUCE_BROWSER = 'chrome' +LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 951ddf8418..6de7861d71 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -109,7 +109,7 @@ MITX_FEATURES['SAUCE'] = { 'ENABLED' : os.environ.get('ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), 'VERSION' : os.environ.get('SAUCE_VERSION'), 'DEVICE' : os.environ.get('SAUCE_DEVICE'), @@ -122,4 +122,4 @@ MITX_FEATURES['SAUCE'] = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) LETTUCE_SERVER_PORT = choice(PORTS) -LETTUCE_BROWSER = 'chrome' +LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') From 027cbfd09a85001bc20023942c86e8458c72085a Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 15:15:34 -0400 Subject: [PATCH 113/179] Added logic to test_acceptance.sh so that the proper tests will be skipped for each browser This logic also covers if a lettuce_browser is specified Removing browser matrix from readme --- README.md | 4 ---- cms/envs/acceptance.py | 2 +- jenkins/test_acceptance.sh | 12 ++++++++++-- lms/envs/acceptance.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9439d27799..0261f87b46 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ - - Selenium Tests Status - - This is the main edX platform which consists of LMS and Studio. See [code.edx.org](http://code.edx.org/) for other parts of the edX code base. diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 37107c7f70..335a027c7c 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -19,7 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, diff --git a/jenkins/test_acceptance.sh b/jenkins/test_acceptance.sh index b7a244fe99..1bc88744bf 100755 --- a/jenkins/test_acceptance.sh +++ b/jenkins/test_acceptance.sh @@ -30,10 +30,18 @@ TESTS_FAILED=0 # /usr/bin/Xvfb :1 -screen 0 1024x268x24 # This allows us to run Chrome without a display export DISPLAY=:1 +SKIP_TESTS="" + +if [ ! -z ${LETTUCE_BROWSER+x} ]; then + SKIP_TESTS="--tag -$(tr '[:lower:]' '[:upper:]' <<< ${LETTUCE_BROWSER:0:1})${LETTUCE_BROWSER:1}" +fi +if [ ! -z ${SAUCE_ENABLED+x} ]; then + SKIP_TESTS="--tag -Sauce --tag -$(tr '[:lower:]' '[:upper:]' <<< ${SAUCE_BROWSER:0:1})${SAUCE_BROWSER:1}" +fi # Run the lms and cms acceptance tests # (the -v flag turns off color in the output) -rake test_acceptance_lms["-v 3"] || TESTS_FAILED=1 -rake test_acceptance_cms["-v 3"] || TESTS_FAILED=1 +rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1 +rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1 [ $TESTS_FAILED == '0' ] diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 6de7861d71..60c5d04997 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -20,7 +20,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, From 58bb6e1ea37bdddb3ed5e21cd9dd966864ec5b04 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 15:46:12 -0400 Subject: [PATCH 114/179] Changed tags to be lowercase to prevent preprocessing Changed to have a default for device since it isn't always needed Tags are more clear Fixed stylistic issues --- .../features/advanced-settings.feature | 12 +++++------ .../contentstore/features/checklists.feature | 4 ++-- .../features/video-editor.feature | 4 ++-- cms/envs/acceptance.py | 17 ++++++++-------- common/djangoapps/terrain/browser.py | 20 +++++++++---------- jenkins/test_acceptance.sh | 4 ++-- .../courseware/features/login.feature | 2 +- .../courseware/features/signup.feature | 2 +- .../courseware/features/video.feature | 2 +- lms/envs/acceptance.py | 17 ++++++++-------- 10 files changed, 41 insertions(+), 43 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 2f0d396e63..767dafb796 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -13,7 +13,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized - @Sauce + @skip_sauce 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 @@ -22,7 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged - @Sauce + @skip_sauce 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 @@ -30,7 +30,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - @Sauce + @skip_sauce 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 for "discussion_topics" @@ -38,7 +38,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted - @Sauce + @skip_sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "display_name" @@ -47,7 +47,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window - @Sauce + @skip_sauce 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 @@ -55,7 +55,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string - @Sauce + @skip_sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 72cff726f4..28a38b307e 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -11,7 +11,7 @@ Feature: Course checklists And They are correctly selected after reloading the page # CHROME ONLY, due to issues getting link to be active in firefox - @Firefox + @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 @@ -20,7 +20,7 @@ Feature: Course checklists Then I am brought back to the course outline in the correct state # CHROME ONLY, due to issues getting link to be active in firefox - @Firefox + @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 diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index d75f21e9c0..6f5fbd48b9 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -14,13 +14,13 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save - @Sauce + @skip_sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions - @Sauce + @skip_sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component And I have set "show captions" to True diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 335a027c7c..f51a697f36 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -22,10 +22,10 @@ from random import choice PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES={ +DESIRED_CAPABILITIES = { 'chrome': DesiredCapabilities.CHROME, 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, 'firefox': DesiredCapabilities.FIREFOX, @@ -98,16 +98,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'ENABLED' : os.environ.get('ENABLED'), + 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), - 'VERSION' : os.environ.get('SAUCE_VERSION'), - 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), + 'VERSION' : os.environ.get('SAUCE_VERSION', ''), + 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME'), - 'CUSTOM_TAGS' : {} + 'BUILD' : os.environ.get('JOB_NAME', 'CMS TESTS'), } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 223418972d..15c822e159 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -56,13 +56,13 @@ SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) config = {"username": SAUCE.get('USERNAME'), "access-key": SAUCE.get('ACCESS_ID')} +world.absorb(SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') -desired_capabilities['version'] = SAUCE.get('VERSION', '') -desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') -desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') -desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') -desired_capabilities['custom-data'] = SAUCE.get('CUSTOM_TAGS', '') +desired_capabilities['platform'] = SAUCE.get('PLATFORM') +desired_capabilities['version'] = SAUCE.get('VERSION') +desired_capabilities['device-type'] = SAUCE.get('DEVICE') +desired_capabilities['name'] = SAUCE.get('SESSION') +desired_capabilities['build'] = SAUCE.get('BUILD') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -71,7 +71,7 @@ desired_capabilities['max-duration'] = 3600 desired_capabilities['public'] = 'public restricted' jobid='' -base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] +base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) @@ -98,7 +98,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if SAUCE.get('ENABLED'): + if world.SAUCE_ENABLED: world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -130,7 +130,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not SAUCE.get('ENABLED'): + if not world.SAUCE_ENABLED: world.browser.driver.set_window_size(1280, 1024) @@ -181,6 +181,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if SAUCE.get('ENABLED'): + if world.SAUCE_ENABLED: set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/jenkins/test_acceptance.sh b/jenkins/test_acceptance.sh index 1bc88744bf..aaa7cfc3da 100755 --- a/jenkins/test_acceptance.sh +++ b/jenkins/test_acceptance.sh @@ -33,10 +33,10 @@ export DISPLAY=:1 SKIP_TESTS="" if [ ! -z ${LETTUCE_BROWSER+x} ]; then - SKIP_TESTS="--tag -$(tr '[:lower:]' '[:upper:]' <<< ${LETTUCE_BROWSER:0:1})${LETTUCE_BROWSER:1}" + SKIP_TESTS="--tag -skip_$LETTUCE_BROWSER" fi if [ ! -z ${SAUCE_ENABLED+x} ]; then - SKIP_TESTS="--tag -Sauce --tag -$(tr '[:lower:]' '[:upper:]' <<< ${SAUCE_BROWSER:0:1})${SAUCE_BROWSER:1}" + SKIP_TESTS="--tag -skip_sauce --tag -skip_$SAUCE_BROWSER" fi # Run the lms and cms acceptance tests diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 28cba2e874..5c777fd64f 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -12,7 +12,7 @@ Feature: Login in as a registered user Then I should see the login error message "This account has not been activated" # CHROME ONLY, firefox will not redirect properly - @Firefox + @skip_firefox Scenario: Login to an activated account Given I am an edX user And I am an activated user diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index e723071fd5..c1fce04b54 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -4,7 +4,7 @@ Feature: Sign in I want to signup for a student account # CHROME ONLY, firefox will not redirect properly - @Firefox + @skip_firefox Scenario: Sign up from the homepage Given I visit the homepage When I click the link with the text "Register Now" diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index e68e8b1ada..260887290e 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -12,7 +12,7 @@ Feature: Video component Then when I view the video it has rendered in Youtube mode #Firefox doesn't have HTML5 - @Firefox + @skip_firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode Then when I view the video it has autoplay enabled diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 60c5d04997..e4928301d7 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -23,10 +23,10 @@ from random import choice PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES={ +DESIRED_CAPABILITIES = { 'chrome': DesiredCapabilities.CHROME, 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, 'firefox': DesiredCapabilities.FIREFOX, @@ -106,16 +106,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'ENABLED' : os.environ.get('ENABLED'), + 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), - 'VERSION' : os.environ.get('SAUCE_VERSION'), - 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), + 'VERSION' : os.environ.get('SAUCE_VERSION', ''), + 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME'), - 'CUSTOM_TAGS' : {} + 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command From 0b6e62984e3992f349c3eec7463b619522b28556 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 10:58:18 -0400 Subject: [PATCH 115/179] Using requests instead of a HTTPConnect --- cms/envs/acceptance.py | 2 +- common/djangoapps/terrain/browser.py | 12 +++++------- lms/envs/acceptance.py | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index f51a697f36..493e2e1028 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -101,7 +101,7 @@ MITX_FEATURES['SAUCE'] = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), 'VERSION' : os.environ.get('SAUCE_VERSION', ''), 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 15c822e159..d351f7433a 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -43,7 +43,7 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 # https://gist.github.com/santiycr/1644439 -import httplib +import requests import base64 try: import json @@ -75,12 +75,10 @@ base64string = base64.encodestring('{}:{}'.format(config['username'], config['ac def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) - connection = httplib.HTTPConnection("saucelabs.com") - connection.request('PUT', '/rest/v1/%s/jobs/%s' % (config['username'], jobid), - body_content, - headers={"Authorization": "Basic %s" % base64string}) - result = connection.getresponse() - return result.status == 200 + result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], jobid), + data=body_content, + headers={"Authorization": "Basic {}".format(base64string)}) + return result.status_code == 200 @before.harvest diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e4928301d7..618d52a995 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -109,7 +109,7 @@ MITX_FEATURES['SAUCE'] = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), 'VERSION' : os.environ.get('SAUCE_VERSION', ''), 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), @@ -117,6 +117,7 @@ MITX_FEATURES['SAUCE'] = { 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), } + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) From 35e5f4cab5a00384acad61fa86751f59bf65b626 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 11:39:27 -0400 Subject: [PATCH 116/179] Moved global things in browser.py to functions --- cms/envs/acceptance.py | 2 +- common/djangoapps/terrain/browser.py | 53 ++++++++++++++-------------- lms/envs/acceptance.py | 2 +- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 493e2e1028..8b208123cc 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -97,7 +97,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True # Information needed to utilize Sauce Labs. -MITX_FEATURES['SAUCE'] = { +SAUCE = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index d351f7433a..0068e587b0 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -51,41 +51,40 @@ except ImportError: import simplejson as json -SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) - -config = {"username": SAUCE.get('USERNAME'), -"access-key": SAUCE.get('ACCESS_ID')} - -world.absorb(SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') -desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = SAUCE.get('PLATFORM') -desired_capabilities['version'] = SAUCE.get('VERSION') -desired_capabilities['device-type'] = SAUCE.get('DEVICE') -desired_capabilities['name'] = SAUCE.get('SESSION') -desired_capabilities['build'] = SAUCE.get('BUILD') -desired_capabilities['video-upload-on-pass'] = False -desired_capabilities['sauce-advisor'] = False -desired_capabilities['record-screenshots'] = False -desired_capabilities['selenium-version'] = "2.34.0" -desired_capabilities['max-duration'] = 3600 -desired_capabilities['public'] = 'public restricted' -jobid='' - -base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) - result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], jobid), + config = get_username_and_key() + base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] + result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 +def make_desired_capabilities(): + desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) + desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') + desired_capabilities['version'] = settings.SAUCE.get('VERSION') + desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE') + desired_capabilities['name'] = settings.SAUCE.get('SESSION') + desired_capabilities['build'] = settings.SAUCE.get('BUILD') + desired_capabilities['video-upload-on-pass'] = False + desired_capabilities['sauce-advisor'] = False + desired_capabilities['record-screenshots'] = False + desired_capabilities['selenium-version'] = "2.34.0" + desired_capabilities['max-duration'] = 3600 + desired_capabilities['public'] = 'public restricted' + return desired_capabilities + +def get_username_and_key(): + return {"username": settings.SAUCE.get('USERNAME'),"access-key": settings.SAUCE.get('ACCESS_ID')} + @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ + world.absorb(settings.SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu @@ -97,13 +96,13 @@ def initial_setup(server): # Get a browser session if world.SAUCE_ENABLED: + config = get_username_and_key() world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), - **desired_capabilities + **make_desired_capabilities() ) - global jobid - jobid = world.browser.driver.session_id + world.absorb(world.browser.driver.session_id, 'jobid') else: world.browser = Browser(browser_driver) @@ -180,5 +179,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if world.SAUCE_ENABLED: - set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) + set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 618d52a995..80e8fd980e 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -105,7 +105,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True # Information needed to utilize Sauce Labs. -MITX_FEATURES['SAUCE'] = { +SAUCE = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), From 22b3f2b161041315a7ec925343b7eb1d8ecd53a4 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 13:04:28 -0400 Subject: [PATCH 117/179] Factored out Sauce related things to one file LMS was arbitrarily chosen for now. Fixed up pylint and pep8 errors Fixed up pylint and pep8 errors Changed naming to be better Changed Sauce Info to obtaining a JSON string --- cms/envs/acceptance.py | 30 +----------------- common/djangoapps/terrain/browser.py | 11 ++++--- lms/envs/acceptance.py | 32 +------------------ lms/envs/sauce.py | 46 ++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 65 deletions(-) create mode 100644 lms/envs/sauce.py diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 8b208123cc..708583719e 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -8,7 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from lms.envs.sauce import * # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -19,22 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, - 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES = { - 'chrome': DesiredCapabilities.CHROME, - 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, - 'firefox': DesiredCapabilities.FIREFOX, - 'opera': DesiredCapabilities.OPERA, - 'iphone': DesiredCapabilities.IPHONE, - 'ipad': DesiredCapabilities.IPAD, - 'safari': DesiredCapabilities.SAFARI, - 'android': DesiredCapabilities.ANDROID -} def seed(): return os.getppid() @@ -96,19 +81,6 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -# Information needed to utilize Sauce Labs. -SAUCE = { - 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), - 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), - 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), - 'VERSION' : os.environ.get('SAUCE_VERSION', ''), - 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), - 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME', 'CMS TESTS'), -} - # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0068e587b0..80c47433b7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -55,11 +55,12 @@ def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) config = get_username_and_key() base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), + result = requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 + def make_desired_capabilities(): desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') @@ -75,8 +76,9 @@ def make_desired_capabilities(): desired_capabilities['public'] = 'public restricted' return desired_capabilities + def get_username_and_key(): - return {"username": settings.SAUCE.get('USERNAME'),"access-key": settings.SAUCE.get('ACCESS_ID')} + return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} @before.harvest @@ -99,7 +101,7 @@ def initial_setup(server): config = get_username_and_key() world.browser = Browser( 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), **make_desired_capabilities() ) world.absorb(world.browser.driver.session_id, 'jobid') @@ -147,7 +149,6 @@ def clear_data(scenario): world.spew('scenario_dict') - @after.each_scenario def reset_databases(scenario): ''' @@ -179,5 +180,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if world.SAUCE_ENABLED: - set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) + set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 80e8fd980e..969f461640 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -8,8 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * - -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from .sauce import * # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -20,22 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, - 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES = { - 'chrome': DesiredCapabilities.CHROME, - 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, - 'firefox': DesiredCapabilities.FIREFOX, - 'opera': DesiredCapabilities.OPERA, - 'iphone': DesiredCapabilities.IPHONE, - 'ipad': DesiredCapabilities.IPAD, - 'safari': DesiredCapabilities.SAFARI, - 'android': DesiredCapabilities.ANDROID -} def seed(): return os.getppid() @@ -104,20 +88,6 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -# Information needed to utilize Sauce Labs. -SAUCE = { - 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), - 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), - 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), - 'VERSION' : os.environ.get('SAUCE_VERSION', ''), - 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), - 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), -} - - # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py new file mode 100644 index 0000000000..2692037082 --- /dev/null +++ b/lms/envs/sauce.py @@ -0,0 +1,46 @@ +""" +This config file extends the test environment configuration +so that we can run the lettuce acceptance tests on SauceLabs. +""" + +# We intentionally define lots of variables that aren't used, and +# want to import all variables from base settings files +# pylint: disable=W0401, W0614 + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +import os +import json + +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] + +DESIRED_CAPABILITIES = { + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} + +DEFAULT_CONFIG='{"PLATFORM":"Linux", "BROWSER":"chrome", "VERISON":"", "DEVICE":""}' + +SAUCE_INFO = json.loads(os.environ.get('SAUCE_INFO', DEFAULT_CONFIG)) + +# Information needed to utilize Sauce Labs. +SAUCE = { + 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), + 'USERNAME': os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO.get('BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO.get('PLATFORM', 'Linux'), + 'VERSION': SAUCE_INFO.get('VERSION', ''), + 'DEVICE': SAUCE_INFO.get('DEVICE', ''), + 'SESSION': 'Jenkins Acceptance Tests', + 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), +} From 4c3dcda7e8c794f24b6905c534c8e0445d0d7928 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 16 Aug 2013 15:25:31 -0400 Subject: [PATCH 118/179] Hack to get around many types of string encodings Changed the port listing Changing to better readability --- lms/envs/sauce.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index 2692037082..1704edd68b 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -9,12 +9,11 @@ so that we can run the lettuce acceptance tests on SauceLabs. from selenium.webdriver.common.desired_capabilities import DesiredCapabilities import os -import json PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, + 5050, 5555, 5432, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9080, 9090, 9876, 9999, 49221, 55001] DESIRED_CAPABILITIES = { @@ -28,19 +27,25 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } -DEFAULT_CONFIG='{"PLATFORM":"Linux", "BROWSER":"chrome", "VERISON":"", "DEVICE":""}' -SAUCE_INFO = json.loads(os.environ.get('SAUCE_INFO', DEFAULT_CONFIG)) +#HACK +#This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON +#This is the simplest way to adhere to all of these requirements and still be readible +DEFAULT_CONFIG = 'Linux-chrome--' + +SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') +if len(SAUCE_INFO) !=4: + SAUCE_INFO = DEFAULT_CONFIG.split('-') # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO.get('BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM': SAUCE_INFO.get('PLATFORM', 'Linux'), - 'VERSION': SAUCE_INFO.get('VERSION', ''), - 'DEVICE': SAUCE_INFO.get('DEVICE', ''), + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[0].lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO[0], + 'VERSION': SAUCE_INFO[2], + 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), } From bb7cbf4d6263c30de056758c6e39d0cc532a3179 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 19 Aug 2013 09:01:40 -0400 Subject: [PATCH 119/179] If sauce is not enabled, allow full range of ports Forgot an import --- cms/envs/acceptance.py | 4 ++-- lms/envs/acceptance.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 708583719e..3b89e2e988 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -18,7 +18,7 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -from random import choice +from random import choice, randint def seed(): @@ -84,5 +84,5 @@ USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) -LETTUCE_SERVER_PORT = choice(PORTS) +LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 969f461640..232e8b86b4 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -18,7 +18,7 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -from random import choice +from random import choice, randint def seed(): @@ -66,7 +66,7 @@ DATABASES = { # Set up XQueue information so that the lms will send # requests to a mock XQueue server running locally -XQUEUE_PORT = choice(PORTS) +XQUEUE_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) XQUEUE_INTERFACE = { "url": "http://127.0.0.1:%d" % XQUEUE_PORT, "django_auth": { @@ -91,5 +91,5 @@ USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) -LETTUCE_SERVER_PORT = choice(PORTS) +LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') From 21f75ff250a7a4a687d0bb0bd47d6ee71cb2cfc6 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 19 Aug 2013 09:08:02 -0400 Subject: [PATCH 120/179] Various stylistic and pylint fixes / changes Version numbers have very different ranges for different browsers so not having a dictionary of those. Fixed a whitespace issue Fixed pylint/pep8 violations Don't need django_url Spacing issues Changed how commenting works Forgot one Used wrong name Changed around importing Remove django_url Fixed function orderingn Made logic nicer for getting a new browser Modifying tests to run in opera Needed to increase time to account for slow sauce loading Now safari LMS works Forgot an assert statement Skipping a few tests for opera --- .../features/advanced-settings.feature | 7 +- .../contentstore/features/checklists.feature | 6 +- .../contentstore/features/course-team.py | 3 +- .../contentstore/features/grading.py | 9 +- .../features/video-editor.feature | 4 +- common/djangoapps/terrain/browser.py | 110 +++++++++--------- common/djangoapps/terrain/steps.py | 11 +- .../courseware/features/login.feature | 2 +- .../courseware/features/navigation.feature | 2 + .../courseware/features/signup.feature | 2 +- .../courseware/features/video.feature | 2 +- lms/envs/sauce.py | 13 ++- 12 files changed, 93 insertions(+), 78 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 767dafb796..b2941ac7a5 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -2,7 +2,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 -#Sauce labs does not play nicely with CodeMirror Scenario: A course author sees default advanced settings Given I have opened a new course in Studio @@ -13,6 +12,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio @@ -22,6 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio @@ -30,6 +31,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio @@ -38,6 +40,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio @@ -47,6 +50,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio @@ -55,6 +59,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 28a38b307e..1649cd0749 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -10,8 +10,9 @@ Feature: Course checklists Then I can check and uncheck tasks in a checklist And They are correctly selected after reloading the page - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome @skip_firefox + @skip_opera Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -19,8 +20,9 @@ Feature: Course checklists And I press the browser back button Then I am brought back to the course outline in the correct state - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome @skip_firefox + @skip_opera Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index ab68050866..8b31d325e5 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,7 +2,6 @@ #pylint: disable=W0621 from lettuce import world, step -from lettuce.django import django_url from common import create_studio_user from django.contrib.auth.models import Group from auth.authz import get_course_groupname_for_role, get_user_by_email @@ -92,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name): @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): - world.visit(django_url('logout')) + world.visit('logout') world.visit('/') signin_css = 'a.action-signin' diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 719b3f7f7c..93e44b3893 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -112,10 +112,10 @@ def changes_not_persisted(step): @step(u'I see the assignment type "(.*)"$') def i_see_the_assignment_type(_step, name): - assignment_css = '#course-grading-assignment-name' - assignments = world.css_find(assignment_css) - types = [ele['value'] for ele in assignments] - assert name in types + assignment_css = '#course-grading-assignment-name' + assignments = world.css_find(assignment_css) + types = [ele['value'] for ele in assignments] + assert name in types @step(u'I change the highest grade range to "(.*)"$') @@ -144,6 +144,7 @@ def cannot_edit_fail(_step): pass # We should get this exception on failing to edit the element + @step(u'I change the grace period to "(.*)"$') def i_change_grace_period(_step, grace_period): grace_period_css = '#course-grading-graceperiod' diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index 6f5fbd48b9..7117926c60 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -1,8 +1,6 @@ Feature: Video Component Editor As a course author, I want to be able to create video components. - #Sauce Labs cannot delete cookies - Scenario: User can view Video metadata Given I have created a Video component And I edit the component @@ -14,12 +12,14 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save + # Sauce Labs cannot delete cookies @skip_sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions + # Sauce Labs cannot delete cookies @skip_sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 80c47433b7..5820ad46f7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -12,6 +12,9 @@ from django.core.management import call_command from django.conf import settings from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from requests import put +from base64 import encodestring +from json import dumps # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -42,27 +45,32 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 -# https://gist.github.com/santiycr/1644439 -import requests -import base64 -try: - import json -except ImportError: - import simplejson as json + +def get_username_and_key(): + """ + Returns the Sauce Labs username and access ID as set by environment variables + """ + return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} def set_job_status(jobid, passed=True): - body_content = json.dumps({"passed": passed}) + """ + Sets the job status on sauce labs + """ + body_content = dumps({"passed": passed}) config = get_username_and_key() - base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - result = requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), + base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] + result = put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 def make_desired_capabilities(): - desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) + """ + Returns a DesiredCapabilities object corresponding to the environment sauce parameters + """ + desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') desired_capabilities['version'] = settings.SAUCE.get('VERSION') desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE') @@ -77,60 +85,54 @@ def make_desired_capabilities(): return desired_capabilities -def get_username_and_key(): - return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} - - @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ - world.absorb(settings.SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') - browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED') - # There is an issue with ChromeDriver2 r195627 on Ubuntu - # in which we sometimes get an invalid browser session. - # This is a work-around to ensure that we get a valid session. - success = False - num_attempts = 0 - while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: + if not world.SAUCE_ENABLED: + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') - # Get a browser session - if world.SAUCE_ENABLED: - config = get_username_and_key() - world.browser = Browser( - 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), - **make_desired_capabilities() - ) - world.absorb(world.browser.driver.session_id, 'jobid') - else: + # There is an issue with ChromeDriver2 r195627 on Ubuntu + # in which we sometimes get an invalid browser session. + # This is a work-around to ensure that we get a valid session. + success = False + num_attempts = 0 + while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: world.browser = Browser(browser_driver) + # Try to visit the main page + # If the browser session is invalid, this will + # raise a WebDriverException + try: + world.visit('/') + + except WebDriverException: + world.browser.quit() + num_attempts += 1 + + else: + success = True + + # If we were unable to get a valid session within the limit of attempts, + # then we cannot run the tests. + if not success: + raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) + + world.browser.driver.set_window_size(1280, 1024) + + else: + config = get_username_and_key() + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), + **make_desired_capabilities() + ) world.browser.driver.implicitly_wait(30) - # Try to visit the main page - # If the browser session is invalid, this will - # raise a WebDriverException - try: - world.visit('/') - - except WebDriverException: - world.browser.quit() - num_attempts += 1 - - else: - success = True - - # If we were unable to get a valid session within the limit of attempts, - # then we cannot run the tests. - if not success: - raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) - - # Set the browser size to 1280x1024 - if not world.SAUCE_ENABLED: - world.browser.driver.set_window_size(1280, 1024) + world.absorb(world.browser.driver.session_id, 'jobid') @before.each_scenario diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e11ed19ea..f13b3ff932 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -99,7 +99,7 @@ def i_am_logged_in_user(step): @step('I am not logged in$') def i_am_not_logged_in(step): - world.visit(django_url('logout')) + world.visit('logout') @step('I am staff for course "([^"]*)"$') @@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text): @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page') def should_see_in_the_page(step, doesnt_appear, text): + multiplier = 1 + if world.SAUCE_ENABLED: + multiplier = 2 if doesnt_appear: - assert world.browser.is_text_not_present(text, wait_time=5) + assert world.browser.is_text_not_present(text, wait_time=5*multiplier) else: - assert world.browser.is_text_present(text, wait_time=5) + assert world.browser.is_text_present(text, wait_time=5*multiplier) @step('I am logged in$') @@ -150,7 +153,7 @@ def i_am_logged_in(step): world.log_in(username='robot', password='test') world.browser.visit(django_url('/')) # You should not see the login link - world.is_css_not_present('a#login') + assert world.is_css_not_present('a#login') @step(u'I am an edX user$') diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 5c777fd64f..4165a9bb9f 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -11,7 +11,7 @@ Feature: Login in as a registered user And I submit my credentials on the login form Then I should see the login error message "This account has not been activated" - # CHROME ONLY, firefox will not redirect properly + # firefox will not redirect properly when the whole suite is run @skip_firefox Scenario: Login to an activated account Given I am an edX user diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 8fd8b54c1a..70cc93fd93 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -13,6 +13,8 @@ Feature: Navigate Course When I click on subsection "2" Then I should see the content of subsection "2" + # Clicking on the sequence link doesn't work on opera through sauce + @skip_opera Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index c1fce04b54..3c9f491f7d 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -3,7 +3,7 @@ Feature: Sign in As a new user I want to signup for a student account - # CHROME ONLY, firefox will not redirect properly + # firefox will not redirect properly @skip_firefox Scenario: Sign up from the homepage Given I visit the homepage diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 260887290e..6c8299f2c5 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -11,7 +11,7 @@ Feature: Video component Given the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode - #Firefox doesn't have HTML5 + # Firefox doesn't have HTML5 @skip_firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index 1704edd68b..e33d4dff62 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -27,25 +27,26 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } +PLATFORMS = ['Linux', 'OS X 10.8', 'OS X 10.6', 'Windows 8', 'Windows 7', 'Windows XP'] #HACK #This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON -#This is the simplest way to adhere to all of these requirements and still be readible +#This is the simplest way to adhere to all of these requirements and still be readable DEFAULT_CONFIG = 'Linux-chrome--' SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') -if len(SAUCE_INFO) !=4: - SAUCE_INFO = DEFAULT_CONFIG.split('-') +if len(SAUCE_INFO) != 4: + SAUCE_INFO = DEFAULT_CONFIG.split('-') # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[0].lower(), DesiredCapabilities.CHROME), - 'PLATFORM': SAUCE_INFO[0], + 'PLATFORM': SAUCE_INFO[0] if SAUCE_INFO[0] in PLATFORMS else 'Linux', + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1].lower(), DesiredCapabilities.CHROME), 'VERSION': SAUCE_INFO[2], 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', - 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), + 'BUILD': os.environ.get('BUILD_DISPLAY_NAME', 'LETTUCE TESTS'), } From b2480b5f00782cffa4020db8f493ca5b93192c8d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 20 Aug 2013 10:43:05 -0400 Subject: [PATCH 121/179] Changed way feature flags are interpreted Configurations must be defined before hand Reduced pylint violations We only support 4 browsers --- .../contentstore/features/checklists.feature | 2 -- .../contentstore/features/upload.py | 3 +- .../courseware/features/navigation.feature | 2 -- lms/envs/sauce.py | 29 +++++++++++++++---- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 1649cd0749..b48d8608b6 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -12,7 +12,6 @@ Feature: Course checklists # There are issues getting link to be active in browsers other than chrome @skip_firefox - @skip_opera Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -22,7 +21,6 @@ Feature: Course checklists # There are issues getting link to be active in browsers other than chrome @skip_firefox - @skip_opera Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index a989d6c07f..882b36e6b2 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -10,6 +10,7 @@ import os TEST_ROOT = settings.COMMON_TEST_DATA_ROOT + @step(u'I go to the files and uploads page') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' @@ -106,8 +107,8 @@ def get_index(file_name): def get_file(file_name): index = get_index(file_name) assert index != -1 - url_css = 'a.filename' + def get_url(): return world.css_find(url_css)[index]._element.get_attribute('href') url = world.retry_on_exception(get_url) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 70cc93fd93..8fd8b54c1a 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -13,8 +13,6 @@ Feature: Navigate Course When I click on subsection "2" Then I should see the content of subsection "2" - # Clicking on the sequence link doesn't work on opera through sauce - @skip_opera Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index e33d4dff62..e4764f7cf8 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -27,24 +27,41 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } -PLATFORMS = ['Linux', 'OS X 10.8', 'OS X 10.6', 'Windows 8', 'Windows 7', 'Windows XP'] +ALL_CONFIG = { + 'Linux-chrome--': ['Linux', 'chrome', '', ''], + 'Windows 8-chrome--': ['Windows 8', 'chrome', '', ''], + 'Windows 7-chrome--': ['Windows 7', 'chrome', '', ''], + 'Windows XP-chrome--': ['Windows XP', 'chrome', '', ''], + 'OS X 10.8-chrome--': ['OS X 10.8', 'chrome', '', ''], + 'OS X 10.6-chrome--': ['OS X 10.6', 'chrome', '', ''], + + 'Linux-firefox-23-': ['Linux', 'firefox', '23', ''], + 'Windows 8-firefox-23-': ['Windows 8', 'firefox', '23', ''], + 'Windows 7-firefox-23-': ['Windows 7', 'firefox', '23', ''], + 'Windows XP-firefox-23-': ['Windows XP', 'firefox', '23', ''], + + 'OS X 10.8-safari-6-': ['OS X 10.8', 'safari', '6', ''], + + 'Windows 8-internetexplorer-10-': ['Windows 8', 'internetexplorer', '10', ''], +} #HACK #This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON #This is the simplest way to adhere to all of these requirements and still be readable +# PLATFORM-BROWSER-VERSION_NUM-DEVICE DEFAULT_CONFIG = 'Linux-chrome--' -SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') -if len(SAUCE_INFO) != 4: - SAUCE_INFO = DEFAULT_CONFIG.split('-') +SAUCE_CONFIG = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG) + +SAUCE_INFO = ALL_CONFIG[SAUCE_CONFIG] # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'PLATFORM': SAUCE_INFO[0] if SAUCE_INFO[0] in PLATFORMS else 'Linux', - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1].lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO[0], + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1]), 'VERSION': SAUCE_INFO[2], 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', From 151782acf052fc3a3c5c722d5668855514687a36 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 21 Aug 2013 16:23:48 -0400 Subject: [PATCH 122/179] LMS acceptance tests use mixed modulestore --- common/djangoapps/terrain/browser.py | 5 ++-- common/djangoapps/terrain/course_helpers.py | 6 ++-- .../xmodule/modulestore/tests/django_utils.py | 29 ------------------- lms/envs/acceptance.py | 18 ++++++++---- 4 files changed, 17 insertions(+), 41 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c2bf2bbbf3..24dd9f3729 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -108,9 +108,10 @@ def reset_databases(scenario): mongo = MongoClient() mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db']) _CONTENTSTORE.clear() - modulestore = xmodule.modulestore.django.modulestore() + + modulestore = xmodule.modulestore.django.editable_modulestore() modulestore.collection.drop() - xmodule.modulestore.django._MODULESTORES.clear() + xmodule.modulestore.django.clear_existing_modulestores() # Uncomment below to trigger a screenshot on error diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index eca3290080..fc01d25d66 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -10,7 +10,7 @@ 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 modulestore +from xmodule.modulestore.django import editable_modulestore from xmodule.contentstore.django import contentstore from urllib import quote_plus @@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False): @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()" - modulestore().collection.drop() + editable_modulestore().collection.drop() contentstore().fs_files.drop() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 87156ec0dd..dabdd00e43 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -210,32 +210,3 @@ class ModuleStoreTestCase(TestCase): # Call superclass implementation super(ModuleStoreTestCase, self)._post_teardown() - - - def assert2XX(self, status_code, msg=None): - """ - Assert that the given value is a success status (between 200 and 299) - """ - msg = self._formatMessage(msg, "%s is not a success status" % safe_repr(status_code)) - self.assertTrue(status_code >= 200 and status_code < 300, msg=msg) - - def assert3XX(self, status_code, msg=None): - """ - Assert that the given value is a redirection status (between 300 and 399) - """ - msg = self._formatMessage(msg, "%s is not a redirection status" % safe_repr(status_code)) - self.assertTrue(status_code >= 300 and status_code < 400, msg=msg) - - def assert4XX(self, status_code, msg=None): - """ - Assert that the given value is a client error status (between 400 and 499) - """ - msg = self._formatMessage(msg, "%s is not a client error status" % safe_repr(status_code)) - self.assertTrue(status_code >= 400 and status_code < 500, msg=msg) - - def assert5XX(self, status_code, msg=None): - """ - Assert that the given value is a server error status (between 500 and 599) - """ - msg = self._formatMessage(msg, "%s is not a server error status" % safe_repr(status_code)) - self.assertTrue(status_code >= 500 and status_code < 600, msg=msg) diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 1e188d3b45..a5c455288a 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -35,15 +35,21 @@ modulestore_options = { MODULESTORE = { 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options - }, - 'direct': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options + 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', + 'OPTIONS': { + 'mappings': {}, + 'stores': { + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options + } + } + } } } +MODULESTORE['direct'] = MODULESTORE['default'] + CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { From 109d3c3d100f67fe2b49feecddd6573dcf37d027 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 21 Aug 2013 16:52:37 -0400 Subject: [PATCH 123/179] Refactored to remove test#XXX methods from ModuleStoreTestCase --- .../contentstore/tests/test_assets.py | 4 +-- .../contentstore/tests/test_contentstore.py | 30 ++++++++-------- .../tests/test_course_settings.py | 6 ++-- .../contentstore/tests/test_item.py | 2 +- .../contentstore/tests/test_textbooks.py | 22 ++++++------ .../contentstore/tests/test_users.py | 34 +++++++++---------- .../xmodule/modulestore/tests/django_utils.py | 2 +- 7 files changed, 50 insertions(+), 50 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py index b627237729..2f158cfda6 100644 --- a/cms/djangoapps/contentstore/tests/test_assets.py +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase): f = BytesIO("sample content") f.name = "sample.txt" resp = self.client.post(self.url, {"name": "my-name", "file": f}) - self.assert2XX(resp.status_code) + self.assertEquals(resp.status_code, 200) def test_no_file(self): resp = self.client.post(self.url, {"name": "file.txt"}) - self.assert4XX(resp.status_code) + self.assertEquals(resp.status_code, 400) def test_get(self): resp = self.client.get(self.url) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..2e94b42476 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1367,7 +1367,7 @@ class ContentStoreTest(ModuleStoreTestCase): 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'Chapter 2') # go to various pages @@ -1377,92 +1377,92 @@ class ContentStoreTest(ModuleStoreTestCase): kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # export page resp = self.client.get(reverse('export_course', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # manage users resp = self.client.get(reverse('manage_users', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # course info resp = self.client.get(reverse('course_info', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # settings_details resp = self.client.get(reverse('settings_details', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # settings_details resp = self.client.get(reverse('settings_grading', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # static_pages resp = self.client.get(reverse('static_pages', kwargs={'org': loc.org, 'course': loc.course, 'coursename': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # static_pages resp = self.client.get(reverse('asset_index', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # go look at a subsection page subsection_location = loc.replace(category='sequential', name='test_sequence') resp = self.client.get(reverse('edit_subsection', kwargs={'location': subsection_location.url()})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # go look at the Edit page unit_location = loc.replace(category='vertical', name='test_vertical') resp = self.client.get(reverse('edit_unit', kwargs={'location': unit_location.url()})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # delete a component del_loc = loc.replace(category='html', name='test_html') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a unit del_loc = loc.replace(category='vertical', name='test_vertical') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a unit del_loc = loc.replace(category='sequential', name='test_sequence') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a chapter del_loc = loc.replace(category='chapter', name='chapter_2') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) def test_import_into_new_course_id(self): module_store = modulestore('direct') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2007ba2f69..f413820aac 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -430,12 +430,12 @@ class CourseGraderUpdatesTest(CourseTestCase): def test_get(self): resp = self.client.get(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) def test_delete(self): resp = self.client.delete(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) def test_post(self): grader = { @@ -446,5 +446,5 @@ class CourseGraderUpdatesTest(CourseTestCase): "weight": 17.3, } resp = self.client.post(self.url, grader) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 260444a8f7..e5ff992cb8 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase): resp.content, "application/json" ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) class TestCreateItem(CourseTestCase): diff --git a/cms/djangoapps/contentstore/tests/test_textbooks.py b/cms/djangoapps/contentstore/tests/test_textbooks.py index a21a1b1023..950d0f780e 100644 --- a/cms/djangoapps/contentstore/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/tests/test_textbooks.py @@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase): def test_view_index(self): "Basic check that the textbook index page responds correctly" resp = self.client.get(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # we don't have resp.context right now, # due to bugs in our testing harness :( if resp.context: @@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) self.assertEqual(self.course.pdf_textbooks, obj) @@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) self.assertEqual(content, obj) @@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # reload course store = get_modulestore(self.course.location) @@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) obj = json.loads(resp.content) self.assertIn("error", obj) @@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) self.assertNotIn("Location", resp) @@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase): def test_get_1(self): "Get the first textbook" resp = self.client.get(self.url1) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) compare = json.loads(resp.content) self.assertEqual(compare, self.textbook1) def test_get_2(self): "Get the second textbook" resp = self.client.get(self.url2) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) compare = json.loads(resp.content) self.assertEqual(compare, self.textbook2) @@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase): def test_delete(self): "Delete a textbook by ID" resp = self.client.delete(self.url1) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) course = self.store.get_item(self.course.location) self.assertEqual(course.pdf_textbooks, [self.textbook2]) @@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase): ) self.assertEqual(resp.status_code, 201) resp2 = self.client.get(url) - self.assert2XX(resp2.status_code) + self.assertEqual(resp2.status_code, 200) compare = json.loads(resp2.content) self.assertEqual(compare, textbook) course = self.store.get_item(self.course.location) @@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase): ) self.assertEqual(resp.status_code, 201) resp2 = self.client.get(self.url2) - self.assert2XX(resp2.status_code) + self.assertEqual(resp2.status_code, 200) compare = json.loads(resp2.content) self.assertEqual(compare, replacement) course = self.store.get_item(self.course.location) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index cbb8aa8b01..80b2364c43 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase): def test_detail_inactive(self): resp = self.client.get(self.inactive_detail_url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) result = json.loads(resp.content) self.assertFalse(result["active"]) def test_detail_invalid(self): resp = self.client.get(self.invalid_detail_url) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 404) result = json.loads(resp.content) self.assertIn("error", result) @@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, data={"role": None}, ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) self.assert_not_enrolled() @@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) self.assert_not_enrolled() @@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase): data={"role": "staff"}, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase): data={"role": "instructor"}, HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) @@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase): data={"role": "instructor"}, HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) @@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase): }) resp = self.client.delete(self_url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB user = User.objects.get(email=self.user.email) groups = [g.name for g in user.groups.all()] @@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase): self.ext_user.save() resp = self.client.delete(self.detail_url) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) # reload user from DB @@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) self.assert_enrolled() def test_staff_to_instructor_still_enrolled(self): @@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) self.assert_enrolled() def assert_not_enrolled(self): diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index dabdd00e43..00e34ce41a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -1,5 +1,5 @@ """ -eoduleStore configuration for test cases. +Modulestore configuration for test cases. """ from uuid import uuid4 From 8165a033b143c0c14a1d4adac54b37637c6b346d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 21 Aug 2013 19:42:59 -0400 Subject: [PATCH 124/179] Pep8/pylint fixes Fixed test_masquerade failure due to not clearing the modulestore between tests. --- common/djangoapps/course_groups/tests/tests.py | 2 +- common/djangoapps/external_auth/tests/test_shib.py | 2 +- common/lib/xmodule/xmodule/js/libpeerconnection.log | 0 common/lib/xmodule/xmodule/modulestore/django.py | 3 +-- .../xmodule/xmodule/modulestore/tests/django_utils.py | 3 +-- lms/djangoapps/courseware/tests/modulestore_config.py | 8 ++++++-- lms/djangoapps/courseware/tests/test_masquerade.py | 9 +++++++-- lms/djangoapps/courseware/tests/tests.py | 6 +++--- lms/djangoapps/instructor/tests/test_access.py | 1 + lms/djangoapps/instructor/tests/test_legacy_xss.py | 1 + lms/djangoapps/staticbook/tests.py | 1 + 11 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/libpeerconnection.log diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index debdc68c26..a17df56a71 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -17,7 +17,7 @@ from xmodule.modulestore.tests.django_utils import mixed_store_config # cms.envs.test doesn't. TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_MAPPING = { 'edX/toy/2012_Fall': 'xml' } +TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'} TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index 187acdb595..d48948c6ae 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -18,7 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.django import editable_modulestore -from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from external_auth.models import ExternalAuthMap from external_auth.views import shib_login, course_specific_login, course_specific_register diff --git a/common/lib/xmodule/xmodule/js/libpeerconnection.log b/common/lib/xmodule/xmodule/js/libpeerconnection.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index adafbc1253..b239e5f1d4 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -103,10 +103,9 @@ def editable_modulestore(name='default'): store = store.modulestores['default'] # At this point, we either have the ability to create - # items in the store, or we do not. + # items in the store, or we do not. if hasattr(store, 'create_xmodule'): return store else: return None - diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 00e34ce41a..e7b0b98824 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -5,8 +5,7 @@ Modulestore configuration for test cases. from uuid import uuid4 from django.test import TestCase from xmodule.modulestore.django import editable_modulestore, \ - editable_modulestore, clear_existing_modulestores -from unittest.util import safe_repr + clear_existing_modulestores def mixed_store_config(data_dir, mappings): diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py index e4ee86878f..74fd3da57f 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -1,6 +1,10 @@ +""" +Define test configuration for modulestores. +""" + from xmodule.modulestore.tests.django_utils import xml_store_config, \ - mongo_store_config, draft_mongo_store_config,\ - mixed_store_config + mongo_store_config, draft_mongo_store_config,\ + mixed_store_config from django.conf import settings diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 3122dc6477..0ec320b605 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -16,17 +16,22 @@ from django.contrib.auth.models import Group, User from courseware.access import _course_staff_group_name from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.django import modulestore, clear_existing_modulestores import json @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) -class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): +class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check for staff being able to masquerade as student. """ def setUp(self): + + # Clear out the modulestores, causing them to reload + clear_existing_modulestores() + self.graded_course = modulestore().get_course("edX/graded/2012_Fall") # Create staff account diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 995c7a352c..4486a6a032 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -64,9 +64,9 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): ) items = store.get_items( - location_query, - course_id=course_id, - depth=2 + location_query, + course_id=course_id, + depth=2 ) if len(items) < 1: diff --git a/lms/djangoapps/instructor/tests/test_access.py b/lms/djangoapps/instructor/tests/test_access.py index ee2e91f766..1874e88f22 100644 --- a/lms/djangoapps/instructor/tests/test_access.py +++ b/lms/djangoapps/instructor/tests/test_access.py @@ -85,6 +85,7 @@ class TestInstructorAccessAllow(ModuleStoreTestCase): group = Group.objects.get(name=get_access_group_name(self.course, 'staff')) self.assertIn(user, group.user_set.all()) + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAccessRevoke(ModuleStoreTestCase): """ Test access revoke. """ diff --git a/lms/djangoapps/instructor/tests/test_legacy_xss.py b/lms/djangoapps/instructor/tests/test_legacy_xss.py index 784838fc4c..d748876032 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_xss.py +++ b/lms/djangoapps/instructor/tests/test_legacy_xss.py @@ -14,6 +14,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from instructor.views import legacy + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestXss(ModuleStoreTestCase): def setUp(self): diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py index c18b3663e7..135150a2d1 100644 --- a/lms/djangoapps/staticbook/tests.py +++ b/lms/djangoapps/staticbook/tests.py @@ -36,6 +36,7 @@ HTML_BOOK = { ], } + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class StaticBookTest(ModuleStoreTestCase): """ From 2ba0d40d7b69c2997d0d47bd10a302aa0b68c1a6 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:02:58 -0400 Subject: [PATCH 125/179] fix pep8 violations --- cms/djangoapps/contentstore/management/commands/import.py | 2 +- cms/djangoapps/contentstore/tests/test_import_nostatic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 520e36f4d2..e0d58b32f0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index fc68975ebb..24b00dbbf3 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -1,6 +1,6 @@ #pylint: disable=E1101 ''' -Tests for importing with no static +Tests for importing with no static ''' from django.test.client import Client From ef98c54f5bed8efbdfb8b232882bfada8dd5eb3c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:06:10 -0400 Subject: [PATCH 126/179] fix some pylint violations --- cms/djangoapps/contentstore/tests/test_import_nostatic.py | 2 +- lms/djangoapps/courseware/tests/test_module_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 24b00dbbf3..1eb36bc75f 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -52,7 +52,7 @@ class MongoCollectionFindWrapper(object): class ContentStoreImportNoStaticTest(ModuleStoreTestCase): """ Tests that rely on the toy and test_import_course courses. - TODO: refactor using CourseFactory so they do not. + NOTE: refactor using CourseFactory so they do not. """ def setUp(self): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index bf8d52da6f..56659b7c2b 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -384,7 +384,7 @@ class TestHtmlModifiers(ModuleStoreTestCase): def test_get_course_info_section(self): self.course.lms.static_asset_path = "toy_course_dir" get_course_info_section(self.request, self.course, "handouts") - # TODO: check handouts output...right now test course seems to have no such content + # NOTE: check handouts output...right now test course seems to have no such content # at least this makes sure get_course_info_section returns without exception def test_course_link_rewrite(self): From fb3a5bf9db75307fa973774c5d3c9ddca507ab40 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:10:36 -0400 Subject: [PATCH 127/179] remove unused class --- .../tests/test_import_nostatic.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 1eb36bc75f..aad6ffbfe4 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -29,25 +29,6 @@ TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex -class MongoCollectionFindWrapper(object): - ''' - MongoCollectionFindWrapper for testing. - ''' - def __init__(self, original): - """ - intit func - """ - self.original = original - self.counter = 0 - - def find(self, query, *args, **kwargs): - """ - find func - """ - self.counter = self.counter + 1 - return self.original(query, *args, **kwargs) - - @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreImportNoStaticTest(ModuleStoreTestCase): """ From 078ad4b25e2dd51ada41422a974ba130991655bd Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 21 Aug 2013 20:31:23 -0400 Subject: [PATCH 128/179] Added comment justifying ModuleStoreTestCase design. --- .../xmodule/modulestore/tests/django_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index e7b0b98824..1f856d7eba 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -131,6 +131,22 @@ class ModuleStoreTestCase(TestCase): 3. Use factories (e.g. `CourseFactory`, `ItemFactory`) to populate the modulestore with test data. + + NOTE: + * For Mongo-backed courses (created with `CourseFactory`), + the state of the course will be reset before/after each + test method executes. + + * For XML-backed courses, the course state will NOT + reset between test methods (although it will reset + between test classes) + + The reason is: XML courses are not editable, so to reset + a course you have to reload it from disk, which is slow. + + If you do need to reset an XML course, use + `clear_existing_modulestores()` directly in + your `setUp()` method. """ @staticmethod From 0c1c3f11af676a4bf0449e9a183ddea96bb10e9e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:48:56 -0400 Subject: [PATCH 129/179] loop in static import testing into common/* tests --- cms/djangoapps/contentstore/tests/test_contentstore.py | 2 +- common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..2a69fc451c 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -476,7 +476,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): content_store = contentstore() module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store) + import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True) course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') course = module_store.get_item(course_location) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 17036a16bf..f047ef8e41 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -11,6 +11,7 @@ from xmodule.tests import DATA_DIR from xmodule.modulestore import Location from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.contentstore.mongo import MongoContentStore from xmodule.modulestore.tests.test_modulestore import check_path_to_location @@ -46,9 +47,12 @@ class TestMongoModuleStore(object): def initdb(): # connect to the db store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) + # since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class + # as well + content_store = MongoContentStore(HOST, DB) # Explicitly list the courses to load (don't want the big one) courses = ['toy', 'simple'] - import_from_xml(store, DATA_DIR, courses) + import_from_xml(store, DATA_DIR, courses, static_content_store=content_store) return store @staticmethod From 77a38afaeb01f4b295af483d7b3ec991fbf35a4c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 22:00:33 -0400 Subject: [PATCH 130/179] add some draft courseware importing paths in common/* tests --- .../xmodule/modulestore/tests/test_mongo.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index f047ef8e41..53128b919f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -10,6 +10,7 @@ from xblock.runtime import KeyValueStore, InvalidScopeError from xmodule.tests import DATA_DIR from xmodule.modulestore import Location from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore +from xmodule.modulestore.draft import DraftModuleStore from xmodule.modulestore.xml_importer import import_from_xml from xmodule.contentstore.mongo import MongoContentStore @@ -36,7 +37,7 @@ class TestMongoModuleStore(object): # is ok only as long as none of the tests modify the db. # If (when!) that changes, need to either reload the db, or load # once and copy over to a tmp db for each test. - cls.store = cls.initdb() + cls.store, cls.content_store, cls.draft_store = cls.initdb() @classmethod def teardownClass(cls): @@ -50,10 +51,14 @@ class TestMongoModuleStore(object): # since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class # as well content_store = MongoContentStore(HOST, DB) + # + # Also test draft store imports + # + draft_store = DraftModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) # Explicitly list the courses to load (don't want the big one) - courses = ['toy', 'simple'] - import_from_xml(store, DATA_DIR, courses, static_content_store=content_store) - return store + courses = ['toy', 'simple', 'simple_with_draft'] + import_from_xml(store, DATA_DIR, courses, draft_store=draft_store, static_content_store=content_store) + return store, content_store, draft_store @staticmethod def destroy_db(connection): @@ -81,10 +86,11 @@ class TestMongoModuleStore(object): def test_get_courses(self): '''Make sure the course objects loaded properly''' courses = self.store.get_courses() - assert_equals(len(courses), 2) + assert_equals(len(courses), 3) courses.sort(key=lambda c: c.id) assert_equals(courses[0].id, 'edX/simple/2012_Fall') - assert_equals(courses[1].id, 'edX/toy/2012_Fall') + assert_equals(courses[1].id, 'edX/simple_with_draft/2012_Fall') + assert_equals(courses[2].id, 'edX/toy/2012_Fall') def test_loads(self): assert_not_equals( @@ -133,7 +139,7 @@ class TestMongoModuleStore(object): Assumes the information is desired for courses[1] ('toy' course). """ - return courses[1].tabs[index]['name'] + return courses[2].tabs[index]['name'] # There was a bug where model.save was not getting called after the static tab name # was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that From 842556d284a6429a9738b7dc1bfd9a73fecdb65e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 22 Aug 2013 08:57:10 -0400 Subject: [PATCH 131/179] add new test data with draft content --- common/test/data/simple_with_draft/README.md | 2 + common/test/data/simple_with_draft/course.xml | 31 ++++++++++ .../drafts/vertical/test_vertical.xml | 5 ++ .../data/simple_with_draft/html/toylab.html | 3 + .../problem/L1_Problem_1.xml | 43 +++++++++++++ .../simple_with_draft/problem/ps01-simple.xml | 62 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 common/test/data/simple_with_draft/README.md create mode 100644 common/test/data/simple_with_draft/course.xml create mode 100644 common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml create mode 100644 common/test/data/simple_with_draft/html/toylab.html create mode 100644 common/test/data/simple_with_draft/problem/L1_Problem_1.xml create mode 100644 common/test/data/simple_with_draft/problem/ps01-simple.xml diff --git a/common/test/data/simple_with_draft/README.md b/common/test/data/simple_with_draft/README.md new file mode 100644 index 0000000000..69ff6b4ed0 --- /dev/null +++ b/common/test/data/simple_with_draft/README.md @@ -0,0 +1,2 @@ +This is a simple, but non-trivial, course using multiple module types and some nested structure. + diff --git a/common/test/data/simple_with_draft/course.xml b/common/test/data/simple_with_draft/course.xml new file mode 100644 index 0000000000..c130686012 --- /dev/null +++ b/common/test/data/simple_with_draft/course.xml @@ -0,0 +1,31 @@ + + +

    + + +
    + + +
    + + + +
    +
    + + diff --git a/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml b/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml new file mode 100644 index 0000000000..4433d282a4 --- /dev/null +++ b/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml @@ -0,0 +1,5 @@ + + + Foobar - edit in draft + + \ No newline at end of file diff --git a/common/test/data/simple_with_draft/html/toylab.html b/common/test/data/simple_with_draft/html/toylab.html new file mode 100644 index 0000000000..81df84bd63 --- /dev/null +++ b/common/test/data/simple_with_draft/html/toylab.html @@ -0,0 +1,3 @@ +Lab 2A: Superposition Experiment + +

    Isn't the toy course great?

    diff --git a/common/test/data/simple_with_draft/problem/L1_Problem_1.xml b/common/test/data/simple_with_draft/problem/L1_Problem_1.xml new file mode 100644 index 0000000000..2ba0617904 --- /dev/null +++ b/common/test/data/simple_with_draft/problem/L1_Problem_1.xml @@ -0,0 +1,43 @@ + + +

    +

    Finger Exercise 1

    +

    +

    +Here are two definitions:

    +
      +
    1. +

      +Declarative knowledge refers to statements of fact.

      +
    2. +
    3. +

      +Imperative knowledge refers to 'how to' methods.

      +
    4. +
    +

    +Which of the following choices is correct?

    +
      +
    1. +

      +Statement 1 is true, Statement 2 is false

      +
    2. +
    3. +

      +Statement 1 is false, Statement 2 is true

      +
    4. +
    5. +

      +Statement 1 and Statement 2 are both false

      +
    6. +
    7. +

      +Statement 1 and Statement 2 are both true

      +
    8. +
    +

    + + + +

    +
    diff --git a/common/test/data/simple_with_draft/problem/ps01-simple.xml b/common/test/data/simple_with_draft/problem/ps01-simple.xml new file mode 100644 index 0000000000..e70d8f2c8d --- /dev/null +++ b/common/test/data/simple_with_draft/problem/ps01-simple.xml @@ -0,0 +1,62 @@ +