merged master
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
|
||||
@@ -131,6 +132,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
|
||||
@@ -140,6 +142,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
|
||||
@@ -149,6 +152,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
|
||||
@@ -164,12 +168,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.
|
||||
@@ -227,4 +233,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)
|
||||
100
common/djangoapps/static_replace/__init__.py
Normal file
100
common/djangoapps/static_replace/__init__.py
Normal file
@@ -0,0 +1,100 @@
|
||||
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)
|
||||
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
|
||||
else:
|
||||
course_path = "/".join((data_directory, rest))
|
||||
try:
|
||||
if staticfiles_storage.exists(rest):
|
||||
url = staticfiles_storage.url(rest)
|
||||
else:
|
||||
url = staticfiles_storage.url(course_path)
|
||||
# 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, course_path])
|
||||
|
||||
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()
|
||||
77
common/djangoapps/static_replace/test/test_static_replace.py
Normal file
77
common/djangoapps/static_replace/test/test_static_replace.py
Normal file
@@ -0,0 +1,77 @@
|
||||
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.staticfiles_storage')
|
||||
def test_storage_url_exists(mock_storage):
|
||||
mock_storage.exists.return_value = True
|
||||
mock_storage.url.return_value = '/static/file.png'
|
||||
|
||||
assert_equals('"/static/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
mock_storage.exists.called_once_with('file.png')
|
||||
mock_storage.url.called_once_with('data_dir/file.png')
|
||||
|
||||
|
||||
@patch('static_replace.staticfiles_storage')
|
||||
def test_storage_url_not_exists(mock_storage):
|
||||
mock_storage.exists.return_value = False
|
||||
mock_storage.url.return_value = '/static/data_dir/file.png'
|
||||
|
||||
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
mock_storage.exists.called_once_with('file.png')
|
||||
mock_storage.url.called_once_with('file.png')
|
||||
|
||||
|
||||
@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_storage.url.side_effect = Exception
|
||||
|
||||
mock_storage.exists.return_value = True
|
||||
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
|
||||
|
||||
mock_storage.exists.return_value = False
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user