diff --git a/.travis.yml b/.travis.yml index 51cfabd..ba13c47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: python python: - "2.7" + - "3.5" + - "3.6" + - "3.7" + - "3.8" install: - "gem install coveralls-lcov" diff --git a/manage.py b/manage.py index 91936b0..c45f582 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import absolute_import import sys import os diff --git a/requirements/base.txt b/requirements/base.txt index 60b2288..795150b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,6 @@ # edX Internal Requirements -git+https://github.com/edx/XBlock.git@xblock-0.4.7#egg=XBlock==0.4.7 +Xblock==1.2.9 # edx-submissions -git+https://github.com/edx/edx-submissions.git@7c766502058e04bc9094e6cbe286e949794b80b3#egg=edx-submissions +edx-submissions==3.0.2 +djangorestframework==3.7.7 # Now needed by edx-submissions \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 1843252..138de59 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,9 +1,9 @@ -r test.txt # Debug tools -django-debug-panel==0.8.1 -django-debug-toolbar==1.3.2 -django-pdb==0.4.2 +django-debug-panel==0.8.3 +django-debug-toolbar==1.11 +django-pdb==0.6.2 sqlparse==0.1.10 # runserver_plus @@ -12,4 +12,4 @@ Werkzeug==0.10.4 # required by edx-submission, other package will install 2015 version otherwise pytz==2012h -git+https://github.com/edx/xblock-utils.git#egg=xblock-utils +xblock-utils==1.2.2 diff --git a/requirements/test-acceptance.txt b/requirements/test-acceptance.txt index ecf0bfa..3fc38f3 100644 --- a/requirements/test-acceptance.txt +++ b/requirements/test-acceptance.txt @@ -1,2 +1,2 @@ -bok_choy==0.4.3 -nose==1.3.3 +bok_choy==1.0.1 +nose==1.3.7 diff --git a/requirements/test.txt b/requirements/test.txt index dccb04f..c827f09 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,13 +1,13 @@ -r base.txt -r test-acceptance.txt -coverage==3.7.1 +coverage==5.1 ddt==1.0.0 -django-nose==1.4.1 +django-nose==1.4.6 mock==1.3.0 pep8==1.5.7 -pylint<1.0 +pylint<3.0 -git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs -git+https://github.com/edx/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n_tools -git+https://github.com/edx/xblock-sdk.git@e12e35159ed7733543778f1ddc26ca227d36632b#egg=xblock-sdk +django-pyfs==2.0 +edx-i18n-tools +git+https://github.com/edx/xblock-sdk.git@331e50bed2a3c9014d998cc4428e92bf658baf8d#egg=xblock-sdk diff --git a/settings/base.py b/settings/base.py index fcf3422..32721a7 100644 --- a/settings/base.py +++ b/settings/base.py @@ -2,6 +2,7 @@ Base settings for UBCPI. """ +from __future__ import absolute_import import os import sys @@ -119,10 +120,6 @@ 'django.contrib.admin', 'django.contrib.admindocs', - # Third party - 'django_extensions', - 'south', - # XBlock 'workbench', # 'sample_xblocks.basic', # Needs to be an app for template lookup diff --git a/settings/test.py b/settings/test.py index bb6ef93..58372e7 100644 --- a/settings/test.py +++ b/settings/test.py @@ -3,6 +3,7 @@ """ # Inherit from base settings +from __future__ import absolute_import from .base import * # pylint:disable=W0614,W0401 TEST_APPS = ( diff --git a/setup.py b/setup.py index 879171f..fbe13f9 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ """Setup for ubcpi XBlock.""" +from __future__ import absolute_import import os from setuptools import setup diff --git a/ubcpi/answer_pool.py b/ubcpi/answer_pool.py index 76d393e..3912931 100644 --- a/ubcpi/answer_pool.py +++ b/ubcpi/answer_pool.py @@ -1,7 +1,9 @@ +from __future__ import absolute_import import random import copy -import persistence as sas_api -from utils import _ # pylint: disable=unused-import +from . import persistence as sas_api +from .utils import _ # pylint: disable=unused-import +from six.moves import range # min number of answers for each answers # so that we don't end up no answers for an option @@ -37,7 +39,7 @@ def get_max_size(pool, num_option, item_length): max_items = POOL_SIZE / item_length # existing items plus the reserved for min size. If there is an option has 1 item, POOL_OPTION_MIN_SIZE - 1 space # is reserved. - existing = POOL_OPTION_MIN_SIZE * num_option + sum([max(0, len(pool.get(i, {})) - 5) for i in xrange(num_option)]) + existing = POOL_OPTION_MIN_SIZE * num_option + sum([max(0, len(pool.get(i, {})) - 5) for i in range(num_option)]) return int(max_items - existing) @@ -83,7 +85,7 @@ def offer_simple(pool, answer, rationale, student_id, options): """ existing = pool.setdefault(answer, {}) if len(existing) >= get_max_size(pool, len(options), POOL_ITEM_LENGTH_SIMPLE): - student_id_to_remove = random.choice(existing.keys()) + student_id_to_remove = random.choice(list(existing.keys())) del existing[student_id_to_remove] existing[student_id] = {} pool[answer] = existing @@ -285,10 +287,11 @@ def get_other_answers_simple(pool, seeded_answers, get_student_item_dict, num_re # loop until we have enough answers to return or when there is nothing more to return while len(ret) < num_responses and merged_pool: - for option, students in merged_pool.items(): + for option in list(merged_pool.keys()): + students = merged_pool[option] rationale = None while students: - student = random.choice(students.keys()) + student = random.choice(list(students.keys())) # remove the chosen answer from pool content = students.pop(student, None) @@ -336,10 +339,10 @@ def get_other_answers_random(pool, seeded_answers, get_student_item_dict, num_re # clean up answers so that all keys are int pool = {int(k): v for k, v in pool.items()} seeded = {'seeded'+str(index): answer for index, answer in enumerate(seeded_answers)} - merged_pool = seeded.keys() + merged_pool = list(seeded.keys()) for key in pool: - merged_pool += pool[key].keys() + merged_pool += list(pool[key].keys()) # shuffle random.shuffle(merged_pool) @@ -430,14 +433,14 @@ def refresh_answers(answers_shown, option, pool, seeded_answers, get_student_ite rationale = None while available_seeds: - key = random.choice(available_seeds.keys()) + key = random.choice(list(available_seeds.keys())) rationale = available_seeds.pop(key, None) if rationale is not None: answer['rationale'] = rationale break; while available_students and rationale is None: - key = random.choice(available_students.keys()) + key = random.choice(list(available_students.keys())) # remove the chosen answer from pool content = available_students.pop(key, None) diff --git a/ubcpi/persistence.py b/ubcpi/persistence.py index 6390dbf..2e1ff50 100644 --- a/ubcpi/persistence.py +++ b/ubcpi/persistence.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from submissions import api as sub_api """ diff --git a/ubcpi/serialize.py b/ubcpi/serialize.py index 26acc56..611fb53 100644 --- a/ubcpi/serialize.py +++ b/ubcpi/serialize.py @@ -1,6 +1,8 @@ +from __future__ import absolute_import from lxml import etree -from utils import _ +from .utils import _ +import six IMAGE_ATTRIBUTES = {'position': 'image_position', 'show_fields': 'image_show_fields', 'alt': 'image_alt'} @@ -29,7 +31,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 _parse_boolean(boolean_str): @@ -56,10 +58,10 @@ def parse_image_xml(root): image_dict['image_url'] = _safe_get_text(image_el) - for attr, key in IMAGE_ATTRIBUTES.iteritems(): + for attr, key in six.iteritems(IMAGE_ATTRIBUTES): if attr in image_el.attrib: image_dict[key] = int(image_el.attrib[attr]) \ - if unicode(image_el.attrib[attr]).isnumeric() else unicode(image_el.attrib[attr]) + if six.text_type(image_el.attrib[attr]).isnumeric() else six.text_type(image_el.attrib[attr]) return image_dict @@ -241,8 +243,8 @@ def parse_from_xml(root): else: seeds = parse_seeds_xml(seeds_el) - algo = unicode(root.attrib['algorithm']) if 'algorithm' in root.attrib else None - num_responses = unicode(root.attrib['num_responses']) if 'num_responses' in root.attrib else None + algo = six.text_type(root.attrib['algorithm']) if 'algorithm' in root.attrib else None + num_responses = six.text_type(root.attrib['num_responses']) if 'num_responses' in root.attrib else None return { 'display_name': display_name, @@ -290,7 +292,7 @@ def serialize_image(image_dict, root): image.text = image_dict.get('image_url', '') for attr in ['image_position', 'image_show_fields', 'image_alt']: if image_dict.get(attr) is not None: - image.set(attr[6:], unicode(image_dict.get(attr))) + image.set(attr[6:], six.text_type(image_dict.get(attr))) def serialize_seeds(seeds, block): @@ -307,7 +309,7 @@ def serialize_seeds(seeds, block): for seed_dict in block.seeds: seed = etree.SubElement(seeds, 'seed') # options in xml starts with 1 - seed.set('option', unicode(seed_dict.get('answer', 0) + 1)) + seed.set('option', six.text_type(seed_dict.get('answer', 0) + 1)) seed.text = seed_dict.get('rationale', '') @@ -327,15 +329,15 @@ def serialize_to_xml(root, block): if block.rationale_size is not None: if block.rationale_size.get('min'): - root.set('rationale_size_min', unicode(block.rationale_size.get('min'))) + root.set('rationale_size_min', six.text_type(block.rationale_size.get('min'))) if block.rationale_size.get('max'): - root.set('rationale_size_max', unicode(block.rationale_size['max'])) + root.set('rationale_size_max', six.text_type(block.rationale_size['max'])) if block.algo: if block.algo.get('name'): root.set('algorithm', block.algo.get('name')) if block.algo.get('num_responses'): - root.set('num_responses', unicode(block.algo.get('num_responses'))) + root.set('num_responses', six.text_type(block.algo.get('num_responses'))) display_name = etree.SubElement(root, 'display_name') display_name.text = block.display_name diff --git a/ubcpi/static/js/spec/ubcpi_edit_spec.js b/ubcpi/static/js/spec/ubcpi_edit_spec.js index 8ca5521..10e41e6 100644 --- a/ubcpi/static/js/spec/ubcpi_edit_spec.js +++ b/ubcpi/static/js/spec/ubcpi_edit_spec.js @@ -381,7 +381,74 @@ describe('UBCPI_Edit module', function () { expect(mockNotify.calls.count()).toBe(3); }); }); - }) + }); + + describe('EditSettingsControllerWithoutOptionalParam', function () { + var $rootScope, createController, controller, expectedCorrectRationale; + + beforeEach(inject(function ($controller, _$rootScope_) { + mockConfig.data = { + "image_position_locations": {"below": "Appears below", "above": "Appears above"}, + "rationale_size": {"max": 32000, "min": 1}, + "display_name": "Peer Instruction", + "algo": {"num_responses": "#", "name": "simple"}, + "algos": { + "simple": "System will select one of each option to present to the students.", + "random": "Completely random selection from the response pool." + }, + "correct_answer": 1, + "seeds": [ + {answer:2, rationale:'rationale3'}, + {answer:1, rationale:'rationale2'}, + {answer:0, rationale:'rationale1'}, + {answer:2, rationale:'rationale3'}, + {answer:1, rationale:'rationale2'}, + {answer:0, rationale:'rationale1'} + + ], + "question_text": { + "text": "What is the answer to life, the universe and everything?", + "image_show_fields": 0, + "image_url": "", + "image_position": "below", + "image_alt": "" + }, + "options": [ + { + "text": "21", + "image_show_fields": 0, + "image_url": "", + "image_position": "below", + "image_alt": "" + } + ] + }; + + $rootScope = _$rootScope_; + createController = function (params) { + return $controller( + 'EditSettingsController', { + $scope: $rootScope, + $stateParams: params || {} + }); + }; + controller = createController(); + expectedCorrectRationale = {"text": ""}; + })); + + it('should have correct initial states', function () { + expect(controller.algos).toBe(mockConfig.data.algos); + expect(controller.data.display_name).toBe(mockConfig.data.display_name); + expect(controller.data.question_text).toBe(mockConfig.data.question_text); + expect(controller.data.rationale_size).toBe(mockConfig.data.rationale_size); + expect(controller.image_position_locations).toBe(mockConfig.data.image_position_locations); + expect(controller.data.options).toBe(mockConfig.data.options); + expect(controller.data.correct_answer).toBe(mockConfig.data.correct_answer); + expect(controller.data.correct_rationale).toEqual(expectedCorrectRationale); + expect(controller.data.algo).toBe(mockConfig.data.algo); + expect(controller.data.seeds).toBe(mockConfig.data.seeds); + }); + }); }); describe('PIEdit function', function () { diff --git a/ubcpi/static/js/src/ubcpi_edit.js b/ubcpi/static/js/src/ubcpi_edit.js index 0d0eb7c..98de8a1 100644 --- a/ubcpi/static/js/src/ubcpi_edit.js +++ b/ubcpi/static/js/src/ubcpi_edit.js @@ -88,6 +88,8 @@ angular.module("ubcpi_edit", ['ngMessages', 'ngSanitize', 'ngCookies', 'gettext' self.data.correct_answer = data.correct_answer; if (data.correct_rationale) self.data.correct_rationale = data.correct_rationale; + else + self.data.correct_rationale = {"text": ""}; self.data.algo = data.algo; self.data.seeds = data.seeds; diff --git a/ubcpi/test/test_answer_pool.py b/ubcpi/test/test_answer_pool.py index 6b80f2a..a2bfca0 100644 --- a/ubcpi/test/test_answer_pool.py +++ b/ubcpi/test/test_answer_pool.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import unittest from ddt import file_data, ddt @@ -6,6 +7,7 @@ validate_seeded_answers_random, validate_seeded_answers, get_other_answers, get_other_answers_simple, \ get_other_answers_random, get_max_size, POOL_ITEM_LENGTH_SIMPLE, refresh_answers from ubcpi.persistence import Answers, VOTE_KEY, RATIONALE_KEY +from six.moves import range @ddt @@ -38,9 +40,9 @@ def test_simple_algo_insert_max(self): def test_simple_algo_drop_from_pool(self): options = ['optionA', 'optionB', 'optionC'] - pool = {'optionA': {i: {} for i in xrange(6)}} - with patch('random.choice', return_value="0"): - with patch('ubcpi.answer_pool.get_max_size', return_value="6"): + pool = {'optionA': {i: {} for i in range(6)}} + with patch('random.choice', return_value=0): + with patch('ubcpi.answer_pool.get_max_size', return_value=6): offer_answer(pool, options[0], "some rationale", "test student 7", {'name': 'simple'}, options) # make sure student "0" for optionA is removed @@ -153,7 +155,7 @@ def test_offer_answer_invalid_algo(self): def test_get_max_size(self): self.assertEqual(get_max_size({}, 3, POOL_ITEM_LENGTH_SIMPLE), 102) self.assertEqual(get_max_size({}, 10, POOL_ITEM_LENGTH_SIMPLE), 67) - self.assertEqual(get_max_size({1: {i: {} for i in xrange(10)}}, 10, POOL_ITEM_LENGTH_SIMPLE), 62) + self.assertEqual(get_max_size({1: {i: {} for i in range(10)}}, 10, POOL_ITEM_LENGTH_SIMPLE), 62) @patch( 'ubcpi.persistence.get_answers_for_student', diff --git a/ubcpi/test/test_lms.py b/ubcpi/test/test_lms.py index c615f3c..4fe3824 100644 --- a/ubcpi/test/test_lms.py +++ b/ubcpi/test/test_lms.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import json import os @@ -9,6 +10,7 @@ from ubcpi.persistence import Answers, VOTE_KEY, RATIONALE_KEY from ubcpi.ubcpi import truncate_rationale, MAX_RATIONALE_SIZE_IN_EVENT +import six @ddt @@ -50,7 +52,8 @@ def test_render_student_view_with_revised_answer(self, xblock, mock): def test_submit_answer(self, xblock, data, mock): # patch get_other_answers to avoid randomness mock.return_value = data['expect1']['other_answers'] - resp = self.request(xblock, 'submit_answer', json.dumps(data['post1']), response_format='json') + resp = self.request(xblock, 'submit_answer', json.dumps(data['post1']).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, data['expect1']) # check the student is recorded @@ -69,7 +72,8 @@ def test_submit_answer(self, xblock, data, mock): # submit revised answer - resp = self.request(xblock, 'submit_answer', json.dumps(data['post2']), response_format='json') + resp = self.request(xblock, 'submit_answer', json.dumps(data['post2']).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, data['expect2']) # check the student is recorded @@ -92,21 +96,22 @@ def test_submit_answer(self, xblock, data, mock): @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml'), user_id='Bob') def test_submit_answer_errors(self, xblock, data): with self.assertRaises(PermissionDenied): - self.request(xblock, 'submit_answer', json.dumps(data['post1']), response_format='json') + self.request(xblock, 'submit_answer', json.dumps(data['post1']).encode('utf8')) @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml'), user_id='Bob') def test_get_stats(self, xblock): stats = {"revised": {"1": 1}, "original": {"0": 1}} xblock.stats = stats - resp = self.request(xblock, 'get_stats', '{}', response_format='json') + resp = self.request(xblock, 'get_stats', b'{}') + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, stats) @patch('ubcpi.ubcpi.PeerInstructionXBlock.resource_string') @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml'), user_id='Bob') def test_get_asset(self, xblock, mock): mock.return_value = 'test' - resp = self.request(xblock, 'get_asset', 'f=test.html', request_method='POST') - self.assertEqual(resp, 'test') + resp = self.request(xblock, 'get_asset', b'f=test.html', request_method='POST') + self.assertEqual(resp, b'test') @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml'), user_id='Bob') def test_get_student_item_dict(self, xblock): @@ -208,7 +213,7 @@ def test_truncate_rationale(self): self.assertTrue(was_truncated) def check_fields(self, xblock, data): - for key, value in data.iteritems(): + for key, value in six.iteritems(data): self.assertIsNotNone(getattr(xblock, key)) self.assertEqual(getattr(xblock, key), value) @@ -218,24 +223,29 @@ def check_fields(self, xblock, data): def test_refresh_other_answers(self, xblock, data, mock): # patch get_other_answers to avoid randomness mock.return_value = data['expect']['other_answers'] - resp = self.request(xblock, 'submit_answer', json.dumps(data['submit_answer_param']), response_format='json') + resp = self.request(xblock, 'submit_answer', json.dumps(data['submit_answer_param']).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, data['expect']) original_other_ans = [ans['rationale'] for ans in xblock.other_answers_shown['answers'] if ans['option'] == int(data['refresh_params']['option'])] - resp = self.request(xblock, 'refresh_other_answers', json.dumps({}), response_format='json') + resp = self.request(xblock, 'refresh_other_answers', json.dumps({}).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, {'error': 'Missing option'}) - resp = self.request(xblock, 'refresh_other_answers', json.dumps({'option': -1}), response_format='json') + resp = self.request(xblock, 'refresh_other_answers', json.dumps({'option': -1}).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(resp, {'error': 'Invalid option'}) - resp = self.request(xblock, 'refresh_other_answers', json.dumps(data['refresh_params']), response_format='json') + resp = self.request(xblock, 'refresh_other_answers', json.dumps(data['refresh_params']).encode('utf8')) + resp = json.loads(resp.decode('utf8')) self.assertEqual(xblock.other_answers_refresh_count[data['refresh_params']['option']] , 1) self.assertEqual(len(xblock.other_answers_shown_history), 3) refreshed_other_ans = [ans['rationale'] for ans in xblock.other_answers_shown['answers'] if ans['option'] == int(data['refresh_params']['option'])] # rationale of the refreshed option should be chanaged self.assertNotEqual(original_other_ans, refreshed_other_ans) - resp = self.request(xblock, 'refresh_other_answers', json.dumps(data['refresh_params']), response_format='json') + resp = self.request(xblock, 'refresh_other_answers', json.dumps(data['refresh_params']).encode('utf8')) + resp = json.loads(resp.decode('utf8')) # refresh count of the option should be increased self.assertEqual(xblock.other_answers_refresh_count[data['refresh_params']['option']] , 2) self.assertEqual(len(xblock.other_answers_shown_history), 3) diff --git a/ubcpi/test/test_serialize.py b/ubcpi/test/test_serialize.py index ab4fb8b..06f18c3 100644 --- a/ubcpi/test/test_serialize.py +++ b/ubcpi/test/test_serialize.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from collections import namedtuple from doctest import Example @@ -87,15 +88,15 @@ def test_parse_from_xml_errors(self, data): def test_serialize_image(self, data): root = etree.Element('option') serialize_image(data['expect'], root) - self.assertXmlEqual(etree.tostring(root), "".join(data['xml'])) + self.assertXmlEqual(etree.tostring(root).decode('utf-8'), "".join(data['xml'])) @file_data('data/parse_options_xml.json') def test_serialize_options(self, data): - xblock_class = namedtuple('PeerInstructionXBlock', data['expect'].keys()) + xblock_class = namedtuple('PeerInstructionXBlock', list(data['expect'].keys())) block = xblock_class(**data['expect']) root = etree.Element('options') serialize_options(root, block) - self.assertXmlEqual(etree.tostring(root), "".join(data['xml'])) + self.assertXmlEqual(etree.tostring(root).decode('utf-8'), "".join(data['xml'])) @file_data('data/parse_seeds_xml.json') def test_serialize_seeds(self, data): @@ -103,12 +104,12 @@ def test_serialize_seeds(self, data): block = xblock_class(seeds=data['expect']) root = etree.Element('seeds') serialize_seeds(root, block) - self.assertXmlEqual(etree.tostring(root), "".join(data['xml'])) + self.assertXmlEqual(etree.tostring(root).decode('utf-8'), "".join(data['xml'])) @file_data('data/parse_from_xml.json') def test_serialize_to_xml(self, data): - xblock_class = namedtuple('PeerInstructionXBlock', data['expect'].keys()) + xblock_class = namedtuple('PeerInstructionXBlock', list(data['expect'].keys())) block = xblock_class(**data['expect']) root = etree.Element('ubcpi') serialize_to_xml(root, block) - self.assertXmlEqual(etree.tostring(root), "".join(data['xml'])) + self.assertXmlEqual(etree.tostring(root).decode('utf-8'), "".join(data['xml'])) diff --git a/ubcpi/test/test_studio.py b/ubcpi/test/test_studio.py index 459c796..e21aa33 100644 --- a/ubcpi/test/test_studio.py +++ b/ubcpi/test/test_studio.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import json import os from lxml import etree @@ -6,6 +7,7 @@ from mock import MagicMock, Mock from workbench.test_utils import scenario, XBlockHandlerTestCaseMixin from ubcpi.ubcpi import PeerInstructionXBlock +import six @ddt @@ -20,21 +22,23 @@ def test_render_studio_view(self, xblock): def test_studio_submit(self, xblock, data): xblock.runtime.modulestore = MagicMock() xblock.runtime.modulestore.has_published_version.return_value = False - resp = self.request(xblock, 'studio_submit', json.dumps(data), response_format='json') - self.assertTrue(resp['success'], msg=resp.get('msg')) + resp = self.request(xblock, 'studio_submit', json.dumps(data).encode('utf8')) + resp_json = json.loads(resp.decode('utf8')) + self.assertTrue(resp_json['success'], msg=resp_json.get('msg')) self.check_fields(xblock, data) @file_data('data/validate_form.json') @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml')) def test_validate_form(self, xblock, data): - resp = self.request(xblock, 'validate_form', json.dumps(data), response_format='json') - self.assertTrue(resp['success'], msg=resp.get('msg')) + resp = self.request(xblock, 'validate_form', json.dumps(data).encode('utf8')) + resp_json = json.loads(resp.decode('utf8')) + self.assertTrue(resp_json['success'], msg=resp_json.get('msg')) @file_data('data/validate_form_errors.json') @scenario(os.path.join(os.path.dirname(__file__), 'data/basic_scenario.xml')) def test_validate_form_errors(self, xblock, data): - resp = self.request(xblock, 'validate_form', json.dumps(data['post']), response_format='json') - self.assertEqual(resp, data['error']) + resp = self.request(xblock, 'validate_form', json.dumps(data['post']).encode('utf8')) + self.assertEqual(json.loads(resp.decode('utf8')), data['error']) @file_data('data/parse_from_xml.json') def test_parse_xml(self, data): @@ -45,6 +49,6 @@ def test_parse_xml(self, data): self.check_fields(xblock, data['expect']) def check_fields(self, xblock, data): - for key, value in data.iteritems(): + for key, value in six.iteritems(data): self.assertIsNotNone(getattr(xblock, key)) self.assertEqual(getattr(xblock, key), value) diff --git a/ubcpi/ubcpi.py b/ubcpi/ubcpi.py index fb7294d..f423256 100644 --- a/ubcpi/ubcpi.py +++ b/ubcpi/ubcpi.py @@ -1,4 +1,5 @@ """A Peer Instruction tool for edX by the University of British Columbia.""" +from __future__ import absolute_import import os import random from copy import deepcopy @@ -13,13 +14,14 @@ # For supporting manual revision of scores. Commented out for now. # from xblock.scorable import ScorableXBlockMixin, Score from xblock.fields import Scope, String, List, Dict, Integer, DateTime, Float -from xblock.fragment import Fragment +from web_fragments.fragment import Fragment from xblockutils.publish_event import PublishEventMixin from .utils import _, get_language # pylint: disable=unused-import -from answer_pool import offer_answer, validate_seeded_answers, get_other_answers, get_other_answers_count, refresh_answers -import persistence as sas_api -from serialize import parse_from_xml, serialize_to_xml +from .answer_pool import offer_answer, validate_seeded_answers, get_other_answers, get_other_answers_count, refresh_answers +from . import persistence as sas_api +from .serialize import parse_from_xml, serialize_to_xml +import six STATUS_NEW = 0 STATUS_ANSWERED = 1 @@ -29,7 +31,7 @@ # of 64k in size. Because we are storing rationale and revised rationale both in the the field, the max size # for the rationale is half MAX_RATIONALE_SIZE = 32000 -MAX_RATIONALE_SIZE_IN_EVENT = settings.TRACK_MAX_EVENT / 4 +MAX_RATIONALE_SIZE_IN_EVENT = settings.TRACK_MAX_EVENT // 4 # max number of times the student can refresh to see other student answers shown to them. # afterward, will fallback to only return seeded answers @@ -48,7 +50,7 @@ def truncate_rationale(rationale, max_length=MAX_RATIONALE_SIZE_IN_EVENT): was_truncated (bool): returns true if the rationale is truncated """ - if isinstance(rationale, basestring) and max_length is not None and len(rationale) > max_length: + if isinstance(rationale, six.string_types) and max_length is not None and len(rationale) > max_length: return rationale[0:max_length], True else: return rationale, False @@ -117,7 +119,7 @@ def get_student_item_dict(self, anonymous_user_id=None): if self.scope_ids.user_id is None: student_id = '' else: - student_id = unicode(self.scope_ids.user_id) + student_id = six.text_type(self.scope_ids.user_id) student_item_dict = dict( student_id=student_id, @@ -164,7 +166,7 @@ def _serialize_opaque_key(self, key): if hasattr(key, 'to_deprecated_string'): return key.to_deprecated_string() else: - return unicode(key) + return six.text_type(key) @XBlock.needs('user') @@ -469,7 +471,7 @@ def serialize_asset_key_with_slash(asset_key): Args: asset_key (str): Asset key to generate URL """ - url = unicode(asset_key) + url = six.text_type(asset_key) if not url.startswith('/'): url = '/' + url return url @@ -644,8 +646,8 @@ def get_current_stats(self): """ # convert key into integers as json.dump and json.load convert integer dictionary key into string self.stats = { - 'original': {int(k): v for k, v in self.stats['original'].iteritems()}, - 'revised': {int(k): v for k, v in self.stats['revised'].iteritems()} + 'original': {int(k): v for k, v in six.iteritems(self.stats['original'])}, + 'revised': {int(k): v for k, v in six.iteritems(self.stats['revised'])} } return self.stats @@ -775,7 +777,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): # TODO: more validation - for key, value in config.iteritems(): + for key, value in six.iteritems(config): setattr(block, key, value) return block