Merge branch 'master' into feature/diana/close-oe-problems
Conflicts: common/lib/xmodule/xmodule/tests/test_combined_open_ended.py common/lib/xmodule/xmodule/tests/test_self_assessment.py common/lib/xmodule/xmodule/timeparse.py
This commit is contained in:
@@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
from . import app_settings
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
|
||||
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
"""
|
||||
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
||||
@@ -108,11 +109,14 @@ def instance_key(model, instance_or_pk):
|
||||
getattr(instance_or_pk, 'pk', instance_or_pk),
|
||||
)
|
||||
|
||||
|
||||
def set_cached_content(content):
|
||||
cache.set(str(content.location), content)
|
||||
|
||||
|
||||
def get_cached_content(location):
|
||||
return cache.get(str(location))
|
||||
|
||||
|
||||
def del_cached_content(location):
|
||||
cache.delete(str(location))
|
||||
|
||||
@@ -12,7 +12,7 @@ from xmodule.exceptions import NotFoundError
|
||||
class StaticContentServer(object):
|
||||
def process_request(self, request):
|
||||
# look to see if the request is prefixed with 'c4x' tag
|
||||
if request.path.startswith('/' + XASSET_LOCATION_TAG +'/'):
|
||||
if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'):
|
||||
loc = StaticContent.get_location_from_path(request.path)
|
||||
# first look in our cache so we don't have to round-trip to the DB
|
||||
content = get_cached_content(loc)
|
||||
@@ -21,7 +21,9 @@ class StaticContentServer(object):
|
||||
try:
|
||||
content = contentstore().find(loc)
|
||||
except NotFoundError:
|
||||
raise Http404
|
||||
response = HttpResponse()
|
||||
response.status_code = 404
|
||||
return response
|
||||
|
||||
# since we fetched it from DB, let's cache it going forward
|
||||
set_cached_content(content)
|
||||
|
||||
@@ -13,6 +13,7 @@ from .models import CourseUserGroup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_course_cohorted(course_id):
|
||||
"""
|
||||
Given a course id, return a boolean for whether or not the course is
|
||||
@@ -115,6 +116,7 @@ def get_course_cohorts(course_id):
|
||||
|
||||
### Helpers for cohort management views
|
||||
|
||||
|
||||
def get_cohort_by_name(course_id, name):
|
||||
"""
|
||||
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
||||
@@ -124,6 +126,7 @@ def get_cohort_by_name(course_id, name):
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
name=name)
|
||||
|
||||
|
||||
def get_cohort_by_id(course_id, cohort_id):
|
||||
"""
|
||||
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
||||
@@ -133,6 +136,7 @@ def get_cohort_by_id(course_id, cohort_id):
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
id=cohort_id)
|
||||
|
||||
|
||||
def add_cohort(course_id, name):
|
||||
"""
|
||||
Add a cohort to a course. Raises ValueError if a cohort of the same name already
|
||||
@@ -148,12 +152,14 @@ def add_cohort(course_id, name):
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
name=name)
|
||||
|
||||
|
||||
class CohortConflict(Exception):
|
||||
"""
|
||||
Raised when user to be added is already in another cohort in same course.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def add_user_to_cohort(cohort, username_or_email):
|
||||
"""
|
||||
Look up the given user, and if successful, add them to the specified cohort.
|
||||
@@ -211,4 +217,3 @@ def delete_empty_cohort(course_id, name):
|
||||
name, course_id))
|
||||
|
||||
cohort.delete()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.db import models
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseUserGroup(models.Model):
|
||||
"""
|
||||
This model represents groups of users in a course. Groups may have different types,
|
||||
@@ -30,5 +31,3 @@ class CourseUserGroup(models.Model):
|
||||
COHORT = 'cohort'
|
||||
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
|
||||
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
|
||||
# NOTE: running this with the lms.envs.test config works without
|
||||
# manually overriding the modulestore. However, running with
|
||||
# cms.envs.test doesn't.
|
||||
# cms.envs.test doesn't.
|
||||
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
return {
|
||||
@@ -28,6 +29,7 @@ def xml_store_config(data_dir):
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestCohorts(django.test.TestCase):
|
||||
|
||||
@@ -184,6 +186,3 @@ class TestCohorts(django.test.TestCase):
|
||||
self.assertTrue(
|
||||
is_commentable_cohorted(course.id, to_id("Feedback")),
|
||||
"Feedback was listed as cohorted. Should be.")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import track.views
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def json_http_response(data):
|
||||
"""
|
||||
Return an HttpResponse with the data json-serialized and the right content
|
||||
@@ -29,6 +30,7 @@ def json_http_response(data):
|
||||
"""
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
|
||||
|
||||
def split_by_comma_and_whitespace(s):
|
||||
"""
|
||||
Split a string both by commas and whitespice. Returns a list.
|
||||
@@ -177,6 +179,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
|
||||
'conflict': conflict,
|
||||
'unknown': unknown})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_POST
|
||||
def remove_user_from_cohort(request, course_id, cohort_id):
|
||||
|
||||
@@ -5,8 +5,9 @@ django admin pages for courseware model
|
||||
from external_auth.models import *
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class ExternalAuthMapAdmin(admin.ModelAdmin):
|
||||
search_fields = ['external_id','user__username']
|
||||
search_fields = ['external_id', 'user__username']
|
||||
date_hierarchy = 'dtcreated'
|
||||
|
||||
admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin)
|
||||
|
||||
@@ -12,20 +12,20 @@ file and check it in at the same time as your model changes. To do that,
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class ExternalAuthMap(models.Model):
|
||||
class Meta:
|
||||
unique_together = (('external_id', 'external_domain'), )
|
||||
external_id = models.CharField(max_length=255, db_index=True)
|
||||
external_domain = models.CharField(max_length=255, db_index=True)
|
||||
external_credentials = models.TextField(blank=True) # JSON dictionary
|
||||
external_credentials = models.TextField(blank=True) # JSON dictionary
|
||||
external_email = models.CharField(max_length=255, db_index=True)
|
||||
external_name = models.CharField(blank=True,max_length=255, db_index=True)
|
||||
external_name = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
user = models.OneToOneField(User, unique=True, db_index=True, null=True)
|
||||
internal_password = models.CharField(blank=True, max_length=31) # randomly generated
|
||||
dtcreated = models.DateTimeField('creation date',auto_now_add=True)
|
||||
dtsignup = models.DateTimeField('signup date',null=True) # set after signup
|
||||
|
||||
internal_password = models.CharField(blank=True, max_length=31) # randomly generated
|
||||
dtcreated = models.DateTimeField('creation date', auto_now_add=True)
|
||||
dtsignup = models.DateTimeField('signup date', null=True) # set after signup
|
||||
|
||||
def __unicode__(self):
|
||||
s = "[%s] = (%s / %s)" % (self.external_id, self.external_name, self.external_email)
|
||||
return s
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ from django.test import TestCase, LiveServerTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
|
||||
class MyFetcher(HTTPFetcher):
|
||||
"""A fetcher that uses server-internal calls for performing HTTP
|
||||
requests.
|
||||
requests.
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
@@ -42,7 +43,7 @@ class MyFetcher(HTTPFetcher):
|
||||
if headers and 'Accept' in headers:
|
||||
data['CONTENT_TYPE'] = headers['Accept']
|
||||
response = self.client.get(url, data)
|
||||
|
||||
|
||||
# Translate the test client response to the fetcher's HTTP response abstraction
|
||||
content = response.content
|
||||
final_url = url
|
||||
@@ -60,6 +61,7 @@ class MyFetcher(HTTPFetcher):
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
class OpenIdProviderTest(TestCase):
|
||||
|
||||
# def setUp(self):
|
||||
@@ -78,7 +80,7 @@ class OpenIdProviderTest(TestCase):
|
||||
provider_url = reverse('openid-provider-xrds')
|
||||
factory = RequestFactory()
|
||||
request = factory.request()
|
||||
abs_provider_url = request.build_absolute_uri(location = provider_url)
|
||||
abs_provider_url = request.build_absolute_uri(location=provider_url)
|
||||
|
||||
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
||||
# in the test environment, we either need a live server that works with the default
|
||||
@@ -86,10 +88,10 @@ class OpenIdProviderTest(TestCase):
|
||||
# Here we do the latter:
|
||||
fetcher = MyFetcher(self.client)
|
||||
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
||||
|
||||
|
||||
# now we can begin the login process by invoking a local openid client,
|
||||
# with a pointer to the (also-local) openid provider:
|
||||
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url):
|
||||
with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
|
||||
url = reverse('openid-login')
|
||||
resp = self.client.post(url)
|
||||
code = 200
|
||||
@@ -107,7 +109,7 @@ class OpenIdProviderTest(TestCase):
|
||||
provider_url = reverse('openid-provider-login')
|
||||
factory = RequestFactory()
|
||||
request = factory.request()
|
||||
abs_provider_url = request.build_absolute_uri(location = provider_url)
|
||||
abs_provider_url = request.build_absolute_uri(location=provider_url)
|
||||
|
||||
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
||||
# in the test environment, we either need a live server that works with the default
|
||||
@@ -115,10 +117,10 @@ class OpenIdProviderTest(TestCase):
|
||||
# Here we do the latter:
|
||||
fetcher = MyFetcher(self.client)
|
||||
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
||||
|
||||
|
||||
# now we can begin the login process by invoking a local openid client,
|
||||
# with a pointer to the (also-local) openid provider:
|
||||
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url):
|
||||
with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
|
||||
url = reverse('openid-login')
|
||||
resp = self.client.post(url)
|
||||
code = 200
|
||||
@@ -143,43 +145,43 @@ class OpenIdProviderTest(TestCase):
|
||||
self.assertContains(resp, '<input type="submit" value="Continue" />', html=True)
|
||||
# this should work on the server:
|
||||
self.assertContains(resp, '<input name="openid.realm" type="hidden" value="http://testserver/" />', html=True)
|
||||
|
||||
|
||||
# not included here are elements that will vary from run to run:
|
||||
# <input name="openid.return_to" type="hidden" value="http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H" />
|
||||
# <input name="openid.assoc_handle" type="hidden" value="{HMAC-SHA1}{50ff8120}{rh87+Q==}" />
|
||||
|
||||
|
||||
|
||||
|
||||
def testOpenIdSetup(self):
|
||||
if not settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
|
||||
return
|
||||
url = reverse('openid-provider-login')
|
||||
post_args = {
|
||||
"openid.mode" : "checkid_setup",
|
||||
"openid.return_to" : "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H",
|
||||
"openid.assoc_handle" : "{HMAC-SHA1}{50ff8120}{rh87+Q==}",
|
||||
"openid.claimed_id" : "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
"openid.ns" : "http://specs.openid.net/auth/2.0",
|
||||
"openid.realm" : "http://testserver/",
|
||||
"openid.identity" : "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
"openid.ns.ax" : "http://openid.net/srv/ax/1.0",
|
||||
"openid.ax.mode" : "fetch_request",
|
||||
"openid.ax.required" : "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname",
|
||||
"openid.ax.type.fullname" : "http://axschema.org/namePerson",
|
||||
"openid.ax.type.lastname" : "http://axschema.org/namePerson/last",
|
||||
"openid.ax.type.firstname" : "http://axschema.org/namePerson/first",
|
||||
"openid.ax.type.nickname" : "http://axschema.org/namePerson/friendly",
|
||||
"openid.ax.type.email" : "http://axschema.org/contact/email",
|
||||
"openid.ax.type.old_email" : "http://schema.openid.net/contact/email",
|
||||
"openid.ax.type.old_nickname" : "http://schema.openid.net/namePerson/friendly",
|
||||
"openid.ax.type.old_fullname" : "http://schema.openid.net/namePerson",
|
||||
"openid.mode": "checkid_setup",
|
||||
"openid.return_to": "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H",
|
||||
"openid.assoc_handle": "{HMAC-SHA1}{50ff8120}{rh87+Q==}",
|
||||
"openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
"openid.ns": "http://specs.openid.net/auth/2.0",
|
||||
"openid.realm": "http://testserver/",
|
||||
"openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
"openid.ns.ax": "http://openid.net/srv/ax/1.0",
|
||||
"openid.ax.mode": "fetch_request",
|
||||
"openid.ax.required": "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname",
|
||||
"openid.ax.type.fullname": "http://axschema.org/namePerson",
|
||||
"openid.ax.type.lastname": "http://axschema.org/namePerson/last",
|
||||
"openid.ax.type.firstname": "http://axschema.org/namePerson/first",
|
||||
"openid.ax.type.nickname": "http://axschema.org/namePerson/friendly",
|
||||
"openid.ax.type.email": "http://axschema.org/contact/email",
|
||||
"openid.ax.type.old_email": "http://schema.openid.net/contact/email",
|
||||
"openid.ax.type.old_nickname": "http://schema.openid.net/namePerson/friendly",
|
||||
"openid.ax.type.old_fullname": "http://schema.openid.net/namePerson",
|
||||
}
|
||||
resp = self.client.post(url, post_args)
|
||||
code = 200
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
|
||||
|
||||
|
||||
|
||||
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
||||
# in the test environment, we either need a live server that works with the default
|
||||
# fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
|
||||
@@ -196,11 +198,11 @@ class OpenIdProviderLiveServerTest(LiveServerTestCase):
|
||||
provider_url = reverse('openid-provider-xrds')
|
||||
factory = RequestFactory()
|
||||
request = factory.request()
|
||||
abs_provider_url = request.build_absolute_uri(location = provider_url)
|
||||
abs_provider_url = request.build_absolute_uri(location=provider_url)
|
||||
|
||||
# now we can begin the login process by invoking a local openid client,
|
||||
# with a pointer to the (also-local) openid provider:
|
||||
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url):
|
||||
with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
|
||||
url = reverse('openid-login')
|
||||
resp = self.client.post(url)
|
||||
code = 200
|
||||
|
||||
@@ -215,7 +215,7 @@ def ssl_dn_extract_info(dn):
|
||||
else:
|
||||
return None
|
||||
return (user, email, fullname)
|
||||
|
||||
|
||||
|
||||
def ssl_get_cert_from_request(request):
|
||||
"""
|
||||
@@ -460,7 +460,7 @@ def provider_login(request):
|
||||
openid_request.answer(False), {})
|
||||
|
||||
# checkid_setup, so display login page
|
||||
# (by falling through to the provider_login at the
|
||||
# (by falling through to the provider_login at the
|
||||
# bottom of this method).
|
||||
elif openid_request.mode == 'checkid_setup':
|
||||
if openid_request.idSelect():
|
||||
@@ -482,7 +482,7 @@ def provider_login(request):
|
||||
|
||||
# handle login redirection: these are also sent to this view function,
|
||||
# but are distinguished by lacking the openid mode. We also know that
|
||||
# they are posts, because they come from the popup
|
||||
# they are posts, because they come from the popup
|
||||
elif request.method == 'POST' and 'openid_setup' in request.session:
|
||||
# get OpenID request from session
|
||||
openid_setup = request.session['openid_setup']
|
||||
@@ -495,7 +495,7 @@ def provider_login(request):
|
||||
return default_render_failure(request, "Invalid OpenID trust root")
|
||||
|
||||
# check if user with given email exists
|
||||
# Failure is redirected to this method (by using the original URL),
|
||||
# Failure is redirected to this method (by using the original URL),
|
||||
# which will bring up the login dialog.
|
||||
email = request.POST.get('email', None)
|
||||
try:
|
||||
@@ -542,17 +542,17 @@ def provider_login(request):
|
||||
# missing fields is up to the Consumer. The proper change
|
||||
# should only return the username, however this will likely
|
||||
# break the CS50 client. Temporarily we will be returning
|
||||
# username filling in for fullname in addition to username
|
||||
# username filling in for fullname in addition to username
|
||||
# as sreg nickname.
|
||||
|
||||
# Note too that this is hardcoded, and not really responding to
|
||||
|
||||
# Note too that this is hardcoded, and not really responding to
|
||||
# the extensions that were registered in the first place.
|
||||
results = {
|
||||
'nickname': user.username,
|
||||
'email': user.email,
|
||||
'fullname': user.username
|
||||
}
|
||||
|
||||
|
||||
# the request succeeded:
|
||||
return provider_respond(server, openid_request, response, results)
|
||||
|
||||
|
||||
@@ -12,34 +12,35 @@ import mitxmako.middleware
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MakoLoader(object):
|
||||
"""
|
||||
This is a Django loader object which will load the template as a
|
||||
Mako template if the first line is "## mako". It is based off BaseLoader
|
||||
in django.template.loader.
|
||||
"""
|
||||
|
||||
|
||||
is_usable = False
|
||||
|
||||
def __init__(self, base_loader):
|
||||
# base_loader is an instance of a BaseLoader subclass
|
||||
self.base_loader = base_loader
|
||||
|
||||
|
||||
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
|
||||
|
||||
|
||||
if module_directory is None:
|
||||
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
|
||||
module_directory = tempfile.mkdtemp()
|
||||
|
||||
|
||||
self.module_directory = module_directory
|
||||
|
||||
|
||||
|
||||
|
||||
def __call__(self, template_name, template_dirs=None):
|
||||
return self.load_template(template_name, template_dirs)
|
||||
|
||||
def load_template(self, template_name, template_dirs=None):
|
||||
source, file_path = self.load_template_source(template_name, template_dirs)
|
||||
|
||||
|
||||
if source.startswith("## mako\n"):
|
||||
# This is a mako template
|
||||
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name)
|
||||
@@ -56,23 +57,24 @@ class MakoLoader(object):
|
||||
# This allows for correct identification (later) of the actual template that does
|
||||
# not exist.
|
||||
return source, file_path
|
||||
|
||||
|
||||
def load_template_source(self, template_name, template_dirs=None):
|
||||
# Just having this makes the template load as an instance, instead of a class.
|
||||
return self.base_loader.load_template_source(template_name, template_dirs)
|
||||
|
||||
def reset(self):
|
||||
self.base_loader.reset()
|
||||
|
||||
|
||||
|
||||
class MakoFilesystemLoader(MakoLoader):
|
||||
is_usable = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
MakoLoader.__init__(self, FilesystemLoader())
|
||||
|
||||
|
||||
|
||||
class MakoAppDirectoriesLoader(MakoLoader):
|
||||
is_usable = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
MakoLoader.__init__(self, AppDirectoriesLoader())
|
||||
|
||||
@@ -20,13 +20,15 @@ from mitxmako import middleware
|
||||
django_variables = ['lookup', 'output_encoding', 'encoding_errors']
|
||||
|
||||
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
|
||||
|
||||
|
||||
class Template(MakoTemplate):
|
||||
"""
|
||||
This bridges the gap between a Mako template and a djano template. It can
|
||||
be rendered like it is a django template because the arguments are transformed
|
||||
in a way that MakoTemplate can understand.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Overrides base __init__ to provide django variable overrides"""
|
||||
if not kwargs.get('no_django', False):
|
||||
@@ -34,8 +36,8 @@ class Template(MakoTemplate):
|
||||
overrides['lookup'] = overrides['lookup']['main']
|
||||
kwargs.update(overrides)
|
||||
super(Template, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
def render(self, context_instance):
|
||||
"""
|
||||
This takes a render call with a context (from Django) and translates
|
||||
@@ -43,7 +45,7 @@ class Template(MakoTemplate):
|
||||
"""
|
||||
# collapse context_instance to a single dictionary for mako
|
||||
context_dictionary = {}
|
||||
|
||||
|
||||
# In various testing contexts, there might not be a current request context.
|
||||
if middleware.requestcontext is not None:
|
||||
for d in middleware.requestcontext:
|
||||
@@ -53,5 +55,5 @@ class Template(MakoTemplate):
|
||||
context_dictionary['settings'] = settings
|
||||
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
|
||||
context_dictionary['django_context'] = context_instance
|
||||
|
||||
|
||||
return super(Template, self).render_unicode(**context_dictionary)
|
||||
|
||||
@@ -2,14 +2,15 @@ from django.template import loader
|
||||
from django.template.base import Template, Context
|
||||
from django.template.loader import get_template, select_template
|
||||
|
||||
|
||||
def django_template_include(file_name, mako_context):
|
||||
"""
|
||||
This can be used within a mako template to include a django template
|
||||
in the way that a django-style {% include %} does. Pass it context
|
||||
which can be the mako context ('context') or a dictionary.
|
||||
"""
|
||||
|
||||
dictionary = dict( mako_context )
|
||||
|
||||
dictionary = dict(mako_context)
|
||||
return loader.render_to_string(file_name, dictionary=dictionary)
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
||||
This allows a mako template to call a template tag function (written
|
||||
for django templates) that is an "inclusion tag". These functions are
|
||||
decorated with @register.inclusion_tag.
|
||||
|
||||
|
||||
-func: This is the function that is registered as an inclusion tag.
|
||||
You must import it directly using a python import statement.
|
||||
-file_name: This is the filename of the template, passed into the
|
||||
@@ -29,10 +30,10 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
||||
a copy of the django context is available as 'django_context'.
|
||||
-*args and **kwargs are the arguments to func.
|
||||
"""
|
||||
|
||||
|
||||
if takes_context:
|
||||
args = [django_context] + list(args)
|
||||
|
||||
|
||||
_dict = func(*args, **kwargs)
|
||||
if isinstance(file_name, Template):
|
||||
t = file_name
|
||||
@@ -40,14 +41,12 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
||||
t = select_template(file_name)
|
||||
else:
|
||||
t = get_template(file_name)
|
||||
|
||||
|
||||
nodelist = t.nodelist
|
||||
|
||||
|
||||
new_context = Context(_dict)
|
||||
csrf_token = django_context.get('csrf_token', None)
|
||||
if csrf_token is not None:
|
||||
new_context['csrf_token'] = csrf_token
|
||||
|
||||
return nodelist.render(new_context)
|
||||
|
||||
|
||||
return nodelist.render(new_context)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from staticfiles.storage import staticfiles_storage
|
||||
from staticfiles import finders
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def try_staticfiles_lookup(path):
|
||||
"""
|
||||
Try to lookup a path in staticfiles_storage. If it fails, return
|
||||
a dead link instead of raising an exception.
|
||||
"""
|
||||
try:
|
||||
url = staticfiles_storage.url(path)
|
||||
except Exception as err:
|
||||
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
|
||||
path, str(err)))
|
||||
# Just return the original path; don't kill everything.
|
||||
url = path
|
||||
return url
|
||||
|
||||
|
||||
def replace(static_url, prefix=None, course_namespace=None):
|
||||
if prefix is None:
|
||||
prefix = ''
|
||||
else:
|
||||
prefix = prefix + '/'
|
||||
|
||||
quote = static_url.group('quote')
|
||||
|
||||
servable = (
|
||||
# If in debug mode, we'll serve up anything that the finders can find
|
||||
(settings.DEBUG and finders.find(static_url.group('rest'), True)) or
|
||||
# Otherwise, we'll only serve up stuff that the storages can find
|
||||
staticfiles_storage.exists(static_url.group('rest'))
|
||||
)
|
||||
|
||||
if servable:
|
||||
return static_url.group(0)
|
||||
else:
|
||||
# don't error if file can't be found
|
||||
# cdodge: to support the change over to Mongo backed content stores, lets
|
||||
# use the utility functions in StaticContent.py
|
||||
if static_url.group('prefix') == '/static/' and not isinstance(modulestore(), XMLModuleStore):
|
||||
if course_namespace is None:
|
||||
raise BaseException('You must pass in course_namespace when remapping static content urls with MongoDB stores')
|
||||
url = StaticContent.convert_legacy_static_url(static_url.group('rest'), course_namespace)
|
||||
else:
|
||||
url = try_staticfiles_lookup(prefix + static_url.group('rest'))
|
||||
|
||||
new_link = "".join([quote, url, quote])
|
||||
return new_link
|
||||
|
||||
|
||||
|
||||
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
|
||||
|
||||
def replace_url(static_url):
|
||||
return replace(static_url, staticfiles_prefix, course_namespace = course_namespace)
|
||||
|
||||
return re.sub(r"""
|
||||
(?x) # flags=re.VERBOSE
|
||||
(?P<quote>\\?['"]) # the opening quotes
|
||||
(?P<prefix>{prefix}) # the prefix
|
||||
(?P<rest>.*?) # everything else in the url
|
||||
(?P=quote) # the first matching closing quote
|
||||
""".format(prefix=replace_prefix), replace_url, text)
|
||||
99
common/djangoapps/static_replace/__init__.py
Normal file
99
common/djangoapps/static_replace/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from staticfiles.storage import staticfiles_storage
|
||||
from staticfiles import finders
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _url_replace_regex(prefix):
|
||||
return r"""
|
||||
(?x) # flags=re.VERBOSE
|
||||
(?P<quote>\\?['"]) # the opening quotes
|
||||
(?P<prefix>{prefix}) # theeprefix
|
||||
(?P<rest>.*?) # everything else in the url
|
||||
(?P=quote) # the first matching closing quote
|
||||
""".format(prefix=prefix)
|
||||
|
||||
|
||||
def try_staticfiles_lookup(path):
|
||||
"""
|
||||
Try to lookup a path in staticfiles_storage. If it fails, return
|
||||
a dead link instead of raising an exception.
|
||||
"""
|
||||
try:
|
||||
url = staticfiles_storage.url(path)
|
||||
except Exception as err:
|
||||
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
|
||||
path, str(err)))
|
||||
# Just return the original path; don't kill everything.
|
||||
url = path
|
||||
return url
|
||||
|
||||
|
||||
def replace_course_urls(text, course_id):
|
||||
"""
|
||||
Replace /course/$stuff urls with /courses/$course_id/$stuff urls
|
||||
|
||||
text: The text to replace
|
||||
course_module: A CourseDescriptor
|
||||
|
||||
returns: text with the links replaced
|
||||
"""
|
||||
|
||||
|
||||
def replace_course_url(match):
|
||||
quote = match.group('quote')
|
||||
rest = match.group('rest')
|
||||
return "".join([quote, '/courses/' + course_id + '/', rest, quote])
|
||||
|
||||
return re.sub(_url_replace_regex('/course/'), replace_course_url, text)
|
||||
|
||||
|
||||
def replace_static_urls(text, data_directory, course_namespace=None):
|
||||
"""
|
||||
Replace /static/$stuff urls either with their correct url as generated by collectstatic,
|
||||
(/static/$md5_hashed_stuff) or by the course-specific content static url
|
||||
/static/$course_data_dir/$stuff, or, if course_namespace is not None, by the
|
||||
correct url in the contentstore (c4x://)
|
||||
|
||||
text: The source text to do the substitution in
|
||||
data_directory: The directory in which course data is stored
|
||||
course_namespace: The course identifier used to distinguish static content for this course in studio
|
||||
"""
|
||||
|
||||
def replace_static_url(match):
|
||||
original = match.group(0)
|
||||
prefix = match.group('prefix')
|
||||
quote = match.group('quote')
|
||||
rest = match.group('rest')
|
||||
|
||||
# course_namespace is not None, then use studio style urls
|
||||
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
|
||||
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
|
||||
# If we're in debug mode, and the file as requested exists, then don't change the links
|
||||
elif (settings.DEBUG and finders.find(rest, True)):
|
||||
return original
|
||||
# Otherwise, look the file up in staticfiles_storage without the data directory
|
||||
else:
|
||||
try:
|
||||
url = staticfiles_storage.url(rest)
|
||||
# And if that fails, assume that it's course content, and add manually data directory
|
||||
except Exception as err:
|
||||
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
|
||||
rest, str(err)))
|
||||
url = "".join([prefix, data_directory, '/', rest])
|
||||
|
||||
return "".join([quote, url, quote])
|
||||
|
||||
return re.sub(
|
||||
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)),
|
||||
replace_static_url,
|
||||
text
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
###
|
||||
### Script for importing courseware from XML format
|
||||
###
|
||||
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.core.cache import get_cache
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = \
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
staticfiles_cache = get_cache('staticfiles')
|
||||
staticfiles_cache.clear()
|
||||
65
common/djangoapps/static_replace/test/test_static_replace.py
Normal file
65
common/djangoapps/static_replace/test/test_static_replace.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from nose.tools import assert_equals
|
||||
from static_replace import replace_static_urls, replace_course_urls
|
||||
from mock import patch, Mock
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
DATA_DIRECTORY = 'data_dir'
|
||||
COURSE_ID = 'org/course/run'
|
||||
NAMESPACE = Location('org', 'course', 'run', None, None)
|
||||
STATIC_SOURCE = '"/static/file.png"'
|
||||
|
||||
|
||||
def test_multi_replace():
|
||||
course_source = '"/course/file.png"'
|
||||
|
||||
assert_equals(
|
||||
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY),
|
||||
replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY)
|
||||
)
|
||||
assert_equals(
|
||||
replace_course_urls(course_source, COURSE_ID),
|
||||
replace_course_urls(replace_course_urls(course_source, COURSE_ID), COURSE_ID)
|
||||
)
|
||||
|
||||
|
||||
@patch('static_replace.finders')
|
||||
@patch('static_replace.settings')
|
||||
def test_debug_no_modify(mock_settings, mock_finders):
|
||||
mock_settings.DEBUG = True
|
||||
mock_finders.find.return_value = True
|
||||
|
||||
assert_equals(STATIC_SOURCE, replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
|
||||
mock_finders.find.assert_called_once_with('file.png', True)
|
||||
|
||||
|
||||
@patch('static_replace.StaticContent')
|
||||
@patch('static_replace.modulestore')
|
||||
def test_mongo_filestore(mock_modulestore, mock_static_content):
|
||||
|
||||
mock_modulestore.return_value = Mock(MongoModuleStore)
|
||||
mock_static_content.convert_legacy_static_url.return_value = "c4x://mock_url"
|
||||
|
||||
# No namespace => no change to path
|
||||
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
|
||||
# Namespace => content url
|
||||
assert_equals(
|
||||
'"' + mock_static_content.convert_legacy_static_url.return_value + '"',
|
||||
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, NAMESPACE)
|
||||
)
|
||||
|
||||
mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE)
|
||||
|
||||
|
||||
@patch('static_replace.settings')
|
||||
@patch('static_replace.modulestore')
|
||||
@patch('static_replace.staticfiles_storage')
|
||||
def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
|
||||
mock_modulestore.return_value = Mock(XMLModuleStore)
|
||||
mock_settings.DEBUG = False
|
||||
mock_storage.url.side_effect = Exception
|
||||
|
||||
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_site_status_msg(course_id):
|
||||
"""
|
||||
Look for a file settings.STATUS_MESSAGE_PATH. If found, read it,
|
||||
|
||||
@@ -57,7 +57,7 @@ from student.userprofile. '''
|
||||
d[key] = item
|
||||
return d
|
||||
|
||||
extracted = [{'up':extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples]
|
||||
extracted = [{'up': extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples]
|
||||
fp = open('transfer_users.txt', 'w')
|
||||
json.dump(extracted, fp)
|
||||
fp.close()
|
||||
|
||||
@@ -3,6 +3,7 @@ from optparse import make_option
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--list',
|
||||
|
||||
@@ -8,6 +8,7 @@ from student.models import UserProfile, CourseEnrollment
|
||||
|
||||
from student.views import _do_create_account, get_random_post_override
|
||||
|
||||
|
||||
def create(n, course_id):
|
||||
"""Create n users, enrolling them in course_id if it's not None"""
|
||||
for i in range(n):
|
||||
@@ -15,6 +16,7 @@ def create(n, course_id):
|
||||
if course_id is not None:
|
||||
CourseEnrollment.objects.create(user=user, course_id=course_id)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Create N new users, with random parameters.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Command(BaseCommand):
|
||||
outputfile = datetime.utcnow().strftime("pearson-dump-%Y%m%d-%H%M%S.json")
|
||||
else:
|
||||
outputfile = args[0]
|
||||
|
||||
|
||||
# construct the query object to dump:
|
||||
registrations = TestCenterRegistration.objects.all()
|
||||
if 'course_id' in options and options['course_id']:
|
||||
@@ -44,24 +44,24 @@ class Command(BaseCommand):
|
||||
if 'exam_series_code' in options and options['exam_series_code']:
|
||||
registrations = registrations.filter(exam_series_code=options['exam_series_code'])
|
||||
|
||||
# collect output:
|
||||
# collect output:
|
||||
output = []
|
||||
for registration in registrations:
|
||||
if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending:
|
||||
continue
|
||||
record = {'username' : registration.testcenter_user.user.username,
|
||||
'email' : registration.testcenter_user.email,
|
||||
'first_name' : registration.testcenter_user.first_name,
|
||||
'last_name' : registration.testcenter_user.last_name,
|
||||
'client_candidate_id' : registration.client_candidate_id,
|
||||
'client_authorization_id' : registration.client_authorization_id,
|
||||
'course_id' : registration.course_id,
|
||||
'exam_series_code' : registration.exam_series_code,
|
||||
'accommodation_request' : registration.accommodation_request,
|
||||
'accommodation_code' : registration.accommodation_code,
|
||||
'registration_status' : registration.registration_status(),
|
||||
'demographics_status' : registration.demographics_status(),
|
||||
'accommodation_status' : registration.accommodation_status(),
|
||||
record = {'username': registration.testcenter_user.user.username,
|
||||
'email': registration.testcenter_user.email,
|
||||
'first_name': registration.testcenter_user.first_name,
|
||||
'last_name': registration.testcenter_user.last_name,
|
||||
'client_candidate_id': registration.client_candidate_id,
|
||||
'client_authorization_id': registration.client_authorization_id,
|
||||
'course_id': registration.course_id,
|
||||
'exam_series_code': registration.exam_series_code,
|
||||
'accommodation_request': registration.accommodation_request,
|
||||
'accommodation_code': registration.accommodation_code,
|
||||
'registration_status': registration.registration_status(),
|
||||
'demographics_status': registration.demographics_status(),
|
||||
'accommodation_status': registration.accommodation_status(),
|
||||
}
|
||||
if len(registration.upload_error_message) > 0:
|
||||
record['registration_error'] = registration.upload_error_message
|
||||
@@ -71,8 +71,7 @@ class Command(BaseCommand):
|
||||
record['needs_uploading'] = True
|
||||
|
||||
output.append(record)
|
||||
|
||||
|
||||
# dump output:
|
||||
with open(outputfile, 'w') as outfile:
|
||||
dump(output, outfile, indent=2)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class Command(BaseCommand):
|
||||
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
|
||||
])
|
||||
|
||||
# define defaults, even thought 'store_true' shouldn't need them.
|
||||
# define defaults, even thought 'store_true' shouldn't need them.
|
||||
# (call_command will set None as default value for all options that don't have one,
|
||||
# so one cannot rely on presence/absence of flags in that world.)
|
||||
option_list = BaseCommand.option_list + (
|
||||
@@ -56,7 +56,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# field
|
||||
uploaded_at = datetime.utcnow()
|
||||
|
||||
@@ -100,7 +100,7 @@ class Command(BaseCommand):
|
||||
extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
for tcu in TestCenterUser.objects.order_by('id'):
|
||||
if tcu.needs_uploading: # or dump_all
|
||||
if tcu.needs_uploading: # or dump_all
|
||||
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
|
||||
for csv_field, model_field
|
||||
in Command.CSV_TO_MODEL_FIELDS.items())
|
||||
|
||||
@@ -116,4 +116,3 @@ class Command(BaseCommand):
|
||||
tcuser.save()
|
||||
except TestCenterUser.DoesNotExist:
|
||||
Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from student.views import course_from_id
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
# registration info:
|
||||
@@ -16,23 +17,23 @@ class Command(BaseCommand):
|
||||
'--accommodation_request',
|
||||
action='store',
|
||||
dest='accommodation_request',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--accommodation_code',
|
||||
action='store',
|
||||
dest='accommodation_code',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--client_authorization_id',
|
||||
action='store',
|
||||
dest='client_authorization_id',
|
||||
),
|
||||
# exam info:
|
||||
),
|
||||
# exam info:
|
||||
make_option(
|
||||
'--exam_series_code',
|
||||
action='store',
|
||||
dest='exam_series_code',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--eligibility_appointment_date_first',
|
||||
action='store',
|
||||
@@ -51,32 +52,32 @@ class Command(BaseCommand):
|
||||
action='store',
|
||||
dest='authorization_id',
|
||||
help='ID we receive from Pearson for a particular authorization'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--upload_status',
|
||||
action='store',
|
||||
dest='upload_status',
|
||||
help='status value assigned by Pearson'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--upload_error_message',
|
||||
action='store',
|
||||
dest='upload_error_message',
|
||||
help='error message provided by Pearson on a failure.'
|
||||
),
|
||||
),
|
||||
# control values:
|
||||
make_option(
|
||||
'--ignore_registration_dates',
|
||||
action='store_true',
|
||||
dest='ignore_registration_dates',
|
||||
help='find exam info for course based on exam_series_code, even if the exam is not active.'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--create_dummy_exam',
|
||||
action='store_true',
|
||||
dest='create_dummy_exam',
|
||||
help='create dummy exam info for course, even if course exists'
|
||||
),
|
||||
),
|
||||
)
|
||||
args = "<student_username course_id>"
|
||||
help = "Create or modify a TestCenterRegistration entry for a given Student"
|
||||
@@ -103,7 +104,7 @@ class Command(BaseCommand):
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
|
||||
|
||||
|
||||
# get an "exam" object. Check to see if a course_id was specified, and use information from that:
|
||||
exam = None
|
||||
create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
|
||||
@@ -115,14 +116,14 @@ class Command(BaseCommand):
|
||||
exam = examlist[0] if len(examlist) > 0 else None
|
||||
else:
|
||||
exam = course.current_test_center_exam
|
||||
except ItemNotFoundError:
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
else:
|
||||
# otherwise use explicit values (so we don't have to define a course):
|
||||
# otherwise use explicit values (so we don't have to define a course):
|
||||
exam_name = "Dummy Placeholder Name"
|
||||
exam_info = { 'Exam_Series_Code': our_options['exam_series_code'],
|
||||
'First_Eligible_Appointment_Date' : our_options['eligibility_appointment_date_first'],
|
||||
'Last_Eligible_Appointment_Date' : our_options['eligibility_appointment_date_last'],
|
||||
exam_info = {'Exam_Series_Code': our_options['exam_series_code'],
|
||||
'First_Eligible_Appointment_Date': our_options['eligibility_appointment_date_first'],
|
||||
'Last_Eligible_Appointment_Date': our_options['eligibility_appointment_date_last'],
|
||||
}
|
||||
exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info)
|
||||
# update option values for date_first and date_last to use YYYY-MM-DD format
|
||||
@@ -134,15 +135,15 @@ class Command(BaseCommand):
|
||||
raise CommandError("Exam for course_id {} does not exist".format(course_id))
|
||||
|
||||
exam_code = exam.exam_series_code
|
||||
|
||||
UPDATE_FIELDS = ( 'accommodation_request',
|
||||
|
||||
UPDATE_FIELDS = ('accommodation_request',
|
||||
'accommodation_code',
|
||||
'client_authorization_id',
|
||||
'exam_series_code',
|
||||
'eligibility_appointment_date_first',
|
||||
'eligibility_appointment_date_last',
|
||||
)
|
||||
|
||||
|
||||
# create and save the registration:
|
||||
needs_updating = False
|
||||
registrations = get_testcenter_registration(student, course_id, exam_code)
|
||||
@@ -152,29 +153,29 @@ class Command(BaseCommand):
|
||||
if fieldname in our_options and registration.__getattribute__(fieldname) != our_options[fieldname]:
|
||||
needs_updating = True;
|
||||
else:
|
||||
accommodation_request = our_options.get('accommodation_request','')
|
||||
accommodation_request = our_options.get('accommodation_request', '')
|
||||
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
||||
needs_updating = True
|
||||
|
||||
|
||||
|
||||
if needs_updating:
|
||||
# first update the record with the new values, if any:
|
||||
for fieldname in UPDATE_FIELDS:
|
||||
if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields:
|
||||
if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields:
|
||||
registration.__setattr__(fieldname, our_options[fieldname])
|
||||
|
||||
# the registration form normally populates the data dict with
|
||||
|
||||
# the registration form normally populates the data dict with
|
||||
# the accommodation request (if any). But here we want to
|
||||
# specify only those values that might change, so update the dict with existing
|
||||
# values.
|
||||
form_options = dict(our_options)
|
||||
for propname in TestCenterRegistrationForm.Meta.fields:
|
||||
if propname not in form_options:
|
||||
if propname not in form_options:
|
||||
form_options[propname] = registration.__getattribute__(propname)
|
||||
form = TestCenterRegistrationForm(instance=registration, data=form_options)
|
||||
if form.is_valid():
|
||||
form.update_and_save()
|
||||
print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format(student.username, course_id, exam_code)
|
||||
print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format(student.username, course_id, exam_code)
|
||||
else:
|
||||
if (len(form.errors) > 0):
|
||||
print "Field Form errors encountered:"
|
||||
@@ -185,24 +186,22 @@ class Command(BaseCommand):
|
||||
print "Non-field Form errors encountered:"
|
||||
for nonfielderror in form.non_field_errors:
|
||||
print "Non-field Form Error: %s" % nonfielderror
|
||||
|
||||
|
||||
else:
|
||||
print "No changes necessary to make to existing user's registration."
|
||||
|
||||
|
||||
# override internal values:
|
||||
change_internal = False
|
||||
if 'exam_series_code' in our_options:
|
||||
exam_code = our_options['exam_series_code']
|
||||
registration = get_testcenter_registration(student, course_id, exam_code)[0]
|
||||
for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id']:
|
||||
for internal_field in ['upload_error_message', 'upload_status', 'authorization_id']:
|
||||
if internal_field in our_options:
|
||||
registration.__setattr__(internal_field, our_options[internal_field])
|
||||
change_internal = True
|
||||
|
||||
|
||||
if change_internal:
|
||||
print "Updated confirmation information in existing user's registration."
|
||||
registration.save()
|
||||
else:
|
||||
print "No changes necessary to make to confirmation information in existing user's registration."
|
||||
|
||||
|
||||
|
||||
@@ -5,60 +5,61 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import TestCenterUser, TestCenterUserForm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
# demographics:
|
||||
# demographics:
|
||||
make_option(
|
||||
'--first_name',
|
||||
action='store',
|
||||
dest='first_name',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--middle_name',
|
||||
action='store',
|
||||
dest='middle_name',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--last_name',
|
||||
action='store',
|
||||
dest='last_name',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--suffix',
|
||||
action='store',
|
||||
dest='suffix',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--salutation',
|
||||
action='store',
|
||||
dest='salutation',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--address_1',
|
||||
action='store',
|
||||
dest='address_1',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--address_2',
|
||||
action='store',
|
||||
dest='address_2',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--address_3',
|
||||
action='store',
|
||||
dest='address_3',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--city',
|
||||
action='store',
|
||||
dest='city',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--state',
|
||||
action='store',
|
||||
dest='state',
|
||||
help='Two letter code (e.g. MA)'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--postal_code',
|
||||
action='store',
|
||||
@@ -75,12 +76,12 @@ class Command(BaseCommand):
|
||||
action='store',
|
||||
dest='phone',
|
||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--extension',
|
||||
action='store',
|
||||
dest='extension',
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--phone_country_code',
|
||||
action='store',
|
||||
@@ -92,7 +93,7 @@ class Command(BaseCommand):
|
||||
action='store',
|
||||
dest='fax',
|
||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--fax_country_code',
|
||||
action='store',
|
||||
@@ -103,26 +104,26 @@ class Command(BaseCommand):
|
||||
'--company_name',
|
||||
action='store',
|
||||
dest='company_name',
|
||||
),
|
||||
),
|
||||
# internal values:
|
||||
make_option(
|
||||
'--client_candidate_id',
|
||||
action='store',
|
||||
dest='client_candidate_id',
|
||||
help='ID we assign a user to identify them to Pearson'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--upload_status',
|
||||
action='store',
|
||||
dest='upload_status',
|
||||
help='status value assigned by Pearson'
|
||||
),
|
||||
),
|
||||
make_option(
|
||||
'--upload_error_message',
|
||||
action='store',
|
||||
dest='upload_error_message',
|
||||
help='error message provided by Pearson on a failure.'
|
||||
),
|
||||
),
|
||||
)
|
||||
args = "<student_username>"
|
||||
help = "Create or modify a TestCenterUser entry for a given Student"
|
||||
@@ -142,20 +143,20 @@ class Command(BaseCommand):
|
||||
student = User.objects.get(username=username)
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
needs_updating = testcenter_user.needs_update(our_options)
|
||||
needs_updating = testcenter_user.needs_update(our_options)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
# do additional initialization here:
|
||||
testcenter_user = TestCenterUser.create(student)
|
||||
needs_updating = True
|
||||
|
||||
|
||||
if needs_updating:
|
||||
# the registration form normally populates the data dict with
|
||||
# the registration form normally populates the data dict with
|
||||
# all values from the testcenter_user. But here we only want to
|
||||
# specify those values that change, so update the dict with existing
|
||||
# values.
|
||||
form_options = dict(our_options)
|
||||
for propname in TestCenterUser.user_provided_fields():
|
||||
if propname not in form_options:
|
||||
if propname not in form_options:
|
||||
form_options[propname] = testcenter_user.__getattribute__(propname)
|
||||
form = TestCenterUserForm(instance=testcenter_user, data=form_options)
|
||||
if form.is_valid():
|
||||
@@ -170,21 +171,20 @@ class Command(BaseCommand):
|
||||
errorlist.append("Non-field Form errors encountered:")
|
||||
for nonfielderror in form.non_field_errors:
|
||||
errorlist.append("Non-field Form Error: {}".format(nonfielderror))
|
||||
raise CommandError("\n".join(errorlist))
|
||||
raise CommandError("\n".join(errorlist))
|
||||
else:
|
||||
print "No changes necessary to make to existing user's demographics."
|
||||
|
||||
|
||||
# override internal values:
|
||||
change_internal = False
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
for internal_field in [ 'upload_error_message', 'upload_status', 'client_candidate_id']:
|
||||
for internal_field in ['upload_error_message', 'upload_status', 'client_candidate_id']:
|
||||
if internal_field in our_options:
|
||||
testcenter_user.__setattr__(internal_field, our_options[internal_field])
|
||||
change_internal = True
|
||||
|
||||
|
||||
if change_internal:
|
||||
testcenter_user.save()
|
||||
print "Updated confirmation information in existing user's demographics."
|
||||
else:
|
||||
print "No changes necessary to make to confirmation information in existing user's demographics."
|
||||
|
||||
|
||||
@@ -46,10 +46,10 @@ class Command(BaseCommand):
|
||||
if not hasattr(settings, value):
|
||||
raise CommandError('No entry in the AWS settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
|
||||
|
||||
# check additional required settings for import and export:
|
||||
if options['mode'] in ('export', 'both'):
|
||||
for value in ['LOCAL_EXPORT','SFTP_EXPORT']:
|
||||
for value in ['LOCAL_EXPORT', 'SFTP_EXPORT']:
|
||||
if value not in settings.PEARSON:
|
||||
raise CommandError('No entry in the PEARSON settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
@@ -57,9 +57,9 @@ class Command(BaseCommand):
|
||||
source_dir = settings.PEARSON['LOCAL_EXPORT']
|
||||
if not os.path.isdir(source_dir):
|
||||
os.makedirs(source_dir)
|
||||
|
||||
|
||||
if options['mode'] in ('import', 'both'):
|
||||
for value in ['LOCAL_IMPORT','SFTP_IMPORT']:
|
||||
for value in ['LOCAL_IMPORT', 'SFTP_IMPORT']:
|
||||
if value not in settings.PEARSON:
|
||||
raise CommandError('No entry in the PEARSON settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
@@ -76,7 +76,7 @@ class Command(BaseCommand):
|
||||
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
|
||||
password=settings.PEARSON['SFTP_PASSWORD'])
|
||||
sftp = paramiko.SFTPClient.from_transport(t)
|
||||
|
||||
|
||||
if mode == 'export':
|
||||
try:
|
||||
sftp.chdir(files_to)
|
||||
@@ -92,7 +92,7 @@ class Command(BaseCommand):
|
||||
except IOError:
|
||||
raise CommandError('SFTP source path does not exist: {}'.format(files_from))
|
||||
for filename in sftp.listdir('.'):
|
||||
# skip subdirectories
|
||||
# skip subdirectories
|
||||
if not S_ISDIR(sftp.stat(filename).st_mode):
|
||||
sftp.get(filename, files_to + '/' + filename)
|
||||
# delete files from sftp server once they are successfully pulled off:
|
||||
@@ -112,7 +112,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
for filename in os.listdir(files_from):
|
||||
source_file = os.path.join(files_from, filename)
|
||||
# use mode as name of directory into which to write files
|
||||
# use mode as name of directory into which to write files
|
||||
dest_file = os.path.join(mode, filename)
|
||||
upload_file_to_s3(bucket, source_file, dest_file)
|
||||
if deleteAfterCopy:
|
||||
@@ -135,17 +135,17 @@ class Command(BaseCommand):
|
||||
k.set_contents_from_filename(source_file)
|
||||
|
||||
def export_pearson():
|
||||
options = { 'dest-from-settings' : True }
|
||||
options = {'dest-from-settings': True}
|
||||
call_command('pearson_export_cdd', **options)
|
||||
call_command('pearson_export_ead', **options)
|
||||
mode = 'export'
|
||||
sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy = False)
|
||||
sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy=False)
|
||||
s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True)
|
||||
|
||||
def import_pearson():
|
||||
mode = 'import'
|
||||
try:
|
||||
sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy = True)
|
||||
sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy=True)
|
||||
s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False)
|
||||
except Exception as e:
|
||||
dog_http_api.event('Pearson Import failure', str(e))
|
||||
|
||||
@@ -17,30 +17,31 @@ from student.models import User, TestCenterRegistration, TestCenterUser, get_tes
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_tc_user(username):
|
||||
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||
options = {
|
||||
'first_name' : 'TestFirst',
|
||||
'last_name' : 'TestLast',
|
||||
'address_1' : 'Test Address',
|
||||
'city' : 'TestCity',
|
||||
'state' : 'Alberta',
|
||||
'postal_code' : 'A0B 1C2',
|
||||
'country' : 'CAN',
|
||||
'phone' : '252-1866',
|
||||
'phone_country_code' : '1',
|
||||
'first_name': 'TestFirst',
|
||||
'last_name': 'TestLast',
|
||||
'address_1': 'Test Address',
|
||||
'city': 'TestCity',
|
||||
'state': 'Alberta',
|
||||
'postal_code': 'A0B 1C2',
|
||||
'country': 'CAN',
|
||||
'phone': '252-1866',
|
||||
'phone_country_code': '1',
|
||||
}
|
||||
call_command('pearson_make_tc_user', username, **options)
|
||||
return TestCenterUser.objects.get(user=user)
|
||||
|
||||
|
||||
def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code = 'exam1', accommodation_code = None):
|
||||
|
||||
options = { 'exam_series_code' : exam_code,
|
||||
'eligibility_appointment_date_first' : '2013-01-01T00:00',
|
||||
'eligibility_appointment_date_last' : '2013-12-31T23:59',
|
||||
'accommodation_code' : accommodation_code,
|
||||
'create_dummy_exam' : True,
|
||||
|
||||
|
||||
def create_tc_registration(username, course_id='org1/course1/term1', exam_code='exam1', accommodation_code=None):
|
||||
|
||||
options = {'exam_series_code': exam_code,
|
||||
'eligibility_appointment_date_first': '2013-01-01T00:00',
|
||||
'eligibility_appointment_date_last': '2013-12-31T23:59',
|
||||
'accommodation_code': accommodation_code,
|
||||
'create_dummy_exam': True,
|
||||
}
|
||||
|
||||
call_command('pearson_make_tc_registration', username, course_id, **options)
|
||||
@@ -48,21 +49,23 @@ def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code
|
||||
registrations = get_testcenter_registration(user, course_id, exam_code)
|
||||
return registrations[0]
|
||||
|
||||
|
||||
def create_multiple_registrations(prefix='test'):
|
||||
username1 = '{}_multiple1'.format(prefix)
|
||||
create_tc_user(username1)
|
||||
create_tc_registration(username1)
|
||||
create_tc_registration(username1, course_id = 'org1/course2/term1')
|
||||
create_tc_registration(username1, exam_code = 'exam2')
|
||||
create_tc_registration(username1, course_id='org1/course2/term1')
|
||||
create_tc_registration(username1, exam_code='exam2')
|
||||
username2 = '{}_multiple2'.format(prefix)
|
||||
create_tc_user(username2)
|
||||
create_tc_registration(username2)
|
||||
username3 = '{}_multiple3'.format(prefix)
|
||||
create_tc_user(username3)
|
||||
create_tc_registration(username3, course_id = 'org1/course2/term1')
|
||||
create_tc_registration(username3, course_id='org1/course2/term1')
|
||||
username4 = '{}_multiple4'.format(prefix)
|
||||
create_tc_user(username4)
|
||||
create_tc_registration(username4, exam_code = 'exam2')
|
||||
create_tc_registration(username4, exam_code='exam2')
|
||||
|
||||
|
||||
def get_command_error_text(*args, **options):
|
||||
stderr_string = None
|
||||
@@ -75,21 +78,22 @@ def get_command_error_text(*args, **options):
|
||||
# But these are actually translated into nice messages,
|
||||
# and sys.exit(1) is then called. For testing, we
|
||||
# want to catch what sys.exit throws, and get the
|
||||
# relevant text either from stdout or stderr.
|
||||
# relevant text either from stdout or stderr.
|
||||
if (why1.message > 0):
|
||||
stderr_string = sys.stderr.getvalue()
|
||||
else:
|
||||
raise why1
|
||||
except Exception, why:
|
||||
raise why
|
||||
|
||||
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
if stderr_string is None:
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
return stderr_string
|
||||
|
||||
|
||||
|
||||
def get_error_string_for_management_call(*args, **options):
|
||||
stdout_string = None
|
||||
old_stdout = sys.stdout
|
||||
@@ -103,7 +107,7 @@ def get_error_string_for_management_call(*args, **options):
|
||||
# But these are actually translated into nice messages,
|
||||
# and sys.exit(1) is then called. For testing, we
|
||||
# want to catch what sys.exit throws, and get the
|
||||
# relevant text either from stdout or stderr.
|
||||
# relevant text either from stdout or stderr.
|
||||
if (why1.message == 1):
|
||||
stdout_string = sys.stdout.getvalue()
|
||||
stderr_string = sys.stderr.getvalue()
|
||||
@@ -111,15 +115,15 @@ def get_error_string_for_management_call(*args, **options):
|
||||
raise why1
|
||||
except Exception, why:
|
||||
raise why
|
||||
|
||||
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
if stdout_string is None:
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
return stdout_string, stderr_string
|
||||
|
||||
|
||||
|
||||
def get_file_info(dirpath):
|
||||
filelist = os.listdir(dirpath)
|
||||
@@ -132,43 +136,45 @@ def get_file_info(dirpath):
|
||||
numlines = len(filecontents)
|
||||
return filepath, numlines
|
||||
else:
|
||||
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath,filelist))
|
||||
|
||||
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath, filelist))
|
||||
|
||||
|
||||
class PearsonTestCase(TestCase):
|
||||
'''
|
||||
Base class for tests running Pearson-related commands
|
||||
'''
|
||||
import_dir = mkdtemp(prefix="import")
|
||||
export_dir = mkdtemp(prefix="export")
|
||||
|
||||
|
||||
def assertErrorContains(self, error_message, expected):
|
||||
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
def delete_temp_dir(dirname):
|
||||
if os.path.exists(dirname):
|
||||
for filename in os.listdir(dirname):
|
||||
os.remove(os.path.join(dirname, filename))
|
||||
os.rmdir(dirname)
|
||||
|
||||
|
||||
# clean up after any test data was dumped to temp directory
|
||||
delete_temp_dir(self.import_dir)
|
||||
delete_temp_dir(self.export_dir)
|
||||
|
||||
|
||||
# and clean up the database:
|
||||
# TestCenterUser.objects.all().delete()
|
||||
# TestCenterRegistration.objects.all().delete()
|
||||
|
||||
|
||||
class PearsonCommandTestCase(PearsonTestCase):
|
||||
|
||||
def test_missing_demographic_fields(self):
|
||||
# We won't bother to test all details of form validation here.
|
||||
# We won't bother to test all details of form validation here.
|
||||
# It is enough to show that it works here, but deal with test cases for the form
|
||||
# validation in the student tests, not these management tests.
|
||||
username = 'baduser'
|
||||
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||
options = {}
|
||||
error_string = get_command_error_text('pearson_make_tc_user', username, **options)
|
||||
error_string = get_command_error_text('pearson_make_tc_user', username, **options)
|
||||
self.assertTrue(error_string.find('Field Form errors encountered:') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: city') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: first_name') >= 0)
|
||||
@@ -178,11 +184,11 @@ class PearsonCommandTestCase(PearsonTestCase):
|
||||
self.assertTrue(error_string.find('Field Form Error: phone') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: address_1') >= 0)
|
||||
self.assertErrorContains(error_string, 'Field Form Error: address_1')
|
||||
|
||||
|
||||
def test_create_good_testcenter_user(self):
|
||||
testcenter_user = create_tc_user("test_good_user")
|
||||
self.assertIsNotNone(testcenter_user)
|
||||
|
||||
|
||||
def test_create_good_testcenter_registration(self):
|
||||
username = 'test_good_registration'
|
||||
create_tc_user(username)
|
||||
@@ -192,21 +198,21 @@ class PearsonCommandTestCase(PearsonTestCase):
|
||||
def test_cdd_missing_option(self):
|
||||
error_string = get_command_error_text('pearson_export_cdd', **{})
|
||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||
|
||||
|
||||
def test_ead_missing_option(self):
|
||||
error_string = get_command_error_text('pearson_export_ead', **{})
|
||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||
|
||||
def test_export_single_cdd(self):
|
||||
# before we generate any tc_users, we expect there to be nothing to output:
|
||||
options = { 'dest-from-settings' : True }
|
||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
||||
options = {'dest-from-settings': True}
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# generating a tc_user should result in a line in the output
|
||||
# generating a tc_user should result in a line in the output
|
||||
username = 'test_single_cdd'
|
||||
create_tc_user(username)
|
||||
call_command('pearson_export_cdd', **options)
|
||||
@@ -221,23 +227,23 @@ class PearsonCommandTestCase(PearsonTestCase):
|
||||
os.remove(filepath)
|
||||
|
||||
# if we modify the record, then it should be output again:
|
||||
user_options = { 'first_name' : 'NewTestFirst', }
|
||||
user_options = {'first_name': 'NewTestFirst', }
|
||||
call_command('pearson_make_tc_user', username, **user_options)
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def test_export_single_ead(self):
|
||||
# before we generate any registrations, we expect there to be nothing to output:
|
||||
options = { 'dest-from-settings' : True }
|
||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
||||
options = {'dest-from-settings': True}
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# generating a registration should result in a line in the output
|
||||
# generating a registration should result in a line in the output
|
||||
username = 'test_single_ead'
|
||||
create_tc_user(username)
|
||||
create_tc_registration(username)
|
||||
@@ -251,7 +257,7 @@ class PearsonCommandTestCase(PearsonTestCase):
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
# if we modify the record, then it should be output again:
|
||||
create_tc_registration(username, accommodation_code='EQPMNT')
|
||||
call_command('pearson_export_ead', **options)
|
||||
@@ -261,8 +267,8 @@ class PearsonCommandTestCase(PearsonTestCase):
|
||||
|
||||
def test_export_multiple(self):
|
||||
create_multiple_registrations("export")
|
||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
||||
options = { 'dest-from-settings' : True }
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
options = {'dest-from-settings': True}
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
|
||||
@@ -294,6 +300,7 @@ S3_BUCKET = 'edx-pearson-archive'
|
||||
AWS_ACCESS_KEY_ID = 'put yours here'
|
||||
AWS_SECRET_ACCESS_KEY = 'put yours here'
|
||||
|
||||
|
||||
class PearsonTransferTestCase(PearsonTestCase):
|
||||
'''
|
||||
Class for tests running Pearson transfers
|
||||
@@ -302,14 +309,14 @@ class PearsonTransferTestCase(PearsonTestCase):
|
||||
def test_transfer_config(self):
|
||||
with self.settings(DATADOG_API='FAKE_KEY'):
|
||||
# TODO: why is this failing with the wrong error message?!
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **{'mode' : 'garbage'})
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **{'mode': 'garbage'})
|
||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||
with self.settings(DATADOG_API='FAKE_KEY'):
|
||||
stderrmsg = get_command_error_text('pearson_transfer')
|
||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||
with self.settings(DATADOG_API='FAKE_KEY',
|
||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
||||
'LOCAL_IMPORT' : self.import_dir }):
|
||||
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'LOCAL_IMPORT': self.import_dir}):
|
||||
stderrmsg = get_command_error_text('pearson_transfer')
|
||||
self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
|
||||
|
||||
@@ -317,16 +324,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('export_missing_dest')
|
||||
with self.settings(DATADOG_API='FAKE_KEY',
|
||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
||||
'SFTP_EXPORT' : 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
||||
'S3_BUCKET' : S3_BUCKET,
|
||||
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'SFTP_EXPORT': 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
||||
options = { 'mode' : 'export'}
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'export'}
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||
self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
|
||||
|
||||
@@ -334,16 +341,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations("transfer_export")
|
||||
with self.settings(DATADOG_API='FAKE_KEY',
|
||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
||||
'SFTP_EXPORT' : 'results/topvue',
|
||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
||||
'S3_BUCKET' : S3_BUCKET,
|
||||
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'SFTP_EXPORT': 'results/topvue',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
||||
options = { 'mode' : 'export'}
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'export'}
|
||||
# call_command('pearson_transfer', **options)
|
||||
# # confirm that the export directory is still empty:
|
||||
# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
|
||||
@@ -352,16 +359,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('import_missing_src')
|
||||
with self.settings(DATADOG_API='FAKE_KEY',
|
||||
PEARSON={'LOCAL_IMPORT' : self.import_dir,
|
||||
'SFTP_IMPORT' : 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
||||
'S3_BUCKET' : S3_BUCKET,
|
||||
PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||
'SFTP_IMPORT': 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
||||
options = { 'mode' : 'import'}
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'import'}
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||
self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
|
||||
|
||||
@@ -369,15 +376,15 @@ class PearsonTransferTestCase(PearsonTestCase):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('import_missing_src')
|
||||
with self.settings(DATADOG_API='FAKE_KEY',
|
||||
PEARSON={'LOCAL_IMPORT' : self.import_dir,
|
||||
'SFTP_IMPORT' : 'results',
|
||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
||||
'S3_BUCKET' : S3_BUCKET,
|
||||
PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||
'SFTP_IMPORT': 'results',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
||||
options = { 'mode' : 'import'}
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'import'}
|
||||
call_command('pearson_transfer', **options)
|
||||
self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")
|
||||
|
||||
@@ -185,4 +185,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -36,7 +36,7 @@ class Migration(SchemaMigration):
|
||||
for column in ASKBOT_AUTH_USER_COLUMNS:
|
||||
db.delete_column('auth_user', column)
|
||||
except Exception as ex:
|
||||
print "Couldn't remove askbot because of {0} -- it was probably never here to begin with.".format(ex)
|
||||
print "Couldn't remove askbot because of {0} -- it was probably never here to begin with.".format(ex)
|
||||
|
||||
def backwards(self, orm):
|
||||
raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.")
|
||||
|
||||
@@ -152,4 +152,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -238,4 +238,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -169,4 +169,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -107,6 +107,7 @@ class UserProfile(models.Model):
|
||||
TEST_CENTER_STATUS_ACCEPTED = "Accepted"
|
||||
TEST_CENTER_STATUS_ERROR = "Error"
|
||||
|
||||
|
||||
class TestCenterUser(models.Model):
|
||||
"""This is our representation of the User for in-person testing, and
|
||||
specifically for Pearson at this point. A few things to note:
|
||||
@@ -190,7 +191,7 @@ class TestCenterUser(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def user_provided_fields():
|
||||
return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
return ['first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
|
||||
|
||||
@@ -208,7 +209,7 @@ class TestCenterUser(models.Model):
|
||||
@staticmethod
|
||||
def _generate_edx_id(prefix):
|
||||
NUM_DIGITS = 12
|
||||
return u"{}{:012}".format(prefix, randint(1, 10**NUM_DIGITS-1))
|
||||
return u"{}{:012}".format(prefix, randint(1, 10 ** NUM_DIGITS - 1))
|
||||
|
||||
@staticmethod
|
||||
def _generate_candidate_id():
|
||||
@@ -237,10 +238,11 @@ class TestCenterUser(models.Model):
|
||||
def is_pending(self):
|
||||
return not self.is_accepted and not self.is_rejected
|
||||
|
||||
|
||||
class TestCenterUserForm(ModelForm):
|
||||
class Meta:
|
||||
model = TestCenterUser
|
||||
fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
fields = ('first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
|
||||
|
||||
@@ -313,7 +315,8 @@ ACCOMMODATION_CODES = (
|
||||
('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}
|
||||
|
||||
|
||||
class TestCenterRegistration(models.Model):
|
||||
"""
|
||||
@@ -389,10 +392,10 @@ class TestCenterRegistration(models.Model):
|
||||
elif self.uploaded_at is None:
|
||||
return 'Add'
|
||||
elif self.registration_is_rejected:
|
||||
# Assume that if the registration was rejected before,
|
||||
# it is more likely this is the (first) correction
|
||||
# Assume that if the registration was rejected before,
|
||||
# it is more likely this is the (first) correction
|
||||
# than a second correction in flight before the first was
|
||||
# processed.
|
||||
# processed.
|
||||
return 'Add'
|
||||
else:
|
||||
# TODO: decide what to send when we have uploaded an initial version,
|
||||
@@ -417,7 +420,7 @@ class TestCenterRegistration(models.Model):
|
||||
|
||||
@classmethod
|
||||
def create(cls, testcenter_user, exam, accommodation_request):
|
||||
registration = cls(testcenter_user = testcenter_user)
|
||||
registration = cls(testcenter_user=testcenter_user)
|
||||
registration.course_id = exam.course_id
|
||||
registration.accommodation_request = accommodation_request.strip()
|
||||
registration.exam_series_code = exam.exam_series_code
|
||||
@@ -501,7 +504,7 @@ class TestCenterRegistration(models.Model):
|
||||
return self.accommodation_code.split('*')
|
||||
|
||||
def get_accommodation_names(self):
|
||||
return [ ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes() ]
|
||||
return [ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes()]
|
||||
|
||||
@property
|
||||
def registration_signup_url(self):
|
||||
@@ -512,7 +515,7 @@ class TestCenterRegistration(models.Model):
|
||||
return "Accepted"
|
||||
elif self.demographics_is_rejected:
|
||||
return "Rejected"
|
||||
else:
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
def accommodation_status(self):
|
||||
@@ -522,7 +525,7 @@ class TestCenterRegistration(models.Model):
|
||||
return "Accepted"
|
||||
elif self.accommodation_is_rejected:
|
||||
return "Rejected"
|
||||
else:
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
def registration_status(self):
|
||||
@@ -532,12 +535,12 @@ class TestCenterRegistration(models.Model):
|
||||
return "Rejected"
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
|
||||
|
||||
class TestCenterRegistrationForm(ModelForm):
|
||||
class Meta:
|
||||
model = TestCenterRegistration
|
||||
fields = ( 'accommodation_request', 'accommodation_code' )
|
||||
fields = ('accommodation_request', 'accommodation_code')
|
||||
|
||||
def clean_accommodation_request(self):
|
||||
code = self.cleaned_data['accommodation_request']
|
||||
@@ -576,6 +579,7 @@ def get_testcenter_registration(user, course_id, exam_series_code):
|
||||
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
|
||||
get_testcenter_registration.__test__ = False
|
||||
|
||||
|
||||
def unique_id_for_user(user):
|
||||
"""
|
||||
Return a unique id for a user, suitable for inserting into
|
||||
@@ -666,6 +670,7 @@ class CourseEnrollmentAllowed(models.Model):
|
||||
|
||||
#### Helper methods for use from python manage.py shell and other classes.
|
||||
|
||||
|
||||
def get_user_by_username_or_email(username_or_email):
|
||||
"""
|
||||
Return a User object, looking up by email if username_or_email contains a
|
||||
@@ -767,4 +772,3 @@ def update_user_information(sender, instance, created, **kwargs):
|
||||
log = logging.getLogger("mitx.discussion")
|
||||
log.error(unicode(e))
|
||||
log.error("update user info to discussion failed for user with id: " + str(instance.id))
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ COURSE_2 = 'edx/full/6.002_Spring_2012'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseEndingTest(TestCase):
|
||||
"""Test things related to course endings: certificates, surveys, etc"""
|
||||
|
||||
@@ -40,7 +41,7 @@ class CourseEndingTest(TestCase):
|
||||
{'status': 'processing',
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': False,})
|
||||
'show_survey_button': False, })
|
||||
|
||||
cert_status = {'status': 'unavailable'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
|
||||
@@ -50,6 +50,7 @@ from statsd import statsd
|
||||
log = logging.getLogger("mitx.student")
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
|
||||
|
||||
def csrf_token(context):
|
||||
''' A csrf token that can be included in a form.
|
||||
'''
|
||||
@@ -73,8 +74,8 @@ def index(request, extra_context={}, user=None):
|
||||
'''
|
||||
|
||||
# The course selection work is done in courseware.courses.
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
if domain==False: # do explicit check, because domain=None is valid
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
if domain == False: # do explicit check, because domain=None is valid
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
|
||||
courses = get_courses(None, domain=domain)
|
||||
@@ -97,6 +98,7 @@ import re
|
||||
day_pattern = re.compile('\s\d+,\s')
|
||||
multimonth_pattern = re.compile('\s?\-\s?\S+\s')
|
||||
|
||||
|
||||
def get_date_for_press(publish_date):
|
||||
import datetime
|
||||
# strip off extra months, and just use the first:
|
||||
@@ -107,6 +109,7 @@ def get_date_for_press(publish_date):
|
||||
date = datetime.datetime.strptime(date, "%B, %Y")
|
||||
return date
|
||||
|
||||
|
||||
def press(request):
|
||||
json_articles = cache.get("student_press_json_articles")
|
||||
if json_articles == None:
|
||||
@@ -148,6 +151,7 @@ def cert_info(user, course):
|
||||
|
||||
return _cert_info(user, course, certificate_status_for_student(user, course.id))
|
||||
|
||||
|
||||
def _cert_info(user, course, cert_status):
|
||||
"""
|
||||
Implements the logic for cert_info -- split out for testing.
|
||||
@@ -175,7 +179,7 @@ def _cert_info(user, course, cert_status):
|
||||
|
||||
d = {'status': status,
|
||||
'show_download_url': status == 'ready',
|
||||
'show_disabled_download_button': status == 'generating',}
|
||||
'show_disabled_download_button': status == 'generating', }
|
||||
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||
course.end_of_course_survey_url is not None):
|
||||
@@ -204,6 +208,7 @@ def _cert_info(user, course, cert_status):
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def dashboard(request):
|
||||
@@ -237,9 +242,9 @@ def dashboard(request):
|
||||
show_courseware_links_for = frozenset(course.id for course in courses
|
||||
if has_access(request.user, course, 'load'))
|
||||
|
||||
cert_statuses = { course.id: cert_info(request.user, course) for course in courses}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course in courses}
|
||||
|
||||
exam_registrations = { course.id: exam_registration_info(request.user, course) for course in courses}
|
||||
exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
@@ -248,7 +253,7 @@ def dashboard(request):
|
||||
'message': message,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for' : show_courseware_links_for,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'cert_statuses': cert_statuses,
|
||||
'news': top_news,
|
||||
'exam_registrations': exam_registrations,
|
||||
@@ -312,7 +317,7 @@ def change_enrollment(request):
|
||||
'error': 'enrollment in {} not allowed at this time'
|
||||
.format(course.display_name)}
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
org, course_num, run = course_id.split("/")
|
||||
statsd.increment("common.student.enrollment",
|
||||
tags=["org:{0}".format(org),
|
||||
"course:{0}".format(course_num),
|
||||
@@ -326,7 +331,7 @@ def change_enrollment(request):
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment.delete()
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
org, course_num, run = course_id.split("/")
|
||||
statsd.increment("common.student.unenrollment",
|
||||
tags=["org:{0}".format(org),
|
||||
"course:{0}".format(course_num),
|
||||
@@ -345,7 +350,7 @@ def change_enrollment(request):
|
||||
def accounts_login(request, error=""):
|
||||
|
||||
|
||||
return render_to_response('accounts_login.html', { 'error': error })
|
||||
return render_to_response('accounts_login.html', {'error': error})
|
||||
|
||||
|
||||
|
||||
@@ -424,6 +429,7 @@ def change_setting(request):
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'location': up.location, }))
|
||||
|
||||
|
||||
def _do_create_account(post_vars):
|
||||
"""
|
||||
Given cleaned post variables, create the User and UserProfile objects, as well as the
|
||||
@@ -551,7 +557,7 @@ def create_account(request, post_override=None):
|
||||
|
||||
# Ok, looks like everything is legit. Create the account.
|
||||
ret = _do_create_account(post_vars)
|
||||
if isinstance(ret,HttpResponse): # if there was an error then return that
|
||||
if isinstance(ret, HttpResponse): # if there was an error then return that
|
||||
return ret
|
||||
(user, profile, registration) = ret
|
||||
|
||||
@@ -591,7 +597,7 @@ def create_account(request, post_override=None):
|
||||
eamap.user = login_user
|
||||
eamap.dtsignup = datetime.datetime.now()
|
||||
eamap.save()
|
||||
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'],eamap))
|
||||
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap))
|
||||
|
||||
if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
|
||||
log.debug('bypassing activation email')
|
||||
@@ -603,6 +609,7 @@ def create_account(request, post_override=None):
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def exam_registration_info(user, course):
|
||||
""" Returns a Registration object if the user is currently registered for a current
|
||||
exam of the course. Returns None if the user is not registered, or if there is no
|
||||
@@ -620,6 +627,7 @@ def exam_registration_info(user, course):
|
||||
registration = None
|
||||
return registration
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def begin_exam_registration(request, course_id):
|
||||
@@ -663,6 +671,7 @@ def begin_exam_registration(request, course_id):
|
||||
|
||||
return render_to_response('test_center_register.html', context)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_exam_registration(request, post_override=None):
|
||||
'''
|
||||
@@ -725,7 +734,7 @@ def create_exam_registration(request, post_override=None):
|
||||
# this registration screen.
|
||||
|
||||
else:
|
||||
accommodation_request = post_vars.get('accommodation_request','')
|
||||
accommodation_request = post_vars.get('accommodation_request', '')
|
||||
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
||||
needs_saving = True
|
||||
log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id))
|
||||
@@ -834,16 +843,17 @@ def password_reset(request):
|
||||
|
||||
form = PasswordResetForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save(use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request,
|
||||
domain_override = request.get_host())
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
form.save(use_https=request.is_secure(),
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
request=request,
|
||||
domain_override=request.get_host())
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||
else:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def reactivation_email(request):
|
||||
''' Send an e-mail to reactivate a deactivated account, or to
|
||||
@@ -856,6 +866,7 @@ def reactivation_email(request):
|
||||
'error': 'No inactive user with this e-mail exists'}))
|
||||
return reactivation_email_for_user(user)
|
||||
|
||||
|
||||
def reactivation_email_for_user(user):
|
||||
reg = Registration.objects.get(user=user)
|
||||
|
||||
@@ -996,11 +1007,11 @@ def pending_name_changes(request):
|
||||
|
||||
changes = list(PendingNameChange.objects.all())
|
||||
js = {'students': [{'new_name': c.new_name,
|
||||
'rationale':c.rationale,
|
||||
'old_name':UserProfile.objects.get(user=c.user).name,
|
||||
'email':c.user.email,
|
||||
'uid':c.user.id,
|
||||
'cid':c.id} for c in changes]}
|
||||
'rationale': c.rationale,
|
||||
'old_name': UserProfile.objects.get(user=c.user).name,
|
||||
'email': c.user.email,
|
||||
'uid': c.user.id,
|
||||
'cid': c.id} for c in changes]}
|
||||
return render_to_response('name_changes.html', js)
|
||||
|
||||
|
||||
@@ -1057,6 +1068,8 @@ def accept_name_change(request):
|
||||
|
||||
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
|
||||
# Will need to get replaced by something that actually ties into TestCenterUser
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def test_center_login(request):
|
||||
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
|
||||
|
||||
@@ -45,4 +45,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
complete_apps = ['track']
|
||||
|
||||
@@ -48,4 +48,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
complete_apps = ['track']
|
||||
|
||||
@@ -2,21 +2,20 @@ from django.db import models
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TrackingLog(models.Model):
|
||||
dtcreated = models.DateTimeField('creation date',auto_now_add=True)
|
||||
username = models.CharField(max_length=32,blank=True)
|
||||
ip = models.CharField(max_length=32,blank=True)
|
||||
dtcreated = models.DateTimeField('creation date', auto_now_add=True)
|
||||
username = models.CharField(max_length=32, blank=True)
|
||||
ip = models.CharField(max_length=32, blank=True)
|
||||
event_source = models.CharField(max_length=32)
|
||||
event_type = models.CharField(max_length=512,blank=True)
|
||||
event_type = models.CharField(max_length=512, blank=True)
|
||||
event = models.TextField(blank=True)
|
||||
agent = models.CharField(max_length=256,blank=True)
|
||||
page = models.CharField(max_length=512,blank=True,null=True)
|
||||
agent = models.CharField(max_length=256, blank=True)
|
||||
page = models.CharField(max_length=512, blank=True, null=True)
|
||||
time = models.DateTimeField('event time')
|
||||
host = models.CharField(max_length=64,blank=True)
|
||||
host = models.CharField(max_length=64, blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source,
|
||||
self.event_type, self.page, self.event)
|
||||
return s
|
||||
|
||||
|
||||
|
||||
@@ -17,19 +17,21 @@ from track.models import TrackingLog
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host']
|
||||
LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', 'page', 'time', 'host']
|
||||
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||
event['time'] = dateutil.parser.parse(event['time'])
|
||||
tldat = TrackingLog(**dict( (x,event[x]) for x in LOGFIELDS ))
|
||||
tldat = TrackingLog(**dict((x, event[x]) for x in LOGFIELDS))
|
||||
try:
|
||||
tldat.save()
|
||||
except Exception as err:
|
||||
log.exception(err)
|
||||
|
||||
|
||||
def user_track(request):
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
username = request.user.username
|
||||
@@ -87,13 +89,14 @@ def server_track(request, event_type, event, page=None):
|
||||
"host": request.META['SERVER_NAME'],
|
||||
}
|
||||
|
||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||
return
|
||||
log_event(event)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def view_tracking_log(request,args=''):
|
||||
def view_tracking_log(request, args=''):
|
||||
if not request.user.is_staff:
|
||||
return redirect('/')
|
||||
nlen = 100
|
||||
@@ -104,16 +107,15 @@ def view_tracking_log(request,args=''):
|
||||
nlen = int(arg)
|
||||
if arg.startswith('username='):
|
||||
username = arg[9:]
|
||||
|
||||
|
||||
record_instances = TrackingLog.objects.all().order_by('-time')
|
||||
if username:
|
||||
record_instances = record_instances.filter(username=username)
|
||||
record_instances = record_instances[0:nlen]
|
||||
|
||||
|
||||
# fix dtstamp
|
||||
fmt = '%a %d-%b-%y %H:%M:%S' # "%Y-%m-%d %H:%M:%S %Z%z"
|
||||
for rinst in record_instances:
|
||||
rinst.dtstr = rinst.time.replace(tzinfo=pytz.utc).astimezone(pytz.timezone('US/Eastern')).strftime(fmt)
|
||||
|
||||
return render_to_response('tracking_log.html',{'records':record_instances})
|
||||
|
||||
return render_to_response('tracking_log.html', {'records': record_instances})
|
||||
|
||||
@@ -58,4 +58,3 @@ def cache_if_anonymous(view_func):
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _decorated
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import time, datetime
|
||||
import time
|
||||
import datetime
|
||||
import re
|
||||
import calendar
|
||||
|
||||
|
||||
def time_to_date(time_obj):
|
||||
"""
|
||||
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
|
||||
@@ -9,6 +11,7 @@ def time_to_date(time_obj):
|
||||
# TODO change to using the isoformat() function on datetime. js date can parse those
|
||||
return calendar.timegm(time_obj) * 1000
|
||||
|
||||
|
||||
def jsdate_to_time(field):
|
||||
"""
|
||||
Convert a universal time (iso format) or msec since epoch to a time obj
|
||||
@@ -16,9 +19,9 @@ def jsdate_to_time(field):
|
||||
if field is None:
|
||||
return field
|
||||
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
|
||||
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
||||
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
||||
return d.utctimetuple()
|
||||
elif isinstance(field, int) or isinstance(field, float):
|
||||
return time.gmtime(field / 1000)
|
||||
elif isinstance(field, time.struct_time):
|
||||
return field
|
||||
return field
|
||||
|
||||
@@ -13,7 +13,7 @@ def expect_json(view_function):
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
|
||||
# e.g. 'charset', so we can't do a direct string compare
|
||||
if request.META.get('CONTENT_TYPE','').lower().startswith("application/json"):
|
||||
if request.META.get('CONTENT_TYPE', '').lower().startswith("application/json"):
|
||||
cloned_request = copy.copy(request)
|
||||
cloned_request.POST = cloned_request.POST.copy()
|
||||
cloned_request.POST.update(json.loads(request.body))
|
||||
|
||||
@@ -93,6 +93,7 @@ def accepts(request, media_type):
|
||||
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
|
||||
return media_type in [t for (t, p, q) in accept]
|
||||
|
||||
|
||||
def debug_request(request):
|
||||
"""Return a pretty printed version of the request"""
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@ import re
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import static_replace
|
||||
|
||||
from django.conf import settings
|
||||
from functools import wraps
|
||||
from static_replace import replace_urls
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from xmodule.seq_module import SequenceModule
|
||||
from xmodule.vertical_module import VerticalModule
|
||||
|
||||
log = logging.getLogger("mitx.xmodule_modifiers")
|
||||
|
||||
|
||||
def wrap_xmodule(get_html, module, template, context=None):
|
||||
"""
|
||||
Wraps the results of get_html in a standard <section> with identifying
|
||||
@@ -32,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
|
||||
def _get_html():
|
||||
context.update({
|
||||
'content': get_html(),
|
||||
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
|
||||
'display_name': module.metadata.get('display_name') if module.metadata is not None else None,
|
||||
'class_': module.__class__.__name__,
|
||||
'module_name': module.js_module_name
|
||||
})
|
||||
@@ -49,10 +50,11 @@ def replace_course_urls(get_html, course_id):
|
||||
"""
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
|
||||
return static_replace.replace_course_urls(get_html(), course_id)
|
||||
return _get_html
|
||||
|
||||
def replace_static_urls(get_html, prefix, course_namespace=None):
|
||||
|
||||
def replace_static_urls(get_html, data_dir, course_namespace=None):
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the old get_html function and substitutes urls of the form /static/...
|
||||
@@ -61,7 +63,7 @@ def replace_static_urls(get_html, prefix, course_namespace=None):
|
||||
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return replace_urls(get_html(), staticfiles_prefix=prefix, course_namespace = course_namespace)
|
||||
return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
|
||||
return _get_html
|
||||
|
||||
|
||||
@@ -99,7 +101,7 @@ def add_histogram(get_html, module, user):
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
|
||||
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
|
||||
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
|
||||
return get_html()
|
||||
|
||||
module_id = module.id
|
||||
@@ -115,35 +117,35 @@ def add_histogram(get_html, module, user):
|
||||
# doesn't like symlinks)
|
||||
filepath = filename
|
||||
data_dir = osfs.root_path.rsplit('/')[-1]
|
||||
giturl = module.metadata.get('giturl','https://github.com/MITx')
|
||||
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
|
||||
giturl = module.metadata.get('giturl', 'https://github.com/MITx')
|
||||
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
|
||||
else:
|
||||
edit_link = False
|
||||
# Need to define all the variables that are about to be used
|
||||
giturl = ""
|
||||
data_dir = ""
|
||||
source_file = module.metadata.get('source_file','') # source used to generate the problem XML, eg latex or word
|
||||
source_file = module.metadata.get('source_file', '') # source used to generate the problem XML, eg latex or word
|
||||
|
||||
# useful to indicate to staff if problem has been released or not
|
||||
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
|
||||
now = time.gmtime()
|
||||
is_released = "unknown"
|
||||
mstart = getattr(module.descriptor,'start')
|
||||
mstart = getattr(module.descriptor, 'start')
|
||||
if mstart is not None:
|
||||
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
|
||||
|
||||
staff_context = {'definition': module.definition.get('data'),
|
||||
'metadata': json.dumps(module.metadata, indent=4),
|
||||
'location': module.location,
|
||||
'xqa_key': module.metadata.get('xqa_key',''),
|
||||
'source_file' : source_file,
|
||||
'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file),
|
||||
'xqa_key': module.metadata.get('xqa_key', ''),
|
||||
'source_file': source_file,
|
||||
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
|
||||
'category': str(module.__class__.__name__),
|
||||
# Template uses element_id in js function names, so can't allow dashes
|
||||
'element_id': module.location.html_id().replace('-','_'),
|
||||
'element_id': module.location.html_id().replace('-', '_'),
|
||||
'edit_link': edit_link,
|
||||
'user': user,
|
||||
'xqa_server' : settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa'),
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
|
||||
'histogram': json.dumps(histogram),
|
||||
'render_histogram': render_histogram,
|
||||
'module_content': get_html(),
|
||||
@@ -152,4 +154,3 @@ def add_histogram(get_html, module, user):
|
||||
return render_to_string("staff_problem_info.html", staff_context)
|
||||
|
||||
return _get_html
|
||||
|
||||
|
||||
@@ -121,9 +121,9 @@ def evaluator(variables, functions, string, cs=False):
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
|
||||
@@ -75,7 +75,7 @@ global_context = {'random': random,
|
||||
'draganddrop': verifiers.draganddrop}
|
||||
|
||||
# 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('mitx.' + __name__)
|
||||
|
||||
@@ -453,7 +453,7 @@ class LoncapaProblem(object):
|
||||
exec code in context, context
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + code)
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<','<')
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<', '<')
|
||||
raise responsetypes.LoncapaProblemError(msg)
|
||||
finally:
|
||||
sys.path = original_path
|
||||
@@ -502,7 +502,7 @@ class LoncapaProblem(object):
|
||||
'id': problemtree.get('id'),
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,}}
|
||||
'hintmode': hintmode, }}
|
||||
|
||||
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
||||
the_input = input_type_cls(self.system, problemtree, state)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -17,17 +17,17 @@ from nltk.tree import Tree
|
||||
ARROWS = ('<->', '->')
|
||||
|
||||
## Defines a simple pyparsing tokenizer for chemical equations
|
||||
elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be',
|
||||
'Bh','Bi','Bk','Br','C','Ca','Cd','Ce','Cf','Cl','Cm',
|
||||
'Cn','Co','Cr','Cs','Cu','Db','Ds','Dy','Er','Es','Eu',
|
||||
'F','Fe','Fl','Fm','Fr','Ga','Gd','Ge','H','He','Hf',
|
||||
'Hg','Ho','Hs','I','In','Ir','K','Kr','La','Li','Lr',
|
||||
'Lu','Lv','Md','Mg','Mn','Mo','Mt','N','Na','Nb','Nd',
|
||||
'Ne','Ni','No','Np','O','Os','P','Pa','Pb','Pd','Pm',
|
||||
'Po','Pr','Pt','Pu','Ra','Rb','Re','Rf','Rg','Rh','Rn',
|
||||
'Ru','S','Sb','Sc','Se','Sg','Si','Sm','Sn','Sr','Ta',
|
||||
'Tb','Tc','Te','Th','Ti','Tl','Tm','U','Uuo','Uup',
|
||||
'Uus','Uut','V','W','Xe','Y','Yb','Zn','Zr']
|
||||
elements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be',
|
||||
'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm',
|
||||
'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu',
|
||||
'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf',
|
||||
'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr',
|
||||
'Lu', 'Lv', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd',
|
||||
'Ne', 'Ni', 'No', 'Np', 'O', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm',
|
||||
'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn',
|
||||
'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta',
|
||||
'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'U', 'Uuo', 'Uup',
|
||||
'Uus', 'Uut', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']
|
||||
digits = map(str, range(10))
|
||||
symbols = list("[](){}^+-/")
|
||||
phases = ["(s)", "(l)", "(g)", "(aq)"]
|
||||
@@ -252,7 +252,7 @@ def _get_final_tree(s):
|
||||
'''
|
||||
tokenized = tokenizer.parseString(s)
|
||||
parsed = parser.parse(tokenized)
|
||||
merged = _merge_children(parsed, {'S','group'})
|
||||
merged = _merge_children(parsed, {'S', 'group'})
|
||||
final = _clean_parse_tree(merged)
|
||||
return final
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
# Used by responsetypes and capa_problem
|
||||
|
||||
|
||||
class CorrectMap(object):
|
||||
"""
|
||||
Stores map between answer_id and response evaluation result for each question
|
||||
@@ -152,6 +153,3 @@ class CorrectMap(object):
|
||||
if not isinstance(other_cmap, CorrectMap):
|
||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||
self.cmap.update(other_cmap.get_dict())
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ log = logging.getLogger('mitx.' + __name__)
|
||||
registry = TagRegistry()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MathRenderer(object):
|
||||
tags = ['math']
|
||||
|
||||
@@ -77,6 +79,7 @@ registry.register(MathRenderer)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SolutionRenderer(object):
|
||||
'''
|
||||
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
|
||||
@@ -97,4 +100,3 @@ class SolutionRenderer(object):
|
||||
return etree.XML(html)
|
||||
|
||||
registry.register(SolutionRenderer)
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
registry = TagRegistry()
|
||||
|
||||
|
||||
class Attribute(object):
|
||||
"""
|
||||
Allows specifying required and optional attributes for input types.
|
||||
@@ -413,7 +414,7 @@ class JavascriptInput(InputTypeBase):
|
||||
return [Attribute('params', None),
|
||||
Attribute('problem_state', None),
|
||||
Attribute('display_class', None),
|
||||
Attribute('display_file', None),]
|
||||
Attribute('display_file', None), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
@@ -477,12 +478,13 @@ class TextLine(InputTypeBase):
|
||||
|
||||
def _extra_context(self):
|
||||
return {'do_math': self.do_math,
|
||||
'preprocessor': self.preprocessor,}
|
||||
'preprocessor': self.preprocessor, }
|
||||
|
||||
registry.register(TextLine)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FileSubmission(InputTypeBase):
|
||||
"""
|
||||
Upload some files (e.g. for programming assignments)
|
||||
@@ -508,7 +510,7 @@ class FileSubmission(InputTypeBase):
|
||||
Convert the list of allowed files to a convenient format.
|
||||
"""
|
||||
return [Attribute('allowed_files', '[]', transform=cls.parse_files),
|
||||
Attribute('required_files', '[]', transform=cls.parse_files),]
|
||||
Attribute('required_files', '[]', transform=cls.parse_files), ]
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
@@ -524,7 +526,7 @@ class FileSubmission(InputTypeBase):
|
||||
self.msg = FileSubmission.submitted_msg
|
||||
|
||||
def _extra_context(self):
|
||||
return {'queue_len': self.queue_len,}
|
||||
return {'queue_len': self.queue_len, }
|
||||
return context
|
||||
|
||||
registry.register(FileSubmission)
|
||||
@@ -582,7 +584,7 @@ class CodeInput(InputTypeBase):
|
||||
|
||||
def _extra_context(self):
|
||||
"""Defined queue_len, add it """
|
||||
return {'queue_len': self.queue_len,}
|
||||
return {'queue_len': self.queue_len, }
|
||||
|
||||
registry.register(CodeInput)
|
||||
|
||||
@@ -606,7 +608,7 @@ class Schematic(InputTypeBase):
|
||||
Attribute('parts', None),
|
||||
Attribute('analyses', None),
|
||||
Attribute('initial_value', None),
|
||||
Attribute('submit_analyses', None),]
|
||||
Attribute('submit_analyses', None), ]
|
||||
|
||||
return context
|
||||
|
||||
@@ -614,6 +616,7 @@ registry.register(Schematic)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImageInput(InputTypeBase):
|
||||
"""
|
||||
Clickable image as an input field. Element should specify the image source, height,
|
||||
@@ -635,7 +638,7 @@ class ImageInput(InputTypeBase):
|
||||
"""
|
||||
return [Attribute('src'),
|
||||
Attribute('height'),
|
||||
Attribute('width'),]
|
||||
Attribute('width'), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
@@ -660,6 +663,7 @@ registry.register(ImageInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Crystallography(InputTypeBase):
|
||||
"""
|
||||
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
|
||||
@@ -728,18 +732,19 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
"""
|
||||
Can set size of text field.
|
||||
"""
|
||||
return [Attribute('size', '20'),]
|
||||
return [Attribute('size', '20'), ]
|
||||
|
||||
def _extra_context(self):
|
||||
"""
|
||||
TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded.
|
||||
"""
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js',}
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
|
||||
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DragAndDropInput(InputTypeBase):
|
||||
"""
|
||||
Input for drag and drop problems. Allows student to drag and drop images and
|
||||
|
||||
@@ -186,9 +186,9 @@ class LoncapaResponse(object):
|
||||
tree = etree.Element('span')
|
||||
|
||||
# problem author can make this span display:inline
|
||||
if self.xml.get('inline',''):
|
||||
tree.set('class','inline')
|
||||
|
||||
if self.xml.get('inline', ''):
|
||||
tree.set('class', 'inline')
|
||||
|
||||
for item in self.xml:
|
||||
# call provided procedure to do the rendering
|
||||
item_xhtml = renderer(item)
|
||||
@@ -1294,7 +1294,7 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# State associated with the queueing request
|
||||
queuestate = {'key': queuekey,
|
||||
'time': qtime,}
|
||||
'time': qtime, }
|
||||
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
|
||||
@@ -8,6 +8,7 @@ import xml.sax.saxutils as saxutils
|
||||
|
||||
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def tst_render_template(template, context):
|
||||
"""
|
||||
A test version of render to template. Renders to the repr of the context, completely ignoring
|
||||
@@ -25,7 +26,7 @@ test_system = Mock(
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id = 'student'
|
||||
anonymous_student_id='student'
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from capa import customrender
|
||||
# just a handy shortcut
|
||||
lookup_tag = customrender.registry.get_class_for_tag
|
||||
|
||||
|
||||
def extract_context(xml):
|
||||
"""
|
||||
Given an xml element corresponding to the output of test_system.render_template, get back the
|
||||
@@ -15,9 +16,11 @@ def extract_context(xml):
|
||||
"""
|
||||
return eval(xml.text)
|
||||
|
||||
|
||||
def quote_attr(s):
|
||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||
|
||||
|
||||
class HelperTest(unittest.TestCase):
|
||||
'''
|
||||
Make sure that our helper function works!
|
||||
@@ -50,7 +53,7 @@ class SolutionRenderTest(unittest.TestCase):
|
||||
# our test_system "renders" templates to a div with the repr of the context
|
||||
xml = renderer.get_html()
|
||||
context = extract_context(xml)
|
||||
self.assertEqual(context, {'id' : 'solution_12'})
|
||||
self.assertEqual(context, {'id': 'solution_12'})
|
||||
|
||||
|
||||
class MathRenderTest(unittest.TestCase):
|
||||
@@ -65,12 +68,11 @@ class MathRenderTest(unittest.TestCase):
|
||||
renderer = lookup_tag('math')(test_system, element)
|
||||
|
||||
self.assertEqual(renderer.mathstr, mathjax_out)
|
||||
|
||||
|
||||
def test_parsing(self):
|
||||
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
|
||||
self.check_parse('$abc', '$abc')
|
||||
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
|
||||
|
||||
|
||||
|
||||
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated by CoffeeScript 1.3.3
|
||||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var MinimaxProblemDisplay, root,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated by CoffeeScript 1.3.3
|
||||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var TestProblemGenerator, root,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated by CoffeeScript 1.3.3
|
||||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var TestProblemGrader, root,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated by CoffeeScript 1.3.3
|
||||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ lookup_tag = inputtypes.registry.get_class_for_tag
|
||||
def quote_attr(s):
|
||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||
|
||||
|
||||
class OptionInputTest(unittest.TestCase):
|
||||
'''
|
||||
Make sure option inputs work
|
||||
@@ -100,7 +101,7 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
'input_type': expected_input_type,
|
||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||
('foil2', '<text>This is foil Two.</text>'),
|
||||
('foil3', 'This is foil Three.'),],
|
||||
('foil3', 'This is foil Three.'), ],
|
||||
'name_array_suffix': expected_suffix, # what is this for??
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ class JavascriptInputTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': '3',}
|
||||
state = {'value': '3', }
|
||||
the_input = lookup_tag('javascriptinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -149,7 +150,7 @@ class JavascriptInputTest(unittest.TestCase):
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,}
|
||||
'problem_state': problem_state, }
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -165,7 +166,7 @@ class TextLineTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
state = {'value': 'BumbleBee', }
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -193,7 +194,7 @@ class TextLineTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
state = {'value': 'BumbleBee', }
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -231,7 +232,7 @@ class FileSubmissionTest(unittest.TestCase):
|
||||
|
||||
state = {'value': 'BumbleBee.py',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
'feedback': {'message': '3'}, }
|
||||
input_class = lookup_tag('filesubmission')
|
||||
the_input = input_class(test_system, element, state)
|
||||
|
||||
@@ -275,7 +276,7 @@ class CodeInputTest(unittest.TestCase):
|
||||
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
'feedback': {'message': '3'}, }
|
||||
|
||||
input_class = lookup_tag('codeinput')
|
||||
the_input = input_class(test_system, element, state)
|
||||
@@ -488,7 +489,7 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'H2OYeah',}
|
||||
state = {'value': 'H2OYeah', }
|
||||
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
@@ -16,6 +16,7 @@ from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from capa.xqueue_interface import dateformat
|
||||
|
||||
|
||||
class MultiChoiceTest(unittest.TestCase):
|
||||
def test_MC_grade(self):
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
||||
@@ -295,16 +296,16 @@ class CodeResponseTest(unittest.TestCase):
|
||||
old_cmap = CorrectMap()
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now())
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
|
||||
|
||||
# Message format common to external graders
|
||||
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
||||
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg': grader_msg})
|
||||
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg': grader_msg})
|
||||
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
||||
correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
|
||||
incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
|
||||
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,}
|
||||
'incorrect': incorrect_score_msg, }
|
||||
|
||||
# Incorrect queuekey, state should not be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
@@ -325,7 +326,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
|
||||
new_cmap = CorrectMap()
|
||||
new_cmap.update(old_cmap)
|
||||
npoints = 1 if correctness=='correct' else 0
|
||||
npoints = 1 if correctness == 'correct' else 0
|
||||
new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None)
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
||||
@@ -361,7 +362,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
latest_timestamp = datetime.now()
|
||||
queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp)
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
|
||||
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
|
||||
test_lcp.correct_map.update(cmap)
|
||||
|
||||
@@ -412,6 +413,7 @@ class ChoiceResponseTest(unittest.TestCase):
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
|
||||
|
||||
|
||||
class JavascriptResponseTest(unittest.TestCase):
|
||||
|
||||
def test_jr_grade(self):
|
||||
@@ -424,4 +426,3 @@ class JavascriptResponseTest(unittest.TestCase):
|
||||
|
||||
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
|
||||
|
||||
@@ -51,15 +51,17 @@ def convert_files_to_filenames(answers):
|
||||
new_answers = dict()
|
||||
for answer_id in answers.keys():
|
||||
answer = answers[answer_id]
|
||||
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
||||
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
||||
new_answers[answer_id] = [f.name for f in answer]
|
||||
else:
|
||||
new_answers[answer_id] = answers[answer_id]
|
||||
return new_answers
|
||||
|
||||
|
||||
def is_list_of_files(files):
|
||||
return isinstance(files, list) and all(is_file(f) for f in files)
|
||||
|
||||
|
||||
def is_file(file_to_test):
|
||||
'''
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
@@ -79,11 +81,10 @@ def find_with_default(node, path, default):
|
||||
|
||||
Returns:
|
||||
node.find(path).text if the find succeeds, default otherwise.
|
||||
|
||||
|
||||
"""
|
||||
v = node.find(path)
|
||||
if v is not None:
|
||||
return v.text
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
dateformat = '%Y%m%d%H%M%S'
|
||||
|
||||
|
||||
def make_hashkey(seed):
|
||||
'''
|
||||
Generate a string key by hashing
|
||||
@@ -29,9 +30,9 @@ 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)
|
||||
}
|
||||
"""
|
||||
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
||||
return json.dumps({'lms_callback_url': lms_callback_url,
|
||||
'lms_key': lms_key,
|
||||
'queue_name': queue_name })
|
||||
'queue_name': queue_name})
|
||||
|
||||
|
||||
def parse_xreply(xreply):
|
||||
@@ -96,18 +97,18 @@ class XQueueInterface(object):
|
||||
|
||||
|
||||
def _login(self):
|
||||
payload = { 'username': self.auth['username'],
|
||||
'password': self.auth['password'] }
|
||||
payload = {'username': self.auth['username'],
|
||||
'password': self.auth['password']}
|
||||
return self._http_post(self.url + '/xqueue/login/', payload)
|
||||
|
||||
|
||||
def _send_to_queue(self, header, body, files_to_upload):
|
||||
payload = {'xqueue_header': header,
|
||||
'xqueue_body' : body}
|
||||
'xqueue_body': body}
|
||||
files = {}
|
||||
if files_to_upload is not None:
|
||||
for f in files_to_upload:
|
||||
files.update({ f.name: f })
|
||||
files.update({f.name: f})
|
||||
|
||||
return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ A handy util to print a django-debug-screen-like stack trace with
|
||||
values of local variables.
|
||||
"""
|
||||
|
||||
import sys, traceback
|
||||
import sys
|
||||
import traceback
|
||||
from django.utils.encoding import smart_unicode
|
||||
|
||||
|
||||
@@ -48,5 +49,3 @@ def supertrace(max_len=160):
|
||||
print s
|
||||
except:
|
||||
print "<ERROR WHILE PRINTING VALUE>"
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ setup(
|
||||
"html = xmodule.html_module:HtmlDescriptor",
|
||||
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"error = xmodule.error_module:ErrorDescriptor",
|
||||
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
|
||||
"problem = xmodule.capa_module:CapaDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
|
||||
@@ -51,7 +51,7 @@ class ABTestModule(XModule):
|
||||
|
||||
def get_shared_state(self):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
|
||||
def get_child_descriptors(self):
|
||||
active_locations = set(self.definition['data']['group_content'][self.group])
|
||||
return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
|
||||
@@ -171,7 +171,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
|
||||
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
|
||||
return xml_object
|
||||
|
||||
|
||||
|
||||
|
||||
def has_dynamic_children(self):
|
||||
return True
|
||||
|
||||
@@ -29,6 +29,7 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
|
||||
# Generated this many different variants of problems with rerandomize=per_student
|
||||
NUM_RANDOMIZATION_BINS = 20
|
||||
|
||||
|
||||
def randomization_bin(seed, problem_id):
|
||||
"""
|
||||
Pick a randomization bin for the problem given the user's seed and a problem id.
|
||||
@@ -43,6 +44,7 @@ def randomization_bin(seed, problem_id):
|
||||
# get the first few digits of the hash, convert to an int, then mod.
|
||||
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
|
||||
|
||||
|
||||
def only_one(lst, default="", process=lambda x: x):
|
||||
"""
|
||||
If lst is empty, returns default
|
||||
@@ -283,7 +285,7 @@ class CapaModule(XModule):
|
||||
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
state=None, # Tabula rasa
|
||||
state=None, # Tabula rasa
|
||||
seed=self.seed, system=self.system)
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
@@ -302,7 +304,7 @@ class CapaModule(XModule):
|
||||
html = warning
|
||||
try:
|
||||
html += self.lcp.get_html()
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
log.exception(err)
|
||||
raise
|
||||
|
||||
@@ -315,7 +317,7 @@ class CapaModule(XModule):
|
||||
# check button is context-specific.
|
||||
|
||||
# Put a "Check" button if unlimited attempts or still some left
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts-1:
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts - 1:
|
||||
check_button = "Check"
|
||||
else:
|
||||
# Will be final check so let user know that
|
||||
@@ -369,7 +371,7 @@ class CapaModule(XModule):
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
|
||||
return self.system.replace_urls(html)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -490,7 +492,7 @@ class CapaModule(XModule):
|
||||
new_answers = dict()
|
||||
for answer_id in answers:
|
||||
try:
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id])}
|
||||
except TypeError:
|
||||
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
|
||||
new_answer = {answer_id: answers[answer_id]}
|
||||
@@ -561,9 +563,9 @@ class CapaModule(XModule):
|
||||
current_time = datetime.datetime.now()
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
waittime_between_requests = self.system.xqueue['waittime']
|
||||
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
|
||||
try:
|
||||
old_state = self.lcp.get_state()
|
||||
@@ -596,7 +598,7 @@ class CapaModule(XModule):
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
|
||||
# render problem into HTML
|
||||
@@ -707,7 +709,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor,self).editable_metadata_fields
|
||||
subset = super(CapaDescriptor, self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
subset.remove('markdown')
|
||||
return subset
|
||||
|
||||
@@ -54,14 +54,16 @@ ACCEPT_FILE_UPLOAD = False
|
||||
TRUE_DICT = ["True", True, "TRUE", "true"]
|
||||
|
||||
HUMAN_TASK_TYPE = {
|
||||
'selfassessment' : "Self Assessment",
|
||||
'openended' : "External Grader",
|
||||
'selfassessment': "Self Assessment",
|
||||
'openended': "External Grader",
|
||||
}
|
||||
|
||||
|
||||
class IncorrectMaxScoreError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class CombinedOpenEndedModule(XModule):
|
||||
"""
|
||||
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
log=logging.getLogger(__name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RubricParsingError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class CombinedOpenEndedRubric(object):
|
||||
|
||||
def __init__ (self, system, view_only = False):
|
||||
@@ -27,13 +29,15 @@ class CombinedOpenEndedRubric(object):
|
||||
success = False
|
||||
try:
|
||||
rubric_categories = self.extract_categories(rubric_xml)
|
||||
html = self.system.render_template('open_ended_rubric.html',
|
||||
{'categories' : rubric_categories,
|
||||
html = self.system.render_template('open_ended_rubric.html',
|
||||
{'categories': rubric_categories,
|
||||
'has_score': self.has_score,
|
||||
'view_only': self.view_only})
|
||||
success = True
|
||||
except:
|
||||
raise RubricParsingError("[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml))
|
||||
error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)
|
||||
log.error(error_message)
|
||||
raise RubricParsingError(error_message)
|
||||
return success, html
|
||||
|
||||
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
|
||||
@@ -58,8 +62,8 @@ class CombinedOpenEndedRubric(object):
|
||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||
},
|
||||
{ category: "Category 2 Name",
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
{text: "Option 3 Name", points: 2]}]
|
||||
|
||||
'''
|
||||
@@ -75,7 +79,7 @@ class CombinedOpenEndedRubric(object):
|
||||
|
||||
|
||||
def extract_category(self, category):
|
||||
'''
|
||||
'''
|
||||
construct an individual category
|
||||
{category: "Category 1 Name",
|
||||
options: [{text: "Option 1 text", points: 1},
|
||||
@@ -108,7 +112,7 @@ class CombinedOpenEndedRubric(object):
|
||||
autonumbering = True
|
||||
# parse options
|
||||
for option in optionsxml:
|
||||
if option.tag != 'option':
|
||||
if option.tag != 'option':
|
||||
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
else:
|
||||
pointstr = option.get("points")
|
||||
@@ -125,7 +129,7 @@ class CombinedOpenEndedRubric(object):
|
||||
cur_points = cur_points + 1
|
||||
else:
|
||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
|
||||
|
||||
|
||||
selected = score == points
|
||||
optiontext = option.text
|
||||
options.append({'text': option.text, 'points': points, 'selected': selected})
|
||||
|
||||
@@ -9,12 +9,13 @@ from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
class ConditionalModule(XModule):
|
||||
'''
|
||||
Blocks child module from showing unless certain conditions are met.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
@@ -37,13 +38,13 @@ class ConditionalModule(XModule):
|
||||
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
|
||||
"""
|
||||
In addition to the normal XModule init, provide:
|
||||
|
||||
|
||||
self.condition = string describing condition required
|
||||
|
||||
"""
|
||||
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
|
||||
self.contents = None
|
||||
self.condition = self.metadata.get('condition','')
|
||||
self.condition = self.metadata.get('condition', '')
|
||||
#log.debug('conditional module required=%s' % self.required_modules_list)
|
||||
|
||||
def _get_required_modules(self):
|
||||
@@ -56,7 +57,7 @@ class ConditionalModule(XModule):
|
||||
def is_condition_satisfied(self):
|
||||
self._get_required_modules()
|
||||
|
||||
if self.condition=='require_completed':
|
||||
if self.condition == 'require_completed':
|
||||
# all required modules must be completed, as determined by
|
||||
# the modules .is_completed() method
|
||||
for module in self.required_modules:
|
||||
@@ -70,7 +71,7 @@ class ConditionalModule(XModule):
|
||||
else:
|
||||
log.debug('conditional module: %s IS completed' % module)
|
||||
return True
|
||||
elif self.condition=='require_attempted':
|
||||
elif self.condition == 'require_attempted':
|
||||
# all required modules must be attempted, as determined by
|
||||
# the modules .is_attempted() method
|
||||
for module in self.required_modules:
|
||||
@@ -111,9 +112,10 @@ class ConditionalModule(XModule):
|
||||
|
||||
# for now, just deal with one child
|
||||
html = self.contents[0]
|
||||
|
||||
|
||||
return json.dumps({'html': html})
|
||||
|
||||
|
||||
class ConditionalDescriptor(SequenceDescriptor):
|
||||
module_class = ConditionalModule
|
||||
|
||||
@@ -125,7 +127,7 @@ class ConditionalDescriptor(SequenceDescriptor):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')]
|
||||
required_module_list = [tuple(x.split('/', 1)) for x in self.metadata.get('required', '').split('&')]
|
||||
self.required_module_locations = []
|
||||
for (tag, name) in required_module_list:
|
||||
loc = self.location.dict()
|
||||
@@ -133,9 +135,8 @@ class ConditionalDescriptor(SequenceDescriptor):
|
||||
loc['name'] = name
|
||||
self.required_module_locations.append(Location(loc))
|
||||
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
|
||||
|
||||
|
||||
def get_required_module_descriptors(self):
|
||||
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
|
||||
not children of this module"""
|
||||
return [self.system.load_item(loc) for loc in self.required_module_locations]
|
||||
|
||||
|
||||
@@ -11,15 +11,16 @@ from xmodule.modulestore import Location
|
||||
from .django import contentstore
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class StaticContent(object):
|
||||
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None):
|
||||
self.location = loc
|
||||
self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||
self.content_type = content_type
|
||||
self.data = data
|
||||
self.last_modified_at = last_modified_at
|
||||
self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None
|
||||
# optional information about where this file was imported from. This is needed to support import/export
|
||||
# optional information about where this file was imported from. This is needed to support import/export
|
||||
# cycles
|
||||
self.import_path = import_path
|
||||
|
||||
@@ -29,7 +30,7 @@ class StaticContent(object):
|
||||
|
||||
@staticmethod
|
||||
def generate_thumbnail_name(original_name):
|
||||
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
||||
return ('{0}' + XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
||||
|
||||
@staticmethod
|
||||
def compute_location(org, course, name, revision=None, is_thumbnail=False):
|
||||
@@ -41,7 +42,7 @@ class StaticContent(object):
|
||||
|
||||
def get_url_path(self):
|
||||
return StaticContent.get_url_path_from_location(self.location)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_url_path_from_location(location):
|
||||
if location is not None:
|
||||
@@ -56,15 +57,15 @@ class StaticContent(object):
|
||||
|
||||
@staticmethod
|
||||
def get_id_from_location(location):
|
||||
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
|
||||
'category' : location.category, 'name' : location.name,
|
||||
'revision' : location.revision}
|
||||
return {'tag': location.tag, 'org': location.org, 'course': location.course,
|
||||
'category': location.category, 'name': location.name,
|
||||
'revision': location.revision}
|
||||
@staticmethod
|
||||
def get_location_from_path(path):
|
||||
# remove leading / character if it is there one
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
|
||||
return Location(path.split('/'))
|
||||
|
||||
@staticmethod
|
||||
@@ -77,7 +78,7 @@ class StaticContent(object):
|
||||
return StaticContent.get_url_path_from_location(loc)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ContentStore(object):
|
||||
'''
|
||||
@@ -95,14 +96,14 @@ class ContentStore(object):
|
||||
|
||||
[
|
||||
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
||||
|
||||
....
|
||||
@@ -117,7 +118,7 @@ class ContentStore(object):
|
||||
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
|
||||
|
||||
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
|
||||
thumbnail_name, is_thumbnail = True)
|
||||
thumbnail_name, is_thumbnail=True)
|
||||
|
||||
# if we're uploading an image, then let's generate a thumbnail so that we can
|
||||
# serve it up when needed without having to rescale on the fly
|
||||
@@ -129,7 +130,7 @@ class ContentStore(object):
|
||||
# @todo: move the thumbnail size to a configuration setting?!?
|
||||
im = Image.open(StringIO.StringIO(content.data))
|
||||
|
||||
# I've seen some exceptions from the PIL library when trying to save palletted
|
||||
# I've seen some exceptions from the PIL library when trying to save palletted
|
||||
# PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
|
||||
im = im.convert('RGB')
|
||||
size = 128, 128
|
||||
@@ -139,7 +140,7 @@ class ContentStore(object):
|
||||
thumbnail_file.seek(0)
|
||||
|
||||
# store this thumbnail as any other piece of content
|
||||
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
||||
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
||||
'image/jpeg', thumbnail_file)
|
||||
|
||||
contentstore().save(thumbnail_content)
|
||||
@@ -149,7 +150,3 @@ class ContentStore(object):
|
||||
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
|
||||
|
||||
return thumbnail_content, thumbnail_file_location
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
|
||||
_CONTENTSTORE = None
|
||||
|
||||
|
||||
def load_function(path):
|
||||
"""
|
||||
Load a function by name.
|
||||
|
||||
@@ -17,14 +17,14 @@ import os
|
||||
|
||||
class MongoContentStore(ContentStore):
|
||||
def __init__(self, host, db, port=27017, user=None, password=None, **kwargs):
|
||||
logging.debug( 'Using MongoDB for static content serving at host={0} db={1}'.format(host,db))
|
||||
logging.debug('Using MongoDB for static content serving at host={0} db={1}'.format(host, db))
|
||||
_db = Connection(host=host, port=port, **kwargs)[db]
|
||||
|
||||
if user is not None and password is not None:
|
||||
_db.authenticate(user, password)
|
||||
|
||||
self.fs = gridfs.GridFS(_db)
|
||||
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
|
||||
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
|
||||
|
||||
|
||||
def save(self, content):
|
||||
@@ -33,24 +33,24 @@ class MongoContentStore(ContentStore):
|
||||
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
|
||||
self.delete(id)
|
||||
|
||||
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
|
||||
|
||||
fp.write(content.data)
|
||||
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def delete(self, id):
|
||||
if self.fs.exists({"_id" : id}):
|
||||
if self.fs.exists({"_id": id}):
|
||||
self.fs.delete(id)
|
||||
|
||||
def find(self, location):
|
||||
id = StaticContent.get_id_from_location(location)
|
||||
try:
|
||||
with self.fs.get(id) as fp:
|
||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
|
||||
fp.uploadDate, thumbnail_location = fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path = fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
|
||||
fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
except NoFile:
|
||||
raise NotFoundError()
|
||||
|
||||
@@ -76,25 +76,25 @@ class MongoContentStore(ContentStore):
|
||||
self.export(asset_location, output_directory)
|
||||
|
||||
def get_all_content_thumbnails_for_course(self, location):
|
||||
return self._get_all_content_for_course(location, get_thumbnails = True)
|
||||
return self._get_all_content_for_course(location, get_thumbnails=True)
|
||||
|
||||
def get_all_content_for_course(self, location):
|
||||
return self._get_all_content_for_course(location, get_thumbnails = False)
|
||||
return self._get_all_content_for_course(location, get_thumbnails=False)
|
||||
|
||||
def _get_all_content_for_course(self, location, get_thumbnails = False):
|
||||
def _get_all_content_for_course(self, location, get_thumbnails=False):
|
||||
'''
|
||||
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
|
||||
|
||||
[
|
||||
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
||||
|
||||
....
|
||||
@@ -102,10 +102,7 @@ class MongoContentStore(ContentStore):
|
||||
]
|
||||
'''
|
||||
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
|
||||
course=location.course,org=location.org)
|
||||
course=location.course, org=location.org)
|
||||
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
|
||||
items = self.fs_files.find(location_to_query(course_filter))
|
||||
return list(items)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -147,37 +147,37 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
"""
|
||||
Return a dict which is a copy of the default grading policy
|
||||
"""
|
||||
default = {"GRADER" : [
|
||||
default = {"GRADER": [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
"type": "Homework",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"short_label": "HW",
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"weight" : 0.15
|
||||
"type": "Lab",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"weight" : 0.3
|
||||
"type": "Midterm Exam",
|
||||
"short_label": "Midterm",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"weight" : 0.4
|
||||
"type": "Final Exam",
|
||||
"short_label": "Final",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"Pass" : 0.5
|
||||
"GRADE_CUTOFFS": {
|
||||
"Pass": 0.5
|
||||
}}
|
||||
return copy.deepcopy(default)
|
||||
|
||||
@@ -230,8 +230,8 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
# bleh, have to parse the XML here to just pull out the url_name attribute
|
||||
# I don't think it's stored anywhere in the instance.
|
||||
course_file = StringIO(xml_data.encode('ascii','ignore'))
|
||||
xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot()
|
||||
course_file = StringIO(xml_data.encode('ascii', 'ignore'))
|
||||
xml_obj = etree.parse(course_file, parser=edx_xml_parser).getroot()
|
||||
|
||||
policy_dir = None
|
||||
url_name = xml_obj.get('url_name', xml_obj.get('slug'))
|
||||
@@ -329,7 +329,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def raw_grader(self, value):
|
||||
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
|
||||
self._grading_policy['RAW_GRADER'] = value
|
||||
self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value
|
||||
self.definition['data'].setdefault('grading_policy', {})['GRADER'] = value
|
||||
|
||||
@property
|
||||
def grade_cutoffs(self):
|
||||
@@ -338,7 +338,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@grade_cutoffs.setter
|
||||
def grade_cutoffs(self, value):
|
||||
self._grading_policy['GRADE_CUTOFFS'] = value
|
||||
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
|
||||
self.definition['data'].setdefault('grading_policy', {})['GRADE_CUTOFFS'] = value
|
||||
|
||||
|
||||
@property
|
||||
@@ -377,7 +377,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
Return list of topic ids defined in course policy.
|
||||
"""
|
||||
topics = self.metadata.get("discussion_topics", {})
|
||||
return [d["id"] for d in topics.values()]
|
||||
return [d["id"] for d in topics.values()]
|
||||
|
||||
|
||||
@property
|
||||
@@ -436,10 +436,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
scale = 300.0 # about a year
|
||||
if announcement:
|
||||
days = (now - announcement).days
|
||||
score = -exp(-days/scale)
|
||||
score = -exp(-days / scale)
|
||||
else:
|
||||
days = (now - start).days
|
||||
score = exp(days/scale)
|
||||
score = exp(days / scale)
|
||||
return score
|
||||
|
||||
def _sorting_dates(self):
|
||||
@@ -501,16 +501,16 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
xmoduledescriptors.append(s)
|
||||
|
||||
# The xmoduledescriptors included here are only the ones that have scores.
|
||||
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
|
||||
section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)}
|
||||
|
||||
section_format = s.metadata.get('format', "")
|
||||
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
|
||||
graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
|
||||
|
||||
all_descriptors.extend(xmoduledescriptors)
|
||||
all_descriptors.append(s)
|
||||
|
||||
return { 'graded_sections' : graded_sections,
|
||||
'all_descriptors' : all_descriptors,}
|
||||
return {'graded_sections': graded_sections,
|
||||
'all_descriptors': all_descriptors, }
|
||||
|
||||
|
||||
@staticmethod
|
||||
@@ -636,7 +636,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# *end* of the same day, not the same time. It's going to be used as the
|
||||
# end of the exam overall, so we don't want the exam to disappear too soon.
|
||||
# It's also used optionally as the registration end date, so time matters there too.
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
if self.last_eligible_appointment_date is None:
|
||||
raise ValueError("Last appointment date must be specified")
|
||||
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
|
||||
@@ -715,4 +715,3 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
@@ -52,13 +52,17 @@ em, i {
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-style: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: 1em 0;
|
||||
padding: 0 0 0 1em;
|
||||
|
||||
@@ -6,6 +6,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class DiscussionModule(XModule):
|
||||
js = {'coffee':
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
@@ -30,6 +31,7 @@ class DiscussionModule(XModule):
|
||||
self.title = xml_data.attrib['for']
|
||||
self.discussion_category = xml_data.attrib['discussion_category']
|
||||
|
||||
|
||||
class DiscussionDescriptor(RawDescriptor):
|
||||
module_class = DiscussionModule
|
||||
template_dir_name = "discussion"
|
||||
|
||||
@@ -8,12 +8,14 @@ log = logging.getLogger(__name__)
|
||||
|
||||
ErrorLog = namedtuple('ErrorLog', 'tracker errors')
|
||||
|
||||
|
||||
def exc_info_to_str(exc_info):
|
||||
"""Given some exception info, convert it into a string using
|
||||
the traceback.format_exception() function.
|
||||
"""
|
||||
return ''.join(traceback.format_exception(*exc_info))
|
||||
|
||||
|
||||
def in_exception_handler():
|
||||
'''Is there an active exception?'''
|
||||
return sys.exc_info() != (None, None, None)
|
||||
@@ -44,6 +46,7 @@ def make_error_tracker():
|
||||
|
||||
return ErrorLog(error_tracker, errors)
|
||||
|
||||
|
||||
def null_error_tracker(msg):
|
||||
'''A dummy error tracker that just ignores the messages'''
|
||||
pass
|
||||
|
||||
@@ -49,6 +49,7 @@ def invalid_args(func, argdict):
|
||||
if keywords: return set() # All accepted
|
||||
return set(argdict) - set(args)
|
||||
|
||||
|
||||
def grader_from_conf(conf):
|
||||
"""
|
||||
This creates a CourseGrader from a configuration (such as in course_settings.py).
|
||||
@@ -80,7 +81,7 @@ def grader_from_conf(conf):
|
||||
subgrader_class = SingleSectionGrader
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
|
||||
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
|
||||
# See note above concerning 'name'.
|
||||
if bad_args.issuperset({name}):
|
||||
@@ -90,7 +91,7 @@ def grader_from_conf(conf):
|
||||
log.warning("Invalid arguments for a subgrader: %s", bad_args)
|
||||
for key in bad_args:
|
||||
del subgraderconf[key]
|
||||
|
||||
|
||||
subgrader = subgrader_class(**subgraderconf)
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
|
||||
@@ -210,13 +211,13 @@ class SingleSectionGrader(CourseGrader):
|
||||
break
|
||||
|
||||
if foundScore or generate_random_scores:
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2,15)
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2, 15)
|
||||
possible = random.randint(earned, 15)
|
||||
else: # We found the score
|
||||
else: # We found the score
|
||||
earned = foundScore.earned
|
||||
possible = foundScore.possible
|
||||
|
||||
|
||||
percent = earned / float(possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(name=self.name,
|
||||
percent=percent,
|
||||
@@ -245,7 +246,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
min_count defines how many assignments are expected throughout the course. Placeholder
|
||||
scores (of 0) will be inserted if the number of matching sections in the course is < min_count.
|
||||
If there number of matching sections in the course is > min_count, min_count will be ignored.
|
||||
|
||||
|
||||
show_only_average is to suppress the display of each assignment in this grader and instead
|
||||
only show the total score of this grader in the breakdown.
|
||||
|
||||
@@ -257,7 +258,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
short_label is similar to section_type, but shorter. For example, for Homework it would be
|
||||
"HW".
|
||||
|
||||
|
||||
starting_index is the first number that will appear. For example, starting_index=3 and
|
||||
min_count = 2 would produce the labels "Assignment 3", "Assignment 4"
|
||||
|
||||
@@ -296,16 +297,16 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
breakdown = []
|
||||
for i in range(max(self.min_count, len(scores))):
|
||||
if i < len(scores) or generate_random_scores:
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2,15)
|
||||
possible = random.randint(earned, 15)
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2, 15)
|
||||
possible = random.randint(earned, 15)
|
||||
section_name = "Generated"
|
||||
|
||||
|
||||
else:
|
||||
earned = scores[i].earned
|
||||
possible = scores[i].possible
|
||||
section_name = scores[i].section
|
||||
|
||||
|
||||
percentage = earned / float(possible)
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index=i + self.starting_index,
|
||||
section_type=self.section_type,
|
||||
@@ -318,7 +319,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + self.starting_index, section_type=self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index=i + self.starting_index, short_label=self.short_label)
|
||||
|
||||
|
||||
breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category})
|
||||
|
||||
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
|
||||
@@ -328,13 +329,13 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent=total_percent, section_type=self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label=self.short_label)
|
||||
|
||||
|
||||
if self.show_only_average:
|
||||
breakdown = []
|
||||
|
||||
|
||||
if not self.hide_average:
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
|
||||
129
common/lib/xmodule/xmodule/grading_service_module.py
Normal file
129
common/lib/xmodule/xmodule/grading_service_module.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# This class gives a common interface for logging into the grading controller
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException, ConnectionError, HTTPError
|
||||
import sys
|
||||
|
||||
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
|
||||
from lxml import etree
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GradingService(object):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
self.url = config['url']
|
||||
self.login_url = self.url + '/login/'
|
||||
self.session = requests.session()
|
||||
self.system = config['system']
|
||||
|
||||
def _login(self):
|
||||
"""
|
||||
Log into the staff grading service.
|
||||
|
||||
Raises requests.exceptions.HTTPError if something goes wrong.
|
||||
|
||||
Returns the decoded json dict of the response.
|
||||
"""
|
||||
response = self.session.post(self.login_url,
|
||||
{'username': self.username,
|
||||
'password': self.password, })
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json
|
||||
|
||||
def post(self, url, data, allow_redirects=False):
|
||||
"""
|
||||
Make a post request to the grading controller
|
||||
"""
|
||||
try:
|
||||
op = lambda: self.session.post(url, data=data,
|
||||
allow_redirects=allow_redirects)
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
def get(self, url, params, allow_redirects=False):
|
||||
"""
|
||||
Make a get request to the grading controller
|
||||
"""
|
||||
log.debug(params)
|
||||
op = lambda: self.session.get(url,
|
||||
allow_redirects=allow_redirects,
|
||||
params=params)
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
|
||||
def _try_with_login(self, operation):
|
||||
"""
|
||||
Call operation(), which should return a requests response object. If
|
||||
the request fails with a 'login_required' error, call _login() and try
|
||||
the operation again.
|
||||
|
||||
Returns the result of operation(). Does not catch exceptions.
|
||||
"""
|
||||
response = operation()
|
||||
if (response.json
|
||||
and response.json.get('success') == False
|
||||
and response.json.get('error') == 'login_required'):
|
||||
# apparrently we aren't logged in. Try to fix that.
|
||||
r = self._login()
|
||||
if r and not r.get('success'):
|
||||
log.warning("Couldn't log into staff_grading backend. Response: %s",
|
||||
r)
|
||||
# try again
|
||||
response = operation()
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def _render_rubric(self, response, view_only=False):
|
||||
"""
|
||||
Given an HTTP Response with the key 'rubric', render out the html
|
||||
required to display the rubric and put it back into the response
|
||||
|
||||
returns the updated response as a dictionary that can be serialized later
|
||||
|
||||
"""
|
||||
try:
|
||||
response_json = json.loads(response)
|
||||
except:
|
||||
response_json = response
|
||||
|
||||
try:
|
||||
if 'rubric' in response_json:
|
||||
rubric = response_json['rubric']
|
||||
rubric_renderer = CombinedOpenEndedRubric(self.system, False)
|
||||
success, rubric_html = rubric_renderer.render_rubric(rubric)
|
||||
response_json['rubric'] = rubric_html
|
||||
return response_json
|
||||
# if we can't parse the rubric into HTML,
|
||||
except etree.XMLSyntaxError, RubricParsingError:
|
||||
log.exception("Cannot parse rubric string. Raw string: {0}"
|
||||
.format(rubric))
|
||||
return {'success': False,
|
||||
'error': 'Error displaying submission'}
|
||||
except ValueError:
|
||||
log.exception("Error parsing response: {0}".format(response))
|
||||
return {'success': False,
|
||||
'error': "Error displaying submission"}
|
||||
@@ -1,5 +1,6 @@
|
||||
from lxml import etree
|
||||
|
||||
|
||||
def check_html(html):
|
||||
'''
|
||||
Check whether the passed in html string can be parsed by lxml.
|
||||
|
||||
@@ -133,7 +133,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
|
||||
# TODO (ichuang): remove this after migration
|
||||
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
|
||||
definition['filename'] = [ filepath, filename ]
|
||||
definition['filename'] = [filepath, filename]
|
||||
|
||||
return definition
|
||||
|
||||
@@ -180,6 +180,7 @@ class AboutDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
template_dir_name = "about"
|
||||
|
||||
|
||||
class StaticTabDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
@@ -187,6 +188,7 @@ class StaticTabDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
template_dir_name = "statictab"
|
||||
|
||||
|
||||
class CourseInfoDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
|
||||
@@ -10,7 +10,8 @@ class @HTMLEditingDescriptor
|
||||
lineWrapping: true
|
||||
})
|
||||
|
||||
$(@advanced_editor.getWrapperElement()).addClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
@$advancedEditorWrapper = $(@advanced_editor.getWrapperElement())
|
||||
@$advancedEditorWrapper.addClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
|
||||
# This is a workaround for the fact that tinyMCE's baseURL property is not getting correctly set on AWS
|
||||
# instances (like sandbox). It is not necessary to explicitly set baseURL when running locally.
|
||||
@@ -43,16 +44,21 @@ class @HTMLEditingDescriptor
|
||||
theme_advanced_blockformats : "p,pre,h1,h2,h3",
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
setup : HTMLEditingDescriptor.setupTinyMCE,
|
||||
setup : @setupTinyMCE,
|
||||
# Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered.
|
||||
# The tinyMCE callback passes in the editor as a paramter.
|
||||
init_instance_callback: @focusVisualEditor
|
||||
})
|
||||
|
||||
@showingVisualEditor = true
|
||||
@element.on('click', '.editor-tabs .tab', this, @onSwitchEditor)
|
||||
# Doing these find operations within onSwitchEditor leads to sporadic failures on Chrome (version 20 and older).
|
||||
$element = $(element)
|
||||
@$htmlTab = $element.find('.html-tab')
|
||||
@$visualTab = $element.find('.visual-tab')
|
||||
|
||||
@setupTinyMCE: (ed) ->
|
||||
@element.on('click', '.editor-tabs .tab', @onSwitchEditor)
|
||||
|
||||
setupTinyMCE: (ed) =>
|
||||
ed.addButton('wrapAsCode', {
|
||||
title : 'Code',
|
||||
image : '/static/images/ico-tinymce-code.png',
|
||||
@@ -67,22 +73,23 @@ class @HTMLEditingDescriptor
|
||||
command.setActive('wrapAsCode', e.nodeName == 'CODE')
|
||||
)
|
||||
|
||||
onSwitchEditor: (e)=>
|
||||
@visualEditor = ed
|
||||
|
||||
onSwitchEditor: (e) =>
|
||||
e.preventDefault();
|
||||
|
||||
if not $(e.currentTarget).hasClass('current')
|
||||
element = e.data.element
|
||||
$currentTarget = $(e.currentTarget)
|
||||
if not $currentTarget.hasClass('current')
|
||||
$currentTarget.addClass('current')
|
||||
@$mceToolbar.toggleClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
@$advancedEditorWrapper.toggleClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
|
||||
$(e.currentTarget).addClass('current')
|
||||
$(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
$(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass)
|
||||
|
||||
visualEditor = @getVisualEditor(element)
|
||||
if $(e.currentTarget).attr('data-tab') is 'visual'
|
||||
$(element).find('.html-tab').removeClass('current')
|
||||
visualEditor = @getVisualEditor()
|
||||
if $currentTarget.data('tab') is 'visual'
|
||||
@$htmlTab.removeClass('current')
|
||||
@showVisualEditor(visualEditor)
|
||||
else
|
||||
$(element).find('.visual-tab').removeClass('current')
|
||||
@$visualTab.removeClass('current')
|
||||
@showAdvancedEditor(visualEditor)
|
||||
|
||||
# Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
|
||||
@@ -104,20 +111,24 @@ class @HTMLEditingDescriptor
|
||||
@focusVisualEditor(visualEditor)
|
||||
@showingVisualEditor = true
|
||||
|
||||
focusVisualEditor: (visualEditor) ->
|
||||
focusVisualEditor: (visualEditor) =>
|
||||
visualEditor.focus()
|
||||
if not @$mceToolbar?
|
||||
@$mceToolbar = $(@element).find('table.mceToolbar')
|
||||
|
||||
getVisualEditor: (element) ->
|
||||
getVisualEditor: () ->
|
||||
###
|
||||
Returns the instance of TinyMCE.
|
||||
This is different from the textarea that exists in the HTML template (@tiny_mce_textarea.
|
||||
|
||||
Pulled out as a helper method for unit test.
|
||||
###
|
||||
return tinyMCE.get($(element).find('.tiny-mce').attr('id'))
|
||||
return @visualEditor
|
||||
|
||||
save: ->
|
||||
@element.off('click', '.editor-tabs .tab', @onSwitchEditor)
|
||||
text = @advanced_editor.getValue()
|
||||
visualEditor = @getVisualEditor(@element)
|
||||
visualEditor = @getVisualEditor()
|
||||
if @showingVisualEditor and visualEditor.isDirty()
|
||||
text = visualEditor.getContent({no_events: 1})
|
||||
data: text
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# This is a simple class that just hides the error container
|
||||
# and message container when they are empty
|
||||
# Can (and should be) expanded upon when our problem list
|
||||
# becomes more sophisticated
|
||||
class @PeerGrading
|
||||
constructor: (element) ->
|
||||
@peer_grading_container = $('.peer-grading')
|
||||
@use_single_location = @peer_grading_container.data('use-single-location')
|
||||
@peer_grading_outer_container = $('.peer-grading-container')
|
||||
@ajax_url = @peer_grading_container.data('ajax-url')
|
||||
@error_container = $('.error-container')
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
@problem_button = $('.problem-button')
|
||||
@problem_button.click @show_results
|
||||
|
||||
@problem_list = $('.problem-list')
|
||||
@construct_progress_bar()
|
||||
|
||||
if @use_single_location
|
||||
@activate_problem()
|
||||
|
||||
construct_progress_bar: () =>
|
||||
problems = @problem_list.find('tr').next()
|
||||
problems.each( (index, element) =>
|
||||
problem = $(element)
|
||||
progress_bar = problem.find('.progress-bar')
|
||||
bar_value = parseInt(problem.data('graded'))
|
||||
bar_max = parseInt(problem.data('required')) + bar_value
|
||||
progress_bar.progressbar({value: bar_value, max: bar_max})
|
||||
)
|
||||
|
||||
show_results: (event) =>
|
||||
location_to_fetch = $(event.target).data('location')
|
||||
data = {'location' : location_to_fetch}
|
||||
$.postWithPrefix "#{@ajax_url}problem", data, (response) =>
|
||||
if response.success
|
||||
@peer_grading_outer_container.after(response.html).remove()
|
||||
backend = new PeerGradingProblemBackend(@ajax_url, false)
|
||||
new PeerGradingProblem(backend)
|
||||
else
|
||||
@gentle_alert response.error
|
||||
|
||||
activate_problem: () =>
|
||||
backend = new PeerGradingProblemBackend(@ajax_url, false)
|
||||
new PeerGradingProblem(backend)
|
||||
@@ -0,0 +1,475 @@
|
||||
##################################
|
||||
#
|
||||
# This is the JS that renders the peer grading problem page.
|
||||
# Fetches the correct problem and/or calibration essay
|
||||
# and sends back the grades
|
||||
#
|
||||
# Should not be run when we don't have a location to send back
|
||||
# to the server
|
||||
#
|
||||
# PeerGradingProblemBackend -
|
||||
# makes all the ajax requests and provides a mock interface
|
||||
# for testing purposes
|
||||
#
|
||||
# PeerGradingProblem -
|
||||
# handles the rendering and user interactions with the interface
|
||||
#
|
||||
##################################
|
||||
class @PeerGradingProblemBackend
|
||||
constructor: (ajax_url, mock_backend) ->
|
||||
@mock_backend = mock_backend
|
||||
@ajax_url = ajax_url
|
||||
@mock_cnt = 0
|
||||
|
||||
post: (cmd, data, callback) ->
|
||||
if @mock_backend
|
||||
callback(@mock(cmd, data))
|
||||
else
|
||||
# if this post request fails, the error callback will catch it
|
||||
$.post(@ajax_url + cmd, data, callback)
|
||||
.error => callback({success: false, error: "Error occured while performing this operation"})
|
||||
|
||||
mock: (cmd, data) ->
|
||||
if cmd == 'is_student_calibrated'
|
||||
# change to test each version
|
||||
response =
|
||||
success: true
|
||||
calibrated: @mock_cnt >= 2
|
||||
else if cmd == 'show_calibration_essay'
|
||||
#response =
|
||||
# success: false
|
||||
# error: "There was an error"
|
||||
@mock_cnt++
|
||||
response =
|
||||
success: true
|
||||
submission_id: 1
|
||||
submission_key: 'abcd'
|
||||
student_response: '''
|
||||
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
|
||||
|
||||
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
|
||||
'''
|
||||
prompt: '''
|
||||
<h2>S11E3: Metal Bands</h2>
|
||||
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
|
||||
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
|
||||
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
|
||||
'''
|
||||
rubric: '''
|
||||
<table class="rubric"><tbody><tr><th>Purpose</th>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
|
||||
</td>
|
||||
</tr><tr><th>Organization</th>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
|
||||
</td>
|
||||
</tr></tbody></table>
|
||||
'''
|
||||
max_score: 4
|
||||
else if cmd == 'get_next_submission'
|
||||
response =
|
||||
success: true
|
||||
submission_id: 1
|
||||
submission_key: 'abcd'
|
||||
student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa.
|
||||
|
||||
Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum.
|
||||
|
||||
Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. '''
|
||||
prompt: '''
|
||||
<h2>S11E3: Metal Bands</h2>
|
||||
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
|
||||
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
|
||||
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
|
||||
'''
|
||||
rubric: '''
|
||||
<table class="rubric"><tbody><tr><th>Purpose</th>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
|
||||
</td>
|
||||
</tr><tr><th>Organization</th>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
|
||||
</td>
|
||||
</tr></tbody></table>
|
||||
'''
|
||||
max_score: 4
|
||||
else if cmd == 'save_calibration_essay'
|
||||
response =
|
||||
success: true
|
||||
actual_score: 2
|
||||
else if cmd == 'save_grade'
|
||||
response =
|
||||
success: true
|
||||
|
||||
return response
|
||||
|
||||
class @PeerGradingProblem
|
||||
constructor: (backend) ->
|
||||
@prompt_wrapper = $('.prompt-wrapper')
|
||||
@backend = backend
|
||||
|
||||
|
||||
# get the location of the problem
|
||||
@location = $('.peer-grading').data('location')
|
||||
# prevent this code from trying to run
|
||||
# when we don't have a location
|
||||
if(!@location)
|
||||
return
|
||||
|
||||
# get the other elements we want to fill in
|
||||
@submission_container = $('.submission-container')
|
||||
@prompt_container = $('.prompt-container')
|
||||
@rubric_container = $('.rubric-container')
|
||||
@flag_student_container = $('.flag-student-container')
|
||||
@calibration_panel = $('.calibration-panel')
|
||||
@grading_panel = $('.grading-panel')
|
||||
@content_panel = $('.content-panel')
|
||||
@grading_message = $('.grading-message')
|
||||
@grading_message.hide()
|
||||
|
||||
@grading_wrapper =$('.grading-wrapper')
|
||||
@calibration_feedback_panel = $('.calibration-feedback')
|
||||
@interstitial_page = $('.interstitial-page')
|
||||
@interstitial_page.hide()
|
||||
|
||||
@error_container = $('.error-container')
|
||||
|
||||
@submission_key_input = $("input[name='submission-key']")
|
||||
@essay_id_input = $("input[name='essay-id']")
|
||||
@feedback_area = $('.feedback-area')
|
||||
|
||||
@score_selection_container = $('.score-selection-container')
|
||||
@rubric_selection_container = $('.rubric-selection-container')
|
||||
@grade = null
|
||||
@calibration = null
|
||||
|
||||
@submit_button = $('.submit-button')
|
||||
@action_button = $('.action-button')
|
||||
@calibration_feedback_button = $('.calibration-feedback-button')
|
||||
@interstitial_page_button = $('.interstitial-page-button')
|
||||
@flag_student_checkbox = $('.flag-checkbox')
|
||||
|
||||
Collapsible.setCollapsibles(@content_panel)
|
||||
|
||||
# Set up the click event handlers
|
||||
@action_button.click -> history.back()
|
||||
@calibration_feedback_button.click =>
|
||||
@calibration_feedback_panel.hide()
|
||||
@grading_wrapper.show()
|
||||
@is_calibrated_check()
|
||||
|
||||
@interstitial_page_button.click =>
|
||||
@interstitial_page.hide()
|
||||
@is_calibrated_check()
|
||||
|
||||
@is_calibrated_check()
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Ajax calls to the backend
|
||||
#
|
||||
##########
|
||||
is_calibrated_check: () =>
|
||||
@backend.post('is_student_calibrated', {location: @location}, @calibration_check_callback)
|
||||
|
||||
fetch_calibration_essay: () =>
|
||||
@backend.post('show_calibration_essay', {location: @location}, @render_calibration)
|
||||
|
||||
fetch_submission_essay: () =>
|
||||
@backend.post('get_next_submission', {location: @location}, @render_submission)
|
||||
|
||||
# finds the scores for each rubric category
|
||||
get_score_list: () =>
|
||||
# find the number of categories:
|
||||
num_categories = $('table.rubric tr').length
|
||||
|
||||
score_lst = []
|
||||
# get the score for each one
|
||||
for i in [0..(num_categories-1)]
|
||||
score = $("input[name='score-selection-#{i}']:checked").val()
|
||||
score_lst.push(score)
|
||||
|
||||
return score_lst
|
||||
|
||||
construct_data: () ->
|
||||
data =
|
||||
rubric_scores: @get_score_list()
|
||||
score: @grade
|
||||
location: @location
|
||||
submission_id: @essay_id_input.val()
|
||||
submission_key: @submission_key_input.val()
|
||||
feedback: @feedback_area.val()
|
||||
submission_flagged: @flag_student_checkbox.is(':checked')
|
||||
return data
|
||||
|
||||
|
||||
submit_calibration_essay: ()=>
|
||||
data = @construct_data()
|
||||
@backend.post('save_calibration_essay', data, @calibration_callback)
|
||||
|
||||
submit_grade: () =>
|
||||
data = @construct_data()
|
||||
@backend.post('save_grade', data, @submission_callback)
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Callbacks for various events
|
||||
#
|
||||
##########
|
||||
|
||||
# called after we perform an is_student_calibrated check
|
||||
calibration_check_callback: (response) =>
|
||||
if response.success
|
||||
# if we haven't been calibrating before
|
||||
if response.calibrated and (@calibration == null or @calibration == false)
|
||||
@calibration = false
|
||||
@fetch_submission_essay()
|
||||
# If we were calibrating before and no longer need to,
|
||||
# show the interstitial page
|
||||
else if response.calibrated and @calibration == true
|
||||
@calibration = false
|
||||
@render_interstitial_page()
|
||||
else
|
||||
@calibration = true
|
||||
@fetch_calibration_essay()
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error contacting the grading service")
|
||||
|
||||
|
||||
# called after we submit a calibration score
|
||||
calibration_callback: (response) =>
|
||||
if response.success
|
||||
@render_calibration_feedback(response)
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error saving calibration score")
|
||||
|
||||
# called after we submit a submission score
|
||||
submission_callback: (response) =>
|
||||
if response.success
|
||||
@is_calibrated_check()
|
||||
@grading_message.fadeIn()
|
||||
@grading_message.html("<p>Grade sent successfully.</p>")
|
||||
else
|
||||
if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error occurred while submitting grade")
|
||||
|
||||
# called after a grade is selected on the interface
|
||||
graded_callback: (event) =>
|
||||
@grade = $("input[name='grade-selection']:checked").val()
|
||||
if @grade == undefined
|
||||
return
|
||||
# check to see whether or not any categories have not been scored
|
||||
num_categories = $('table.rubric tr').length
|
||||
for i in [0..(num_categories-1)]
|
||||
score = $("input[name='score-selection-#{i}']:checked").val()
|
||||
if score == undefined
|
||||
return
|
||||
# show button if we have scores for all categories
|
||||
@show_submit_button()
|
||||
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Rendering methods and helpers
|
||||
#
|
||||
##########
|
||||
# renders a calibration essay
|
||||
render_calibration: (response) =>
|
||||
if response.success
|
||||
|
||||
# load in all the data
|
||||
@submission_container.html("<h3>Training Essay</h3>")
|
||||
@render_submission_data(response)
|
||||
# TODO: indicate that we're in calibration mode
|
||||
@calibration_panel.addClass('current-state')
|
||||
@grading_panel.removeClass('current-state')
|
||||
|
||||
# Display the right text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').show()
|
||||
@grading_panel.find('.calibration-text').show()
|
||||
@calibration_panel.find('.grading-text').hide()
|
||||
@grading_panel.find('.grading-text').hide()
|
||||
@flag_student_container.hide()
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_calibration_essay
|
||||
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("An error occurred while retrieving the next calibration essay")
|
||||
|
||||
# Renders a student submission to be graded
|
||||
render_submission: (response) =>
|
||||
if response.success
|
||||
@submit_button.hide()
|
||||
@submission_container.html("<h3>Submitted Essay</h3>")
|
||||
@render_submission_data(response)
|
||||
|
||||
@calibration_panel.removeClass('current-state')
|
||||
@grading_panel.addClass('current-state')
|
||||
|
||||
# Display the correct text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').hide()
|
||||
@grading_panel.find('.calibration-text').hide()
|
||||
@calibration_panel.find('.grading-text').show()
|
||||
@grading_panel.find('.grading-text').show()
|
||||
@flag_student_container.show()
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_grade
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("An error occured when retrieving the next submission.")
|
||||
|
||||
|
||||
make_paragraphs: (text) ->
|
||||
paragraph_split = text.split(/\n\s*\n/)
|
||||
new_text = ''
|
||||
for paragraph in paragraph_split
|
||||
new_text += "<p>#{paragraph}</p>"
|
||||
return new_text
|
||||
|
||||
# render common information between calibration and grading
|
||||
render_submission_data: (response) =>
|
||||
@content_panel.show()
|
||||
|
||||
@submission_container.append(@make_paragraphs(response.student_response))
|
||||
@prompt_container.html(response.prompt)
|
||||
@rubric_selection_container.html(response.rubric)
|
||||
@submission_key_input.val(response.submission_key)
|
||||
@essay_id_input.val(response.submission_id)
|
||||
@setup_score_selection(response.max_score)
|
||||
|
||||
@submit_button.hide()
|
||||
@action_button.hide()
|
||||
@calibration_feedback_panel.hide()
|
||||
|
||||
|
||||
render_calibration_feedback: (response) =>
|
||||
# display correct grade
|
||||
@calibration_feedback_panel.slideDown()
|
||||
calibration_wrapper = $('.calibration-feedback-wrapper')
|
||||
calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
|
||||
|
||||
|
||||
score = parseInt(@grade)
|
||||
actual_score = parseInt(response.actual_score)
|
||||
|
||||
if score == actual_score
|
||||
calibration_wrapper.append("<p>Congratulations! Your score matches the actual score!</p>")
|
||||
else
|
||||
calibration_wrapper.append("<p>Please try to understand the grading critera better to be more accurate next time.</p>")
|
||||
|
||||
# disable score selection and submission from the grading interface
|
||||
$("input[name='score-selection']").attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
|
||||
render_interstitial_page: () =>
|
||||
@content_panel.hide()
|
||||
@interstitial_page.show()
|
||||
|
||||
render_error: (error_message) =>
|
||||
@error_container.show()
|
||||
@calibration_feedback_panel.hide()
|
||||
@error_container.html(error_message)
|
||||
@content_panel.hide()
|
||||
@action_button.show()
|
||||
|
||||
show_submit_button: () =>
|
||||
@submit_button.show()
|
||||
|
||||
setup_score_selection: (max_score) =>
|
||||
|
||||
# first, get rid of all the old inputs, if any.
|
||||
@score_selection_container.html("""
|
||||
<h3>Overall Score</h3>
|
||||
<p>Choose an overall score for this submission.</p>
|
||||
""")
|
||||
|
||||
# Now create new labels and inputs for each possible score.
|
||||
for score in [0..max_score]
|
||||
id = 'score-' + score
|
||||
label = """<label for="#{id}">#{score}</label>"""
|
||||
|
||||
input = """
|
||||
<input type="radio" name="grade-selection" id="#{id}" value="#{score}"/>
|
||||
""" # " fix broken parsing in emacs
|
||||
@score_selection_container.append(input + label)
|
||||
|
||||
# And now hook up an event handler again
|
||||
$("input[name='score-selection']").change @graded_callback
|
||||
$("input[name='grade-selection']").change @graded_callback
|
||||
|
||||
#mock_backend = false
|
||||
#ajax_url = $('.peer-grading').data('ajax_url')
|
||||
#backend = new PeerGradingProblemBackend(ajax_url, mock_backend)
|
||||
#$(document).ready(() -> new PeerGradingProblem(backend))
|
||||
@@ -34,7 +34,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
return {'module': self,
|
||||
'metadata': self.metadata,
|
||||
'editable_metadata_fields' : self.editable_metadata_fields
|
||||
'editable_metadata_fields': self.editable_metadata_fields
|
||||
}
|
||||
|
||||
def get_html(self):
|
||||
@@ -46,4 +46,3 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
def editable_metadata_fields(self):
|
||||
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
|
||||
return subset
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ class ModuleStore(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
|
||||
@@ -67,7 +67,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
TODO (vshnayder): this may want to live outside the modulestore eventually
|
||||
"""
|
||||
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
try:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=0))
|
||||
except ItemNotFoundError:
|
||||
|
||||
@@ -304,7 +304,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
if location.category == 'static_tab':
|
||||
course = self.get_course_for_item(item.location)
|
||||
existing_tabs = course.tabs or []
|
||||
existing_tabs.append({'type':'static_tab', 'name' : item.metadata.get('display_name'), 'url_slug' : item.location.name})
|
||||
existing_tabs.append({'type': 'static_tab', 'name': item.metadata.get('display_name'), 'url_slug': item.location.name})
|
||||
course.tabs = existing_tabs
|
||||
self.update_metadata(course.location, course.metadata)
|
||||
|
||||
@@ -423,7 +423,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
|
||||
@@ -104,14 +104,14 @@ def path_to_location(modulestore, course_id, location):
|
||||
# module nested in more than one positional module will work.
|
||||
if n > 3:
|
||||
position_list = []
|
||||
for path_index in range(2, n-1):
|
||||
for path_index in range(2, n - 1):
|
||||
category = path[path_index].category
|
||||
if category == 'sequential' or category == 'videosequence':
|
||||
section_desc = modulestore.get_instance(course_id, path[path_index])
|
||||
child_locs = [c.location for c in section_desc.get_children()]
|
||||
# positions are 1-indexed, and should be strings to be consistent with
|
||||
# url parsing.
|
||||
position_list.append(str(child_locs.index(path[path_index+1]) + 1))
|
||||
position_list.append(str(child_locs.index(path[path_index + 1]) + 1))
|
||||
position = "_".join(position_list)
|
||||
|
||||
return (course_id, chapter, section, position)
|
||||
|
||||
@@ -3,6 +3,7 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
@@ -13,7 +14,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
|
||||
if len(dest_modules) != 1:
|
||||
@@ -31,12 +32,12 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
original_loc = Location(module.location)
|
||||
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course, name=dest_location.name)
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
|
||||
@@ -48,8 +49,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
new_children = []
|
||||
for child_loc_url in module.definition['children']:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
new_children = new_children + [child_loc.url()]
|
||||
|
||||
modulestore.update_children(module.location, new_children)
|
||||
@@ -63,8 +64,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
|
||||
@@ -76,13 +77,13 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
|
||||
@@ -90,6 +91,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_course(modulestore, contentstore, source_location):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
@@ -119,7 +121,7 @@ def delete_course(modulestore, contentstore, source_location):
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
print "Deleting {0}...".format(module.location)
|
||||
modulestore.delete_item(module.location)
|
||||
|
||||
@@ -127,4 +129,4 @@ def delete_course(modulestore, contentstore, source_location):
|
||||
print "Deleting {0}...".format(source_location)
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -8,5 +8,3 @@ for i in range(5):
|
||||
TEST_DIR = TEST_DIR / 'test'
|
||||
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
|
||||
126
common/lib/xmodule/xmodule/modulestore/tests/factories.py
Normal file
126
common/lib/xmodule/xmodule/modulestore/tests/factories.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from factory import Factory
|
||||
from time import gmtime
|
||||
from uuid import uuid4
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_COURSE_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
# This logic was taken from the create_new_course method in
|
||||
# cms/djangoapps/contentstore/views.py
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
display_name = kwargs.get('display_name')
|
||||
location = Location('i4x', org, number,
|
||||
'course', Location.clean(display_name))
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.clone_item(template, location)
|
||||
|
||||
# This metadata code was copied from cms/djangoapps/contentstore/views.py
|
||||
if display_name is not None:
|
||||
new_course.metadata['display_name'] = display_name
|
||||
|
||||
new_course.metadata['data_dir'] = uuid4().hex
|
||||
new_course.metadata['start'] = stringify_time(gmtime())
|
||||
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
|
||||
return new_course
|
||||
|
||||
|
||||
class Course:
|
||||
pass
|
||||
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
|
||||
template = 'i4x://edx/templates/course/Empty'
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
display_name = 'Robot Super Course'
|
||||
|
||||
|
||||
class XModuleItemFactory(Factory):
|
||||
"""
|
||||
Factory for XModule items.
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_ITEM_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
display_name = kwargs.get('display_name')
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
# TODO: This needs to be deleted when we have proper storage for static content
|
||||
new_item.metadata['data_dir'] = parent.metadata['data_dir']
|
||||
|
||||
# replace the display name with an optional parameter passed in from the caller
|
||||
if display_name is not None:
|
||||
new_item.metadata['display_name'] = display_name
|
||||
|
||||
store.update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
|
||||
return new_item
|
||||
|
||||
|
||||
class Item:
|
||||
pass
|
||||
|
||||
|
||||
class ItemFactory(XModuleItemFactory):
|
||||
FACTORY_FOR = Item
|
||||
|
||||
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
|
||||
template = 'i4x://edx/templates/chapter/Empty'
|
||||
display_name = 'Section One'
|
||||
@@ -61,6 +61,7 @@ invalid = ("foo", ["foo"], ["foo", "bar"],
|
||||
invalid_dict,
|
||||
invalid_dict2)
|
||||
|
||||
|
||||
def test_is_valid():
|
||||
for v in valid:
|
||||
assert_equals(Location.is_valid(v), True)
|
||||
@@ -68,6 +69,7 @@ def test_is_valid():
|
||||
for v in invalid:
|
||||
assert_equals(Location.is_valid(v), False)
|
||||
|
||||
|
||||
def test_dict():
|
||||
assert_equals("tag://org/course/category/name", Location(input_dict).url())
|
||||
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
|
||||
@@ -76,6 +78,7 @@ def test_dict():
|
||||
assert_equals("tag://org/course/category/name@revision", Location(input_dict).url())
|
||||
assert_equals(input_dict, Location(input_dict).dict())
|
||||
|
||||
|
||||
def test_list():
|
||||
assert_equals("tag://org/course/category/name", Location(input_list).url())
|
||||
assert_equals(input_list + [None], Location(input_list).list())
|
||||
@@ -115,17 +118,18 @@ def test_equality():
|
||||
)
|
||||
|
||||
# All the cleaning functions should do the same thing with these
|
||||
general_pairs = [ ('',''),
|
||||
general_pairs = [('', ''),
|
||||
(' ', '_'),
|
||||
('abc,', 'abc_'),
|
||||
('ab fg!@//\\aj', 'ab_fg_aj'),
|
||||
(u"ab\xA9", "ab_"), # no unicode allowed for now
|
||||
]
|
||||
|
||||
|
||||
def test_clean():
|
||||
pairs = general_pairs + [
|
||||
('a:b', 'a_b'), # no colons in non-name components
|
||||
('a-b', 'a-b'), # dashes ok
|
||||
('a-b', 'a-b'), # dashes ok
|
||||
('a.b', 'a.b'), # dot ok
|
||||
]
|
||||
for input, output in pairs:
|
||||
|
||||
@@ -3,6 +3,7 @@ from nose.tools import assert_equals, assert_raises, assert_not_equals, with_set
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
|
||||
def check_path_to_location(modulestore):
|
||||
'''Make sure that path_to_location works: should be passed a modulestore
|
||||
with the toy and simple courses loaded.'''
|
||||
@@ -22,4 +23,3 @@ def check_path_to_location(modulestore):
|
||||
)
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
|
||||
|
||||
|
||||
@@ -102,4 +102,3 @@ class TestMongoModuleStore(object):
|
||||
def test_path_to_location(self):
|
||||
'''Make sure that path_to_location works'''
|
||||
check_path_to_location(self.store)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from .test_modulestore import check_path_to_location
|
||||
from . import DATA_DIR
|
||||
|
||||
|
||||
class TestXMLModuleStore(object):
|
||||
def test_path_to_location(self):
|
||||
"""Make sure that path_to_location works properly"""
|
||||
@@ -12,5 +13,5 @@ class TestXMLModuleStore(object):
|
||||
print "Starting import"
|
||||
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple'])
|
||||
print "finished import"
|
||||
|
||||
|
||||
check_path_to_location(modulestore)
|
||||
|
||||
@@ -363,7 +363,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# been imported into the cms from xml
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
|
||||
course_data = etree.parse(course_file, parser=edx_xml_parser).getroot()
|
||||
|
||||
org = course_data.get('org')
|
||||
|
||||
@@ -437,7 +437,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name)
|
||||
|
||||
# now import all static tabs which are expected to be stored in
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name)
|
||||
|
||||
self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name)
|
||||
@@ -454,12 +454,12 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
# then look in a override folder based on the course run
|
||||
if os.path.isdir(base_dir / url_name):
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
|
||||
|
||||
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
|
||||
|
||||
for filepath in glob.glob(path/ '*'):
|
||||
for filepath in glob.glob(path / '*'):
|
||||
if not os.path.isdir(filepath):
|
||||
with open(filepath) as f:
|
||||
try:
|
||||
@@ -467,7 +467,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
|
||||
slug = os.path.splitext(os.path.basename(filepath))[0]
|
||||
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
|
||||
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
|
||||
module = HtmlDescriptor(system, definition={'data': html}, **{'location': loc})
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
@@ -555,7 +555,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
Return a dictionary of course_dir -> [(msg, exception_str)], for each
|
||||
course_dir where course loading failed.
|
||||
"""
|
||||
return dict( (k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
return dict((k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
@@ -590,7 +590,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user