Skip to content

Commit

Permalink
Python3 migration
Browse files Browse the repository at this point in the history
- cherry-picked changes submitted by PR #167 on the old UI for py3 migration
- upgrade Coverage.py for it to work with python 3.8
  • Loading branch information
kitsook committed May 19, 2020
1 parent a3d195c commit 3cb4e13
Show file tree
Hide file tree
Showing 19 changed files with 180 additions and 81 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
@@ -1,6 +1,10 @@
language: python
python:
- "2.7"
- "3.5"
- "3.6"
- "3.7"
- "3.8"

install:
- "gem install coveralls-lcov"
Expand Down
1 change: 1 addition & 0 deletions manage.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
from __future__ import absolute_import
import sys
import os

Expand Down
5 changes: 3 additions & 2 deletions 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
8 changes: 4 additions & 4 deletions 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
Expand All @@ -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
4 changes: 2 additions & 2 deletions 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
12 changes: 6 additions & 6 deletions 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
5 changes: 1 addition & 4 deletions settings/base.py
Expand Up @@ -2,6 +2,7 @@
Base settings for UBCPI.
"""

from __future__ import absolute_import
import os
import sys

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions settings/test.py
Expand Up @@ -3,6 +3,7 @@
"""

# Inherit from base settings
from __future__ import absolute_import
from .base import * # pylint:disable=W0614,W0401

TEST_APPS = (
Expand Down
1 change: 1 addition & 0 deletions setup.py
@@ -1,5 +1,6 @@
"""Setup for ubcpi XBlock."""

from __future__ import absolute_import
import os
from setuptools import setup

Expand Down
23 changes: 13 additions & 10 deletions 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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions ubcpi/persistence.py
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from submissions import api as sub_api

"""
Expand Down
24 changes: 13 additions & 11 deletions 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'}

Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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', '')


Expand All @@ -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
Expand Down
69 changes: 68 additions & 1 deletion ubcpi/static/js/spec/ubcpi_edit_spec.js
Expand Up @@ -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 () {
Expand Down
2 changes: 2 additions & 0 deletions ubcpi/static/js/src/ubcpi_edit.js
Expand Up @@ -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;

Expand Down
10 changes: 6 additions & 4 deletions ubcpi/test/test_answer_pool.py
@@ -1,3 +1,4 @@
from __future__ import absolute_import
import unittest
from ddt import file_data, ddt

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 3cb4e13

Please sign in to comment.