Commit 420b0920 by Adam

Merge pull request #467 from edx/fix/adam/file-upload

Fix/adam/file upload
parents 86ee2bca 2b404622
import json import json
from datetime import datetime from datetime import datetime
from pytz import UTC
from django.http import HttpResponse from django.http import HttpResponse
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from dogapi import dog_stats_api from dogapi import dog_stats_api
@dog_stats_api.timed('edxapp.heartbeat') @dog_stats_api.timed('edxapp.heartbeat')
def heartbeat(request): def heartbeat(request):
""" """
Simple view that a loadbalancer can check to verify that the app is up Simple view that a loadbalancer can check to verify that the app is up
""" """
output = { output = {
'date': datetime.now().isoformat(), 'date': datetime.now(UTC).isoformat(),
'courses': [course.location.url() for course in modulestore().get_courses()], 'courses': [course.location.url() for course in modulestore().get_courses()],
} }
return HttpResponse(json.dumps(output, indent=4)) return HttpResponse(json.dumps(output, indent=4))
...@@ -69,30 +69,33 @@ class UserProfile(models.Model): ...@@ -69,30 +69,33 @@ class UserProfile(models.Model):
location = models.CharField(blank=True, max_length=255, db_index=True) location = models.CharField(blank=True, max_length=255, db_index=True)
# Optional demographic data we started capturing from Fall 2012 # Optional demographic data we started capturing from Fall 2012
this_year = datetime.now().year this_year = datetime.now(UTC).year
VALID_YEARS = range(this_year, this_year - 120, -1) VALID_YEARS = range(this_year, this_year - 120, -1)
year_of_birth = models.IntegerField(blank=True, null=True, db_index=True) year_of_birth = models.IntegerField(blank=True, null=True, db_index=True)
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other')) GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True, gender = models.CharField(
choices=GENDER_CHOICES) blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES
)
# [03/21/2013] removed these, but leaving comment since there'll still be # [03/21/2013] removed these, but leaving comment since there'll still be
# p_se and p_oth in the existing data in db. # p_se and p_oth in the existing data in db.
# ('p_se', 'Doctorate in science or engineering'), # ('p_se', 'Doctorate in science or engineering'),
# ('p_oth', 'Doctorate in another field'), # ('p_oth', 'Doctorate in another field'),
LEVEL_OF_EDUCATION_CHOICES = (('p', 'Doctorate'), LEVEL_OF_EDUCATION_CHOICES = (
('m', "Master's or professional degree"), ('p', 'Doctorate'),
('b', "Bachelor's degree"), ('m', "Master's or professional degree"),
('a', "Associate's degree"), ('b', "Bachelor's degree"),
('hs', "Secondary/high school"), ('a', "Associate's degree"),
('jhs', "Junior secondary/junior high/middle school"), ('hs', "Secondary/high school"),
('el', "Elementary/primary school"), ('jhs', "Junior secondary/junior high/middle school"),
('none', "None"), ('el', "Elementary/primary school"),
('other', "Other")) ('none', "None"),
('other', "Other")
)
level_of_education = models.CharField( level_of_education = models.CharField(
blank=True, null=True, max_length=6, db_index=True, blank=True, null=True, max_length=6, db_index=True,
choices=LEVEL_OF_EDUCATION_CHOICES choices=LEVEL_OF_EDUCATION_CHOICES
) )
mailing_address = models.TextField(blank=True, null=True) mailing_address = models.TextField(blank=True, null=True)
goals = models.TextField(blank=True, null=True) goals = models.TextField(blank=True, null=True)
allow_certificate = models.BooleanField(default=1) allow_certificate = models.BooleanField(default=1)
...@@ -307,18 +310,18 @@ class TestCenterUserForm(ModelForm): ...@@ -307,18 +310,18 @@ class TestCenterUserForm(ModelForm):
ACCOMMODATION_REJECTED_CODE = 'NONE' ACCOMMODATION_REJECTED_CODE = 'NONE'
ACCOMMODATION_CODES = ( ACCOMMODATION_CODES = (
(ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'), (ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'),
('EQPMNT', 'Equipment'), ('EQPMNT', 'Equipment'),
('ET12ET', 'Extra Time - 1/2 Exam Time'), ('ET12ET', 'Extra Time - 1/2 Exam Time'),
('ET30MN', 'Extra Time - 30 Minutes'), ('ET30MN', 'Extra Time - 30 Minutes'),
('ETDBTM', 'Extra Time - Double Time'), ('ETDBTM', 'Extra Time - Double Time'),
('SEPRMM', 'Separate Room'), ('SEPRMM', 'Separate Room'),
('SRREAD', 'Separate Room and Reader'), ('SRREAD', 'Separate Room and Reader'),
('SRRERC', 'Separate Room and Reader/Recorder'), ('SRRERC', 'Separate Room and Reader/Recorder'),
('SRRECR', 'Separate Room and Recorder'), ('SRRECR', 'Separate Room and Recorder'),
('SRSEAN', 'Separate Room and Service Animal'), ('SRSEAN', 'Separate Room and Service Animal'),
('SRSGNR', 'Separate Room and Sign Language Interpreter'), ('SRSGNR', 'Separate Room and Sign Language Interpreter'),
) )
ACCOMMODATION_CODE_DICT = {code: name for (code, name) in ACCOMMODATION_CODES} ACCOMMODATION_CODE_DICT = {code: name for (code, name) in ACCOMMODATION_CODES}
...@@ -572,7 +575,6 @@ class TestCenterRegistrationForm(ModelForm): ...@@ -572,7 +575,6 @@ class TestCenterRegistrationForm(ModelForm):
return code return code
def get_testcenter_registration(user, course_id, exam_series_code): def get_testcenter_registration(user, course_id, exam_series_code):
try: try:
tcu = TestCenterUser.objects.get(user=user) tcu = TestCenterUser.objects.get(user=user)
......
...@@ -111,9 +111,9 @@ def get_date_for_press(publish_date): ...@@ -111,9 +111,9 @@ def get_date_for_press(publish_date):
# strip off extra months, and just use the first: # strip off extra months, and just use the first:
date = re.sub(multimonth_pattern, ", ", publish_date) date = re.sub(multimonth_pattern, ", ", publish_date)
if re.search(day_pattern, date): if re.search(day_pattern, date):
date = datetime.datetime.strptime(date, "%B %d, %Y") date = datetime.datetime.strptime(date, "%B %d, %Y").replace(tzinfo=UTC)
else: else:
date = datetime.datetime.strptime(date, "%B, %Y") date = datetime.datetime.strptime(date, "%B, %Y").replace(tzinfo=UTC)
return date return date
...@@ -1100,7 +1100,7 @@ def confirm_email_change(request, key): ...@@ -1100,7 +1100,7 @@ def confirm_email_change(request, key):
meta = up.get_meta() meta = up.get_meta()
if 'old_emails' not in meta: if 'old_emails' not in meta:
meta['old_emails'] = [] meta['old_emails'] = []
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()]) meta['old_emails'].append([user.email, datetime.datetime.now(UTC).isoformat()])
up.set_meta(meta) up.set_meta(meta)
up.save() up.save()
# Send it to the old email... # Send it to the old email...
...@@ -1198,7 +1198,7 @@ def accept_name_change_by_id(id): ...@@ -1198,7 +1198,7 @@ def accept_name_change_by_id(id):
meta = up.get_meta() meta = up.get_meta()
if 'old_names' not in meta: if 'old_names' not in meta:
meta['old_names'] = [] meta['old_names'] = []
meta['old_names'].append([up.name, pnc.rationale, datetime.datetime.now().isoformat()]) meta['old_names'].append([up.name, pnc.rationale, datetime.datetime.now(UTC).isoformat()])
up.set_meta(meta) up.set_meta(meta)
up.name = pnc.new_name up.name = pnc.new_name
......
...@@ -32,6 +32,8 @@ import capa.xqueue_interface as xqueue_interface ...@@ -32,6 +32,8 @@ import capa.xqueue_interface as xqueue_interface
import capa.responsetypes as responsetypes import capa.responsetypes as responsetypes
from capa.safe_exec import safe_exec from capa.safe_exec import safe_exec
from pytz import UTC
# dict of tagname, Response Class -- this should come from auto-registering # dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__]) response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
...@@ -42,13 +44,22 @@ solution_tags = ['solution'] ...@@ -42,13 +44,22 @@ solution_tags = ['solution']
response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
# special problem tags which should be turned into innocuous HTML # special problem tags which should be turned into innocuous HTML
html_transforms = {'problem': {'tag': 'div'}, html_transforms = {
'text': {'tag': 'span'}, 'problem': {'tag': 'div'},
'math': {'tag': 'span'}, 'text': {'tag': 'span'},
} 'math': {'tag': 'span'},
}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"] html_problem_semantics = [
"codeparam",
"responseparam",
"answer",
"script",
"hintgroup",
"openendedparam",
"openendedrubric"
]
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -242,11 +253,15 @@ class LoncapaProblem(object): ...@@ -242,11 +253,15 @@ class LoncapaProblem(object):
return None return None
# Get a list of timestamps of all queueing requests, then convert it to a DateTime object # Get a list of timestamps of all queueing requests, then convert it to a DateTime object
queuetime_strs = [self.correct_map.get_queuetime_str(answer_id) queuetime_strs = [
for answer_id in self.correct_map self.correct_map.get_queuetime_str(answer_id)
if self.correct_map.is_queued(answer_id)] for answer_id in self.correct_map
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) if self.correct_map.is_queued(answer_id)
for qt_str in queuetime_strs] ]
queuetimes = [
datetime.strptime(qt_str, xqueue_interface.dateformat).replace(tzinfo=UTC)
for qt_str in queuetime_strs
]
return max(queuetimes) return max(queuetimes)
...@@ -404,10 +419,16 @@ class LoncapaProblem(object): ...@@ -404,10 +419,16 @@ class LoncapaProblem(object):
# open using ModuleSystem OSFS filestore # open using ModuleSystem OSFS filestore
ifp = self.system.filestore.open(filename) ifp = self.system.filestore.open(filename)
except Exception as err: except Exception as err:
log.warning('Error %s in problem xml include: %s' % ( log.warning(
err, etree.tostring(inc, pretty_print=True))) 'Error %s in problem xml include: %s' % (
log.warning('Cannot find file %s in %s' % ( err, etree.tostring(inc, pretty_print=True)
filename, self.system.filestore)) )
)
log.warning(
'Cannot find file %s in %s' % (
filename, self.system.filestore
)
)
# if debugging, don't fail - just log error # if debugging, don't fail - just log error
# TODO (vshnayder): need real error handling, display to users # TODO (vshnayder): need real error handling, display to users
if not self.system.get('DEBUG'): if not self.system.get('DEBUG'):
...@@ -418,8 +439,11 @@ class LoncapaProblem(object): ...@@ -418,8 +439,11 @@ class LoncapaProblem(object):
# read in and convert to XML # read in and convert to XML
incxml = etree.XML(ifp.read()) incxml = etree.XML(ifp.read())
except Exception as err: except Exception as err:
log.warning('Error %s in problem xml include: %s' % ( log.warning(
err, etree.tostring(inc, pretty_print=True))) 'Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)
)
)
log.warning('Cannot parse XML in %s' % (filename)) log.warning('Cannot parse XML in %s' % (filename))
# if debugging, don't fail - just log error # if debugging, don't fail - just log error
# TODO (vshnayder): same as above # TODO (vshnayder): same as above
...@@ -579,8 +603,9 @@ class LoncapaProblem(object): ...@@ -579,8 +603,9 @@ class LoncapaProblem(object):
# let each Response render itself # let each Response render itself
if problemtree in self.responders: if problemtree in self.responders:
overall_msg = self.correct_map.get_overall_message() overall_msg = self.correct_map.get_overall_message()
return self.responders[problemtree].render_html(self._extract_html, return self.responders[problemtree].render_html(
response_msg=overall_msg) self._extract_html, response_msg=overall_msg
)
# let each custom renderer render itself: # let each custom renderer render itself:
if problemtree.tag in customrender.registry.registered_tags(): if problemtree.tag in customrender.registry.registered_tags():
...@@ -628,9 +653,10 @@ class LoncapaProblem(object): ...@@ -628,9 +653,10 @@ class LoncapaProblem(object):
answer_id = 1 answer_id = 1
input_tags = inputtypes.registry.registered_tags() input_tags = inputtypes.registry.registered_tags()
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x inputfields = tree.xpath(
for x in (input_tags + solution_tags)]), "|".join(['//' + response.tag + '[@id=$id]//' + x for x in (input_tags + solution_tags)]),
id=response_id_str) id=response_id_str
)
# assign one answer_id for each input type or solution type # assign one answer_id for each input type or solution type
for entry in inputfields: for entry in inputfields:
......
...@@ -37,23 +37,27 @@ class CorrectMap(object): ...@@ -37,23 +37,27 @@ class CorrectMap(object):
return self.cmap.__iter__() return self.cmap.__iter__()
# See the documentation for 'set_dict' for the use of kwargs # See the documentation for 'set_dict' for the use of kwargs
def set(self, def set(
answer_id=None, self,
correctness=None, answer_id=None,
npoints=None, correctness=None,
msg='', npoints=None,
hint='', msg='',
hintmode=None, hint='',
queuestate=None, **kwargs): hintmode=None,
queuestate=None,
**kwargs
):
if answer_id is not None: if answer_id is not None:
self.cmap[str(answer_id)] = {'correctness': correctness, self.cmap[str(answer_id)] = {
'npoints': npoints, 'correctness': correctness,
'msg': msg, 'npoints': npoints,
'hint': hint, 'msg': msg,
'hintmode': hintmode, 'hint': hint,
'queuestate': queuestate, 'hintmode': hintmode,
} 'queuestate': queuestate,
}
def __repr__(self): def __repr__(self):
return repr(self.cmap) return repr(self.cmap)
......
...@@ -33,6 +33,7 @@ from shapely.geometry import Point, MultiPoint ...@@ -33,6 +33,7 @@ from shapely.geometry import Point, MultiPoint
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from . import correctmap from . import correctmap
from datetime import datetime from datetime import datetime
from pytz import UTC
from .util import * from .util import *
from lxml import etree from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
...@@ -1365,9 +1366,11 @@ class CodeResponse(LoncapaResponse): ...@@ -1365,9 +1366,11 @@ class CodeResponse(LoncapaResponse):
# Note that submission can be a file # Note that submission can be a file
submission = student_answers[self.answer_id] submission = student_answers[self.answer_id]
except Exception as err: except Exception as err:
log.error('Error in CodeResponse %s: cannot get student answer for %s;' log.error(
' student_answers=%s' % 'Error in CodeResponse %s: cannot get student answer for %s;'
(err, self.answer_id, convert_files_to_filenames(student_answers))) ' student_answers=%s' %
(err, self.answer_id, convert_files_to_filenames(student_answers))
)
raise Exception(err) raise Exception(err)
# We do not support xqueue within Studio. # We do not support xqueue within Studio.
...@@ -1381,19 +1384,20 @@ class CodeResponse(LoncapaResponse): ...@@ -1381,19 +1384,20 @@ class CodeResponse(LoncapaResponse):
#------------------------------------------------------------ #------------------------------------------------------------
qinterface = self.system.xqueue['interface'] qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
anonymous_student_id = self.system.anonymous_student_id anonymous_student_id = self.system.anonymous_student_id
# Generate header # Generate header
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + queuekey = xqueue_interface.make_hashkey(
anonymous_student_id + str(self.system.seed) + qtime + anonymous_student_id + self.answer_id
self.answer_id) )
callback_url = self.system.xqueue['construct_callback']() callback_url = self.system.xqueue['construct_callback']()
xheader = xqueue_interface.make_xheader( xheader = xqueue_interface.make_xheader(
lms_callback_url=callback_url, lms_callback_url=callback_url,
lms_key=queuekey, lms_key=queuekey,
queue_name=self.queue_name) queue_name=self.queue_name
)
# Generate body # Generate body
if is_list_of_files(submission): if is_list_of_files(submission):
...@@ -1406,9 +1410,10 @@ class CodeResponse(LoncapaResponse): ...@@ -1406,9 +1410,10 @@ class CodeResponse(LoncapaResponse):
# Metadata related to the student submission revealed to the external # Metadata related to the student submission revealed to the external
# grader # grader
student_info = {'anonymous_student_id': anonymous_student_id, student_info = {
'submission_time': qtime, 'anonymous_student_id': anonymous_student_id,
} 'submission_time': qtime,
}
contents.update({'student_info': json.dumps(student_info)}) contents.update({'student_info': json.dumps(student_info)})
# Submit request. When successful, 'msg' is the prior length of the # Submit request. When successful, 'msg' is the prior length of the
......
...@@ -18,6 +18,8 @@ from capa.correctmap import CorrectMap ...@@ -18,6 +18,8 @@ from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
from pytz import UTC
class ResponseTest(unittest.TestCase): class ResponseTest(unittest.TestCase):
""" Base class for tests of capa responses.""" """ Base class for tests of capa responses."""
...@@ -333,8 +335,9 @@ class SymbolicResponseTest(ResponseTest): ...@@ -333,8 +335,9 @@ class SymbolicResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEqual(correct_map.get_correctness('1_2_1'), self.assertEqual(
expected_correctness) correct_map.get_correctness('1_2_1'), expected_correctness
)
class OptionResponseTest(ResponseTest): class OptionResponseTest(ResponseTest):
...@@ -702,7 +705,7 @@ class CodeResponseTest(ResponseTest): ...@@ -702,7 +705,7 @@ class CodeResponseTest(ResponseTest):
# Now we queue the LCP # Now we queue the LCP
cmap = CorrectMap() cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids): for i, answer_id in enumerate(answer_ids):
queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) queuestate = CodeResponseTest.make_queuestate(i, datetime.now(UTC))
cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
self.problem.correct_map.update(cmap) self.problem.correct_map.update(cmap)
...@@ -718,7 +721,7 @@ class CodeResponseTest(ResponseTest): ...@@ -718,7 +721,7 @@ class CodeResponseTest(ResponseTest):
old_cmap = CorrectMap() old_cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids): for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i queuekey = 1000 + i
queuestate = CodeResponseTest.make_queuestate(queuekey, datetime.now()) queuestate = CodeResponseTest.make_queuestate(queuekey, datetime.now(UTC))
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
# Message format common to external graders # Message format common to external graders
...@@ -778,13 +781,15 @@ class CodeResponseTest(ResponseTest): ...@@ -778,13 +781,15 @@ class CodeResponseTest(ResponseTest):
cmap = CorrectMap() cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids): for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i queuekey = 1000 + i
latest_timestamp = datetime.now() latest_timestamp = datetime.now(UTC)
queuestate = CodeResponseTest.make_queuestate(queuekey, latest_timestamp) queuestate = CodeResponseTest.make_queuestate(queuekey, latest_timestamp)
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate)) cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
self.problem.correct_map.update(cmap) self.problem.correct_map.update(cmap)
# Queue state only tracks up to second # Queue state only tracks up to second
latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp, dateformat), dateformat) latest_timestamp = datetime.strptime(
datetime.strftime(latest_timestamp, dateformat), dateformat
).replace(tzinfo=UTC)
self.assertEquals(self.problem.get_recentmost_queuetime(), latest_timestamp) self.assertEquals(self.problem.get_recentmost_queuetime(), latest_timestamp)
......
...@@ -30,9 +30,11 @@ def make_xheader(lms_callback_url, lms_key, queue_name): ...@@ -30,9 +30,11 @@ def make_xheader(lms_callback_url, lms_key, queue_name):
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string) 'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
} }
""" """
return json.dumps({'lms_callback_url': lms_callback_url, return json.dumps({
'lms_key': lms_key, 'lms_callback_url': lms_callback_url,
'queue_name': queue_name}) 'lms_key': lms_key,
'queue_name': queue_name
})
def parse_xreply(xreply): def parse_xreply(xreply):
...@@ -60,7 +62,7 @@ class XQueueInterface(object): ...@@ -60,7 +62,7 @@ class XQueueInterface(object):
''' '''
def __init__(self, url, django_auth, requests_auth=None): def __init__(self, url, django_auth, requests_auth=None):
self.url = url self.url = url
self.auth = django_auth self.auth = django_auth
self.session = requests.session(auth=requests_auth) self.session = requests.session(auth=requests_auth)
...@@ -95,13 +97,13 @@ class XQueueInterface(object): ...@@ -95,13 +97,13 @@ class XQueueInterface(object):
return (error, msg) return (error, msg)
def _login(self): def _login(self):
payload = {'username': self.auth['username'], payload = {
'password': self.auth['password']} 'username': self.auth['username'],
'password': self.auth['password']
}
return self._http_post(self.url + '/xqueue/login/', payload) return self._http_post(self.url + '/xqueue/login/', payload)
def _send_to_queue(self, header, body, files_to_upload): def _send_to_queue(self, header, body, files_to_upload):
payload = {'xqueue_header': header, payload = {'xqueue_header': header,
'xqueue_body': body} 'xqueue_body': body}
...@@ -112,7 +114,6 @@ class XQueueInterface(object): ...@@ -112,7 +114,6 @@ class XQueueInterface(object):
return self._http_post(self.url + '/xqueue/submit/', payload, files=files) return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
def _http_post(self, url, data, files=None): def _http_post(self, url, data, files=None):
try: try:
r = self.session.post(url, data=data, files=files) r = self.session.post(url, data=data, files=files)
......
...@@ -1126,8 +1126,12 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -1126,8 +1126,12 @@ class CapaDescriptor(CapaFields, RawDescriptor):
mako_template = "widgets/problem-edit.html" mako_template = "widgets/problem-edit.html"
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
js_module_name = "MarkdownEditingDescriptor" js_module_name = "MarkdownEditingDescriptor"
css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), css = {
resource_string(__name__, 'css/problem/edit.scss')]} 'scss': [
resource_string(__name__, 'css/editor/edit.scss'),
resource_string(__name__, 'css/problem/edit.scss')
]
}
# Capa modules have some additional metadata: # Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they # TODO (vshnayder): do problems have any other metadata? Do they
......
...@@ -80,6 +80,7 @@ class Date(ModelType): ...@@ -80,6 +80,7 @@ class Date(ModelType):
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType): class Timedelta(ModelType):
def from_json(self, time_str): def from_json(self, time_str):
""" """
......
...@@ -19,6 +19,7 @@ import openendedchild ...@@ -19,6 +19,7 @@ import openendedchild
from numpy import median from numpy import median
from datetime import datetime from datetime import datetime
from pytz import UTC
from .combined_open_ended_rubric import CombinedOpenEndedRubric from .combined_open_ended_rubric import CombinedOpenEndedRubric
...@@ -170,7 +171,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -170,7 +171,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if xqueue is None: if xqueue is None:
return {'success': False, 'msg': "Couldn't submit feedback."} return {'success': False, 'msg': "Couldn't submit feedback."}
qinterface = xqueue['interface'] qinterface = xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
anonymous_student_id = system.anonymous_student_id anonymous_student_id = system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime +
anonymous_student_id + anonymous_student_id +
...@@ -224,7 +225,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -224,7 +225,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if xqueue is None: if xqueue is None:
return False return False
qinterface = xqueue['interface'] qinterface = xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
anonymous_student_id = system.anonymous_student_id anonymous_student_id = system.anonymous_student_id
......
...@@ -5,6 +5,7 @@ import re ...@@ -5,6 +5,7 @@ import re
import open_ended_image_submission import open_ended_image_submission
from xmodule.progress import Progress from xmodule.progress import Progress
import capa.xqueue_interface as xqueue_interface
from capa.util import * from capa.util import *
from .peer_grading_service import PeerGradingService, MockPeerGradingService from .peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service import controller_query_service
...@@ -334,12 +335,15 @@ class OpenEndedChild(object): ...@@ -334,12 +335,15 @@ class OpenEndedChild(object):
log.exception("Could not create image and check it.") log.exception("Could not create image and check it.")
if image_ok: if image_ok:
image_key = image_data.name + datetime.now().strftime("%Y%m%d%H%M%S") image_key = image_data.name + datetime.now(UTC).strftime(
xqueue_interface.dateformat
)
try: try:
image_data.seek(0) image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key, success, s3_public_url = open_ended_image_submission.upload_to_s3(
self.s3_interface) image_data, image_key, self.s3_interface
)
except: except:
log.exception("Could not upload image to S3.") log.exception("Could not upload image to S3.")
......
...@@ -14,6 +14,7 @@ from xmodule.modulestore import Location ...@@ -14,6 +14,7 @@ from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from datetime import datetime from datetime import datetime
from pytz import UTC
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -212,7 +213,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -212,7 +213,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'submission_id': '1', 'submission_id': '1',
'grader_id': '1', 'grader_id': '1',
'score': 3} 'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id, student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime} 'submission_time': qtime}
contents = { contents = {
...@@ -233,7 +234,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -233,7 +234,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self): def test_send_to_grader(self):
submission = "This is a student submission" submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id, student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime} 'submission_time': qtime}
contents = self.openendedmodule.payload.copy() contents = self.openendedmodule.payload.copy()
...@@ -632,6 +633,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -632,6 +633,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("reset", {}) module.handle_ajax("reset", {})
self.assertEqual(module.state, "initial") self.assertEqual(module.state, "initial")
class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
""" """
Test if student is able to reset the problem Test if student is able to reset the problem
......
import os, polib import os
import polib
from unittest import TestCase from unittest import TestCase
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import UTC
import extract import extract
from config import CONFIGURATION from config import CONFIGURATION
...@@ -9,6 +11,7 @@ from config import CONFIGURATION ...@@ -9,6 +11,7 @@ from config import CONFIGURATION
# Make sure setup runs only once # Make sure setup runs only once
SETUP_HAS_RUN = False SETUP_HAS_RUN = False
class TestExtract(TestCase): class TestExtract(TestCase):
""" """
Tests functionality of i18n/extract.py Tests functionality of i18n/extract.py
...@@ -19,20 +22,20 @@ class TestExtract(TestCase): ...@@ -19,20 +22,20 @@ class TestExtract(TestCase):
# Skip this test because it takes too long (>1 minute) # Skip this test because it takes too long (>1 minute)
# TODO: figure out how to declare a "long-running" test suite # TODO: figure out how to declare a "long-running" test suite
# and add this test to it. # and add this test to it.
raise SkipTest() raise SkipTest()
global SETUP_HAS_RUN global SETUP_HAS_RUN
# Subtract 1 second to help comparisons with file-modify time succeed, # Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate # since os.path.getmtime() is not millisecond-accurate
self.start_time = datetime.now() - timedelta(seconds=1) self.start_time = datetime.now(UTC) - timedelta(seconds=1)
super(TestExtract, self).setUp() super(TestExtract, self).setUp()
if not SETUP_HAS_RUN: if not SETUP_HAS_RUN:
# Run extraction script. Warning, this takes 1 minute or more # Run extraction script. Warning, this takes 1 minute or more
extract.main() extract.main()
SETUP_HAS_RUN = True SETUP_HAS_RUN = True
def get_files (self): def get_files(self):
""" """
This is a generator. This is a generator.
Returns the fully expanded filenames for all extracted files Returns the fully expanded filenames for all extracted files
...@@ -65,19 +68,21 @@ class TestExtract(TestCase): ...@@ -65,19 +68,21 @@ class TestExtract(TestCase):
entry2.msgid = "This is not a keystring" entry2.msgid = "This is not a keystring"
self.assertTrue(extract.is_key_string(entry1.msgid)) self.assertTrue(extract.is_key_string(entry1.msgid))
self.assertFalse(extract.is_key_string(entry2.msgid)) self.assertFalse(extract.is_key_string(entry2.msgid))
def test_headers(self): def test_headers(self):
"""Verify all headers have been modified""" """Verify all headers have been modified"""
for path in self.get_files(): for path in self.get_files():
po = polib.pofile(path) po = polib.pofile(path)
header = po.header header = po.header
self.assertEqual(header.find('edX translation file'), 0, self.assertEqual(
msg='Missing header in %s:\n"%s"' % \ header.find('edX translation file'),
(os.path.basename(path), header)) 0,
msg='Missing header in %s:\n"%s"' % (os.path.basename(path), header)
)
def test_metadata(self): def test_metadata(self):
"""Verify all metadata has been modified""" """Verify all metadata has been modified"""
for path in self.get_files(): for path in self.get_files():
po = polib.pofile(path) po = polib.pofile(path)
metadata = po.metadata metadata = po.metadata
value = metadata['Report-Msgid-Bugs-To'] value = metadata['Report-Msgid-Bugs-To']
......
import os, string, random, re import os
import string
import random
import re
from polib import pofile from polib import pofile
from unittest import TestCase from unittest import TestCase
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import UTC
import generate import generate
from config import CONFIGURATION from config import CONFIGURATION
class TestGenerate(TestCase): class TestGenerate(TestCase):
""" """
Tests functionality of i18n/generate.py Tests functionality of i18n/generate.py
...@@ -15,7 +20,7 @@ class TestGenerate(TestCase): ...@@ -15,7 +20,7 @@ class TestGenerate(TestCase):
def setUp(self): def setUp(self):
# Subtract 1 second to help comparisons with file-modify time succeed, # Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate # since os.path.getmtime() is not millisecond-accurate
self.start_time = datetime.now() - timedelta(seconds=1) self.start_time = datetime.now(UTC) - timedelta(seconds=1)
def test_merge(self): def test_merge(self):
""" """
...@@ -49,7 +54,7 @@ class TestGenerate(TestCase): ...@@ -49,7 +54,7 @@ class TestGenerate(TestCase):
""" """
This is invoked by test_main to ensure that it runs after This is invoked by test_main to ensure that it runs after
calling generate.main(). calling generate.main().
There should be exactly three merge comment headers There should be exactly three merge comment headers
in our merged .po file. This counts them to be sure. in our merged .po file. This counts them to be sure.
A merge comment looks like this: A merge comment looks like this:
......
...@@ -8,6 +8,7 @@ from xmodule.course_module import CourseDescriptor ...@@ -8,6 +8,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
import datetime import datetime
from pytz import UTC
class Command(BaseCommand): class Command(BaseCommand):
...@@ -41,7 +42,6 @@ class Command(BaseCommand): ...@@ -41,7 +42,6 @@ class Command(BaseCommand):
'whose entry in the certificate table matches STATUS. ' 'whose entry in the certificate table matches STATUS. '
'STATUS can be generating, unavailable, deleted, error ' 'STATUS can be generating, unavailable, deleted, error '
'or notpassing.'), 'or notpassing.'),
) )
def handle(self, *args, **options): def handle(self, *args, **options):
...@@ -83,20 +83,20 @@ class Command(BaseCommand): ...@@ -83,20 +83,20 @@ class Command(BaseCommand):
xq = XQueueCertInterface() xq = XQueueCertInterface()
total = enrolled_students.count() total = enrolled_students.count()
count = 0 count = 0
start = datetime.datetime.now() start = datetime.datetime.now(UTC)
for student in enrolled_students: for student in enrolled_students:
count += 1 count += 1
if count % STATUS_INTERVAL == 0: if count % STATUS_INTERVAL == 0:
# Print a status update with an approximation of # Print a status update with an approximation of
# how much time is left based on how long the last # how much time is left based on how long the last
# interval took # interval took
diff = datetime.datetime.now() - start diff = datetime.datetime.now(UTC) - start
timeleft = diff * (total - count) / STATUS_INTERVAL timeleft = diff * (total - count) / STATUS_INTERVAL
hours, remainder = divmod(timeleft.seconds, 3600) hours, remainder = divmod(timeleft.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format(
count, total, hours, minutes) count, total, hours, minutes)
start = datetime.datetime.now() start = datetime.datetime.now(UTC)
if certificate_status_for_student( if certificate_status_for_student(
student, course_id)['status'] in valid_statuses: student, course_id)['status'] in valid_statuses:
......
...@@ -213,22 +213,28 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -213,22 +213,28 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
return None return None
# Setup system context for module instance # Setup system context for module instance
ajax_url = reverse('modx_dispatch', ajax_url = reverse(
kwargs=dict(course_id=course_id, 'modx_dispatch',
location=descriptor.location.url(), kwargs=dict(
dispatch=''), course_id=course_id,
) location=descriptor.location.url(),
dispatch=''
),
)
# Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash.
ajax_url = ajax_url.rstrip('/') ajax_url = ajax_url.rstrip('/')
def make_xqueue_callback(dispatch='score_update'): def make_xqueue_callback(dispatch='score_update'):
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
relative_xqueue_callback_url = reverse('xqueue_callback', relative_xqueue_callback_url = reverse(
kwargs=dict(course_id=course_id, 'xqueue_callback',
userid=str(user.id), kwargs=dict(
mod_id=descriptor.location.url(), course_id=course_id,
dispatch=dispatch), userid=str(user.id),
) mod_id=descriptor.location.url(),
dispatch=dispatch
),
)
return xqueue_callback_url_prefix + relative_xqueue_callback_url return xqueue_callback_url_prefix + relative_xqueue_callback_url
# Default queuename is course-specific and is derived from the course that # Default queuename is course-specific and is derived from the course that
...@@ -313,10 +319,12 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -313,10 +319,12 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
tags = ["org:{0}".format(org), tags = [
"course:{0}".format(course_num), "org:{0}".format(org),
"run:{0}".format(run), "course:{0}".format(course_num),
"score_bucket:{0}".format(score_bucket)] "run:{0}".format(run),
"score_bucket:{0}".format(score_bucket)
]
if grade_bucket_type is not None: if grade_bucket_type is not None:
tags.append('type:%s' % grade_bucket_type) tags.append('type:%s' % grade_bucket_type)
...@@ -326,38 +334,41 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -326,38 +334,41 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from # that the xml was loaded from
system = ModuleSystem(track_function=track_function, system = ModuleSystem(
render_template=render_to_string, track_function=track_function,
ajax_url=ajax_url, render_template=render_to_string,
xqueue=xqueue, ajax_url=ajax_url,
# TODO (cpennington): Figure out how to share info between systems xqueue=xqueue,
filestore=descriptor.system.resources_fs, # TODO (cpennington): Figure out how to share info between systems
get_module=inner_get_module, filestore=descriptor.system.resources_fs,
user=user, get_module=inner_get_module,
# TODO (cpennington): This should be removed when all html from user=user,
# a module is coming through get_html and is therefore covered # TODO (cpennington): This should be removed when all html from
# by the replace_static_urls code below # a module is coming through get_html and is therefore covered
replace_urls=partial( # by the replace_static_urls code below
static_replace.replace_static_urls, replace_urls=partial(
data_directory=getattr(descriptor, 'data_dir', None), static_replace.replace_static_urls,
course_namespace=descriptor.location._replace(category=None, name=None), data_directory=getattr(descriptor, 'data_dir', None),
), course_namespace=descriptor.location._replace(category=None, name=None),
node_path=settings.NODE_PATH, ),
xblock_model_data=xblock_model_data, node_path=settings.NODE_PATH,
publish=publish, xblock_model_data=xblock_model_data,
anonymous_student_id=unique_id_for_user(user), publish=publish,
course_id=course_id, anonymous_student_id=unique_id_for_user(user),
open_ended_grading_interface=open_ended_grading_interface, course_id=course_id,
s3_interface=s3_interface, open_ended_grading_interface=open_ended_grading_interface,
cache=cache, s3_interface=s3_interface,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), cache=cache,
) can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
)
# pass position specified in URL to module through ModuleSystem # pass position specified in URL to module through ModuleSystem
system.set('position', position) system.set('position', position)
system.set('DEBUG', settings.DEBUG) system.set('DEBUG', settings.DEBUG)
if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'):
system.set('psychometrics_handler', # set callback for updating PsychometricsData system.set(
make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) 'psychometrics_handler', # set callback for updating PsychometricsData
make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())
)
try: try:
module = descriptor.xmodule(system) module = descriptor.xmodule(system)
...@@ -381,13 +392,14 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -381,13 +392,14 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id))
_get_html = module.get_html _get_html = module.get_html
if wrap_xmodule_display == True: if wrap_xmodule_display is True:
_get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html')
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
_get_html, _get_html,
getattr(descriptor, 'data_dir', None), getattr(descriptor, 'data_dir', None),
course_namespace=module.location._replace(category=None, name=None)) course_namespace=module.location._replace(category=None, name=None)
)
# Allow URLs of the form '/course/' refer to the root of multicourse directory # Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course # hierarchy of this course
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment