diff --git a/openassessment/assessment/models/base.py b/openassessment/assessment/models/base.py index 43243ec66c..ed17272cf8 100644 --- a/openassessment/assessment/models/base.py +++ b/openassessment/assessment/models/base.py @@ -19,6 +19,7 @@ import logging import math +import six from lazy import lazy from django.core.cache import cache @@ -105,7 +106,7 @@ def content_hash_from_dict(rubric_dict): rubric_dict.pop("content_hash", None) canonical_form = json.dumps(rubric_dict, sort_keys=True) - return sha1(canonical_form).hexdigest() + return sha1(canonical_form.encode('utf-8')).hexdigest() @staticmethod def structure_hash_from_dict(rubric_dict): @@ -137,7 +138,7 @@ def structure_hash_from_dict(rubric_dict): for criterion in rubric_dict.get('criteria', []) ] canonical_form = json.dumps(structure, sort_keys=True) - return sha1(canonical_form).hexdigest() + return sha1(canonical_form.encode('utf-8')).hexdigest() class Criterion(models.Model): @@ -510,7 +511,7 @@ def get_median_score_dict(cls, scores_dict): """ median_scores = {} - for criterion, criterion_scores in scores_dict.iteritems(): + for criterion, criterion_scores in six.iteritems(scores_dict): criterion_score = Assessment.get_median_score(criterion_scores) median_scores[criterion] = criterion_score return median_scores @@ -673,7 +674,7 @@ def create_from_option_names(cls, assessment, selected, feedback=None): # Validate that we have selections for all criteria # This will raise an exception if we're missing any selections/feedback required for criteria - cls._check_all_criteria_assessed(rubric_index, selected.keys(), feedback.keys()) + cls._check_all_criteria_assessed(rubric_index, list(selected.keys()), list(feedback.keys())) # Retrieve the criteria/option/feedback for criteria that have options. # Since we're using the rubric's index, we'll get an `InvalidRubricSelection` error @@ -684,13 +685,13 @@ def create_from_option_names(cls, assessment, selected, feedback=None): 'option': rubric_index.find_option(criterion_name, option_name), 'feedback': feedback.get(criterion_name, u"")[0:cls.MAX_FEEDBACK_SIZE], } - for criterion_name, option_name in selected.iteritems() + for criterion_name, option_name in six.iteritems(selected) ] # Some criteria may have feedback but no options, only feedback. # For these, we set `option` to None, indicating that the assessment part # is not associated with any option, only a criterion. - for criterion_name, feedback_text in feedback.iteritems(): + for criterion_name, feedback_text in six.iteritems(feedback): if criterion_name not in selected: assessment_parts.append({ 'criterion': rubric_index.find_criterion(criterion_name), @@ -742,7 +743,7 @@ def create_from_option_points(cls, assessment, selected): 'criterion': rubric_index.find_criterion(criterion_name), 'option': rubric_index.find_option_for_points(criterion_name, option_points), } - for criterion_name, option_points in selected.iteritems() + for criterion_name, option_points in six.iteritems(selected) ] # Add in feedback-only criteria diff --git a/openassessment/assessment/models/training.py b/openassessment/assessment/models/training.py index 17e13497c4..a97f5ad84a 100644 --- a/openassessment/assessment/models/training.py +++ b/openassessment/assessment/models/training.py @@ -4,6 +4,8 @@ from hashlib import sha1 import json +import six + from django.core.cache import cache from django.db import models @@ -54,7 +56,7 @@ def create_example(cls, answer, options_selected, rubric): # This will raise `InvalidRubricSelection` if the selected options # do not match the rubric. - for criterion_name, option_name in options_selected.iteritems(): + for criterion_name, option_name in six.iteritems(options_selected): option = rubric.index.find_option(criterion_name, option_name) example.options_selected.add(option) return example @@ -133,7 +135,7 @@ def calculate_hash(answer, options_selected, rubric): 'options_selected': options_selected, 'rubric': rubric.id }) - return sha1(contents).hexdigest() + return sha1(contents.encode('utf-8')).hexdigest() @classmethod def cache_key(cls, answer, options_selected, rubric): diff --git a/openassessment/assessment/test/constants.py b/openassessment/assessment/test/constants.py index 2fc4d7a8b6..f7927ca9b8 100644 --- a/openassessment/assessment/test/constants.py +++ b/openassessment/assessment/test/constants.py @@ -2,6 +2,7 @@ """ Constants used as test data. """ +import six STUDENT_ITEM = { 'student_id': u'𝓽𝓮𝓼𝓽 𝓼𝓽𝓾𝓭𝓮𝓷𝓽', @@ -76,7 +77,7 @@ "expected_points": sum( RUBRIC_OPTIONS[i]["points"] for i in value ) - } for key, value in OPTIONS_SELECTED_CHOICES.iteritems() + } for key, value in six.iteritems(OPTIONS_SELECTED_CHOICES) } EXAMPLES = [ diff --git a/openassessment/fileupload/backends/swift.py b/openassessment/fileupload/backends/swift.py index 4a222f2dbe..de06404366 100644 --- a/openassessment/fileupload/backends/swift.py +++ b/openassessment/fileupload/backends/swift.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ''' Add in /edx/app/edxapp/edx-platform/lms/envs/aws.py: ORA2_SWIFT_URL = AUTH_TOKENS["ORA2_SWIFT_URL"] @@ -11,10 +12,12 @@ 'swift stat -v' to get it. ''' +from __future__ import absolute_import, division, print_function, unicode_literals + import logging -import urlparse import requests +from six.moves.urllib.parse import urlparse import swiftclient from django.conf import settings @@ -96,5 +99,5 @@ def get_settings(): url = getattr(settings, 'ORA2_SWIFT_URL', None) key = getattr(settings, 'ORA2_SWIFT_KEY', None) - url = urlparse.urlparse(url) + url = urlparse(url) return key, url diff --git a/openassessment/workflow/api.py b/openassessment/workflow/api.py index 2ca1f25c30..df1fb8bd6f 100644 --- a/openassessment/workflow/api.py +++ b/openassessment/workflow/api.py @@ -4,6 +4,8 @@ """ import logging +import six + from django.db import DatabaseError from openassessment.assessment.errors import PeerAssessmentError, PeerAssessmentInternalError @@ -340,7 +342,7 @@ def _get_workflow_model(submission_uuid): problem. """ - if not isinstance(submission_uuid, basestring): + if not isinstance(submission_uuid, six.string_types): raise AssessmentWorkflowRequestError("submission_uuid must be a string type") try: diff --git a/openassessment/workflow/models.py b/openassessment/workflow/models.py index 06d166e451..dd6e8ae0ff 100644 --- a/openassessment/workflow/models.py +++ b/openassessment/workflow/models.py @@ -58,7 +58,7 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): an after the fact recording of the last known state of that information so we can search easily. """ - STEPS = ASSESSMENT_API_DICT.keys() + STEPS = list(ASSESSMENT_API_DICT.keys()) STATUSES = [ "waiting", # User has done all necessary assessment but hasn't been diff --git a/openassessment/workflow/test/test_api.py b/openassessment/workflow/test/test_api.py index 8f9c03c3eb..3d341d3fda 100644 --- a/openassessment/workflow/test/test_api.py +++ b/openassessment/workflow/test/test_api.py @@ -46,6 +46,13 @@ @ddt.ddt class TestAssessmentWorkflowApi(CacheResetTest): + def test_status_values_include_all_steps(self): + self.assertIsInstance(AssessmentWorkflow.STEPS, list) + self.assertEqual( + AssessmentWorkflow.STATUS_VALUES, + AssessmentWorkflow.STEPS + AssessmentWorkflow.STATUSES + ) + @ddt.file_data('data/assessments.json') def test_create_workflow(self, data): first_step = data["steps"][0] if data["steps"] else "peer" diff --git a/openassessment/xblock/course_items_listing_mixin.py b/openassessment/xblock/course_items_listing_mixin.py index 479575c087..e612b3ef09 100644 --- a/openassessment/xblock/course_items_listing_mixin.py +++ b/openassessment/xblock/course_items_listing_mixin.py @@ -3,8 +3,12 @@ """ +from __future__ import absolute_import + import json +import six + from webob import Response from xblock.core import XBlock @@ -26,5 +30,5 @@ def get_ora2_responses(self, request, suffix=''): # pylint: disable=unused-argu """ # Import is placed here to avoid model import at project startup. from openassessment.data import OraAggregateData - responses = OraAggregateData.collect_ora2_responses(unicode(self.course_id)) + responses = OraAggregateData.collect_ora2_responses(six.text_type(self.course_id)) return Response(json.dumps(responses), content_type='application/json') diff --git a/openassessment/xblock/data_conversion.py b/openassessment/xblock/data_conversion.py index f5a395838b..d1b2d21d31 100644 --- a/openassessment/xblock/data_conversion.py +++ b/openassessment/xblock/data_conversion.py @@ -2,8 +2,12 @@ Data Conversion utility methods for handling ORA2 XBlock data transformations and validation. """ +from __future__ import absolute_import + import json +import six + def convert_training_examples_list_to_dict(examples_list): """ @@ -85,8 +89,7 @@ def update_assessments_format(assessments): for assessment in assessments: if 'examples' in assessment and assessment['examples']: for example in assessment['examples']: - if (isinstance(example, dict) and - (isinstance(example['answer'], unicode) or isinstance(example['answer'], str))): + if isinstance(example, dict) and isinstance(example['answer'], six.string_types): example['answer'] = { 'parts': [ {'text': example['answer']} diff --git a/openassessment/xblock/grade_mixin.py b/openassessment/xblock/grade_mixin.py index ce79817cf3..2e715254a0 100644 --- a/openassessment/xblock/grade_mixin.py +++ b/openassessment/xblock/grade_mixin.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """ Grade step in the OpenAssessment XBlock. """ +from __future__ import absolute_import, division, print_function, unicode_literals + import copy from lazy import lazy @@ -8,9 +11,10 @@ from django.utils.translation import ugettext as _ -from data_conversion import create_submission_dict from openassessment.assessment.errors import PeerAssessmentError, SelfAssessmentError +from .data_conversion import create_submission_dict + class GradeMixin(object): """Grade Mixin introduces all handlers for displaying grades @@ -410,6 +414,7 @@ def _peer_median_option(self, submission_uuid, criterion): median_scores = peer_api.get_assessment_median_scores(submission_uuid) median_score = median_scores.get(criterion['name'], None) + median_score = -1 if median_score is None else median_score def median_options(): """ @@ -423,7 +428,7 @@ def median_options(): 5. Options A=1, B=3 and C=5, a median score of 6 returns [C] Note: 5 should not happen as a median should never be out of range. """ - last_score = None + last_score = -1 median_options = [] # Sort the options first by name and then by points, so that if there @@ -436,7 +441,7 @@ def median_options(): current_score = option['points'] # If we have reached a new score, then decide what to do next - if current_score is not last_score: + if current_score != last_score: # If the last score we saw was already larger than the median # score, then we must have collected enough so return all @@ -472,7 +477,7 @@ def median_options(): return options[0] return { 'label': u' / '.join([option['label'] for option in options]), - 'points': median_score, + 'points': median_score if median_score != -1 else None, 'explanation': None, } diff --git a/openassessment/xblock/leaderboard_mixin.py b/openassessment/xblock/leaderboard_mixin.py index fc0c1eecd0..ab571f04ce 100644 --- a/openassessment/xblock/leaderboard_mixin.py +++ b/openassessment/xblock/leaderboard_mixin.py @@ -1,6 +1,10 @@ """ Leaderboard step in the OpenAssessment XBlock. """ +from __future__ import absolute_import + +import six + from xblock.core import XBlock from django.utils.translation import ugettext as _ @@ -94,7 +98,7 @@ def render_leaderboard_complete(self, student_item_dict): if 'text' in score['content'] or 'parts' in score['content']: submission = {'answer': score.pop('content')} score['submission'] = create_submission_dict(submission, self.prompts) - elif isinstance(score['content'], basestring): + elif isinstance(score['content'], six.string_types): pass # Currently, we do not handle non-text submissions. else: diff --git a/openassessment/xblock/lms_mixin.py b/openassessment/xblock/lms_mixin.py index c3ec8fbda6..61e0ae2b41 100644 --- a/openassessment/xblock/lms_mixin.py +++ b/openassessment/xblock/lms_mixin.py @@ -2,6 +2,10 @@ Fields and methods used by the LMS and Studio. """ +from __future__ import absolute_import + +import six + from xblock.fields import DateTime, Dict, Float, Scope, String @@ -13,7 +17,7 @@ def from_json(self, access_dict): def to_json(self, access_dict): if access_dict is not None: - return {unicode(k): access_dict[k] for k in access_dict} + return {six.text_type(k): access_dict[k] for k in access_dict} class LmsCompatibilityMixin(object): diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py index 460e603b6f..66dc619892 100644 --- a/openassessment/xblock/openassessmentblock.py +++ b/openassessment/xblock/openassessmentblock.py @@ -1,5 +1,7 @@ """An XBlock where students can read a question and compose their response""" +from __future__ import absolute_import + import copy import datetime as dt import json @@ -9,6 +11,7 @@ from lazy import lazy import pkg_resources import pytz +from six import text_type from webob import Response from xblock.core import XBlock from xblock.fields import Boolean, Integer, List, Scope, String @@ -362,7 +365,7 @@ def get_student_item_dict(self, anonymous_user_id=None): if self.scope_ids.user_id is None: student_id = None else: - student_id = unicode(self.scope_ids.user_id) + student_id = text_type(self.scope_ids.user_id) student_item_dict = dict( student_id=student_id, @@ -1104,7 +1107,7 @@ def _serialize_opaque_key(self, key): if hasattr(key, 'to_deprecated_string'): return key.to_deprecated_string() else: - return unicode(key) + return text_type(key) def get_username(self, anonymous_user_id): """ diff --git a/openassessment/xblock/resolve_dates.py b/openassessment/xblock/resolve_dates.py index cbfb062ee2..f948cb57bb 100644 --- a/openassessment/xblock/resolve_dates.py +++ b/openassessment/xblock/resolve_dates.py @@ -1,10 +1,14 @@ """ Resolve unspecified dates and date strings to datetimes. """ +from __future__ import absolute_import + import datetime as dt from dateutil.parser import parse as parse_date import pytz +import six +from six.moves import range, zip class InvalidDateFormat(Exception): @@ -43,7 +47,7 @@ def _parse_date(value, _): if isinstance(value, dt.datetime): return value.replace(tzinfo=pytz.utc) - elif isinstance(value, basestring): + elif isinstance(value, six.string_types): try: return parse_date(value).replace(tzinfo=pytz.utc) except ValueError: @@ -226,7 +230,7 @@ def resolve_dates(start, end, date_ranges, _): prev_end = step_end # Combine the resolved dates back into a list of tuples - resolved_ranges = zip(resolved_starts, resolved_ends) + resolved_ranges = list(zip(resolved_starts, resolved_ends)) # Now that we have resolved both start and end dates, we can safely compare them for resolved_start, resolved_end in resolved_ranges: diff --git a/openassessment/xblock/schema.py b/openassessment/xblock/schema.py index 9cca820004..7f458d6cd7 100644 --- a/openassessment/xblock/schema.py +++ b/openassessment/xblock/schema.py @@ -2,8 +2,12 @@ Schema for validating and sanitizing data received from the JavaScript client. """ +from __future__ import absolute_import + import dateutil from pytz import utc +import six + from voluptuous import All, Any, In, Invalid, Range, Required, Schema @@ -22,10 +26,10 @@ def utf8_validator(value): """ try: - if isinstance(value, str): + if isinstance(value, six.binary_type): return value.decode('utf-8') else: - return unicode(value) + return six.text_type(value) except (ValueError, TypeError): raise Invalid(u"Could not load unicode from value \"{val}\"".format(val=value)) @@ -51,7 +55,7 @@ def datetime_validator(value): # Parse the date and interpret it as UTC value = dateutil.parser.parse(value).replace(tzinfo=utc) - return unicode(value.isoformat()) + return six.text_type(value.isoformat()) except (ValueError, TypeError): raise Invalid(u"Could not parse datetime from value \"{val}\"".format(val=value)) diff --git a/openassessment/xblock/staff_area_mixin.py b/openassessment/xblock/staff_area_mixin.py index 429dd312ab..034fde6472 100644 --- a/openassessment/xblock/staff_area_mixin.py +++ b/openassessment/xblock/staff_area_mixin.py @@ -6,6 +6,7 @@ from functools import wraps import logging +import six from xblock.core import XBlock from openassessment.assessment.errors import PeerAssessmentInternalError @@ -485,6 +486,6 @@ def _cancel_workflow(self, submission_uuid, comments, requesting_user_id=None): AssessmentWorkflowError, AssessmentWorkflowInternalError ) as ex: - msg = ex.message + msg = six.text_type(ex) logger.exception(msg) return {"success": False, 'msg': msg} diff --git a/openassessment/xblock/staff_assessment_mixin.py b/openassessment/xblock/staff_assessment_mixin.py index b102b517c0..d6bd537064 100644 --- a/openassessment/xblock/staff_assessment_mixin.py +++ b/openassessment/xblock/staff_assessment_mixin.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """"" A mixin for staff grading. """ +from __future__ import absolute_import, division, print_function, unicode_literals + import logging from xblock.core import XBlock @@ -8,9 +11,9 @@ from openassessment.assessment.api import staff as staff_api from openassessment.assessment.errors import StaffAssessmentInternalError, StaffAssessmentRequestError from openassessment.workflow import api as workflow_api -from staff_area_mixin import require_course_staff from .data_conversion import clean_criterion_feedback, create_rubric_dict, verify_assessment_parameters +from .staff_area_mixin import require_course_staff logger = logging.getLogger(__name__) diff --git a/openassessment/xblock/student_training_mixin.py b/openassessment/xblock/student_training_mixin.py index 012a1ad024..f198db2ad0 100644 --- a/openassessment/xblock/student_training_mixin.py +++ b/openassessment/xblock/student_training_mixin.py @@ -1,8 +1,12 @@ """ Student training step in the OpenAssessment XBlock. """ +from __future__ import absolute_import + import logging +import six + from webob import Response from xblock.core import XBlock @@ -64,14 +68,14 @@ def _parse_answer_dict(self, answer): """ parts = answer.get('parts', []) if parts and isinstance(parts[0], dict): - if isinstance(parts[0].get('text'), basestring): + if isinstance(parts[0].get('text'), six.string_types): return create_submission_dict({'answer': answer}, self.prompts) def _parse_answer_list(self, answer): """ Helper to parse answer as a list of strings. """ - if answer and isinstance(answer[0], basestring): + if answer and isinstance(answer[0], six.string_types): return self._parse_answer_string(answer[0]) elif len(answer) == 0: return self._parse_answer_string("") @@ -100,7 +104,7 @@ def _parse_example(self, example): ) answer = example['answer'] submission_dict = None - if isinstance(answer, basestring): + if isinstance(answer, six.string_types): submission_dict = self._parse_answer_string(answer) elif isinstance(answer, dict): submission_dict = self._parse_answer_dict(answer) diff --git a/openassessment/xblock/studio_mixin.py b/openassessment/xblock/studio_mixin.py index 264a95eb8e..4131ef5bfc 100644 --- a/openassessment/xblock/studio_mixin.py +++ b/openassessment/xblock/studio_mixin.py @@ -1,11 +1,15 @@ """ Studio editing view for OpenAssessment XBlock. """ +from __future__ import absolute_import + import copy import logging from uuid import uuid4 import pkg_resources +import six +from six.moves import range, zip from voluptuous import MultipleInvalid from xblock.core import XBlock from xblock.fields import List, Scope @@ -384,7 +388,7 @@ def _get_base_url_path_for_course_assets(self, course_key): placeholder_id = uuid4().hex # create a dummy asset location with a fake but unique name. strip off the name, and return it - url_path = unicode(course_key.make_asset_key('asset', placeholder_id).for_branch(None)) + url_path = six.text_type(course_key.make_asset_key('asset', placeholder_id).for_branch(None)) if not url_path.startswith('/'): url_path = '/' + url_path return url_path.replace(placeholder_id, '') diff --git a/openassessment/xblock/submission_mixin.py b/openassessment/xblock/submission_mixin.py index 07b4fc4b4c..421bd80a51 100644 --- a/openassessment/xblock/submission_mixin.py +++ b/openassessment/xblock/submission_mixin.py @@ -1,16 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + import json import logging +import six from xblock.core import XBlock -from data_conversion import create_submission_dict, prepare_submission_for_serialization from openassessment.fileupload import api as file_upload_api from openassessment.fileupload.exceptions import FileUploadError from openassessment.workflow.errors import AssessmentWorkflowError -from validation import validate_submission +from .data_conversion import create_submission_dict, prepare_submission_for_serialization from .resolve_dates import DISTANT_FUTURE from .user_data import get_user_preferences +from .validation import validate_submission logger = logging.getLogger(__name__) @@ -208,7 +212,10 @@ def save_files_descriptions(self, data, suffix=''): # pylint: disable=unused-ar if 'descriptions' in data: descriptions = data['descriptions'] - if isinstance(descriptions, list) and all(map(lambda description: isinstance(description, basestring), descriptions)): + if ( + isinstance(descriptions, list) and + all(isinstance(description, six.string_types) for description in descriptions) + ): try: self.saved_files_descriptions = json.dumps(descriptions) diff --git a/openassessment/xblock/test/base.py b/openassessment/xblock/test/base.py index 78e5e91e1a..0abbe225c4 100644 --- a/openassessment/xblock/test/base.py +++ b/openassessment/xblock/test/base.py @@ -2,12 +2,15 @@ """ Base class for handler-level testing of the XBlock. """ +from __future__ import absolute_import, print_function + import copy from functools import wraps import json import os.path import mock +from six.moves import zip import webob from workbench.runtime import WorkbenchRuntime @@ -102,7 +105,7 @@ def _wrapped(*args, **kwargs): if isinstance(self, XBlockHandlerTestCaseMixin): # Print a debug message - print "Loading scenario from {path}".format(path=scenario_path) + print("Loading scenario from {path}".format(path=scenario_path)) # Configure the runtime with our user id self.set_user(user_id) @@ -185,7 +188,7 @@ def request(self, xblock, handler_name, content, request_method="POST", response # Create a fake request request = webob.Request(dict()) request.method = request_method - request.body = content + request.body = content.encode('utf-8') # Send the request to the XBlock handler if use_runtime: diff --git a/openassessment/xblock/test/test_grade.py b/openassessment/xblock/test/test_grade.py index 06fc3769f6..bcda608d52 100644 --- a/openassessment/xblock/test/test_grade.py +++ b/openassessment/xblock/test/test_grade.py @@ -6,6 +6,7 @@ import json import ddt +import six from openassessment.assessment.api import peer as peer_api @@ -37,18 +38,18 @@ def test_render_grade(self, xblock): # This isn't strictly speaking part of the grade step rendering, # but we've already done all the setup to get to this point in the flow, # so we might as well verify it here. - resp = self.request(xblock, 'render_submission', json.dumps(dict())) + resp = self.request(xblock, 'render_submission', json.dumps(dict())).decode('utf-8') self.assertIn('response', resp.lower()) self.assertIn('complete', resp.lower()) # Verify that student submission is in the view - self.assertIn(self.SUBMISSION[1], resp.decode('utf-8')) + self.assertIn(self.SUBMISSION[1], resp) - resp = self.request(xblock, 'render_peer_assessment', json.dumps(dict())) + resp = self.request(xblock, 'render_peer_assessment', json.dumps(dict())).decode('utf-8') self.assertIn('peer', resp.lower()) self.assertIn('complete', resp.lower()) - resp = self.request(xblock, 'render_self_assessment', json.dumps(dict())) + resp = self.request(xblock, 'render_self_assessment', json.dumps(dict())).decode('utf-8') self.assertIn('self', resp.lower()) self.assertIn('complete', resp.lower()) @@ -69,15 +70,15 @@ def test_render_grade_self_only(self, xblock): # This isn't strictly speaking part of the grade step rendering, # but we've already done all the setup to get to this point in the flow, # so we might as well verify it here. - resp = self.request(xblock, 'render_submission', json.dumps(dict())) + resp = self.request(xblock, 'render_submission', json.dumps(dict())).decode('utf-8') self.assertIn('response', resp.lower()) self.assertIn('complete', resp.lower()) - resp = self.request(xblock, 'render_peer_assessment', json.dumps(dict())) + resp = self.request(xblock, 'render_peer_assessment', json.dumps(dict())).decode('utf-8') self.assertNotIn('peer', resp.lower()) self.assertNotIn('complete', resp.lower()) - resp = self.request(xblock, 'render_self_assessment', json.dumps(dict())) + resp = self.request(xblock, 'render_self_assessment', json.dumps(dict())).decode('utf-8') self.assertIn('self', resp.lower()) self.assertIn('complete', resp.lower()) @@ -103,12 +104,12 @@ def test_render_grade_feedback_only_criterion(self, xblock): ) # Render the grade section - resp = self.request(xblock, 'render_grade', json.dumps(dict())) + resp = self.request(xblock, 'render_grade', json.dumps(dict())).decode('utf-8') self.assertIn('your response', resp.lower()) # Verify that feedback from each scorer appears in the view - self.assertIn(u'єאςєɭɭєภՇ ฬ๏гк!', resp.decode('utf-8')) - self.assertIn(u'Good job!', resp.decode('utf-8')) + self.assertIn(u'єאςєɭɭєภՇ ฬ๏гк!', resp) + self.assertIn(u'Good job!', resp) @scenario('data/feedback_per_criterion.xml', user_id='Bernard') def test_render_grade_feedback(self, xblock): @@ -251,7 +252,7 @@ def test_peer_update_after_override(self, xblock): # Create all but the last peer assessment of the current user; no peer grade will be available graded_by = xblock.get_assessment_module('peer-assessment')['must_be_graded_by'] - for scorer_sub, scorer_name, assessment in zip(scorer_subs, self.PEERS, PEER_ASSESSMENTS)[:-1]: + for scorer_sub, scorer_name, assessment in list(zip(scorer_subs, self.PEERS, PEER_ASSESSMENTS))[:-1]: self.create_peer_assessment( scorer_sub, scorer_name, @@ -336,7 +337,7 @@ def test_assessment_does_not_match_rubric(self, xblock): # (since it's not part of the original assessment), # but at least it won't display an error. resp = self.request(xblock, 'render_grade', json.dumps({})) - self.assertGreater(resp, 0) + self.assertGreater(len(resp), 0) @ddt.file_data('data/waiting_scenarios.json') @scenario('data/grade_waiting_scenario.xml', user_id='Omar') @@ -405,7 +406,8 @@ def test_submit_feedback(self, xblock): feedback = peer_api.get_assessment_feedback(xblock.submission_uuid) self.assertIsNot(feedback, None) self.assertEqual(feedback['feedback_text'], u'I disliked my assessment') - self.assertItemsEqual( + six.assertCountEqual( + self, feedback['options'], [{'text': u'Option 1'}, {'text': u'Option 2'}] ) @@ -427,7 +429,7 @@ def test_submit_feedback_no_options(self, xblock): # Verify that the feedback was created in the database feedback = peer_api.get_assessment_feedback(xblock.submission_uuid) self.assertIsNot(feedback, None) - self.assertItemsEqual(feedback['options'], []) + six.assertCountEqual(self, feedback['options'], []) @scenario('data/grade_scenario.xml', user_id='Bob') def test_submit_feedback_invalid_options(self, xblock): diff --git a/openassessment/xblock/test/test_staff.py b/openassessment/xblock/test/test_staff.py index 2606b2dabf..8f02e8b159 100644 --- a/openassessment/xblock/test/test_staff.py +++ b/openassessment/xblock/test/test_staff.py @@ -6,6 +6,7 @@ import json import mock +import six from openassessment.assessment.api import staff as staff_api @@ -20,7 +21,7 @@ def _assert_path_and_context(self, xblock, expected_context): path, context = xblock.staff_path_and_context() self.assertEqual('openassessmentblock/staff/oa_staff_grade.html', path) - self.assertItemsEqual(expected_context, context) + six.assertCountEqual(self, expected_context, context) # Verify that we render without error resp = self.request(xblock, 'render_staff_assessment', json.dumps({})) @@ -161,7 +162,7 @@ def test_staff_assess_handler(self, xblock): self.assertEqual(assessment['score_type'], 'ST') self.assertEqual(assessment['feedback'], u'Staff: good job!') - parts = sorted(assessment['parts']) + parts = sorted(assessment['parts'], key=lambda part: part['option']['name']) self.assertEqual(len(parts), 2) self.assertEqual(parts[0]['option']['criterion']['name'], u'Form') self.assertEqual(parts[0]['option']['name'], 'Fair') @@ -201,7 +202,7 @@ def test_permission_error(self, xblock): student_item = xblock.get_student_item_dict() xblock.create_submission(student_item, self.SUBMISSION) resp = self.request(xblock, 'staff_assess', json.dumps(STAFF_GOOD_ASSESSMENT)) - self.assertIn("You do not have permission", resp) + self.assertIn("You do not have permission", resp.decode('utf-8')) @scenario('data/self_assessment_scenario.xml', user_id='Bob') def test_invalid_options(self, xblock): diff --git a/openassessment/xblock/test/test_staff_area.py b/openassessment/xblock/test/test_staff_area.py index 0907d3a628..eaa9bd687c 100644 --- a/openassessment/xblock/test/test_staff_area.py +++ b/openassessment/xblock/test/test_staff_area.py @@ -4,9 +4,9 @@ """ from collections import namedtuple import json -import urllib from mock import MagicMock, Mock, call, patch +from six.moves.urllib.parse import urlencode from openassessment.assessment.api import peer as peer_api from openassessment.assessment.api import self as self_api @@ -169,7 +169,7 @@ def test_staff_area_student_info_no_submission(self, xblock): request.params = {"student_id": "test_student"} # Verify that we can render without error resp = xblock.render_student_info(request) - self.assertIn("a response was not found for this learner.", resp.body.lower()) + self.assertIn("a response was not found for this learner.", resp.body.decode('utf-8').lower()) @scenario('data/peer_only_scenario.xml', user_id='Bob') def test_staff_area_student_info_peer_only(self, xblock): @@ -277,8 +277,8 @@ def test_staff_area_student_info_staff_only_no_options(self, xblock): self.request( xblock, "render_student_info", - urllib.urlencode({"student_username": "Bob"}) - ) + urlencode({"student_username": "Bob"}) + ).decode('utf-8') ) @scenario('data/staff_grade_scenario.xml', user_id='Bob') @@ -415,8 +415,8 @@ def test_staff_area_student_info_image_submission(self, xblock): self.assertEquals('image', context['file_upload_type']) # Check the fully rendered template - payload = urllib.urlencode({"student_username": "Bob"}) - resp = self.request(xblock, "render_student_info", payload) + payload = urlencode({"student_username": "Bob"}) + resp = self.request(xblock, "render_student_info", payload).decode('utf-8') self.assertIn("http://www.example.com/image.jpeg", resp) @scenario('data/self_only_scenario.xml', user_id='Bob') @@ -465,8 +465,8 @@ def test_staff_area_student_info_many_images_submission(self, xblock): self.assertEquals('image', context['file_upload_type']) # Check the fully rendered template - payload = urllib.urlencode({"student_username": "Bob"}) - resp = self.request(xblock, "render_student_info", payload) + payload = urlencode({"student_username": "Bob"}) + resp = self.request(xblock, "render_student_info", payload).decode('utf-8') for i in range(3): self.assertIn("http://www.example.com/image%d.jpeg" % i, resp) self.assertIn("test_description%d" % i, resp) @@ -498,8 +498,8 @@ def test_staff_area_student_info_file_download_url_error(self, xblock): self.assertNotIn('file_url', context['submission']) # Check the fully rendered template - payload = urllib.urlencode({"student_username": "Bob"}) - resp = self.request(xblock, "render_student_info", payload) + payload = urlencode({"student_username": "Bob"}) + resp = self.request(xblock, "render_student_info", payload).decode('utf-8') self.assertIn("Bob Answer", resp) @scenario('data/grade_scenario.xml', user_id='Bob') @@ -559,7 +559,7 @@ def test_staff_area_student_info_full_workflow(self, xblock): request.params = {"student_username": "Bob"} # Verify that we can render without error resp = xblock.render_student_info(request) - self.assertIn("bob answer", resp.body.lower()) + self.assertIn("bob answer", resp.body.decode('utf-8').lower()) @scenario('data/basic_scenario.xml', user_id='Bob') def test_cancel_submission_without_reason(self, xblock): @@ -711,14 +711,14 @@ def test_staff_delete_student_state(self, xblock): request.params = {"student_username": 'Bob'} # Verify that we can see the student's grade resp = xblock.render_student_info(request) - self.assertIn("final grade", resp.body.lower()) + self.assertIn("final grade", resp.body.decode('utf-8').lower()) # Staff user Bob can clear his own submission xblock.clear_student_state('Bob', 'test_course', xblock.scope_ids.usage_id, bob_item['student_id']) # Verify that the submission was cleared resp = xblock.render_student_info(request) - self.assertIn("response was not found", resp.body.lower()) + self.assertIn("response was not found", resp.body.decode('utf-8').lower()) def _verify_staff_assessment_context(self, context, required, ungraded=None, in_progress=None): """ diff --git a/openassessment/xblock/test/test_submission.py b/openassessment/xblock/test/test_submission.py index 62a8807817..ecea5a64ef 100644 --- a/openassessment/xblock/test/test_submission.py +++ b/openassessment/xblock/test/test_submission.py @@ -119,7 +119,7 @@ def test_ability_to_submit_blank_answer_if_text_response_none(self, xblock): @scenario('data/over_grade_scenario.xml', user_id='Alice') def test_closed_submissions(self, xblock): resp = self.request(xblock, 'render_submission', json.dumps(dict())) - self.assertIn("Incomplete", resp) + self.assertIn("Incomplete", resp.decode('utf-8')) @scenario('data/line_breaks.xml') def test_prompt_line_breaks(self, xblock): @@ -127,18 +127,18 @@ def test_prompt_line_breaks(self, xblock): # (backward compatibility in case if prompt_type == 'text') resp = self.request(xblock, 'render_submission', json.dumps(dict())) expected_prompt = u"


Line 1

Line 2

Line 3

" - self.assertIn(expected_prompt, resp) + self.assertIn(expected_prompt, resp.decode('utf-8')) @scenario('data/prompt_html.xml') def test_prompt_html_to_text(self, xblock): resp = self.request(xblock, 'render_submission', json.dumps(dict())) expected_prompt = u"Question 123" - self.assertIn(expected_prompt, resp) + self.assertIn(expected_prompt, resp.decode('utf-8')) xblock.prompts_type = "text" resp = self.request(xblock, 'render_submission', json.dumps(dict())) expected_prompt = "<code><strong>Question 123</strong></code>" - self.assertIn(expected_prompt, resp) + self.assertIn(expected_prompt, resp.decode('utf-8')) @mock_s3 @override_settings( @@ -156,10 +156,10 @@ def test_upload_url(self, xblock): resp = self.request(xblock, 'upload_url', json.dumps({"contentType": "image/jpeg", "filename": "test.jpg"}), response_format='json') self.assertTrue(resp['success']) - self.assertTrue(resp['url'].startswith( - 'https://mybucket.s3.amazonaws.com/submissions_attachments/test_student/test_course/' + - xblock.scope_ids.usage_id - )) + self.assertIn( + '/submissions_attachments/test_student/test_course/' + xblock.scope_ids.usage_id, + resp['url'] + ) @mock_s3 @override_settings( @@ -251,10 +251,10 @@ def test_upload_files_with_uppercase_ext(self, xblock): resp = self.request(xblock, 'upload_url', json.dumps({'contentType': 'filename', 'filename': 'test.PDF'}), response_format='json') self.assertTrue(resp['success']) - self.assertTrue(resp['url'].startswith( - 'https://mybucket.s3.amazonaws.com/submissions_attachments/test_student/test_course/' + - xblock.scope_ids.usage_id - )) + self.assertIn( + '/submissions_attachments/test_student/test_course/' + xblock.scope_ids.usage_id, + resp['url'] + ) class SubmissionRenderTest(XBlockHandlerTestCase): @@ -601,8 +601,8 @@ def test_closed_graded(self, xblock): def test_integration(self, xblock): # Expect that the response step is open and displays the deadline resp = self.request(xblock, 'render_submission', json.dumps(dict())) - self.assertIn('Enter your response to the prompt', resp) - self.assertIn('2999-05-06T00:00:00+00:00', resp) + self.assertIn('Enter your response to the prompt', resp.decode('utf-8')) + self.assertIn('2999-05-06T00:00:00+00:00', resp.decode('utf-8')) # Create a submission for the user xblock.create_submission( @@ -612,7 +612,7 @@ def test_integration(self, xblock): # Expect that the response step is "submitted" resp = self.request(xblock, 'render_submission', json.dumps(dict())) - self.assertIn('your response has been submitted', resp.lower()) + self.assertIn('your response has been submitted', resp.decode('utf-8').lower()) def _assert_path_and_context(self, xblock, expected_path, expected_context): """ diff --git a/openassessment/xblock/validation.py b/openassessment/xblock/validation.py index 12d7de62a6..41a5b0006a 100644 --- a/openassessment/xblock/validation.py +++ b/openassessment/xblock/validation.py @@ -1,8 +1,13 @@ """ Validate changes to an XBlock before it is updated. """ +from __future__ import absolute_import + from collections import Counter +import six +from six.moves import zip + from openassessment.assessment.api.student_training import validate_training_examples from openassessment.assessment.serializers import InvalidRubric, rubric_from_dict from openassessment.xblock.data_conversion import convert_training_examples_list_to_dict @@ -27,7 +32,7 @@ def _match_by_order(items, others): """ # Sort each dictionary by its "name" key, then zip them and return key_func = lambda x: x['order_num'] - return zip(sorted(items, key=key_func), sorted(others, key=key_func)) + return list(zip(sorted(items, key=key_func), sorted(others, key=key_func))) def _duplicates(items): @@ -258,7 +263,7 @@ def validate_dates(start, end, date_ranges, _): try: resolve_dates(start, end, date_ranges, _) except (DateValidationError, InvalidDateFormat) as ex: - return False, unicode(ex) + return False, six.text_type(ex) else: return True, u'' @@ -382,7 +387,7 @@ def validate_submission(submission, prompts, _, text_response='required'): return False, message for submission_part in submission: - if type(submission_part) != unicode: + if type(submission_part) != six.text_type: return False, message return True, u'' diff --git a/openassessment/xblock/xml.py b/openassessment/xblock/xml.py index 899bea8c04..22270c9615 100644 --- a/openassessment/xblock/xml.py +++ b/openassessment/xblock/xml.py @@ -1,6 +1,8 @@ """ Serialize and deserialize OpenAssessment XBlock content to/from XML. """ +from __future__ import absolute_import + from uuid import uuid4 as uuid import logging import json @@ -9,6 +11,7 @@ import defusedxml.ElementTree as safe_etree import lxml.etree as etree import pytz +import six from openassessment.xblock.data_conversion import update_assessments_format from openassessment.xblock.lms_mixin import GroupAccessDict @@ -59,7 +62,7 @@ def _safe_get_text(element): Returns: unicode """ - return unicode(element.text) if element.text is not None else u"" + return six.text_type(element.text) if element.text is not None else u"" def _serialize_prompts(prompts_root, prompts_list): @@ -82,7 +85,7 @@ def _serialize_prompts(prompts_root, prompts_list): # Prompt description prompt_description = etree.SubElement(prompt_el, 'description') - prompt_description.text = unicode(prompt.get('description', u'')) + prompt_description.text = six.text_type(prompt.get('description', u'')) def _serialize_options(options_root, options_list): @@ -105,22 +108,22 @@ def _serialize_options(options_root, options_list): option_el = etree.SubElement(options_root, 'option') # Points (default to 0) - option_el.set('points', unicode(option.get('points', 0))) + option_el.set('points', six.text_type(option.get('points', 0))) # Name (default to a UUID) option_name = etree.SubElement(option_el, 'name') if 'name' in option: - option_name.text = unicode(option['name']) + option_name.text = six.text_type(option['name']) else: - option_name.text = unicode(uuid().hex) + option_name.text = six.text_type(uuid().hex) # Label (default to the option name, then an empty string) option_label = etree.SubElement(option_el, 'label') - option_label.text = unicode(option.get('label', option.get('name', u''))) + option_label.text = six.text_type(option.get('label', option.get('name', u''))) # Explanation (default to empty str) option_explanation = etree.SubElement(option_el, 'explanation') - option_explanation.text = unicode(option.get('explanation', u'')) + option_explanation.text = six.text_type(option.get('explanation', u'')) def _serialize_criteria(criteria_root, criteria_list): @@ -146,17 +149,17 @@ def _serialize_criteria(criteria_root, criteria_list): # Criterion name (default to a UUID) criterion_name = etree.SubElement(criterion_el, u'name') if 'name' in criterion: - criterion_name.text = unicode(criterion['name']) + criterion_name.text = six.text_type(criterion['name']) else: - criterion_name.text = unicode(uuid().hex) + criterion_name.text = six.text_type(uuid().hex) # Criterion label (default to the name, then an empty string) criterion_label = etree.SubElement(criterion_el, 'label') - criterion_label.text = unicode(criterion.get('label', criterion.get('name', u''))) + criterion_label.text = six.text_type(criterion.get('label', criterion.get('name', u''))) # Criterion prompt (default to empty string) criterion_prompt = etree.SubElement(criterion_el, 'prompt') - criterion_prompt.text = unicode(criterion.get('prompt', u'')) + criterion_prompt.text = six.text_type(criterion.get('prompt', u'')) # Criterion feedback disabled, optional, or required # If disabled, do not set the attribute. @@ -194,11 +197,11 @@ def serialize_rubric(rubric_root, oa_block): if oa_block.rubric_feedback_prompt is not None: feedback_prompt = etree.SubElement(rubric_root, 'feedbackprompt') - feedback_prompt.text = unicode(oa_block.rubric_feedback_prompt) + feedback_prompt.text = six.text_type(oa_block.rubric_feedback_prompt) if oa_block.rubric_feedback_default_text is not None: feedback_text = etree.SubElement(rubric_root, 'feedback_default_text') - feedback_text.text = unicode(oa_block.rubric_feedback_default_text) + feedback_text.text = six.text_type(oa_block.rubric_feedback_default_text) def parse_date(date_str, name=""): @@ -223,9 +226,9 @@ def parse_date(date_str, name=""): return None try: # Get the date into ISO format - parsed_date = dateutil.parser.parse(unicode(date_str)).replace(tzinfo=pytz.utc) + parsed_date = dateutil.parser.parse(six.text_type(date_str)).replace(tzinfo=pytz.utc) formatted_date = parsed_date.strftime("%Y-%m-%dT%H:%M:%S") - return unicode(formatted_date) + return six.text_type(formatted_date) except (ValueError, TypeError): msg = ( 'The format of the given date ({date}) for the {name} is invalid. ' @@ -499,8 +502,8 @@ def parse_examples_xml(examples): raise UpdateFromXmlError(u'Each "select" element must have an "option" attribute') example_dict['options_selected'].append({ - 'criterion': unicode(select_el.get('criterion')), - 'option': unicode(select_el.get('option')) + 'criterion': six.text_type(select_el.get('criterion')), + 'option': six.text_type(select_el.get('option')) }) examples_list.append(example_dict) @@ -530,7 +533,7 @@ def parse_assessments_xml(assessments_root): # Assessment name if 'name' in assessment.attrib: - assessment_dict['name'] = unicode(assessment.get('name')) + assessment_dict['name'] = six.text_type(assessment.get('name')) else: raise UpdateFromXmlError('All "assessment" elements must contain a "name" element.') @@ -576,7 +579,7 @@ def parse_assessments_xml(assessments_root): # Staff assessment is the only type to use an explicit required marker if assessment_dict['name'] != 'staff-assessment': raise UpdateFromXmlError('The "required" field is only allowed for staff assessment.') - assessment_dict['required'] = _parse_boolean(unicode(assessment.get('required'))) + assessment_dict['required'] = _parse_boolean(six.text_type(assessment.get('required'))) # Training examples examples = assessment.findall('example') @@ -622,7 +625,7 @@ def serialize_training_examples(examples, assessment_el): for part in parts: part_el = etree.SubElement(answer_el, 'part') - part_el.text = unicode(part.get('text', u'')) + part_el.text = six.text_type(part.get('text', u'')) except: # excuse the bare-except, looking for more information on EDUCATOR-1817 log.exception('Error parsing training example: %s', example_dict) raise @@ -631,8 +634,8 @@ def serialize_training_examples(examples, assessment_el): options_selected = example_dict.get('options_selected', []) for selected_dict in options_selected: select_el = etree.SubElement(example_el, 'select') - select_el.set('criterion', unicode(selected_dict.get('criterion', ''))) - select_el.set('option', unicode(selected_dict.get('option', ''))) + select_el.set('criterion', six.text_type(selected_dict.get('criterion', ''))) + select_el.set('option', six.text_type(selected_dict.get('option', ''))) def serialize_assessments(assessments_root, oa_block): @@ -653,22 +656,22 @@ def serialize_assessments(assessments_root, oa_block): assessment = etree.SubElement(assessments_root, 'assessment') # Set assessment attributes, defaulting to empty values - assessment.set('name', unicode(assessment_dict.get('name', ''))) + assessment.set('name', six.text_type(assessment_dict.get('name', ''))) if 'must_grade' in assessment_dict: - assessment.set('must_grade', unicode(assessment_dict['must_grade'])) + assessment.set('must_grade', six.text_type(assessment_dict['must_grade'])) if 'must_be_graded_by' in assessment_dict: - assessment.set('must_be_graded_by', unicode(assessment_dict['must_be_graded_by'])) + assessment.set('must_be_graded_by', six.text_type(assessment_dict['must_be_graded_by'])) if assessment_dict.get('start') is not None: - assessment.set('start', unicode(assessment_dict['start'])) + assessment.set('start', six.text_type(assessment_dict['start'])) if assessment_dict.get('due') is not None: - assessment.set('due', unicode(assessment_dict['due'])) + assessment.set('due', six.text_type(assessment_dict['due'])) if assessment_dict.get('required') is not None: - assessment.set('required', unicode(assessment_dict['required'])) + assessment.set('required', six.text_type(assessment_dict['required'])) # Training examples examples = assessment_dict.get('examples', []) @@ -693,34 +696,34 @@ def serialize_content_to_xml(oa_block, root): # Set the submission start date if oa_block.submission_start is not None: - root.set('submission_start', unicode(oa_block.submission_start)) + root.set('submission_start', six.text_type(oa_block.submission_start)) # Set submission due date if oa_block.submission_due is not None: - root.set('submission_due', unicode(oa_block.submission_due)) + root.set('submission_due', six.text_type(oa_block.submission_due)) # Set leaderboard show if oa_block.leaderboard_show: - root.set('leaderboard_show', unicode(oa_block.leaderboard_show)) + root.set('leaderboard_show', six.text_type(oa_block.leaderboard_show)) # Set text response if oa_block.text_response: - root.set('text_response', unicode(oa_block.text_response)) + root.set('text_response', six.text_type(oa_block.text_response)) # Set file upload response if oa_block.file_upload_response: - root.set('file_upload_response', unicode(oa_block.file_upload_response)) + root.set('file_upload_response', six.text_type(oa_block.file_upload_response)) # Set File upload settings if oa_block.file_upload_type: - root.set('file_upload_type', unicode(oa_block.file_upload_type)) + root.set('file_upload_type', six.text_type(oa_block.file_upload_type)) # Set File type white listing if oa_block.white_listed_file_types: - root.set('white_listed_file_types', unicode(oa_block.white_listed_file_types_string)) + root.set('white_listed_file_types', six.text_type(oa_block.white_listed_file_types_string)) if oa_block.allow_latex is not None: - root.set('allow_latex', unicode(oa_block.allow_latex)) + root.set('allow_latex', six.text_type(oa_block.allow_latex)) # Set group access setting if not empty if oa_block.group_access: @@ -728,7 +731,7 @@ def serialize_content_to_xml(oa_block, root): # Open assessment displayed title title = etree.SubElement(root, 'title') - title.text = unicode(oa_block.title) + title.text = six.text_type(oa_block.title) # Assessment list assessments_root = etree.SubElement(root, 'assessments') @@ -738,7 +741,7 @@ def serialize_content_to_xml(oa_block, root): prompts_root = etree.SubElement(root, 'prompts') _serialize_prompts(prompts_root, oa_block.prompts) - root.set('prompts_type', unicode(oa_block.prompts_type)) + root.set('prompts_type', six.text_type(oa_block.prompts_type)) # Rubric rubric_root = etree.SubElement(root, 'rubric') @@ -842,37 +845,37 @@ def parse_from_xml(root): # Set it to None by default; we will update it to the latest start date later on submission_start = None if 'submission_start' in root.attrib: - submission_start = parse_date(unicode(root.attrib['submission_start']), name="submission start date") + submission_start = parse_date(six.text_type(root.attrib['submission_start']), name="submission start date") # Retrieve the due date for the submission # Set it to None by default; we will update it to the earliest deadline later on submission_due = None if 'submission_due' in root.attrib: - submission_due = parse_date(unicode(root.attrib['submission_due']), name="submission due date") + submission_due = parse_date(six.text_type(root.attrib['submission_due']), name="submission due date") text_response = None if 'text_response' in root.attrib: - text_response = unicode(root.attrib['text_response']) + text_response = six.text_type(root.attrib['text_response']) file_upload_response = None if 'file_upload_response' in root.attrib: - file_upload_response = unicode(root.attrib['file_upload_response']) + file_upload_response = six.text_type(root.attrib['file_upload_response']) allow_file_upload = None if 'allow_file_upload' in root.attrib: - allow_file_upload = _parse_boolean(unicode(root.attrib['allow_file_upload'])) + allow_file_upload = _parse_boolean(six.text_type(root.attrib['allow_file_upload'])) file_upload_type = None if 'file_upload_type' in root.attrib: - file_upload_type = unicode(root.attrib['file_upload_type']) + file_upload_type = six.text_type(root.attrib['file_upload_type']) white_listed_file_types = None if 'white_listed_file_types' in root.attrib: - white_listed_file_types = unicode(root.attrib['white_listed_file_types']) + white_listed_file_types = six.text_type(root.attrib['white_listed_file_types']) allow_latex = False if 'allow_latex' in root.attrib: - allow_latex = _parse_boolean(unicode(root.attrib['allow_latex'])) + allow_latex = _parse_boolean(six.text_type(root.attrib['allow_latex'])) group_access = {} if 'group_access' in root.attrib: @@ -897,7 +900,7 @@ def parse_from_xml(root): prompts_type = 'text' if 'prompts_type' in root.attrib: - prompts_type = unicode(root.attrib['prompts_type']) + prompts_type = six.text_type(root.attrib['prompts_type']) # Retrieve the leaderboard if it exists, otherwise set it to 0 leaderboard_show = 0