Commit 18a3d5e7 by Diana Huang

Merge branch 'master' into feature/diana/rubric-input

parents 8b3fb33f 9f33e3c7
import csv
import uuid
from collections import defaultdict, OrderedDict
from collections import OrderedDict
from datetime import datetime
from os.path import isdir
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from student.models import TestCenterUser
class Command(BaseCommand):
CSV_TO_MODEL_FIELDS = OrderedDict([
# Skipping optional field CandidateID
("ClientCandidateID", "client_candidate_id"),
("FirstName", "first_name"),
("LastName", "last_name"),
......@@ -34,9 +37,17 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
args = '<output_file>'
option_list = BaseCommand.option_list + (
make_option(
'--dump_all',
action='store_true',
dest='dump_all',
),
)
args = '<output_file_or_dir>'
help = """
Export user information from TestCenterUser model into a tab delimited
Export user demographic information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
def handle(self, *args, **kwargs):
......@@ -44,9 +55,33 @@ class Command(BaseCommand):
print Command.help
return
self.reset_sample_data()
# update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
with open(args[0], "wb") as outfile:
# if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist,
# or exists as a file, then we will just write to it.
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
dest = args[0]
if isdir(dest):
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
else:
destfile = dest
# strings must be in latin-1 format. CSV parser will
# otherwise convert unicode objects to ascii.
def ensure_encoding(value):
if isinstance(value, unicode):
return value.encode('iso-8859-1')
else:
return value
dump_all = kwargs['dump_all']
with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.CSV_TO_MODEL_FIELDS,
delimiter="\t",
......@@ -54,103 +89,14 @@ class Command(BaseCommand):
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'):
record = dict((csv_field, getattr(tcu, model_field))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
writer.writerow(record)
if dump_all or tcu.needs_uploading:
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
writer.writerow(record)
tcu.uploaded_at = uploaded_at
tcu.save()
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
def generate_id():
return "edX{:012}".format(uuid.uuid4().int % (10**12))
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
import csv
import uuid
from collections import defaultdict, OrderedDict
from collections import OrderedDict
from datetime import datetime
from os.path import isdir, join
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from student.models import TestCenterUser
def generate_id():
return "{:012}".format(uuid.uuid4().int % (10**12))
from student.models import TestCenterRegistration
class Command(BaseCommand):
args = '<output_file>'
CSV_TO_MODEL_FIELDS = OrderedDict([
('AuthorizationTransactionType', 'authorization_transaction_type'),
('AuthorizationID', 'authorization_id'),
('ClientAuthorizationID', 'client_authorization_id'),
('ClientCandidateID', 'client_candidate_id'),
('ExamAuthorizationCount', 'exam_authorization_count'),
('ExamSeriesCode', 'exam_series_code'),
('Accommodations', 'accommodation_code'),
('EligibilityApptDateFirst', 'eligibility_appointment_date_first'),
('EligibilityApptDateLast', 'eligibility_appointment_date_last'),
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
args = '<output_file_or_dir>'
help = """
Export user information from TestCenterUser model into a tab delimited
Export user registration information from TestCenterRegistration model into a tab delimited
text file with a format that Pearson expects.
"""
FIELDS = [
'AuthorizationTransactionType',
'AuthorizationID',
'ClientAuthorizationID',
'ClientCandidateID',
'ExamAuthorizationCount',
'ExamSeriesCode',
'EligibilityApptDateFirst',
'EligibilityApptDateLast',
'LastUpdate',
]
option_list = BaseCommand.option_list + (
make_option(
'--dump_all',
action='store_true',
dest='dump_all',
),
make_option(
'--force_add',
action='store_true',
dest='force_add',
),
)
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
# self.reset_sample_data()
# update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
# if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist,
# or exists as a file, then we will just write to it.
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
dest = args[0]
if isdir(dest):
destfile = join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
else:
destfile = dest
with open(args[0], "wb") as outfile:
dump_all = kwargs['dump_all']
with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.FIELDS,
Command.CSV_TO_MODEL_FIELDS,
delimiter="\t",
quoting=csv.QUOTE_MINIMAL,
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id')[:5]:
record = defaultdict(
lambda: "",
AuthorizationTransactionType="Add",
ClientAuthorizationID=generate_id(),
ClientCandidateID=tcu.client_candidate_id,
ExamAuthorizationCount="1",
ExamSeriesCode="6002x001",
EligibilityApptDateFirst="2012/12/15",
EligibilityApptDateLast="2012/12/30",
LastUpdate=datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S")
)
writer.writerow(record)
for tcr in TestCenterRegistration.objects.order_by('id'):
if dump_all or tcr.needs_uploading:
record = dict((csv_field, getattr(tcr, model_field))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d")
record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d")
if kwargs['force_add']:
record['AuthorizationTransactionType'] = 'Add'
writer.writerow(record)
tcr.uploaded_at = uploaded_at
tcr.save()
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
from optparse import make_option
from time import strftime
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_testcenter_registration
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:
make_option(
'--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:
make_option(
'--exam_series_code',
action='store',
dest='exam_series_code',
),
make_option(
'--eligibility_appointment_date_first',
action='store',
dest='eligibility_appointment_date_first',
help='use YYYY-MM-DD format if overriding existing course values, or YYYY-MM-DDTHH:MM if not using an existing course.'
),
make_option(
'--eligibility_appointment_date_last',
action='store',
dest='eligibility_appointment_date_last',
help='use YYYY-MM-DD format if overriding existing course values, or YYYY-MM-DDTHH:MM if not using an existing course.'
),
# internal values:
make_option(
'--authorization_id',
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.'
),
)
args = "<student_username course_id>"
help = "Create or modify a TestCenterRegistration entry for a given Student"
@staticmethod
def is_valid_option(option_name):
base_options = set(option.dest for option in BaseCommand.option_list)
return option_name not in base_options
def handle(self, *args, **options):
username = args[0]
course_id = args[1]
print username, course_id
our_options = dict((k, v) for k, v in options.items()
if Command.is_valid_option(k) and v is not None)
try:
student = User.objects.get(username=username)
except User.DoesNotExist:
raise CommandError("User \"{}\" does not exist".format(username))
try:
testcenter_user = TestCenterUser.objects.get(user=student)
except TestCenterUser.DoesNotExist:
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
# check to see if a course_id was specified, and use information from that:
try:
course = course_from_id(course_id)
if 'ignore_registration_dates' in our_options:
examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')]
exam = examlist[0] if len(examlist) > 0 else None
else:
exam = course.current_test_center_exam
except ItemNotFoundError:
# 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 = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info)
# update option values for date_first and date_last to use YYYY-MM-DD format
# instead of YYYY-MM-DDTHH:MM
our_options['eligibility_appointment_date_first'] = strftime("%Y-%m-%d", exam.first_eligible_appointment_date)
our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
if exam is None:
raise CommandError("Exam for course_id {%s} does not exist".format(course_id))
exam_code = exam.exam_series_code
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)
if len(registrations) > 0:
registration = registrations[0]
for fieldname in UPDATE_FIELDS:
if fieldname in our_options and registration.__getattribute__(fieldname) != our_options[fieldname]:
needs_updating = True;
else:
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:
registration.__setattr__(fieldname, our_options[fieldname])
# 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:
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)
else:
if (len(form.errors) > 0):
print "Field Form errors encountered:"
for fielderror in form.errors:
print "Field Form Error: %s" % fielderror
if (len(form.non_field_errors()) > 0):
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']:
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."
import uuid
from datetime import datetime
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from student.models import TestCenterUser
from student.models import TestCenterUser, TestCenterUserForm
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
# demographics:
make_option(
'--client_candidate_id',
'--first_name',
action='store',
dest='client_candidate_id',
help='ID we assign a user to identify them to Pearson'
dest='first_name',
),
make_option(
'--first_name',
'--middle_name',
action='store',
dest='first_name',
dest='middle_name',
),
make_option(
'--last_name',
......@@ -26,11 +24,31 @@ class Command(BaseCommand):
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',
......@@ -59,14 +77,55 @@ class Command(BaseCommand):
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',
dest='phone_country_code',
help='Phone country code, just "1" for the USA'
),
make_option(
'--fax',
action='store',
dest='fax',
help='Pretty free-form (parens, spaces, dashes), but no country code'
),
make_option(
'--fax_country_code',
action='store',
dest='fax_country_code',
help='Fax country code, just "1" for the USA'
),
make_option(
'--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 a TestCenterUser entry for a given Student"
help = "Create or modify a TestCenterUser entry for a given Student"
@staticmethod
def is_valid_option(option_name):
......@@ -79,7 +138,52 @@ class Command(BaseCommand):
print username
our_options = dict((k, v) for k, v in options.items()
if Command.is_valid_option(k))
if Command.is_valid_option(k) and v is not None)
student = User.objects.get(username=username)
student.test_center_user = TestCenterUser(**our_options)
student.test_center_user.save()
try:
testcenter_user = TestCenterUser.objects.get(user=student)
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
# 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:
form_options[propname] = testcenter_user.__getattribute__(propname)
form = TestCenterUserForm(instance=testcenter_user, data=form_options)
if form.is_valid():
form.update_and_save()
else:
if (len(form.errors) > 0):
print "Field Form errors encountered:"
for fielderror in form.errors:
print "Field Form Error: %s" % fielderror
if (len(form.non_field_errors()) > 0):
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 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']:
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."
......@@ -26,14 +26,17 @@ class Migration(SchemaMigration):
def forwards(self, orm):
"Kill the askbot"
# For MySQL, we're batching the alters together for performance reasons
if db.backend_name == 'mysql':
drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS]
statement = "alter table `auth_user` {0};".format(", ".join(drops))
db.execute(statement)
else:
for column in ASKBOT_AUTH_USER_COLUMNS:
db.delete_column('auth_user', column)
try:
# For MySQL, we're batching the alters together for performance reasons
if db.backend_name == 'mysql':
drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS]
statement = "alter table `auth_user` {0};".format(", ".join(drops))
db.execute(statement)
else:
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)
def backwards(self, orm):
raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.")
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'TestCenterRegistration'
db.create_table('student_testcenterregistration', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('testcenter_user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['student.TestCenterUser'])),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
('client_authorization_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=20, db_index=True)),
('exam_series_code', self.gf('django.db.models.fields.CharField')(max_length=15, db_index=True)),
('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)),
('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)),
('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)),
('accommodation_request', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=1024, blank=True)),
('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
('processed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
('upload_status', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=20, blank=True)),
('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
('authorization_id', self.gf('django.db.models.fields.IntegerField')(null=True, db_index=True)),
('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
))
db.send_create_signal('student', ['TestCenterRegistration'])
# Adding field 'TestCenterUser.uploaded_at'
db.add_column('student_testcenteruser', 'uploaded_at',
self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True),
keep_default=False)
# Adding field 'TestCenterUser.processed_at'
db.add_column('student_testcenteruser', 'processed_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True),
keep_default=False)
# Adding field 'TestCenterUser.upload_status'
db.add_column('student_testcenteruser', 'upload_status',
self.gf('django.db.models.fields.CharField')(db_index=True, default='', max_length=20, blank=True),
keep_default=False)
# Adding field 'TestCenterUser.upload_error_message'
db.add_column('student_testcenteruser', 'upload_error_message',
self.gf('django.db.models.fields.CharField')(default='', max_length=512, blank=True),
keep_default=False)
# Adding field 'TestCenterUser.confirmed_at'
db.add_column('student_testcenteruser', 'confirmed_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True),
keep_default=False)
# Adding index on 'TestCenterUser', fields ['company_name']
db.create_index('student_testcenteruser', ['company_name'])
# Adding unique constraint on 'TestCenterUser', fields ['client_candidate_id']
db.create_unique('student_testcenteruser', ['client_candidate_id'])
def backwards(self, orm):
# Removing unique constraint on 'TestCenterUser', fields ['client_candidate_id']
db.delete_unique('student_testcenteruser', ['client_candidate_id'])
# Removing index on 'TestCenterUser', fields ['company_name']
db.delete_index('student_testcenteruser', ['company_name'])
# Deleting model 'TestCenterRegistration'
db.delete_table('student_testcenterregistration')
# Deleting field 'TestCenterUser.uploaded_at'
db.delete_column('student_testcenteruser', 'uploaded_at')
# Deleting field 'TestCenterUser.processed_at'
db.delete_column('student_testcenteruser', 'processed_at')
# Deleting field 'TestCenterUser.upload_status'
db.delete_column('student_testcenteruser', 'upload_status')
# Deleting field 'TestCenterUser.upload_error_message'
db.delete_column('student_testcenteruser', 'upload_error_message')
# Deleting field 'TestCenterUser.confirmed_at'
db.delete_column('student_testcenteruser', 'confirmed_at')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.courseenrollment': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.testcenterregistration': {
'Meta': {'object_name': 'TestCenterRegistration'},
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}),
'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.testcenteruser': {
'Meta': {'object_name': 'TestCenterUser'},
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
\ No newline at end of file
......@@ -40,6 +40,8 @@ import hashlib
import json
import logging
import uuid
from random import randint
from time import strftime
from django.conf import settings
......@@ -47,10 +49,10 @@ from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.forms import ModelForm, forms
import comment_client as cc
log = logging.getLogger(__name__)
......@@ -125,6 +127,9 @@ class UserProfile(models.Model):
def set_meta(self, js):
self.meta = json.dumps(js)
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:
......@@ -140,6 +145,9 @@ class TestCenterUser(models.Model):
The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system, including oddities such as suffix having
a limit of 255 while last_name only gets 50.
Also storing here the confirmation information received from Pearson (if any)
as to the success or failure of the upload. (VCDC file)
"""
# Our own record keeping...
user = models.ForeignKey(User, unique=True, default=None)
......@@ -150,12 +158,8 @@ class TestCenterUser(models.Model):
# updated_at, this will not get incremented when we do a batch data import.
user_updated_at = models.DateTimeField(db_index=True)
# Unique ID given to us for this User by the Testing Center. It's null when
# we first create the User entry, and is assigned by Pearson later.
candidate_id = models.IntegerField(null=True, db_index=True)
# Unique ID we assign our user for a the Test Center.
client_candidate_id = models.CharField(max_length=50, db_index=True)
# Unique ID we assign our user for the Test Center.
client_candidate_id = models.CharField(unique=True, max_length=50, db_index=True)
# Name
first_name = models.CharField(max_length=30, db_index=True)
......@@ -186,18 +190,369 @@ class TestCenterUser(models.Model):
fax_country_code = models.CharField(max_length=3, blank=True)
# Company
company_name = models.CharField(max_length=50, blank=True)
company_name = models.CharField(max_length=50, blank=True, db_index=True)
# time at which edX sent the registration to the test center
uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True)
# confirmation back from the test center, as well as timestamps
# on when they processed the request, and when we received
# confirmation back.
processed_at = models.DateTimeField(null=True, db_index=True)
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
upload_error_message = models.CharField(max_length=512, blank=True)
# Unique ID given to us for this User by the Testing Center. It's null when
# we first create the User entry, and may be assigned by Pearson later.
# (However, it may never be set if we are always initiating such candidate creation.)
candidate_id = models.IntegerField(null=True, db_index=True)
confirmed_at = models.DateTimeField(null=True, db_index=True)
@property
def needs_uploading(self):
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
@staticmethod
def user_provided_fields():
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']
@property
def email(self):
return self.user.email
def needs_update(self, fields):
for fieldname in TestCenterUser.user_provided_fields():
if fieldname in fields and getattr(self, fieldname) != fields[fieldname]:
return True
return False
@staticmethod
def _generate_edx_id(prefix):
NUM_DIGITS = 12
return u"{}{:012}".format(prefix, randint(1, 10**NUM_DIGITS-1))
@staticmethod
def _generate_candidate_id():
return TestCenterUser._generate_edx_id("edX")
@classmethod
def create(cls, user):
testcenter_user = cls(user=user)
# testcenter_user.candidate_id remains unset
# assign an ID of our own:
cand_id = cls._generate_candidate_id()
while TestCenterUser.objects.filter(client_candidate_id=cand_id).exists():
cand_id = cls._generate_candidate_id()
testcenter_user.client_candidate_id = cand_id
return testcenter_user
@property
def is_accepted(self):
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
@property
def is_rejected(self):
return self.upload_status == TEST_CENTER_STATUS_ERROR
@property
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',
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
def update_and_save(self):
new_user = self.save(commit=False)
# create additional values here:
new_user.user_updated_at = datetime.utcnow()
new_user.save()
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.username))
# add validation:
def clean_country(self):
code = self.cleaned_data['country']
if code and len(code) != 3:
raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG')
return code
def clean(self):
def _can_encode_as_latin(fieldvalue):
try:
fieldvalue.encode('iso-8859-1')
except UnicodeEncodeError:
return False
return True
cleaned_data = super(TestCenterUserForm, self).clean()
# check for interactions between fields:
if 'country' in cleaned_data:
country = cleaned_data.get('country')
if country == 'USA' or country == 'CAN':
if 'state' in cleaned_data and len(cleaned_data['state']) == 0:
self._errors['state'] = self.error_class([u'Required if country is USA or CAN.'])
del cleaned_data['state']
if 'postal_code' in cleaned_data and len(cleaned_data['postal_code']) == 0:
self._errors['postal_code'] = self.error_class([u'Required if country is USA or CAN.'])
del cleaned_data['postal_code']
if 'fax' in cleaned_data and len(cleaned_data['fax']) > 0 and 'fax_country_code' in cleaned_data and len(cleaned_data['fax_country_code']) == 0:
self._errors['fax_country_code'] = self.error_class([u'Required if fax is specified.'])
del cleaned_data['fax_country_code']
# check encoding for all fields:
cleaned_data_fields = [fieldname for fieldname in cleaned_data]
for fieldname in cleaned_data_fields:
if not _can_encode_as_latin(cleaned_data[fieldname]):
self._errors[fieldname] = self.error_class([u'Must only use characters in Latin-1 (iso-8859-1) encoding'])
del cleaned_data[fieldname]
# Always return the full collection of cleaned data.
return cleaned_data
# our own code to indicate that a request has been rejected.
ACCOMMODATION_REJECTED_CODE = 'NONE'
ACCOMMODATION_CODES = (
(ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'),
('EQPMNT', 'Equipment'),
('ET12ET', 'Extra Time - 1/2 Exam Time'),
('ET30MN', 'Extra Time - 30 Minutes'),
('ETDBTM', 'Extra Time - Double Time'),
('SEPRMM', 'Separate Room'),
('SRREAD', 'Separate Room and Reader'),
('SRRERC', 'Separate Room and Reader/Recorder'),
('SRRECR', 'Separate Room and Recorder'),
('SRSEAN', 'Separate Room and Service Animal'),
('SRSGNR', 'Separate Room and Sign Language Interpreter'),
)
ACCOMMODATION_CODE_DICT = { code : name for (code, name) in ACCOMMODATION_CODES }
class TestCenterRegistration(models.Model):
"""
This is our representation of a user's registration for in-person testing,
and specifically for Pearson at this point. A few things to note:
* Pearson only supports Latin-1, so we have to make sure that the data we
capture here will work with that encoding. This is less of an issue
than for the TestCenterUser.
* Registrations are only created here when a user registers to take an exam in person.
The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system.
"""
# to find an exam registration, we key off of the user and course_id.
# If multiple exams per course are possible, we would also need to add the
# exam_series_code.
testcenter_user = models.ForeignKey(TestCenterUser, default=None)
course_id = models.CharField(max_length=128, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
# user_updated_at happens only when the user makes a change to their data,
# and is something Pearson needs to know to manage updates. Unlike
# updated_at, this will not get incremented when we do a batch data import.
# The appointment dates, the exam count, and the accommodation codes can be updated,
# but hopefully this won't happen often.
user_updated_at = models.DateTimeField(db_index=True)
# "client_authorization_id" is our unique identifier for the authorization.
# This must be present for an update or delete to be sent to Pearson.
client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True)
# information about the test, from the course policy:
exam_series_code = models.CharField(max_length=15, db_index=True)
eligibility_appointment_date_first = models.DateField(db_index=True)
eligibility_appointment_date_last = models.DateField(db_index=True)
# this is really a list of codes, using an '*' as a delimiter.
# So it's not a choice list. We use the special value of ACCOMMODATION_REJECTED_CODE
# to indicate the rejection of an accommodation request.
accommodation_code = models.CharField(max_length=64, blank=True)
# store the original text of the accommodation request.
accommodation_request = models.CharField(max_length=1024, blank=True, db_index=True)
# time at which edX sent the registration to the test center
uploaded_at = models.DateTimeField(null=True, db_index=True)
# confirmation back from the test center, as well as timestamps
# on when they processed the request, and when we received
# confirmation back.
processed_at = models.DateTimeField(null=True, db_index=True)
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
upload_error_message = models.CharField(max_length=512, blank=True)
# Unique ID given to us for this registration by the Testing Center. It's null when
# we first create the registration entry, and may be assigned by Pearson later.
# (However, it may never be set if we are always initiating such candidate creation.)
authorization_id = models.IntegerField(null=True, db_index=True)
confirmed_at = models.DateTimeField(null=True, db_index=True)
@property
def candidate_id(self):
return self.testcenter_user.candidate_id
@property
def client_candidate_id(self):
return self.testcenter_user.client_candidate_id
@property
def authorization_transaction_type(self):
if self.authorization_id is not None:
return 'Update'
elif self.uploaded_at is None:
return 'Add'
else:
# TODO: decide what to send when we have uploaded an initial version,
# but have not received confirmation back from that upload. If the
# registration here has been changed, then we don't know if this changed
# registration should be submitted as an 'add' or an 'update'.
#
# If the first registration were lost or in error (e.g. bad code),
# the second should be an "Add". If the first were processed successfully,
# then the second should be an "Update". We just don't know....
return 'Update'
@property
def exam_authorization_count(self):
# TODO: figure out if this should really go in the database (with a default value).
return 1
@classmethod
def create(cls, testcenter_user, exam, accommodation_request):
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
registration.eligibility_appointment_date_first = strftime("%Y-%m-%d", exam.first_eligible_appointment_date)
registration.eligibility_appointment_date_last = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
registration.client_authorization_id = cls._create_client_authorization_id()
# accommodation_code remains blank for now, along with Pearson confirmation information
return registration
@staticmethod
def _generate_authorization_id():
return TestCenterUser._generate_edx_id("edXexam")
@staticmethod
def _create_client_authorization_id():
"""
Return a unique id for a registration, suitable for using as an authorization code
for Pearson. It must fit within 20 characters.
"""
# generate a random value, and check to see if it already is in use here
auth_id = TestCenterRegistration._generate_authorization_id()
while TestCenterRegistration.objects.filter(client_authorization_id=auth_id).exists():
auth_id = TestCenterRegistration._generate_authorization_id()
return auth_id
# methods for providing registration status details on registration page:
@property
def demographics_is_accepted(self):
return self.testcenter_user.is_accepted
@property
def demographics_is_rejected(self):
return self.testcenter_user.is_rejected
@property
def demographics_is_pending(self):
return self.testcenter_user.is_pending
@property
def accommodation_is_accepted(self):
return len(self.accommodation_request) > 0 and len(self.accommodation_code) > 0 and self.accommodation_code != ACCOMMODATION_REJECTED_CODE
@property
def accommodation_is_rejected(self):
return len(self.accommodation_request) > 0 and self.accommodation_code == ACCOMMODATION_REJECTED_CODE
@property
def accommodation_is_pending(self):
return len(self.accommodation_request) > 0 and len(self.accommodation_code) == 0
@property
def accommodation_is_skipped(self):
return len(self.accommodation_request) == 0
@property
def registration_is_accepted(self):
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
@property
def registration_is_rejected(self):
return self.upload_status == TEST_CENTER_STATUS_ERROR
@property
def registration_is_pending(self):
return not self.registration_is_accepted and not self.registration_is_rejected
# methods for providing registration status summary on dashboard page:
@property
def is_accepted(self):
return self.registration_is_accepted and self.demographics_is_accepted
@property
def is_rejected(self):
return self.registration_is_rejected or self.demographics_is_rejected
@property
def is_pending(self):
return not self.is_accepted and not self.is_rejected
def get_accommodation_codes(self):
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() ]
@property
def registration_signup_url(self):
return settings.PEARSONVUE_SIGNINPAGE_URL
class TestCenterRegistrationForm(ModelForm):
class Meta:
model = TestCenterRegistration
fields = ( 'accommodation_request', 'accommodation_code' )
def clean_accommodation_request(self):
code = self.cleaned_data['accommodation_request']
if code and len(code) > 0:
return code.strip()
return code
def update_and_save(self):
registration = self.save(commit=False)
# create additional values here:
registration.user_updated_at = datetime.utcnow()
registration.save()
log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
# TODO: add validation code for values added to accommodation_code field.
def get_testcenter_registration(user, course_id, exam_series_code):
try:
tcu = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
return []
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
e.g. personalized survey links.
"""
# include the secret key as a salt, and to make the ids unique accross
# include the secret key as a salt, and to make the ids unique across
# different LMS installs.
h = hashlib.md5()
h.update(settings.SECRET_KEY)
......
import datetime
import feedparser
import itertools
#import itertools
import json
import logging
import random
import string
import sys
import time
#import time
import urllib
import uuid
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import PasswordResetForm
......@@ -26,18 +27,19 @@ from bs4 import BeautifulSoup
from django.core.cache import cache
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from student.models import (Registration, UserProfile,
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user)
CourseEnrollment, unique_id_for_user,
get_testcenter_registration)
from certificates.models import CertificateStatuses, certificate_status_for_student
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from datetime import date
#from datetime import date
from collections import namedtuple
from courseware.courses import get_courses
......@@ -209,7 +211,7 @@ def _cert_info(user, course, cert_status):
def dashboard(request):
user = request.user
enrollments = CourseEnrollment.objects.filter(user=user)
# Build our courses list for the user, but ignore any courses that no longer
# exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu.
......@@ -239,6 +241,8 @@ def dashboard(request):
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}
# Get the 3 most recent news
top_news = _get_news(top=3)
......@@ -249,6 +253,7 @@ def dashboard(request):
'show_courseware_links_for' : show_courseware_links_for,
'cert_statuses': cert_statuses,
'news': top_news,
'exam_registrations': exam_registrations,
}
return render_to_response('dashboard.html', context)
......@@ -300,7 +305,7 @@ def change_enrollment(request):
try:
course = course_from_id(course_id)
except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existant course {1}"
log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, enrollment.course_id))
return {'success': False, 'error': 'The course requested does not exist.'}
......@@ -466,8 +471,9 @@ def _do_create_account(post_vars):
try:
profile.year_of_birth = int(post_vars['year_of_birth'])
except (ValueError, KeyError):
profile.year_of_birth = None # If they give us garbage, just ignore it instead
# of asking them to put an integer.
# If they give us garbage, just ignore it instead
# of asking them to put an integer.
profile.year_of_birth = None
try:
profile.save()
except Exception:
......@@ -599,6 +605,162 @@ 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
current exam for the course.
"""
exam_info = course.current_test_center_exam
if exam_info is None:
return None
exam_code = exam_info.exam_series_code
registrations = get_testcenter_registration(user, course.id, exam_code)
if registrations:
registration = registrations[0]
else:
registration = None
return registration
@login_required
@ensure_csrf_cookie
def begin_exam_registration(request, course_id):
""" Handles request to register the user for the current
test center exam of the specified course. Called by form
in dashboard.html.
"""
user = request.user
try:
course = (course_from_id(course_id))
except ItemNotFoundError:
# TODO: do more than just log!! The rest will fail, so we should fail right now.
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, course_id))
# get the exam to be registered for:
# (For now, we just assume there is one at most.)
exam_info = course.current_test_center_exam
# determine if the user is registered for this course:
registration = exam_registration_info(user, course)
# we want to populate the registration page with the relevant information,
# if it already exists. Create an empty object otherwise.
try:
testcenteruser = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
testcenteruser = TestCenterUser()
testcenteruser.user = user
context = {'course': course,
'user': user,
'testcenteruser': testcenteruser,
'registration': registration,
'exam_info': exam_info,
}
return render_to_response('test_center_register.html', context)
@ensure_csrf_cookie
def create_exam_registration(request, post_override=None):
'''
JSON call to create a test center exam registration.
Called by form in test_center_register.html
'''
post_vars = post_override if post_override else request.POST
# first determine if we need to create a new TestCenterUser, or if we are making any update
# to an existing TestCenterUser.
username = post_vars['username']
user = User.objects.get(username=username)
course_id = post_vars['course_id']
course = (course_from_id(course_id)) # assume it will be found....
try:
testcenter_user = TestCenterUser.objects.get(user=user)
needs_updating = testcenter_user.needs_update(post_vars)
log.info("User {0} enrolled in course {1} {2}updating demographic info for exam registration".format(user.username, course_id, "" if needs_updating else "not "))
except TestCenterUser.DoesNotExist:
# do additional initialization here:
testcenter_user = TestCenterUser.create(user)
needs_updating = True
log.info("User {0} enrolled in course {1} creating demographic info for exam registration".format(user.username, course_id))
# perform validation:
if needs_updating:
# first perform validation on the user information
# using a Django Form.
form = TestCenterUserForm(instance=testcenter_user, data=post_vars)
if form.is_valid():
form.update_and_save()
else:
response_data = {'success': False}
# return a list of errors...
response_data['field_errors'] = form.errors
response_data['non_field_errors'] = form.non_field_errors()
return HttpResponse(json.dumps(response_data), mimetype="application/json")
# create and save the registration:
needs_saving = False
exam = course.current_test_center_exam
exam_code = exam.exam_series_code
registrations = get_testcenter_registration(user, course_id, exam_code)
if registrations:
registration = registrations[0]
# NOTE: we do not bother to check here to see if the registration has changed,
# because at the moment there is no way for a user to change anything about their
# registration. They only provide an optional accommodation request once, and
# cannot make changes to it thereafter.
# It is possible that the exam_info content has been changed, such as the
# scheduled exam dates, but those kinds of changes should not be handled through
# this registration screen.
else:
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))
if needs_saving:
# do validation of registration. (Mainly whether an accommodation request is too long.)
form = TestCenterRegistrationForm(instance=registration, data=post_vars)
if form.is_valid():
form.update_and_save()
else:
response_data = {'success': False}
# return a list of errors...
response_data['field_errors'] = form.errors
response_data['non_field_errors'] = form.non_field_errors()
return HttpResponse(json.dumps(response_data), mimetype="application/json")
# only do the following if there is accommodation text to send,
# and a destination to which to send it.
# TODO: still need to create the accommodation email templates
# if 'accommodation_request' in post_vars and 'TESTCENTER_ACCOMMODATION_REQUEST_EMAIL' in settings:
# d = {'accommodation_request': post_vars['accommodation_request'] }
#
# # composes accommodation email
# subject = render_to_string('emails/accommodation_email_subject.txt', d)
# # Email subject *must not* contain newlines
# subject = ''.join(subject.splitlines())
# message = render_to_string('emails/accommodation_email.txt', d)
#
# try:
# dest_addr = settings['TESTCENTER_ACCOMMODATION_REQUEST_EMAIL']
# from_addr = user.email
# send_mail(subject, message, from_addr, [dest_addr], fail_silently=False)
# except:
# log.exception(sys.exc_info())
# response_data = {'success': False}
# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ]
# return HttpResponse(json.dumps(response_data), mimetype="application/json")
js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json")
def get_random_post_override():
"""
......@@ -654,7 +816,7 @@ def password_reset(request):
# By default, Django doesn't allow Users with is_active = False to reset their passwords,
# but this bites people who signed up a long time ago, never activated, and forgot their
# password. So for their sake, we'll auto-activate a user for whome password_reset is called.
# password. So for their sake, we'll auto-activate a user for whom password_reset is called.
try:
user = User.objects.get(email=request.POST['email'])
user.is_active = True
......
......@@ -97,6 +97,21 @@ class CourseDescriptor(SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self.test_center_exams = []
test_center_info = self.metadata.get('testcenter_info')
if test_center_info is not None:
for exam_name in test_center_info:
try:
exam_info = test_center_info[exam_name]
self.test_center_exams.append(self.TestCenterExam(self.id, exam_name, exam_info))
except Exception as err:
# If we can't parse the test center exam info, don't break
# the rest of the courseware.
msg = 'Error %s: Unable to load test-center exam info for exam "%s" of course "%s"' % (err, exam_name, self.id)
log.error(msg)
continue
def set_grading_policy(self, policy_str):
"""Parse the policy specified in policy_str, and save it"""
try:
......@@ -362,6 +377,88 @@ class CourseDescriptor(SequenceDescriptor):
"""
return self.metadata.get('end_of_course_survey_url')
class TestCenterExam(object):
def __init__(self, course_id, exam_name, exam_info):
self.course_id = course_id
self.exam_name = exam_name
self.exam_info = exam_info
self.exam_series_code = exam_info.get('Exam_Series_Code') or exam_name
self.display_name = exam_info.get('Exam_Display_Name') or self.exam_series_code
self.first_eligible_appointment_date = self._try_parse_time('First_Eligible_Appointment_Date')
if self.first_eligible_appointment_date is None:
raise ValueError("First appointment date must be specified")
# TODO: If defaulting the last appointment date, it should be the
# *end* of the same day, not the same time. It's going to be used as the
# end of the exam overall, so we don't want the exam to disappear too soon.
# It's also used optionally as the registration end date, so time matters there too.
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified")
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date
# do validation within the exam info:
if self.registration_start_date > self.registration_end_date:
raise ValueError("Registration start date must be before registration end date")
if self.first_eligible_appointment_date > self.last_eligible_appointment_date:
raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_appointment_date:
raise ValueError("Registration end date must be before last appointment date")
def _try_parse_time(self, key):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
if key in self.exam_info:
try:
return parse_time(self.exam_info[key])
except ValueError as e:
msg = "Exam {0} in course {1} loaded with a bad exam_info key '{2}': '{3}'".format(self.exam_name, self.course_id, self.exam_info[key], e)
log.warning(msg)
return None
def has_started(self):
return time.gmtime() > self.first_eligible_appointment_date
def has_ended(self):
return time.gmtime() > self.last_eligible_appointment_date
def has_started_registration(self):
return time.gmtime() > self.registration_start_date
def has_ended_registration(self):
return time.gmtime() > self.registration_end_date
def is_registering(self):
now = time.gmtime()
return now >= self.registration_start_date and now <= self.registration_end_date
@property
def first_eligible_appointment_date_text(self):
return time.strftime("%b %d, %Y", self.first_eligible_appointment_date)
@property
def last_eligible_appointment_date_text(self):
return time.strftime("%b %d, %Y", self.last_eligible_appointment_date)
@property
def registration_end_date_text(self):
return time.strftime("%b %d, %Y", self.registration_end_date)
@property
def current_test_center_exam(self):
exams = [exam for exam in self.test_center_exams if exam.has_started_registration() and not exam.has_ended()]
if len(exams) > 1:
# TODO: output some kind of warning. This should already be
# caught if we decide to do validation at load time.
return exams[0]
elif len(exams) == 1:
return exams[0]
else:
return None
@property
def title(self):
return self.display_name
......
......@@ -24,11 +24,10 @@ class GraphicalSliderToolModule(XModule):
'''
js = {
'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
'js': [
# 3rd party libraries used by graphic slider tool.
# TODO - where to store them - outside xmodule?
resource_string(__name__, 'js/src/graphical_slider_tool/jstat-1.0.0.min.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/state.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'),
......@@ -38,8 +37,8 @@ class GraphicalSliderToolModule(XModule):
resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')
]
}
js_module_name = "GraphicalSliderTool"
......
......@@ -401,7 +401,7 @@ define('Graph', ['logme'], function (logme) {
return false;
}
} else {
logme('MESSAGE: "xticks" were not specified. Using defaults.');
// logme('MESSAGE: "xticks" were not specified. Using defaults.');
return false;
}
......@@ -416,7 +416,7 @@ define('Graph', ['logme'], function (logme) {
return false;
}
} else {
logme('MESSAGE: "yticks" were not specified. Using defaults.');
// logme('MESSAGE: "yticks" were not specified. Using defaults.');
return false;
}
......
......@@ -14,7 +14,9 @@ window.GraphicalSliderTool = function (el) {
// with a unique DOM ID), we will iterate over all children, and for
// each match, we will call GstMain module.
$(el).children('.graphical_slider_tool').each(function (index, value) {
GstMain($(value).attr('id'));
JavascriptLoader.executeModuleScripts($(value), function(){
GstMain($(value).attr('id'));
});
});
});
};
......@@ -19,7 +19,7 @@ define(
if ($('#' + gstId).attr('data-processed') !== 'processed') {
$('#' + gstId).attr('data-processed', 'processed');
} else {
logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
// logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
return;
}
......
......@@ -20,9 +20,9 @@ define('Sliders', ['logme'], function (logme) {
} else if (sliderDiv.length > 1) {
logme('ERROR: Found more than one slider for the parameter "' + paramName + '".');
logme('sliderDiv.length = ', sliderDiv.length);
} else {
logme('MESSAGE: Did not find a slider for the parameter "' + paramName + '".');
}
} // else {
// logme('MESSAGE: Did not find a slider for the parameter "' + paramName + '".');
// }
}
function createSlider(sliderDiv, paramName) {
......
......@@ -24,7 +24,7 @@ define('State', ['logme'], function (logme) {
dynamicElByElId = {};
stateInst += 1;
logme('MESSAGE: Creating state instance # ' + stateInst + '.');
// logme('MESSAGE: Creating state instance # ' + stateInst + '.');
// Initially, there are no parameters to track. So, we will instantiate
// an empty object.
......
......@@ -124,3 +124,7 @@ class RoundTripTestCase(unittest.TestCase):
def test_graphicslidertool_roundtrip(self):
#Test graphicslidertool xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool")
def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"test_exam_registration")
......@@ -7,8 +7,11 @@ TIME_FORMAT = "%Y-%m-%dT%H:%M"
def parse_time(time_str):
"""
Takes a time string in TIME_FORMAT, returns
it as a time_struct. Raises ValueError if the string is not in the right format.
Takes a time string in TIME_FORMAT
Returns it as a time_struct.
Raises ValueError if the string is not in the right format.
"""
return time.strptime(time_str, TIME_FORMAT)
......
......@@ -414,7 +414,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
'xqa_key',
# TODO: This is used by the XMLModuleStore to provide for locations for
# static files, and will need to be removed when that code is removed
'data_dir'
'data_dir',
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta'
)
# cdodge: this is a list of metadata names which are 'system' metadata
......@@ -497,13 +501,27 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
@property
def start(self):
"""
If self.metadata contains start, return it. Else return None.
If self.metadata contains a valid start time, return it as a time struct.
Else return None.
"""
if 'start' not in self.metadata:
return None
return self._try_parse_time('start')
@property
def days_early_for_beta(self):
"""
If self.metadata contains start, return the number, as a float. Else return None.
"""
if 'days_early_for_beta' not in self.metadata:
return None
try:
return float(self.metadata['days_early_for_beta'])
except ValueError:
return None
@property
def own_metadata(self):
"""
Return the metadata that is not inherited, but was defined on this module.
......@@ -715,7 +733,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
Returns a time_struct, or None if metadata key is not present or is invalid.
"""
if key in self.metadata:
try:
......
......@@ -94,12 +94,18 @@ class XmlDescriptor(XModuleDescriptor):
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
'ispublic', # if True, then course is listed for all users; see
'xqa_key', # for xqaa server access
# information about testcenter exams is a dict (of dicts), not a string,
# so it cannot be easily exportable as a course element's attribute.
'testcenter_info',
# VS[compat] Remove once unused.
'name', 'slug')
metadata_to_strip = ('data_dir',
# VS[compat] -- remove the below attrs once everything is in the CMS
'course', 'org', 'url_name', 'filename')
# information about testcenter exams is a dict (of dicts), not a string,
# so it cannot be easily exportable as a course element's attribute.
'testcenter_info',
# VS[compat] -- remove the below attrs once everything is in the CMS
'course', 'org', 'url_name', 'filename')
# A dictionary mapping xml attribute names AttrMaps that describe how
# to import and export them
......
Simple course with test center exam information included in policy.json.
roots/2012_Fall.xml
\ No newline at end of file
<course>
<chapter url_name="Overview">
<videosequence url_name="Toy_Videos">
<html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
<chapter url_name="Ch2">
<html url_name="test_html">
<h2>Welcome</h2>
</html>
</chapter>
</course>
<b>Lab 2A: Superposition Experiment</b>
<p>Isn't the toy course great?</p>
<html filename="toylab.html"/>
\ No newline at end of file
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2011-07-17T12:00",
"display_name": "Toy Course",
"testcenter_info": {
"Midterm_Exam": {
"Exam_Series_Code": "Midterm_Exam",
"First_Eligible_Appointment_Date": "2012-11-09T00:00",
"Last_Eligible_Appointment_Date": "2012-11-09T23:59"
},
"Final_Exam": {
"Exam_Series_Code": "mit6002xfall12a",
"Exam_Display_Name": "Final Exam",
"First_Eligible_Appointment_Date": "2013-01-25T00:00",
"Last_Eligible_Appointment_Date": "2013-01-25T23:59",
"Registration_Start_Date": "2013-01-01T00:00",
"Registration_End_Date": "2013-01-21T23:59"
}
}
},
"chapter/Overview": {
"display_name": "Overview"
},
"chapter/Ch2": {
"display_name": "Chapter 2",
"start": "2015-07-17T12:00"
},
"videosequence/Toy_Videos": {
"display_name": "Toy Videos",
"format": "Lecture Sequence"
},
"html/toylab": {
"display_name": "Toy lab"
},
"video/Video_Resources": {
"display_name": "Video Resources"
},
"video/Welcome": {
"display_name": "Welcome"
}
}
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
\ No newline at end of file
......@@ -257,6 +257,7 @@ Supported fields at the course level:
* "tabs" -- have custom tabs in the courseware. See below for details on config.
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* "show_calculator" (value "Yes" if desired)
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
* TODO: there are others
### Grading policy file contents
......
......@@ -28,12 +28,12 @@ export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
# Temporary workaround for pip/numpy bug. (Jenkin's is unable to pip install numpy successfully, scipy fails to install afterwards.
# We tried pip 1.1, 1.2, all sorts of varieties but it's apparently a pip bug of some kind.
wget -O /tmp/numpy.tar.gz http://pypi.python.org/packages/source/n/numpy/numpy-1.6.2.tar.gz#md5=95ed6c9dcc94af1fc1642ea2a33c1bba
tar -zxvf /tmp/numpy.tar.gz -C /tmp/
python /tmp/numpy-1.6.2/setup.py install
if [ ! -d /mnt/virtualenvs/"$JOB_NAME" ]; then
mkdir -p /mnt/virtualenvs/"$JOB_NAME"
virtualenv /mnt/virtualenvs/"$JOB_NAME"
fi
source /mnt/virtualenvs/"$JOB_NAME"/bin/activate
pip install -q -r pre-requirements.txt
pip install -q -r test-requirements.txt
yes w | pip install -q -r requirements.txt
......@@ -45,7 +45,6 @@ TESTS_FAILED=0
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true
# Don't run the studio tests until feature/cale/cms-master is merged in
# rake phantomjs_jasmine_cms || true
rake coverage:xml coverage:html
......
......@@ -4,13 +4,13 @@ like DISABLE_START_DATES"""
import logging
import time
from datetime import datetime, timedelta
from django.conf import settings
from xmodule.course_module import CourseDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed
......@@ -73,7 +73,7 @@ def has_access(user, obj, action):
raise TypeError("Unknown object type in has_access(): '{0}'"
.format(type(obj)))
def get_access_group_name(obj,action):
def get_access_group_name(obj, action):
'''
Returns group name for user group which has "action" access to the given object.
......@@ -226,9 +226,10 @@ def _has_access_descriptor(user, descriptor, action):
# Check start date
if descriptor.start is not None:
now = time.gmtime()
if now > descriptor.start:
effective_start = _adjust_start_date_for_beta_testers(user, descriptor)
if now > effective_start:
# after start date, everyone can see it
debug("Allow: now > start date")
debug("Allow: now > effective start date")
return True
# otherwise, need staff access
return _has_staff_access_to_descriptor(user, descriptor)
......@@ -328,6 +329,15 @@ def _course_staff_group_name(location):
"""
return 'staff_%s' % Location(location).course
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
def _course_instructor_group_name(location):
"""
......@@ -348,6 +358,51 @@ def _has_global_staff_access(user):
return False
def _adjust_start_date_for_beta_testers(user, descriptor):
"""
If user is in a beta test group, adjust the start date by the appropriate number of
days.
Arguments:
user: A django user. May be anonymous.
descriptor: the XModuleDescriptor the user is trying to get access to, with a
non-None start date.
Returns:
A time, in the same format as returned by time.gmtime(). Either the same as
start, or earlier for beta testers.
NOTE: number of days to adjust should be cached to avoid looking it up thousands of
times per query.
NOTE: For now, this function assumes that the descriptor's location is in the course
the user is looking at. Once we have proper usages and definitions per the XBlock
design, this should use the course the usage is in.
NOTE: If testing manually, make sure MITX_FEATURES['DISABLE_START_DATES'] = False
in envs/dev.py!
"""
if descriptor.days_early_for_beta is None:
# bail early if no beta testing is set up
return descriptor.start
user_groups = [g.name for g in user.groups.all()]
beta_group = course_beta_test_group_name(descriptor.location)
if beta_group in user_groups:
debug("Adjust start time: user in group %s", beta_group)
# time_structs don't support subtraction, so convert to datetimes,
# subtract, convert back.
# (fun fact: datetime(*a_time_struct[:6]) is the beautiful syntax for
# converting time_structs into datetimes)
start_as_datetime = datetime(*descriptor.start[:6])
delta = timedelta(descriptor.days_early_for_beta)
effective = start_as_datetime - delta
# ...and back to time_struct
return effective.timetuple()
return descriptor.start
def _has_instructor_access_to_location(user, location):
return _has_access_to_location(user, location, 'instructor')
......
......@@ -17,7 +17,8 @@ import xmodule.modulestore.django
# Need access to internal func to put users in the right group
from courseware import grades
from courseware.access import _course_staff_group_name
from courseware.access import (has_access, _course_staff_group_name,
course_beta_test_group_name)
from courseware.models import StudentModuleCache
from student.models import Registration
......@@ -238,7 +239,7 @@ class PageLoader(ActivateLoginTestCase):
n = 0
num_bad = 0
all_ok = True
for descriptor in module_store.modules[course_id].itervalues():
for descriptor in module_store.modules[course_id].itervalues():
n += 1
print "Checking ", descriptor.location.url()
#print descriptor.__class__, descriptor.location
......@@ -259,11 +260,11 @@ class PageLoader(ActivateLoginTestCase):
# check content to make sure there were no rendering failures
content = resp.content
if content.find("this module is temporarily unavailable")>=0:
msg = "ERROR unavailable module "
msg = "ERROR unavailable module "
all_ok = False
num_bad += 1
elif isinstance(descriptor, ErrorDescriptor):
msg = "ERROR error descriptor loaded: "
msg = "ERROR error descriptor loaded: "
msg = msg + descriptor.definition['data']['error_msg']
all_ok = False
num_bad += 1
......@@ -286,7 +287,7 @@ class TestCoursesLoadTestCase(PageLoader):
# xmodule.modulestore.django.modulestore().collection.drop()
# store = xmodule.modulestore.django.modulestore()
# is there a way to empty the store?
def test_toy_course_loads(self):
self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
......@@ -453,6 +454,9 @@ class TestViewAuth(PageLoader):
"""Check that enrollment periods work"""
self.run_wrapped(self._do_test_enrollment_period)
def test_beta_period(self):
"""Check that beta-test access works"""
self.run_wrapped(self._do_test_beta_period)
def _do_test_dark_launch(self):
"""Actually do the test, relying on settings to be right."""
......@@ -618,6 +622,38 @@ class TestViewAuth(PageLoader):
self.unenroll(self.toy)
self.assertTrue(self.try_enroll(self.toy))
def _do_test_beta_period(self):
"""Actually test beta periods, relying on settings to be right."""
# trust, but verify :)
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
# Make courses start in the future
tomorrow = time.time() + 24 * 3600
nextday = tomorrow + 24 * 3600
yesterday = time.time() - 24 * 3600
# toy course's hasn't started
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
self.assertFalse(self.toy.has_started())
# but should be accessible for beta testers
self.toy.metadata['days_early_for_beta'] = '2'
# student user shouldn't see it
student_user = user(self.student)
self.assertFalse(has_access(student_user, self.toy, 'load'))
# now add the student to the beta test group
group_name = course_beta_test_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(student_user)
# now the student should see it
self.assertTrue(has_access(student_user, self.toy, 'load'))
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCourseGrader(PageLoader):
"""Check that a course gets graded properly"""
......
......@@ -179,7 +179,7 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
self.assertFalse(has_forum_access(username, course.id, rolename))
def test_add_and_readd_forum_admin_users(self):
def test_add_and_read_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
......
......@@ -2,11 +2,13 @@
from collections import defaultdict
import csv
import itertools
import json
import logging
import os
import requests
import urllib
import json
from StringIO import StringIO
......@@ -19,12 +21,17 @@ from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from courseware import grades
from courseware.access import has_access, get_access_group_name
from courseware.courses import get_course_with_access
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.courses import get_course_with_access
from django_comment_client.models import (Role,
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_COMMUNITY_TA)
from django_comment_client.utils import has_forum_access
from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from courseware.models import StudentModule
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
......@@ -44,13 +51,12 @@ FORUM_ROLE_REMOVE = 'remove'
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course."""
course = get_course_with_access(request.user, course_id, 'staff')
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
msg = ''
......@@ -105,6 +111,16 @@ def instructor_dashboard(request, course_id):
except Group.DoesNotExist:
group = Group(name=grpname) # create the group
group.save()
def get_beta_group(course):
"""
Get the group for beta testers of course.
"""
# Not using get_group because there is no access control action called
# 'beta', so adding it to get_access_group_name doesn't really make
# sense.
name = course_beta_test_group_name(course.location)
(group, created) = Group.objects.get_or_create(name=name)
return group
# process actions from form POST
......@@ -172,6 +188,80 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
elif "Reset student's attempts" in action:
# get the form data
unique_student_identifier=request.POST.get('unique_student_identifier','')
problem_to_reset=request.POST.get('problem_to_reset','')
if problem_to_reset[-4:]==".xml":
problem_to_reset=problem_to_reset[:-4]
# try to uniquely id student by email address or username
try:
if "@" in unique_student_identifier:
student_to_reset=User.objects.get(email=unique_student_identifier)
else:
student_to_reset=User.objects.get(username=unique_student_identifier)
msg+="Found a single student to reset. "
except:
student_to_reset=None
msg+="<font color='red'>Couldn't find student with that email or username. </font>"
if student_to_reset is not None:
# find the module in question
try:
(org, course_name, run)=course_id.split("/")
module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_reset
module_to_reset=StudentModule.objects.get(student_id=student_to_reset.id,
course_id=course_id,
module_state_key=module_state_key)
msg+="Found module to reset. "
except Exception as e:
msg+="<font color='red'>Couldn't find module with that urlname. </font>"
# modify the problem's state
try:
# load the state json
problem_state=json.loads(module_to_reset.state)
old_number_of_attempts=problem_state["attempts"]
problem_state["attempts"]=0
# save
module_to_reset.state=json.dumps(problem_state)
module_to_reset.save()
track.views.server_track(request,
'{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format(
old_attempts=old_number_of_attempts,
student=student_to_reset,
problem=module_to_reset.module_state_key,
instructor=request.user,
course=course_id),
{},
page='idashboard')
msg+="<font color='green'>Module state successfully reset!</font>"
except:
msg+="<font color='red'>Couldn't reset module state. </font>"
elif "Get link to student's progress page" in action:
unique_student_identifier=request.POST.get('unique_student_identifier','')
try:
if "@" in unique_student_identifier:
student_to_reset=User.objects.get(email=unique_student_identifier)
else:
student_to_reset=User.objects.get(username=unique_student_identifier)
progress_url=reverse('student_progress',kwargs={'course_id':course_id,'student_id': student_to_reset.id})
track.views.server_track(request,
'{instructor} requested progress page for {student} in {course}'.format(
student=student_to_reset,
instructor=request.user,
course=course_id),
{},
page='idashboard')
msg+="<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".format(progress_url,student_to_reset.username,student_to_reset.email)
except:
msg+="<font color='red'>Couldn't find student with that username. </font>"
#----------------------------------------
# export grades to remote gradebook
......@@ -237,11 +327,7 @@ def instructor_dashboard(request, course_id):
elif 'List course staff' in action:
group = get_staff_group(course)
msg += 'Staff group = {0}'.format(group.name)
log.debug('staffgrp={0}'.format(group.name))
uset = group.user_set.all()
datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = 'List of Staff in course {0}'.format(course_id)
datatable = _group_members_table(group, "List of Staff", course_id)
track.views.server_track(request, 'list-staff', {}, page='idashboard')
elif 'List course instructors' in action and request.user.is_staff:
......@@ -256,17 +342,8 @@ def instructor_dashboard(request, course_id):
elif action == 'Add course staff':
uname = request.POST['staffuser']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_staff_group(course)
msg += '<font color="green">Added {0} to staff group = {1}</font>'.format(user, group.name)
log.debug('staffgrp={0}'.format(group.name))
user.groups.add(group)
track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard')
group = get_staff_group(course)
msg += add_user_to_group(request, uname, group, 'staff', 'staff')
elif action == 'Add instructor' and request.user.is_staff:
uname = request.POST['instructor']
......@@ -284,17 +361,8 @@ def instructor_dashboard(request, course_id):
elif action == 'Remove course staff':
uname = request.POST['staffuser']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_staff_group(course)
msg += '<font color="green">Removed {0} from staff group = {1}</font>'.format(user, group.name)
log.debug('staffgrp={0}'.format(group.name))
user.groups.remove(group)
track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard')
group = get_staff_group(course)
msg += remove_user_from_group(request, uname, group, 'staff', 'staff')
elif action == 'Remove instructor' and request.user.is_staff:
uname = request.POST['instructor']
......@@ -311,25 +379,49 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard')
#----------------------------------------
# Group management
elif 'List beta testers' in action:
group = get_beta_group(course)
msg += 'Beta test group = {0}'.format(group.name)
datatable = _group_members_table(group, "List of beta_testers", course_id)
track.views.server_track(request, 'list-beta-testers', {}, page='idashboard')
elif action == 'Add beta testers':
users = request.POST['betausers']
log.debug("users: {0!r}".format(users))
group = get_beta_group(course)
for username_or_email in _split_by_comma_and_whitespace(users):
msg += "<p>{0}</p>".format(
add_user_to_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
elif action == 'Remove beta testers':
users = request.POST['betausers']
group = get_beta_group(course)
for username_or_email in _split_by_comma_and_whitespace(users):
msg += "<p>{0}</p>".format(
remove_user_from_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
#----------------------------------------
# forum administration
elif action == 'List course forum admins':
rolename = FORUM_ROLE_ADMINISTRATOR
datatable = {}
msg += _list_course_forum_members(course_id, rolename, datatable)
track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard')
elif action == 'Remove forum admin':
uname = request.POST['forumadmin']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_ADMINISTRATOR, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_ADMINISTRATOR, course_id),
{}, page='idashboard')
elif action == 'Add forum admin':
uname = request.POST['forumadmin']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADD)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_ADMINISTRATOR, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_ADMINISTRATOR, course_id),
{}, page='idashboard')
elif action == 'List course forum moderators':
......@@ -337,35 +429,35 @@ def instructor_dashboard(request, course_id):
datatable = {}
msg += _list_course_forum_members(course_id, rolename, datatable)
track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard')
elif action == 'Remove forum moderator':
uname = request.POST['forummoderator']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_REMOVE)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_MODERATOR, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_MODERATOR, course_id),
{}, page='idashboard')
elif action == 'Add forum moderator':
uname = request.POST['forummoderator']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_ADD)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_MODERATOR, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_MODERATOR, course_id),
{}, page='idashboard')
elif action == 'List course forum community TAs':
rolename = FORUM_ROLE_COMMUNITY_TA
datatable = {}
msg += _list_course_forum_members(course_id, rolename, datatable)
track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard')
elif action == 'Remove forum community TA':
uname = request.POST['forummoderator']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_REMOVE)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_COMMUNITY_TA, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_COMMUNITY_TA, course_id),
{}, page='idashboard')
elif action == 'Add forum community TA':
uname = request.POST['forummoderator']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_ADD)
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id),
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id),
{}, page='idashboard')
#----------------------------------------
......@@ -418,7 +510,7 @@ def instructor_dashboard(request, course_id):
msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections')
msg += msg2
elif action in ['List students in section in remote gradebook',
elif action in ['List students in section in remote gradebook',
'Overload enrollment list using remote gradebook',
'Merge enrollment list with remote gradebook']:
......@@ -431,7 +523,7 @@ def instructor_dashboard(request, course_id):
overload = 'Overload' in action
ret = _do_enroll_students(course, course_id, students, overload=overload)
datatable = ret['datatable']
#----------------------------------------
# psychometrics
......@@ -448,7 +540,7 @@ def instructor_dashboard(request, course_id):
#----------------------------------------
# offline grades?
if use_offline:
msg += "<br/><font color='orange'>Grades from %s</font>" % offline_grades_available(course_id)
......@@ -482,17 +574,17 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
if not rg:
msg = "No remote gradebook defined in course metadata"
return msg, {}
rgurl = settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','')
if not rgurl:
msg = "No remote gradebook url defined in settings.MITX_FEATURES"
return msg, {}
rgname = rg.get('name','')
if not rgname:
msg = "No gradebook name defined in course remote_gradebook metadata"
return msg, {}
if args is None:
args = {}
data = dict(submit=action, gradebook=rgname, user=user.email)
......@@ -522,15 +614,15 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
return msg, datatable
def _list_course_forum_members(course_id, rolename, datatable):
'''
"""
Fills in datatable with forum membership information, for a given role,
so that it will be displayed on instructor dashboard.
course_ID = the ID string for a course
rolename = one of "Administrator", "Moderator", "Community TA"
Returns message status string to append to displayed message, if role is unknown.
'''
"""
# make sure datatable is set up properly for display first, before checking for errors
datatable['header'] = ['Username', 'Full name', 'Roles']
datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id)
......@@ -549,13 +641,13 @@ def _list_course_forum_members(course_id, rolename, datatable):
def _update_forum_role_membership(uname, course, rolename, add_or_remove):
'''
Supports adding a user to a course's forum role
uname = username string for user
course = course object
course = course object
rolename = one of "Administrator", "Moderator", "Community TA"
add_or_remove = one of "add" or "remove"
Returns message status string to append to displayed message, Status is returned if user
Returns message status string to append to displayed message, Status is returned if user
or role is unknown, or if entry already exists when adding, or if entry doesn't exist when removing.
'''
# check that username and rolename are valid:
......@@ -575,21 +667,105 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove):
if add_or_remove == FORUM_ROLE_REMOVE:
if not alreadyexists:
msg ='<font color="red">Error: user "{0}" does not have rolename "{1}", cannot remove</font>'.format(uname, rolename)
else:
else:
user.roles.remove(role)
msg = '<font color="green">Removed "{0}" from "{1}" forum role = "{2}"</font>'.format(user, course.id, rolename)
else:
if alreadyexists:
msg = '<font color="red">Error: user "{0}" already has rolename "{1}", cannot add</font>'.format(uname, rolename)
else:
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')):
else:
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')):
msg = '<font color="red">Error: user "{0}" should first be added as staff before adding as a forum administrator, cannot add</font>'.format(uname)
else:
user.roles.add(role)
msg = '<font color="green">Added "{0}" to "{1}" forum role = "{2}"</font>'.format(user, course.id, rolename)
return msg
def _group_members_table(group, title, course_id):
"""
Return a data table of usernames and names of users in group_name.
Arguments:
group -- a django group.
title -- a descriptive title to show the user
Returns:
a dictionary with keys
'header': ['Username', 'Full name'],
'data': [[username, name] for all users]
'title': "{title} in course {course}"
"""
uset = group.user_set.all()
datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = '{0} in course {1}'.format(title, course_id)
return datatable
def _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, do_add):
"""
Implementation for both add and remove functions, to get rid of shared code. do_add is bool that determines which
to do.
"""
user = None
try:
if '@' in username_or_email:
user = User.objects.get(email=username_or_email)
else:
user = User.objects.get(username=username_or_email)
except User.DoesNotExist:
msg = '<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email)
user = None
if user is not None:
action = "Added" if do_add else "Removed"
prep = "to" if do_add else "from"
msg = '<font color="green">{action} {0} {prep} {1} group = {2}</font>'.format(user, group_title, group.name,
action=action, prep=prep)
if do_add:
user.groups.add(group)
else:
user.groups.remove(group)
event = "add" if do_add else "remove"
track.views.server_track(request, '{event}-{0} {1}'.format(event_name, user, event=event),
{}, page='idashboard')
return msg
def add_user_to_group(request, username_or_email, group, group_title, event_name):
"""
Look up the given user by username (if no '@') or email (otherwise), and add them to group.
Arguments:
request: django request--used for tracking log
username_or_email: who to add. Decide if it's an email by presense of an '@'
group: django group object
group_title: what to call this group in messages to user--e.g. "beta-testers".
event_name: what to call this event when logging to tracking logs.
Returns:
html to insert in the message field
"""
return _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, True)
def remove_user_from_group(request, username_or_email, group, group_title, event_name):
"""
Look up the given user by username (if no '@') or email (otherwise), and remove them from group.
Arguments:
request: django request--used for tracking log
username_or_email: who to remove. Decide if it's an email by presense of an '@'
group: django group object
group_title: what to call this group in messages to user--e.g. "beta-testers".
event_name: what to call this event when logging to tracking logs.
Returns:
html to insert in the message field
"""
return _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, False)
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False):
'''
......@@ -694,12 +870,20 @@ def grade_summary(request, course_id):
# enrollment
def _split_by_comma_and_whitespace(s):
"""
Split a string both by on commas and whitespice.
"""
# Note: split() with no args removes empty strings from output
lists = [x.split() for x in s.split(',')]
# return all of them
return itertools.chain(*lists)
def _do_enroll_students(course, course_id, students, overload=False):
"""Do the actual work of enrolling multiple students, presented as a string
of emails separated by commas or returns"""
ns = [x.split('\n') for x in students.split(',')]
new_students = [item for sublist in ns for item in sublist]
new_students = _split_by_comma_and_whitespace(students)
new_students = [str(s.strip()) for s in new_students]
new_students_lc = [x.lower() for x in new_students]
......@@ -750,7 +934,7 @@ def _do_enroll_students(course, course_id, students, overload=False):
def sf(stat): return [x for x in status if status[x]==stat]
data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'),
data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'),
deleted=sf('deleted'), datatable=datatable)
return data
......
......@@ -340,6 +340,11 @@ STAFF_GRADING_INTERFACE = {
# Used for testing, debugging
MOCK_STAFF_GRADING = False
################################# Pearson TestCenter config ################
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org"
################################# Peer grading config #####################
#By setting up the default settings with an incorrect user name and password,
......
......@@ -19,6 +19,7 @@
@import 'multicourse/home';
@import 'multicourse/dashboard';
@import 'multicourse/testcenter-register';
@import 'multicourse/courses';
@import 'multicourse/course_about';
@import 'multicourse/jobs';
......
......@@ -194,30 +194,9 @@
margin-bottom: 40px;
h3 {
background: url('/static/images/bullet-closed.png') no-repeat left 0.25em;
font-family: $sans-serif;
font-weight: 700;
margin-bottom: 10px;
padding-left: 20px;
cursor: pointer;
}
.answer {
display: none;
color: #3c3c3c;
padding-left: 16px;
font-family: $serif;
li {
line-height: 1.6em;
}
}
// opened states
&.opened {
h3 {
background: url('/static/images/bullet-open.png') no-repeat left 0.25em;
}
margin-bottom: 15px;
}
}
}
......
......@@ -267,13 +267,12 @@
}
.my-course {
@include border-radius(3px);
@include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
clear: both;
@include clearfix;
height: 120px;
margin-right: flex-gutter();
margin-bottom: 10px;
overflow: hidden;
margin-bottom: 50px;
padding-bottom: 50px;
border-bottom: 1px solid $light-gray;
position: relative;
width: flex-grid(12);
z-index: 20;
......@@ -283,13 +282,7 @@
margin-bottom: none;
}
.cover {
background: rgb(225,225,225);
background-size: cover;
background-position: center center;
border: 1px solid rgb(120,120,120);
@include border-left-radius(3px);
@include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.6), 1px 0 0 0 rgba(255,255,255, 0.8));
.cover {
@include box-sizing(border-box);
float: left;
height: 100%;
......@@ -299,100 +292,51 @@
position: relative;
@include transition(all, 0.15s, linear);
width: 200px;
height: 120px;
.shade {
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
rgba(0,0,0, 0.3) 100%));
bottom: 0px;
content: "";
display: block;
left: 0px;
position: absolute;
z-index: 50;
top: 0px;
@include transition(all, 0.15s, linear);
right: 0px;
}
.arrow {
position: absolute;
z-index: 100;
img {
width: 100%;
font-size: 70px;
line-height: 110px;
text-align: center;
text-decoration: none;
color: rgba(0, 0, 0, .7);
opacity: 0;
@include transition(all, 0.15s, linear);
}
&:hover {
.shade {
background: rgba(255,255,255, 0.3);
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
rgba(0,0,0, 0.3) 100%));
}
}
}
.info {
background: rgb(250,250,250);
@include background-image(linear-gradient(-90deg, rgb(253,253,253), rgb(240,240,240)));
@include box-sizing(border-box);
border: 1px solid rgb(190,190,190);
border-left: none;
@include border-right-radius(3px);
left: 201px;
height: 100%;
max-height: 100%;
padding: 0px 10px;
position: absolute;
right: 0px;
top: 0px;
z-index: 2;
@include clearfix;
padding: 0 10px 0 230px;
> hgroup {
@include clearfix;
border-bottom: 1px solid rgb(210,210,210);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
padding: 12px 0px;
padding: 0;
width: 100%;
.university {
background: rgba(255,255,255, 1);
border: 1px solid rgb(180,180,180);
@include border-radius(3px);
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.2), 0 1px 0 0 rgba(255,255,255, 0.6));
color: $lighter-base-font-color;
display: block;
font-style: italic;
font-family: $sans-serif;
font-size: 16px;
font-weight: 800;
@include inline-block;
margin-right: 10px;
margin-bottom: 0;
padding: 5px 10px;
float: left;
font-weight: 400;
margin: 0 0 6px;
text-transform: none;
letter-spacing: 0;
}
h3 {
display: block;
margin-bottom: 0px;
overflow: hidden;
padding-top: 2px;
text-overflow: ellipsis;
white-space: nowrap;
.date-block {
position: absolute;
top: 0;
right: 0;
font-family: $sans-serif;
font-size: 13px;
font-style: italic;
color: $lighter-base-font-color;
}
a {
color: $base-font-color;
font-weight: 700;
text-shadow: 0 1px rgba(255,255,255, 0.6);
h3 a {
display: block;
margin-bottom: 10px;
font-family: $sans-serif;
font-size: 34px;
line-height: 42px;
font-weight: 300;
&:hover {
text-decoration: underline;
}
&:hover {
text-decoration: none;
}
}
}
......@@ -430,71 +374,56 @@
}
.enter-course {
@include button(shiny, $blue);
@include button(simple, $blue);
@include box-sizing(border-box);
@include border-radius(3px);
display: block;
float: left;
font: normal 1rem/1.6rem $sans-serif;
letter-spacing: 1px;
padding: 6px 0px;
text-transform: uppercase;
font: normal 15px/1.6rem $sans-serif;
letter-spacing: 0;
padding: 6px 32px 7px;
text-align: center;
margin-top: 16px;
width: flex-grid(4);
}
}
> a:hover {
.cover {
.shade {
background: rgba(255,255,255, 0.1);
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
rgba(0,0,0, 0.3) 100%));
}
.arrow {
opacity: 1;
}
}
.info {
background: darken(rgb(250,250,250), 5%);
@include background-image(linear-gradient(-90deg, darken(rgb(253,253,253), 3%), darken(rgb(240,240,240), 5%)));
border-color: darken(rgb(190,190,190), 10%);
&.archived {
@include button(simple, #eee);
font: normal 15px/1.6rem $sans-serif;
padding: 6px 32px 7px;
.course-status {
background: darken($yellow, 3%);
border-color: darken(rgb(200,200,200), 3%);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
&:hover {
text-decoration: none;
}
}
.course-status-completed {
background: #888;
color: #fff;
&:hover {
text-decoration: none;
}
}
}
}
.message-status {
@include clearfix;
@include border-radius(3px);
@include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
display: none;
position: relative;
top: -15px;
z-index: 10;
margin: 0 0 20px 0;
margin: 20px 0 10px;
padding: 15px 20px;
font-family: "Open Sans", Verdana, Geneva, sans-serif;
background: #fffcf0;
font-family: $sans-serif;
background: tint($yellow,70%);
border: 1px solid #ccc;
.message-copy {
font-family: $sans-serif;
font-size: 13px;
margin: 0;
a {
font-family: $sans-serif;
}
.grade-value {
font-size: 1.4rem;
font-size: 1.2rem;
font-weight: bold;
}
}
......@@ -502,19 +431,18 @@
.actions {
@include clearfix;
list-style: none;
margin: 15px 0 0 0;
margin: 0;
padding: 0;
.action {
float: left;
margin:0 15px 10px 0;
margin: 0 15px 0 0;
.btn, .cta {
display: inline-block;
}
.btn {
@include button(shiny, $blue);
@include box-sizing(border-box);
@include border-radius(3px);
float: left;
......@@ -524,7 +452,6 @@
text-align: center;
&.disabled {
@include button(shiny, #eee);
cursor: default !important;
&:hover {
......@@ -539,7 +466,6 @@
}
.cta {
@include button(shiny, #666);
float: left;
font: normal 0.8rem/1.2rem $sans-serif;
letter-spacing: 1px;
......@@ -549,6 +475,52 @@
}
}
.exam-registration-number {
font-family: $sans-serif;
font-size: 18px;
a {
font-family: $sans-serif;
}
}
&.exam-register {
.message-copy {
margin-top: 5px;
width: 55%;
}
}
&.exam-schedule {
.exam-button {
margin-top: 5px;
}
}
.exam-button {
@include button(simple, $pink);
margin-top: 0;
float: right;
}
.contact-button {
@include button(simple, $pink);
}
.button {
display: inline-block;
margin-top: 10px;
padding: 9px 18px 10px;
font-size: 13px;
font-weight: bold;
letter-spacing: 0;
&:hover {
text-decoration: none;
}
}
&.is-shown {
display: block;
}
......@@ -577,17 +549,16 @@
a.unenroll {
float: right;
display: block;
font-style: italic;
color: #a0a0a0;
text-decoration: underline;
font-size: .8em;
@include inline-block;
margin-bottom: 40px;
margin-top: 32px;
&:hover {
color: #333;
}
}
}
}
.static-container.help {
section.questions {
float: left;
width: flex-grid(9);
margin-right: flex-gutter();
@include clearfix;
article {
margin-bottom: 40px;
nav.categories {
border: 1px solid rgb(220,220,220);
@include box-sizing(border-box);
float: left;
margin-left: flex-gutter();
padding: 20px;
width: flex-grid(3);
h2 {
border-bottom: 1px solid rgb(220,220,220);
margin-bottom: 40px;
padding-bottom: 20px;
a {
display: block;
letter-spacing: 1px;
margin: 0px -20px;
padding: 12px 0px 12px 20px;
text-align: left;
&:hover {
background: rgb(245,245,245);
text-decoration: none;
}
}
}
}
section.emails {
border: 1px solid rgb(220,220,220);
@include box-sizing(border-box);
float: left;
padding: 20px;
width: flex-grid(3);
.responses {
float: left;
width: flex-grid(9);
ul {
margin-left: 0;
padding-left: 0;
list-style: none;
.category {
padding-top: 40px;
li {
margin-bottom: 1em;
&:first-child {
padding-top: 0px;
}
> h2 {
border-bottom: 1px solid rgb(220,220,220);
margin-bottom: 40px;
padding-bottom: 20px;
}
}
.response {
margin-bottom: 40px;
h3 {
background: url('/static/images/bullet-closed.png') no-repeat left 0.25em;
font-family: $sans-serif;
font-weight: 700;
margin-bottom: 10px;
padding-left: 20px;
cursor: pointer;
}
.answer {
display: none;
color: #3c3c3c;
padding-left: 16px;
font-family: $serif;
li {
line-height: 1.6em;
}
}
// opened states
&.opened {
h3 {
background: url('/static/images/bullet-open.png') no-repeat left 0.25em;
}
}
}
}
}
}
// ==========
$baseline: 20px;
$yellow: rgb(255, 235, 169);
$red: rgb(178, 6, 16);
// ==========
.testcenter-register {
@include clearfix;
padding: 60px 0px 120px;
// reset - horrible, but necessary
p, a, h1, h2, h3, h4, h5, h6 {
font-family: $sans-serif !important;
}
// basic layout
.introduction {
width: flex-grid(12);
}
.message-status-registration {
width: flex-grid(12);
}
.content, aside {
@include box-sizing(border-box);
}
.content {
margin-right: flex-gutter();
width: flex-grid(8);
float: left;
}
aside {
margin: 0;
width: flex-grid(4);
float: left;
}
// introduction
.introduction {
header {
h2 {
margin: 0;
font-family: $sans-serif;
font-size: 16px;
color: $lighter-base-font-color;
}
h1 {
font-family: $sans-serif;
font-size: 34px;
text-align: left;
}
}
}
// content
.content {
background: rgb(255,255,255);
}
// form
.form-fields-primary, .form-fields-secondary {
border-bottom: 1px solid rgba(0,0,0,0.25);
@include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.1));
}
form {
border: 1px solid rgb(216, 223, 230);
@include border-radius(3px);
@include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.2));
.instructions, .note {
margin: 0;
padding: ($baseline*1.5) ($baseline*1.5) 0 ($baseline*1.5);
font-size: 14px;
color: tint($base-font-color, 20%);
strong {
font-weight: normal;
}
.title, .indicator {
color: $base-font-color;
font-weight: 700;
}
}
fieldset {
border-bottom: 1px solid rgba(216, 223, 230, 0.50);
padding: ($baseline*1.5);
}
.form-actions {
@include clearfix();
padding: ($baseline*1.5);
button[type="submit"] {
display: block;
@include button(simple, $blue);
@include box-sizing(border-box);
@include border-radius(3px);
font: bold 15px/1.6rem $sans-serif;
letter-spacing: 0;
padding: ($baseline*0.75) $baseline;
text-align: center;
&:disabled {
opacity: 0.3;
}
}
.action-primary {
float: left;
width: flex-grid(5,8);
margin-right: flex-gutter(2);
}
.action-secondary {
display: block;
float: left;
width: flex-grid(2,8);
margin-top: $baseline;
padding: ($baseline/4);
font-size: 13px;
text-align: right;
text-transform: uppercase;
}
&.error {
}
}
.list-input {
margin: 0;
padding: 0;
list-style: none;
.field {
border-bottom: 1px dotted rgba(216, 223, 230, 0.5);
margin: 0 0 $baseline 0;
padding: 0 0 $baseline 0;
&:last-child {
border: none;
margin-bottom: 0;
padding-bottom: 0;
}
&.disabled, &.submitted {
color: rgba(0,0,0,.25);
label {
cursor: text;
&:after {
margin-left: ($baseline/4);
}
}
textarea, input {
background: rgb(255,255,255);
color: rgba(0,0,0,.25);
}
}
&.disabled {
label:after {
color: rgba(0,0,0,.35);
content: "(Disabled Currently)";
}
}
&.submitted {
label:after {
content: "(Previously Submitted and Not Editable)";
}
.value {
@include border-radius(3px);
border: 1px solid #C8C8C8;
padding: $baseline ($baseline*0.75);
background: #FAFAFA;
}
}
&.error {
label {
color: $red;
}
input, textarea {
border-color: tint($red,50%);
}
}
&.required {
label {
font-weight: bold;
}
label:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
font-family: $sans-serif;
font-style: normal;
}
label {
margin: 0 0 ($baseline/4) 0;
@include transition(color, 0.15s, ease-in-out);
&.is-focused {
color: $blue;
}
}
input, textarea {
width: 100%;
padding: $baseline ($baseline*.75);
&.long {
width: 100%;
}
&.short {
width: 25%;
}
}
textarea.long {
height: ($baseline*5);
}
}
.field-group {
@include clearfix();
border-bottom: 1px dotted rgba(216, 223, 230, 0.5);
margin: 0 0 $baseline 0;
padding: 0 0 $baseline 0;
.field {
display: block;
float: left;
border-bottom: none;
margin: 0 $baseline ($baseline/2) 0;
padding-bottom: 0;
input, textarea {
width: 100%;
}
}
&.addresses {
.field {
width: 45%;
}
}
&.postal-2 {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
&.phoneinfo {
}
}
}
&.disabled {
> .instructions {
display: none;
}
.field {
opacity: 0.6;
.label, label {
cursor: auto;
}
}
.form-actions {
display: none;
}
}
}
// form - specifics
.form-fields-secondary {
display: none;
&.is-shown {
display: block;
}
&.disabled {
fieldset {
opacity: 0.5;
}
}
}
.form-fields-secondary-visibility {
display: block;
margin: 0;
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
font-size: 13px;
&.is-hidden {
display: none;
}
}
// aside
aside {
padding-left: $baseline;
.message-status {
@include border-radius(3px);
margin: 0 0 ($baseline*2) 0;
border: 1px solid #ccc;
padding: 0;
background: tint($yellow,90%);
> * {
padding: $baseline;
}
p {
margin: 0 0 ($baseline/4) 0;
padding: 0;
font-size: 13px;
}
.label, .value {
display: block;
}
h3, h4, h5 {
font-family: $sans-serif;
}
h3 {
border-bottom: 1px solid tint(rgb(0,0,0), 90%);
padding-bottom: ($baseline*0.75);
}
h4 {
margin-bottom: ($baseline/4);
}
.status-list {
list-style: none;
margin: 0;
padding: $baseline;
> .item {
@include clearfix();
margin: 0 0 ($baseline*0.75) 0;
border-bottom: 1px solid tint(rgb(0,0,0), 95%);
padding: 0 0 ($baseline/2) 0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
.title {
margin-bottom: ($baseline/4);
position: relative;
font-weight: bold;
font-size: 14px;
&:after {
position: absolute;
top: 0;
right: $baseline;
margin-left: $baseline;
content: "not started";
text-transform: uppercase;
font-size: 11px;
font-weight: normal;
opacity: 0.5;
}
}
.details, .item, .instructions {
@include transition(opacity, 0.10s, ease-in-out);
font-size: 13px;
opacity: 0.65;
}
&:before {
@include border-radius($baseline);
position: relative;
top: 3px;
display: block;
float: left;
width: ($baseline/2);
height: ($baseline/2);
margin: 0 ($baseline/2) 0 0;
background: $dark-gray;
content: "";
}
// specific states
&.status-processed {
&:before {
background: green;
}
.title:after {
color: green;
content: "processed";
}
&.status-registration {
.exam-link {
font-weight: 600 !important;
}
}
}
&.status-pending {
&:before {
background: transparent;
border: 1px dotted gray;
}
.title:after {
color: gray;
content: "pending";
}
}
&.status-rejected {
&:before {
background: $red;
}
.title:after {
color: red;
content: "rejected";
}
.call-link {
font-weight: bold;
}
}
&.status-initial {
&:before {
background: transparent;
border: 1px dotted gray;
}
.title:after {
color: gray;
}
}
&:hover {
.details, .item, .instructions {
opacity: 1.0;
}
}
}
// sub menus
.accommodations-list, .error-list {
list-style: none;
margin: ($baseline/2) 0;
padding: 0;
font-size: 13px;
.item {
margin: 0 0 ($baseline/4) 0;
padding: 0;
}
}
}
// actions
.contact-link {
font-weight: 600;
}
.actions {
@include box-shadow(inset 0 1px 1px 0px rgba(0,0,0,0.2));
border-top: 1px solid tint(rgb(0,0,0), 90%);
padding-top: ($baseline*0.75);
background: tint($yellow,70%);
font-size: 14px;
.title {
font-size: 14px;
}
.label, .value {
display: inline-block;
}
.label {
margin-right: ($baseline/4);
}
.value {
font-weight: bold;
}
.message-copy {
font-size: 13px;
}
.exam-button {
@include button(simple, $pink);
display: block;
margin: ($baseline/2) 0 0 0;
padding: ($baseline/2) $baseline;
font-size: 13px;
font-weight: bold;
&:hover {
text-decoration: none;
}
}
}
.registration-number {
.label {
text-transform: none;
letter-spacing: 0;
}
}
.registration-processed {
.message-copy {
margin: 0 0 ($baseline/2) 0;
}
}
}
> .details {
border-bottom: 1px solid rgba(216, 223, 230, 0.5);
margin: 0 0 $baseline 0;
padding: 0 $baseline $baseline $baseline;
font-family: $sans-serif;
font-size: 14px;
&:last-child {
border: none;
margin-bottom: 0;
padding-bottom: 0;
}
h4 {
margin: 0 0 ($baseline/2) 0;
font-family: $sans-serif;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #ccc;
}
.label, .value {
display: inline-block;
}
.label {
color: rgba(0,0,0,.65);
margin-right: ($baseline/2);
}
.value {
color: rgb(0,0,0);
font-weight: 600;
}
}
.details-course {
}
.details-registration {
ul {
margin: 0;
padding: 0;
list-style: none;
li {
margin: 0 0 ($baseline/4) 0;
}
}
}
}
// status messages
.message {
@include border-radius(3px);
display: none;
margin: $baseline 0;
padding: ($baseline/2) $baseline;
&.is-shown {
display: block;
}
.message-copy {
font-size: 14px;
}
// registration status
&.message-flash {
@include border-radius(3px);
position: relative;
margin: 0 0 ($baseline*2) 0;
border: 1px solid #ccc;
padding-top: ($baseline*0.75);
background: tint($yellow,70%);
font-size: 14px;
.message-title, .message-copy {
}
.message-title {
font-weight: bold;
font-size: 16px;
margin: 0 0 ($baseline/4) 0;
}
.message-copy {
font-size: 14px;
}
.contact-button {
@include button(simple, $blue);
}
.exam-button {
@include button(simple, $pink);
}
.button {
position: absolute;
top: ($baseline/4);
right: $baseline;
margin: ($baseline/2) 0 0 0;
padding: ($baseline/2) $baseline;
font-size: 13px;
font-weight: bold;
letter-spacing: 0;
&:hover {
text-decoration: none;
}
}
&.message-action {
.message-title, .message-copy {
width: 65%;
}
}
}
// submission error
&.submission-error {
@include box-sizing(border-box);
float: left;
width: flex-grid(8,8);
border: 1px solid tint($red,85%);
background: tint($red,95%);
font-size: 14px;
#submission-error-heading {
margin-bottom: ($baseline/2);
border-bottom: 1px solid tint($red, 85%);
padding-bottom: ($baseline/2);
font-weight: bold;
}
.field-name, .field-error {
display: inline-block;
}
.field-name {
margin-right: ($baseline/4);
}
.field-error {
color: tint($red, 55%);
}
p {
color: $red;
}
ul {
margin: 0 0 ($baseline/2) 0;
padding: 0;
list-style: none;
li {
margin-bottom: ($baseline/2);
padding: 0;
span {
color: $red;
}
a {
color: $red;
text-decoration: none;
&:hover, &:active {
text-decoration: underline;
}
}
}
}
}
// submission success
&.submission-saved {
border: 1px solid tint($blue,85%);
background: tint($blue,95%);
.message-copy {
color: $blue;
}
}
// specific - registration closed
&.registration-closed {
@include border-bottom-radius(0);
margin-top: 0;
border-bottom: 1px solid $light-gray;
padding: $baseline;
background: tint($light-gray,50%);
.message-title {
font-weight: bold;
}
.message-copy {
}
}
}
.is-shown {
display: block;
}
// hidden
.is-hidden {
display: none;
}
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ label {
textarea,
input[type="text"],
input[type="email"],
input[type="password"] {
input[type="password"],
input[type="tel"] {
background: rgb(250,250,250);
border: 1px solid rgb(200,200,200);
@include border-radius(3px);
......
......@@ -36,6 +36,9 @@ table.stat_table td {
a.selectedmode { background-color: yellow; }
textarea {
height: 200px;
}
</style>
<script language="JavaScript" type="text/javascript">
......@@ -58,8 +61,8 @@ function goto( mode)
%endif
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a>
]
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> |
<a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a> ]
</h2>
<div style="text-align:right"><span id="djangopid">${djangopid}</span>
......@@ -138,6 +141,11 @@ function goto( mode)
%endif
<H2>Student-specific grade inspection and adjustment</h2>
<p>edX email address or their username: </p>
<p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p>
<p>and, if you want to reset the number of attempts for a problem, the urlname of that problem</p>
<p> <input type="text" name="problem_to_reset"> <input type="submit" name="action" value="Reset student's attempts"> </p>
%endif
##-----------------------------------------------------------------------------
......@@ -168,7 +176,8 @@ function goto( mode)
<p>
<input type="submit" name="action" value="List course staff members">
<p>
<input type="text" name="staffuser"> <input type="submit" name="action" value="Remove course staff">
<input type="text" name="staffuser">
<input type="submit" name="action" value="Remove course staff">
<input type="submit" name="action" value="Add course staff">
<hr width="40%" style="align:left">
%endif
......@@ -250,7 +259,7 @@ function goto( mode)
%endif
<p>Add students: enter emails, separated by returns or commas;</p>
<p>Add students: enter emails, separated by new lines or commas;</p>
<textarea rows="6" cols="70" name="enroll_multiple"></textarea>
<input type="submit" name="action" value="Enroll multiple students">
......@@ -258,8 +267,29 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Manage Groups'):
%if instructor_access:
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List beta testers">
<p>
Enter usernames or emails for students who should be beta-testers, one per line, or separated by commas. They will get to
see course materials early, as configured via the <tt>days_early_for_beta</tt> option in the course policy.
</p>
<p>
<textarea cols="50" row="30" name="betausers"></textarea>
<input type="submit" name="action" value="Remove beta testers">
<input type="submit" name="action" value="Add beta testers">
</p>
<hr width="40%" style="align:left">
%endif
%endif
</form>
%if msg:
<p></p><p>${msg}</p>
%endif
##-----------------------------------------------------------------------------
##-----------------------------------------------------------------------------
......@@ -312,9 +342,6 @@ function goto( mode)
##-----------------------------------------------------------------------------
## always show msg
%if msg:
<p>${msg}</p>
%endif
##-----------------------------------------------------------------------------
%if modeflag.get('Admin'):
......
......@@ -198,87 +198,132 @@
course_target = reverse('about_course', args=[course.id])
%>
<a href="${course_target}">
<section class="cover" style="background-image: url('${course_image_url(course)}')">
<div class="shade"></div>
<div class="arrow">&#10095;</div>
</section>
<section class="info">
<hgroup>
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
<h3>${course.number} ${course.title}</h3>
</hgroup>
<section class="course-status course-status-completed">
<p>
<a href="${course_target}" class="cover">
<img src="${course_image_url(course)}" />
</a>
<section class="info">
<hgroup>
<p class="date-block">
% if course.has_ended():
Course Completed - <span>${course.end_date_text}</span>
Course Completed - ${course.end_date_text}
% elif course.has_started():
Course Started - <span>${course.start_date_text}</span>
Course Started - ${course.start_date_text}
% else: # hasn't started yet
Course Starts - <span>${course.start_date_text}</span>
Course Starts - ${course.start_date_text}
% endif
</p>
</section>
% if course.id in show_courseware_links_for:
<p class="enter-course">View Courseware</p>
% endif
</section>
</a>
</article>
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
<h3><a href="${course_target}">${course.number} ${course.title}</a></h3>
</hgroup>
<%
cert_status = cert_statuses.get(course.id)
%>
% if course.has_ended() and cert_status:
<%
if cert_status['status'] == 'generating':
status_css_class = 'course-status-certrendering'
elif cert_status['status'] == 'ready':
status_css_class = 'course-status-certavailable'
elif cert_status['status'] == 'notpassing':
status_css_class = 'course-status-certnotavailable'
else:
status_css_class = 'course-status-processing'
testcenter_exam_info = course.current_test_center_exam
registration = exam_registrations.get(course.id)
testcenter_register_target = reverse('begin_exam_registration', args=[course.id])
%>
<div class="message message-status ${status_css_class} is-shown">
% if cert_status['status'] == 'processing':
<p class="message-copy">Final course details are being wrapped up at
this time. Your final standing will be available shortly.</p>
% elif cert_status['status'] in ('generating', 'ready', 'notpassing'):
<p class="message-copy">Your final grade:
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% if cert_status['status'] == 'notpassing':
Grade required for a certificate: <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% endif
</p>
% endif
% if testcenter_exam_info is not None:
% if registration is None and testcenter_exam_info.is_registering():
<div class="message message-status is-shown exam-register">
<a href="${testcenter_register_target}" class="button exam-button" id="exam_register_button">Register for Pearson exam</a>
<p class="message-copy">Registration for the Pearson exam is now open and will close on <strong>${testcenter_exam_info.registration_end_date_text}</strong></p>
</div>
% endif
<!-- display a registration for a current exam, even if the registration period is over -->
% if registration is not None:
% if registration.is_accepted:
<div class="message message-status is-shown exam-schedule">
<a href="${registration.registration_signup_url}" class="button exam-button">Schedule Pearson exam</a>
<p class="exam-registration-number"><a href="${testcenter_register_target}" id="exam_register_link">Registration</a> number: <strong>${registration.client_candidate_id}</strong></p>
<p class="message-copy">Write this down! You’ll need it to schedule your exam.</p>
</div>
% endif
% if registration.is_rejected:
<div class="message message-status is-shown exam-schedule">
<p class="message-copy">Your
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
has been rejected. Please check the information you provided, and try to correct any demographic errors. Otherwise
contact edX for further help.</p>
<a href="mailto:exam-help@edx.org?subject=Pearson VUE Exam - ${get_course_about_section(course, 'university')} ${course.number}" class="button contact-button">Contact exam-help@edx.org</a>
</div>
% endif
% if not registration.is_accepted and not registration.is_rejected:
<div class="message message-status is-shown">
<p class="message-copy">Your
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.</p>
</div>
% endif
% endif
% endif
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
<ul class="actions">
% if cert_status['show_disabled_download_button']:
<li class="action"><span class="btn disabled" href="">
Your Certificate is Generating</span></li>
% elif cert_status['show_download_url']:
<li class="action">
<a class="btn" href="${cert_status['download_url']}"
title="This link will open/download a PDF document">
Download Your PDF Certificate</a></li>
<%
cert_status = cert_statuses.get(course.id)
%>
% if course.has_ended() and cert_status:
<%
if cert_status['status'] == 'generating':
status_css_class = 'course-status-certrendering'
elif cert_status['status'] == 'ready':
status_css_class = 'course-status-certavailable'
elif cert_status['status'] == 'notpassing':
status_css_class = 'course-status-certnotavailable'
else:
status_css_class = 'course-status-processing'
%>
<div class="message message-status ${status_css_class} is-shown">
% if cert_status['status'] == 'processing':
<p class="message-copy">Final course details are being wrapped up at
this time. Your final standing will be available shortly.</p>
% elif cert_status['status'] in ('generating', 'ready', 'notpassing'):
<p class="message-copy">Your final grade:
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% if cert_status['status'] == 'notpassing':
Grade required for a certificate: <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% endif
</p>
% endif
% if cert_status['show_survey_button']:
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
Complete our course feedback survey</a></li>
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
<ul class="actions">
% if cert_status['show_disabled_download_button']:
<li class="action"><span class="disabled">
Your Certificate is Generating</span></li>
% elif cert_status['show_download_url']:
<li class="action">
<a class="btn" href="${cert_status['download_url']}"
title="This link will open/download a PDF document">
Download Your PDF Certificate</a></li>
% endif
% if cert_status['show_survey_button']:
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
Complete our course feedback survey</a></li>
% endif
</ul>
% endif
</ul>
</div>
% endif
</div>
% endif
% if course.id in show_courseware_links_for:
% if course.has_ended():
<a href="${course_target}" class="enter-course archived">View Archived Course</a>
% else:
<a href="${course_target}" class="enter-course">View Course</a>
% endif
% endif
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
</section>
</article>
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
% endfor
% else:
......
......@@ -164,7 +164,7 @@
<section id="video-modal" class="modal home-page-video-modal video-modal">
<div class="inner-wrapper">
<iframe width="640" height="360" src="http://www.youtube.com/embed/IlNU60ZKj3I?showinfo=0" frameborder="0" allowfullscreen></iframe>
<iframe width="640" height="360" src="http://www.youtube.com/embed/XNaiOGxWeto?showinfo=0" frameborder="0" allowfullscreen></iframe>
</div>
</section>
......
......@@ -5,10 +5,6 @@
<%block name="title"><title>FAQ</title></%block>
<%block name="js_extra">
<script src="${static.url('js/faq.js')}"></script>
</%block>
<section class="container about">
<nav>
<a href="${reverse('about_edx')}">Vision</a>
......@@ -17,298 +13,80 @@
<a href="${reverse('contact')}">Contact</a>
</nav>
<section class="faq">
<section class="responses">
<section id="edx_basics_faq" class="category">
<h2>edX Basics</h2>
<article class="response">
<h3 class="question">How do I sign up to take a class?</h3>
<div class ="answer" id="edx_basics_faq_answer_0">
<p>Simply create an edX account (it's free) and then register for the course of your choice (also free). Follow the prompts on the edX website.</p>
</div>
</article>
<article class="response">
<h3 class="question">What does it cost to take a class? Is this really free?</h3>
<div class ="answer" id="edx_basics_faq_answer_1">
<p>EdX courses are free for everyone. All you need is an Internet connection.</p>
</div>
</article>
<article class="response">
<h3 class="question">What happens after I sign up for a course?</h3>
<div class ="answer" id="edx_basics_faq_answer_2">
<p>You will receive an activation email. Follow the prompts in that email to activate your account. You will need to log in each time you access your course(s). Once the course begins, it’s time to hit the virtual books. You can access the lectures, homework, tutorials, etc., for each week, one week at a time.</p>
</div>
</article>
<article class="response">
<h3 class="question">Who can take an edX course?</h3>
<div class ="answer" id="edx_basics_faq_answer_3">
<p>You, your mom, your little brother, your grandfather -- anyone with Internet access can take an edX course. Free.</p>
</div>
</article>
<article class="response">
<h3 class="question">Are the courses only offered in English?</h3>
<div class ="answer" id="edx_basics_faq_answer_4">
<p>Some edX courses include a translation of the lecture in the text bar to the right of the video. Some have the specific option of requesting a course in other languages. Please check your course to determine foreign language options.</p>
</div>
</article>
<article class="response">
<h3 class="question">When will there be more courses on other subjects?</h3>
<div class ="answer" id="edx_basics_faq_answer_5">
<p>We are continually reviewing and creating courses to add to the edX platform. Please check the website for future course announcements. You can also "friend" edX on Facebook – you’ll receive updates and announcements.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I help edX?</h3>
<div class ="answer" id="edx_basics_faq_answer_6">
<p>You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.</p>
</div>
</article>
<section id="the-organization" class="category">
<h2>Organization</h2>
<article class="response">
<h3 class="question">When does my course start and/or finish?</h3>
<div class ="answer" id="edx_basics_faq_answer_7">
<p>You can find the start and stop dates for each course on each course description page.</p>
</div>
<h3>What is edX?</h3>
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning both on-campus and online throughout the world.</p>
</article>
<article class="response">
<h3 class="question">Is there a walk-through of a sample course session?</h3>
<div class ="answer" id="edx_basics_faq_answer_8">
<p> There are video introductions for every course that will give you a good sense of how the course works and what to expect.</p>
</div>
</article>
<article class="response">
<h3 class="question">I don't have the prerequisites for a course that I am interested in. Can I still take the course?</h3>
<div class ="answer" id="edx_basics_faq_answer_9">
<p>We do not check students for prerequisites, so you are allowed to attempt the course. However, if you do not know prerequisite subjects before taking a class, you will have to learn the prerequisite material on your own over the semester, which can be an extremely difficult task.</p>
</div>
</article>
<article class="response">
<h3 class="question">What happens if I have to quit a course, are there any penalties, will I be able to take another course in the future?</h3>
<div class ="answer" id="edx_basics_faq_answer_10">
<p>You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.</p>
</div>
</article>
</section>
<section id="classes_faq" class="category">
<h2>The Classes</h2>
<article class="response">
<h3 class="question">How much work will I have to do to pass my course?</h3>
<div class ="answer" id="classes_faq_answer_0">
<p>The projected hours of study required for each course are described on the specific course description page.</p>
</div>
</article>
<article class="response">
<h3 class="question">What should I do before I take a course (prerequisites)?</h3>
<div class ="answer" id="classes_faq_answer_1">
<p>Each course is different – some have prerequisites, and some don’t. Take a look at your specific course’s recommended prerequisites. If you do not have a particular prerequisite, you may still take the course.</p>
</div>
</article>
<article class="response">
<h3 class="question">What books should I read? (I am interested in reading materials before the class starts).</h3>
<div class ="answer" id="classes_faq_answer_2">
<p>Take a look at the specific course prerequisites. All required academic materials will be provided during the course, within the browser. Some of the course descriptions may list additional resources. For supplemental reading material before or during the course, you can post a question on the course’s Discussion Forum to ask your online coursemates for suggestions.</p>
</div>
</article>
<article class="response">
<h3 class="question"> Can I download the book for my course?</h3>
<div class ="answer" id="classes_faq_answer_3">
<p>EdX book content may only be viewed within the browser, and downloading it violates copyright laws. If you need or want a hard copy of the book, we recommend that you purchase a copy.</p>
</div>
</article>
<article class="response">
<h3 class="question">Can I take more than one course at a time?</h3>
<div class ="answer" id="classes_faq_answer_4">
<p>You may take multiple edX courses, however we recommend checking the requirements on each course description page to determine your available study hours and the demands of the intended courses.</p>
</div>
</article>
<article class="response">
<h3 class="question">How do I log in to take an edX class?</h3>
<div class ="answer" id="classes_faq_answer_5">
<p>Once you sign up for a course and activate your account, click on the "Log In" button on the edx.org home page. You will need to type in your email address and edX password each time you log in.</p>
</div>
</article>
<article class="response">
<h3 class="question">What time is the class?</h3>
<div class ="answer" id="classes_faq_answer_6">
<p>EdX classes take place at your convenience. Prefer to sleep in and study late? No worries. Videos and problem sets are available 24 hours a day, which means you can watch video and complete work whenever you have spare time. You simply log in to your course via the Internet and work through the course material, one week at a time.</p>
</div>
</article>
<article class="response">
<h3 class="question">If I miss a week, how does this affect my grade?</h3>
<div class ="answer" id="classes_faq_answer_7">
<p>It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I meet/find other students?</h3>
<div class ="answer" id="classes_faq_answer_8">
<p>All edX courses have Discussion Forums where you can chat with and help each other within the framework of the Honor Code.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I talk to professors, fellows and teaching assistants?</h3>
<div class ="answer" id="classes_faq_answer_9">
<p>The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.</p>
</div>
<h3>Will edX be adding additional X Universities?</h3>
<p>More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities". Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.</p>
<p>edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more "X Universities."</p>
</article>
</section>
<section id="getting_help_faq" class="category">
<h2>Getting Help</h2>
<section id="students" class="category">
<h2>Students</h2>
<article class="response">
<h3 class="question">Can I re-take a course?</h3>
<div class ="answer" id="getting_help_faq_answer_0">
<p>Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.</p>
</div>
<h3>Who can take edX courses? Will there be an admissions process?</h3>
<p>EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.</p>
</article>
<article class="response">
<h3 class="question">Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?</h3>
<div class ="answer" id="getting_help_faq_answer_1">
<p>Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.</p>
</div>
<h3>Will certificates be awarded?</h3>
<p>Yes. Online learners who demonstrate mastery of subjects can earn a certificate of mastery. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, <em>MITx</em> or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.</p>
</article>
<article class="response">
<h3 class="question">Is there an exam at the end?</h3>
<div class ="answer" id="getting_help_faq_answer_2">
<p>Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.</p>
</div>
<h3>What will the scope of the online courses be? How many? Which faculty?</h3>
<p>Our goal is to offer a wide variety of courses across disciplines. There are currently <a href="/courses">nine courses</a> offered for Fall 2012.</p>
</article>
<article class="response">
<h3 class="question">Will the same courses be offered again in the future?</h3>
<div class ="answer" id="getting_help_faq_answer_3">
<p>Existing edX courses will be re-offered, and more courses added.</p>
</div>
<h3>Who is the learner? Domestic or international? Age range?</h3>
<p>Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don't have a target group of potential learners, as the goal is to make these courses available to anyone in the world - from any demographic - who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.</p>
</article>
<article class="response">
<h3 class="question">Will I get a certificate for taking an edX course?</h3>
<div class ="answer" id="getting_help_faq_answer_4">
<p>Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.</p>
</div>
<h3>Will participating universities' standards apply to all courses offered on the edX platform?</h3>
<p>Yes: the reach changes exponentially, but the rigor remains the same.</p>
</article>
<article class="response">
<h3 class="question">How are edX certificates delivered?</h3>
<div class ="answer" id="getting_help_faq_answer_5">
<p>EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.</p>
</div>
<h3>How do you intend to test whether this approach is improving learning?</h3>
<p>Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.</p>
</article>
<article class="response">
<h3 class="question">What is the difference between a proctored certificate and an honor code certificate?</h3>
<div class ="answer" id="getting_help_faq_answer_6">
<p>A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed <link> the edX honor code </link>.</p>
</div>
<h3>How may I apply to study with edX?</h3>
<p>Simply complete the online <a href="#signup-modal" rel="leanModal">signup form</a>. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.</p>
</article>
<article class="response">
<h3 class="question">Yes. The requirements for both certificates can be independently satisfied.</h3>
<div class ="answer" id="getting_help_faq_answer_7">
<p>It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will my grade be shown on my certificate?</h3>
<div class ="answer" id="getting_help_faq_answer_8">
<p>No. Grades are not displayed on either honor code or proctored certificates.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I talk to professors, fellows and teaching assistants?</h3>
<div class ="answer" id="getting_help_faq_answer_9">
<p>The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.</p>
</div>
</article>
<article class="response">
<h3 class="question">The only certificates distributed with grades by edX were for the initial prototype course.</h3>
<div class ="answer" id="getting_help_faq_answer_10">
<p>You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will my university accept my edX coursework for credit?</h3>
<div class ="answer" id="getting_help_faq_answer_11">
<p>Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.</p>
</div>
</article>
<article class="response">
<h3 class="question">I lost my edX certificate – can you resend it to me?</h3>
<div class ="answer" id="getting_help_faq_answer_12">
<p>Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.</p>
</div>
</article>
</section>
<section id="open_source_faq" class="category">
<h2>edX & Open Source</h2>
<article class="response">
<h3 class="question">What’s open source?</h3>
<div class ="answer" id="open_source_faq_answer_0">
<p>Open source is a philosophy that generally refers to making software freely available for use or modification as users see fit. In exchange for use of the software, users generally add their contributions to the software, making it a public collaboration. The edX platform will be made available as open source code in order to allow world talent to improve and share it on an ongoing basis.</p>
</div>
</article>
<article class="response">
<h3 class="question">When/how can I get the open-source platform technology?</h3>
<div class ="answer" id="open_source_faq_answer_1">
<p>We are still building the edX technology platform and will be making announcements in the future about its availability.</p>
</div>
<h3>How may another university participate in edX? </h3>
<p>If you are from a university interested in discussing edX, please email <a href="mailto:university@edx.org">university@edx.org</a></p>
</article>
</section>
<section id="other_help_faq" class="category">
<h2>Other Help Questions - Account Questions</h2>
<article class="response">
<h3 class="question">My username is taken.</h3>
<div class ="answer" id="other_help_faq_answer_0">
<p>Now’s your chance to be creative: please try a different, more unique username – for example, try adding a random number to the end.</p>
</div>
</article>
<article class="response">
<h3 class="question">Why does my password show on my course login page?</h3>
<div class ="answer" id="other_help_faq_answer_1">
<p>Oops! This may be because of the way you created your account. For example, you may have mistakenly typed your password into the login box.</p>
</div>
</article>
<article class="response">
<h3 class="question">I am having login problems (password/email unrecognized).</h3>
<div class ="answer" id="other_help_faq_answer_2">
<p>Please check your browser’s settings to make sure that you have the current version of Firefox or Chrome, and then try logging in again. If you find access impossible, you may simply create a new account using an alternate email address – the old, unused account will disappear later.</p>
</div>
</article>
<article class="response">
<h3 class="question">I did not receive an activation email.</h3>
<div class ="answer" id="other_help_faq_answer_3">
<p>If you did not receive an activation email it may be because:</p>
<ul>
<li>There was a typo in your email address.</li>
<li>Your spam filter may have caught the activation email. Please check your spam folder. </li>
<li>You may be using an older browser. We recommend downloading the current version of Firefox or Chrome. </li>
<li>JavaScript is disabled in your browser. Please check your browser settings and confirm that JavaScript is enabled. </li>
</ul>
<p>If you continue to have problems activating your account, we recommend that you try creating a new account. There is no need to do anything about the old account. If it is not activated through the link in the email, it will disappear later.</p>
</div>
</article>
<section id="technology-platform" class="category">
<h2>Technology Platform</h2>
<article class="response">
<h3 class="question">Can I delete my account?</h3>
<div class ="answer" id="other_help_faq_answer_4">
<p>There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.</p>
</div>
<h3>What technology will edX use?</h3>
<p>The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.</p>
<p>The first version of the technology was used in the first <em>MITx</em> course, 6.002x Circuits and Electronics, which launched in Spring, 2012.</p>
</article>
<article class="response">
<h3 class="question">I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.</h3>
<div class ="answer" id="other_help_faq_answer_5">
<p>Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.</p>
</div>
<h3>How is this different from what other universities are doing online?</h3>
<p>EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.</p>
</article>
</section>
</section>
<nav class="categories">
<a href="#edx_basics_faq">edX Basics</a>
<a href="#classes_faq">The Classes</a>
<a href="#getting_help_faq">Getting Help</a>
<a href="#open_source_faq">edX & Open source</a>
<a href="#other_help_faq">Other Help Questions - Account Questions</a>
<a href="#organization">Organization</a>
<a href="#students">Students</a>
<a href="#technology-platform">Technology Platform</a>
</nav>
</section>
</section>
......
......@@ -5,40 +5,304 @@
<%block name="title"><title>edX Help</title></%block>
<%block name="js_extra">
<script src="${static.url('js/help.js')}"></script>
</%block>
<section class="static-container help">
<h1>Help</h1>
<hr class="horizontal-divider">
<section class="questions">
<article class="response">
<h2>I tried to sign up, but it says the username is already taken.</h2>
<p>If you have previously signed up for an MITx account, you already have an edX account and can log in with your existing username and password. If you don’t have an MITx account and received this error, it's possible that someone else has already signed up with that username. Please try a different, more unique username &ndash; for example, try adding a random number to the end.</p>
</article>
<article class="response">
<h2>How will I know that the course I have signed up for has started?</h2>
<p>The start date for each course is listed on the right-hand side of the Course About page.</p>
</article>
<article class="response">
<h2>I just signed up into edX. I have not received any form of acknowledgement that I have enrolled.</h2>
<p>You should receive a single activation e-mail. If you did not, it may be because:</p>
<ul>
<li>There was a typo in your e-mail address.</li>
<li>The activation e-mail was caught by your spam filter. Please check your spam folder.</li>
<li>You may be using an older browser. We recommend downloading the current version of Firefox or Chrome.</li>
<li>JavaScript is disabled in your browser. Please confirm it is enabled.</li>
<li>If you run into issues, try recreating your account. There is no need to do anything about the old account, if any. If it is not activated through the link in the e-mail, it will disappear later.</li>
</ul>
</article>
</section>
<section class="responses">
<section id="edx_basics_faq" class="category">
<h2>edX Basics</h2>
<article class="response">
<h3 class="question">How do I sign up to take a class?</h3>
<div class ="answer" id="edx_basics_faq_answer_0">
<p>Simply create an edX account (it's free) and then register for the course of your choice (also free). Follow the prompts on the edX website.</p>
</div>
</article>
<article class="response">
<h3 class="question">What does it cost to take a class? Is this really free?</h3>
<div class ="answer" id="edx_basics_faq_answer_1">
<p>EdX courses are free for everyone. All you need is an Internet connection.</p>
</div>
</article>
<article class="response">
<h3 class="question">What happens after I sign up for a course?</h3>
<div class ="answer" id="edx_basics_faq_answer_2">
<p>You will receive an activation email. Follow the prompts in that email to activate your account. You will need to log in each time you access your course(s). Once the course begins, it’s time to hit the virtual books. You can access the lectures, homework, tutorials, etc., for each week, one week at a time.</p>
</div>
</article>
<article class="response">
<h3 class="question">Who can take an edX course?</h3>
<div class ="answer" id="edx_basics_faq_answer_3">
<p>You, your mom, your little brother, your grandfather -- anyone with Internet access can take an edX course. Free.</p>
</div>
</article>
<article class="response">
<h3 class="question">Are the courses only offered in English?</h3>
<div class ="answer" id="edx_basics_faq_answer_4">
<p>Some edX courses include a translation of the lecture in the text bar to the right of the video. Some have the specific option of requesting a course in other languages. Please check your course to determine foreign language options.</p>
</div>
</article>
<article class="response">
<h3 class="question">When will there be more courses on other subjects?</h3>
<div class ="answer" id="edx_basics_faq_answer_5">
<p>We are continually reviewing and creating courses to add to the edX platform. Please check the website for future course announcements. You can also "friend" edX on Facebook – you’ll receive updates and announcements.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I help edX?</h3>
<div class ="answer" id="edx_basics_faq_answer_6">
<p>You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.</p>
</div>
</article>
<article class="response">
<h3 class="question">When does my course start and/or finish?</h3>
<div class ="answer" id="edx_basics_faq_answer_7">
<p>You can find the start and stop dates for each course on each course description page.</p>
</div>
</article>
<article class="response">
<h3 class="question">Is there a walk-through of a sample course session?</h3>
<div class ="answer" id="edx_basics_faq_answer_8">
<p> There are video introductions for every course that will give you a good sense of how the course works and what to expect.</p>
</div>
</article>
<article class="response">
<h3 class="question">I don't have the prerequisites for a course that I am interested in. Can I still take the course?</h3>
<div class ="answer" id="edx_basics_faq_answer_9">
<p>We do not check students for prerequisites, so you are allowed to attempt the course. However, if you do not know prerequisite subjects before taking a class, you will have to learn the prerequisite material on your own over the semester, which can be an extremely difficult task.</p>
</div>
</article>
<article class="response">
<h3 class="question">What happens if I have to quit a course, are there any penalties, will I be able to take another course in the future?</h3>
<div class ="answer" id="edx_basics_faq_answer_10">
<p>You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.</p>
</div>
</article>
</section>
<section id="classes_faq" class="category">
<h2>The Classes</h2>
<article class="response">
<h3 class="question">How much work will I have to do to pass my course?</h3>
<div class ="answer" id="classes_faq_answer_0">
<p>The projected hours of study required for each course are described on the specific course description page.</p>
</div>
</article>
<article class="response">
<h3 class="question">What should I do before I take a course (prerequisites)?</h3>
<div class ="answer" id="classes_faq_answer_1">
<p>Each course is different – some have prerequisites, and some don’t. Take a look at your specific course’s recommended prerequisites. If you do not have a particular prerequisite, you may still take the course.</p>
</div>
</article>
<article class="response">
<h3 class="question">What books should I read? (I am interested in reading materials before the class starts).</h3>
<div class ="answer" id="classes_faq_answer_2">
<p>Take a look at the specific course prerequisites. All required academic materials will be provided during the course, within the browser. Some of the course descriptions may list additional resources. For supplemental reading material before or during the course, you can post a question on the course’s Discussion Forum to ask your online coursemates for suggestions.</p>
</div>
</article>
<article class="response">
<h3 class="question"> Can I download the book for my course?</h3>
<div class ="answer" id="classes_faq_answer_3">
<p>EdX book content may only be viewed within the browser, and downloading it violates copyright laws. If you need or want a hard copy of the book, we recommend that you purchase a copy.</p>
</div>
</article>
<article class="response">
<h3 class="question">Can I take more than one course at a time?</h3>
<div class ="answer" id="classes_faq_answer_4">
<p>You may take multiple edX courses, however we recommend checking the requirements on each course description page to determine your available study hours and the demands of the intended courses.</p>
</div>
</article>
<article class="response">
<h3 class="question">How do I log in to take an edX class?</h3>
<div class ="answer" id="classes_faq_answer_5">
<p>Once you sign up for a course and activate your account, click on the "Log In" button on the edx.org home page. You will need to type in your email address and edX password each time you log in.</p>
</div>
</article>
<article class="response">
<h3 class="question">What time is the class?</h3>
<div class ="answer" id="classes_faq_answer_6">
<p>EdX classes take place at your convenience. Prefer to sleep in and study late? No worries. Videos and problem sets are available 24 hours a day, which means you can watch video and complete work whenever you have spare time. You simply log in to your course via the Internet and work through the course material, one week at a time.</p>
</div>
</article>
<article class="response">
<h3 class="question">If I miss a week, how does this affect my grade?</h3>
<div class ="answer" id="classes_faq_answer_7">
<p>It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I meet/find other students?</h3>
<div class ="answer" id="classes_faq_answer_8">
<p>All edX courses have Discussion Forums where you can chat with and help each other within the framework of the Honor Code.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I talk to professors, fellows and teaching assistants?</h3>
<div class ="answer" id="classes_faq_answer_9">
<p>The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.</p>
</div>
</article>
</section>
<section id="getting_help_faq" class="category">
<h2>Getting Help</h2>
<article class="response">
<h3 class="question">Can I re-take a course?</h3>
<div class ="answer" id="getting_help_faq_answer_0">
<p>Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.</p>
</div>
</article>
<article class="response">
<h3 class="question">Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?</h3>
<div class ="answer" id="getting_help_faq_answer_1">
<p>Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.</p>
</div>
</article>
<article class="response">
<h3 class="question">Is there an exam at the end?</h3>
<div class ="answer" id="getting_help_faq_answer_2">
<p>Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will the same courses be offered again in the future?</h3>
<div class ="answer" id="getting_help_faq_answer_3">
<p>Existing edX courses will be re-offered, and more courses added.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will I get a certificate for taking an edX course?</h3>
<div class ="answer" id="getting_help_faq_answer_4">
<p>Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.</p>
</div>
</article>
<article class="response">
<h3 class="question">How are edX certificates delivered?</h3>
<div class ="answer" id="getting_help_faq_answer_5">
<p>EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.</p>
</div>
</article>
<article class="response">
<h3 class="question">What is the difference between a proctored certificate and an honor code certificate?</h3>
<div class ="answer" id="getting_help_faq_answer_6">
<p>A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed <link> the edX honor code </link>.</p>
</div>
</article>
<article class="response">
<h3 class="question">Yes. The requirements for both certificates can be independently satisfied.</h3>
<div class ="answer" id="getting_help_faq_answer_7">
<p>It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will my grade be shown on my certificate?</h3>
<div class ="answer" id="getting_help_faq_answer_8">
<p>No. Grades are not displayed on either honor code or proctored certificates.</p>
</div>
</article>
<article class="response">
<h3 class="question">How can I talk to professors, fellows and teaching assistants?</h3>
<div class ="answer" id="getting_help_faq_answer_9">
<p>The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.</p>
</div>
</article>
<article class="response">
<h3 class="question">The only certificates distributed with grades by edX were for the initial prototype course.</h3>
<div class ="answer" id="getting_help_faq_answer_10">
<p>You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.</p>
</div>
</article>
<article class="response">
<h3 class="question">Will my university accept my edX coursework for credit?</h3>
<div class ="answer" id="getting_help_faq_answer_11">
<p>Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.</p>
</div>
</article>
<article class="response">
<h3 class="question">I lost my edX certificate – can you resend it to me?</h3>
<div class ="answer" id="getting_help_faq_answer_12">
<p>Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.</p>
</div>
</article>
</section>
<section id="open_source_faq" class="category">
<h2>edX & Open Source</h2>
<article class="response">
<h3 class="question">What’s open source?</h3>
<div class ="answer" id="open_source_faq_answer_0">
<p>Open source is a philosophy that generally refers to making software freely available for use or modification as users see fit. In exchange for use of the software, users generally add their contributions to the software, making it a public collaboration. The edX platform will be made available as open source code in order to allow world talent to improve and share it on an ongoing basis.</p>
</div>
</article>
<article class="response">
<h3 class="question">When/how can I get the open-source platform technology?</h3>
<div class ="answer" id="open_source_faq_answer_1">
<p>We are still building the edX technology platform and will be making announcements in the future about its availability.</p>
</div>
</article>
</section>
<section id="other_help_faq" class="category">
<h2>Other Help Questions - Account Questions</h2>
<article class="response">
<h3 class="question">My username is taken.</h3>
<div class ="answer" id="other_help_faq_answer_0">
<p>Now’s your chance to be creative: please try a different, more unique username – for example, try adding a random number to the end.</p>
</div>
</article>
<article class="response">
<h3 class="question">Why does my password show on my course login page?</h3>
<div class ="answer" id="other_help_faq_answer_1">
<p>Oops! This may be because of the way you created your account. For example, you may have mistakenly typed your password into the login box.</p>
</div>
</article>
<article class="response">
<h3 class="question">I am having login problems (password/email unrecognized).</h3>
<div class ="answer" id="other_help_faq_answer_2">
<p>Please check your browser’s settings to make sure that you have the current version of Firefox or Chrome, and then try logging in again. If you find access impossible, you may simply create a new account using an alternate email address – the old, unused account will disappear later.</p>
</div>
</article>
<article class="response">
<h3 class="question">I did not receive an activation email.</h3>
<div class ="answer" id="other_help_faq_answer_3">
<p>If you did not receive an activation email it may be because:</p>
<ul>
<li>There was a typo in your email address.</li>
<li>Your spam filter may have caught the activation email. Please check your spam folder. </li>
<li>You may be using an older browser. We recommend downloading the current version of Firefox or Chrome. </li>
<li>JavaScript is disabled in your browser. Please check your browser settings and confirm that JavaScript is enabled. </li>
</ul>
<p>If you continue to have problems activating your account, we recommend that you try creating a new account. There is no need to do anything about the old account. If it is not activated through the link in the email, it will disappear later.</p>
</div>
</article>
<article class="response">
<h3 class="question">Can I delete my account?</h3>
<div class ="answer" id="other_help_faq_answer_4">
<p>There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.</p>
</div>
</article>
<article class="response">
<h3 class="question">I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.</h3>
<div class ="answer" id="other_help_faq_answer_5">
<p>Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.</p>
</div>
</article>
</section>
</section>
<section class="emails">
<h2>Help email</h2>
<nav class="categories">
<a href="#edx_basics_faq">edX Basics</a>
<a href="#classes_faq">The Classes</a>
<a href="#getting_help_faq">Getting Help</a>
<a href="#open_source_faq">edX & Open source</a>
<a href="#other_help_faq">Other Help Questions - Account Questions</a>
</nav>
<ul>
<li><p>System-related questions: <a href="mailto:technical@edx.org">technical@edx.org</a></p></li>
<li><p>Content-related questions: <a href="mailto:content@edx.org">content@edx.org</a></p></li>
<li><p>Bug reports: <a href="mailto:bugs@edx.org">bugs@edx.org</a></p></li>
<li><p>Suggestions: <a href="mailto:suggestions@edx.org">suggestions@edx.org</a></p></li>
</ul>
</section>
</section>
<%!
from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access
from certificates.models import CertificateStatuses
%>
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%block name="title"><title>Pearson VUE Test Center Proctoring - Registration</title></%block>
<%block name="js_extra">
<script type="text/javascript">
(function() {
// if form is disabled or registration is closed
$('#testcenter_register_form.disabled').find('input, textarea, button').attr('disabled','disabled');
// toggling accommodations elements
$('.form-fields-secondary-visibility').click(function(e){
e.preventDefault();
$(this).addClass("is-hidden");
$('.form-fields-secondary').addClass("is-shown");
});
$(document).on('ajax:success', '#testcenter_register_form', function(data, json, xhr) {
if(json.success) {
// when a form is successfully filled out, return back to the dashboard.
location.href="${reverse('dashboard')}";
} else {
// This is performed by the following code that parses the errors returned as json by the
// registration form validation.
var field_errors = json.field_errors;
var non_field_errors = json.non_field_errors;
var fieldname;
var field_errorlist;
var field_error;
var error_msg;
var num_errors = 0;
var error_html = '';
// first get rid of any errors that are already present:
$(".field.error", ".form-actions").removeClass('error');
$("#submission-error-list").html(error_html);
// start to display new errors:
$(".form-actions").addClass("error");
$(".submission-error").addClass("is-shown");
for (fieldname in field_errors) {
// to inform a user of what field the error occurred on, add a class of .error to the .field element.
// for convenience, use the "id" attribute to identify the one matching the errant field's name.
var field_id = "field-" + fieldname;
var field_label = $("[id='"+field_id+"'] label").text();
$("[id='"+field_id+"']").addClass('error');
field_errorlist = field_errors[fieldname];
for (i=0; i < field_errorlist.length; i+= 1) {
field_error = field_errorlist[i];
error_msg = '<span class="field-name">' + field_label + ':</span>' + '<span class="field-error">' + field_error + '</span>';
error_html = error_html + '<li>' + '<a href="#field-' + fieldname + '">' + error_msg + '</a></li>';
num_errors += 1;
}
}
for (i=0; i < non_field_errors.length; i+= 1) {
error_msg = non_field_errors[i];
error_html = error_html + '<li class="to-be-determined">' + error_msg + '</li>';
num_errors += 1;
}
if (num_errors == 1) {
error_msg = 'was an error';
} else {
error_msg = 'were ' + num_errors + ' errors';
}
$('#submission-error-heading').text("We're sorry, but there " + error_msg + " in the information you provided below:")
$("#submission-error-list").html(error_html);
}
});
$("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() {
$("label").removeClass("is-focused");
});
})(this)
</script>
</%block>
<section class="testcenter-register container">
<section class="introduction">
<header>
<hgroup>
<h2><a href="${reverse('dashboard')}">${get_course_about_section(course, 'university')} ${course.number} ${course.title}</a></h2>
% if registration:
<h1>Your Pearson VUE Proctored Exam Registration</h1>
% else:
<h1>Register for a Pearson VUE Proctored Exam</h1>
% endif
</hgroup>
</header>
</section>
<%
exam_help_href = "mailto:exam-help@edx.org?subject=Pearson VUE Exam - " + get_course_about_section(course, 'university') + " - " + course.number
%>
% if registration:
<!-- select one of four registration status banners to display -->
% if registration.is_accepted:
<section class="status message message-flash registration-processed message-action is-shown">
<h3 class="message-title">Your registration for the Pearson exam has been processed</h3>
<p class="message-copy">Your registration number is <strong>${registration.client_candidate_id}</strong>. (Write this down! You’ll need it to schedule your exam.)</p>
<a href="${registration.registration_signup_url}" class="button exam-button">Schedule Pearson exam</a>
</section>
% endif
% if registration.demographics_is_rejected:
<section class="status message message-flash demographics-rejected message-action is-shown">
<h3 class="message-title">Your demographic information contained an error and was rejected</h3>
<p class="message-copy">Please check the information you provided, and correct the errors noted below.
</section>
% endif
% if registration.registration_is_rejected:
<section class="status message message-flash registration-rejected message-action is-shown">
<h3 class="message-title">Your registration for the Pearson exam has been rejected</h3>
<p class="message-copy">Please see your registration status details for more information.</p>
</section>
% endif
% if registration.is_pending:
<section class="status message message-flash registration-pending is-shown">
<h3 class="message-title">Your registration for the Pearson exam is pending</h3>
<p class="message-copy">Once your information is processed, it will be forwarded to Pearson and you will be able to schedule an exam.</p>
</section>
% endif
% endif
<section class="content">
<header>
<h3 class="is-hidden">Registration Form</h3>
</header>
% if exam_info.is_registering():
<form id="testcenter_register_form" method="post" data-remote="true" action="/create_exam_registration">
% else:
<form id="testcenter_register_form" method="post" data-remote="true" action="/create_exam_registration" class="disabled">
<!-- registration closed, so tell them they can't do anything: -->
<div class="status message registration-closed is-shown">
<h3 class="message-title">Registration for this Pearson exam is closed</h3>
<p class="message-copy">Your previous information is available below, however you may not edit any of the information.
</div>
% endif
% if registration:
<p class="instructions">
Please use the following form if you need to update your demographic information used in your Pearson VUE Proctored Exam. Required fields are noted by <strong class="indicator">bold text and an asterisk (*)</strong>.
</p>
% else:
<p class="instructions">
Please provide the following demographic information to register for a Pearson VUE Proctored Exam. Required fields are noted by <strong class="indicator">bold text and an asterisk (*)</strong>.
</p>
% endif
<!-- include these as pass-throughs -->
<input id="id_email" type="hidden" name="email" value="${user.email}" />
<input id="id_username" type="hidden" name="username" value="${user.username}" />
<input id="id_course_id" type="hidden" name="course_id" value="${course.id}" />
<input id="id_exam_series_code" type="hidden" name="exam_series_code" value="${exam_info.exam_series_code}" />
<div class="form-fields-primary">
<fieldset class="group group-form group-form-personalinformation">
<legend class="is-hidden">Personal Information</legend>
<ol class="list-input">
<li class="field" id="field-salutation">
<label for="salutation">Salutation</label>
<input class="short" id="salutation" type="text" name="salutation" value="${testcenteruser.salutation}" placeholder="e.g. Mr., Ms., Mrs., Dr." />
</li>
<li class="field required" id="field-first_name">
<label for="first_name">First Name </label>
<input id="first_name" type="text" name="first_name" value="${testcenteruser.first_name}" placeholder="e.g. Albert" />
</li>
<li class="field" id="field-middle_name">
<label for="middle_name">Middle Name</label>
<input id="middle_name" type="text" name="middle_name" value="${testcenteruser.middle_name}" placeholder="" />
</li>
<li class="field required" id="field-last_name">
<label for="last_name">Last Name</label>
<input id="last_name" type="text" name="last_name" value="${testcenteruser.last_name}" placeholder="e.g. Einstein" />
</li>
<li class="field" id="field-suffix">
<label for="suffix">Suffix</label>
<input class="short" id="suffix" type="text" name="suffix" value="${testcenteruser.suffix}" placeholder="e.g. Jr., Sr. " />
</li>
</ol>
</fieldset>
<fieldset class="group group-form group-form-mailingaddress">
<legend class="is-hidden">Mailing Address</legend>
<ol class="list-input">
<li class="field required" id="field-address_1">
<label class="long" for="address_1">Address Line #1</label>
<input id="address_1" type="text" name="address_1" value="${testcenteruser.address_1}" placeholder="e.g. 112 Mercer Street" />
</li>
<li class="field-group addresses">
<div class="field" id="field-address_2">
<label for="address_2">Address Line #2</label>
<input id="address_2" class="long" type="text" name="address_2" value="${testcenteruser.address_2}" placeholder="e.g. Apartment 123" />
</div>
<div class="field" id="field-address_3">
<label for="address_3">Address Line #3</label>
<input id="address_3" class="long" type="text" name="address_3" value="${testcenteruser.address_3}" placeholder="e.g. Attention: Albert Einstein" />
</div>
</li>
<li class="field required postal-1" id="field-city">
<label for="city">City</label>
<input id="city" class="long" type="text" name="city" value="${testcenteruser.city}" placeholder="e.g. Newark" />
</li>
<li class="field-group postal-2">
<div class="field" id="field-state">
<label for="state">State/Province</label>
<input id="state" class="short" type="text" name="state" value="${testcenteruser.state}" placeholder="e.g. NJ" />
</div>
<div class="field" id="field-postal_code">
<label for="postal_code">Postal Code</label>
<input id="postal_code" class="short" type="text" name="postal_code" value="${testcenteruser.postal_code}" placeholder="e.g. 08540" />
</div>
<div class="field required" id="field-country">
<label class="short" for="country">Country Code</label>
<input id="country" class="short" type="text" name="country" value="${testcenteruser.country}" placeholder="e.g. USA" />
</div>
</li>
</ol>
</fieldset>
<fieldset class="group group-form group-form-contactinformation">
<legend class="is-hidden">Contact &amp; Other Information</legend>
<ol class="list-input">
<li class="field-group phoneinfo">
<div class="field required" id="field-phone">
<label for="phone">Phone Number</label>
<input id="phone" type="tel" name="phone" value="${testcenteruser.phone}" placeholder="e.g. 1-55-555-5555" />
</div>
<div class="field" id="field-extension">
<label for="extension">Extension</label>
<input id="extension" class="short" type="tel" name="extension" value="${testcenteruser.extension}" placeholder="e.g. 555" />
</div>
<div class="field required" id="field-phone_country_code">
<label for="phone_country_code">Phone Country Code</label>
<input id="phone_country_code" class="short" type="text" name="phone_country_code" value="${testcenteruser.phone_country_code}" placeholder="e.g. 1, 44, 976" />
</div>
</li>
<li class="field-group faxinfo">
<div class="field" id="field-fax">
<label for="fax">Fax Number</label>
<input id="fax" type="tel" class="short" name="fax" value="${testcenteruser.fax}" placeholder="e.g. 1-55-555-5555" />
</div>
<div class="field" id="field-fax_country_code">
<label for="fax_country_code">Fax Country Code</label>
<input id="fax_country_code" class="short" type="text" name="fax_country_code" value="${testcenteruser.fax_country_code}" placeholder="e.g. 1, 44, 976" />
</div>
</li>
<li class="field" id="field-company_name">
<label for="company_name">Company</label>
<input id="company_name" type="text" name="company_name" value="${testcenteruser.company_name}" placeholder="e.g. American Association of University Professors" />
</li>
</ol>
</fieldset>
</div>
% if registration:
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
<div class="form-fields-secondary is-shown disabled" id="form-fields-secondary">
% endif
% else:
<div class="form-fields-secondary" id="form-fields-secondary">
% endif
% if registration:
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
<p class="note"><span class="title">Note</span>: Your previous accommodation request below needs to be reviewed in detail <strong>and will add a significant delay to your registration process</strong>.</p>
% endif
% else:
<p class="note"><span class="title">Note</span>: Accommodation requests are not part of your demographic information, <strong>and cannot be changed once submitted</strong>. Accommodation requests, which are reviewed on a case-by-case basis, <strong>will add significant delay to the registration process</strong>.</p>
% endif
<fieldset class="group group-form group-form-optional">
<legend class="is-hidden">Optional Information</legend>
<ol class="list-input">
% if registration:
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
<li class="field submitted" id="field-accommodation_request">
<label for="accommodation_request">Accommodations Requested</label>
<p class="value" id="accommodations">${registration.accommodation_request}</p>
</li>
% endif
% else:
<li class="field" id="field-accommodation_request">
<label for="accommodation_request">Accommodations Requested</label>
<textarea class="long" id="accommodation_request" name="accommodation_request" value="" placeholder=""></textarea>
</li>
% endif
</ol>
</fieldset>
</div>
<div class="form-actions">
% if registration:
<button type="submit" id="submit" class="action action-primary action-update">Update Demographics</button>
<a href="${reverse('dashboard')}" class="action action-secondary action-cancel">Cancel Update</a>
% else:
<button type="submit" id="submit" class="action action-primary action-register">Register for Pearson VUE Test</button>
<a href="${reverse('dashboard')}" class="action action-secondary action-cancel">Cancel Registration</a>
% endif
<div class="message message-status submission-error">
<p id="submission-error-heading" class="message-copy"></p>
<ul id="submission-error-list"></ul>
</div>
</div>
</form>
% if registration:
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
<a class="actions form-fields-secondary-visibility is-hidden" href="#form-fields-secondary">Special (<abbr title="Americans with Disabilities Act">ADA</abbr>) Accommodations</a>
% endif
% else:
<a class="actions form-fields-secondary-visibility" href="#form-fields-secondary">Special (<abbr title="Americans with Disabilities Act">ADA</abbr>) Accommodations</a>
% endif
</section>
<aside>
% if registration:
% if registration.is_accepted:
<div class="message message-status registration-processed is-shown">
% endif
% if registration.is_rejected:
<div class="message message-status registration-rejected is-shown">
% endif
% if registration.is_pending:
<div class="message message-status registration-pending is-shown">
% endif
<h3>Pearson Exam Registration Status</h3>
<ol class="status-list">
<!-- first provide status of demographics -->
% if registration.demographics_is_pending:
<li class="item status status-pending status-demographics">
<h4 class="title">Demographic Information</h4>
<p class="details">The demographic information you most recently provided is pending. You may edit this information at any point before exam registration closes on <strong>${exam_info.registration_end_date_text}</strong></p>
</li>
% endif
% if registration.demographics_is_accepted:
<li class="item status status-processed status-demographics">
<h4 class="title">Demographic Information</h4>
<p class="details">The demographic information you most recently provided has been processed. You may edit this information at any point before exam registration closes on <strong>${exam_info.registration_end_date_text}</strong></p>
</li>
% endif
% if registration.demographics_is_rejected:
<li class="item status status-rejected status-demographics">
<h4 class="title">Demographic Information</h4>
<p class="details">The demographic information you most recently provided has been rejected by Pearson. You can correct and submit it again before the exam registration closes on <strong>${exam_info.registration_end_date_text}</strong>.
The error message is:</p>
<ul class="error-list">
<li class="item">${registration.testcenter_user.upload_error_message}.</li>
</ul>
<p class="action">If the error is not correctable by revising your demographic information, please <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a>.</p>
</li>
% endif
<!-- then provide status of accommodations, if any -->
% if registration.accommodation_is_pending:
<li class="item status status-pending status-accommodations">
<h4 class="title">Accommodations Request</h4>
<p class="details">Your requested accommodations are pending. Within a few days, you should see confirmation here of granted accommodations.</p>
</li>
% endif
% if registration.accommodation_is_accepted:
<li class="item status status-processed status-accommodations">
<h4 class="title">Accommodations Request</h4>
<p class="details">Your requested accommodations have been reviewed and processed. You are allowed:</p>
<ul class="accommodations-list">
% for accommodation_name in registration.get_accommodation_names():
<li class="item">${accommodation_name}</li>
% endfor
</ul>
</li>
% endif
% if registration.accommodation_is_rejected:
<li class="item status status-processed status-accommodations">
<h4 class="title">Accommodations Request</h4>
<p class="details">Your requested accommodations have been reviewed and processed. You are allowed no accommodations.</p>
<p class="action">Please <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a> if you have any questions.</p>
</li>
% endif
<!-- finally provide status of registration -->
% if registration.registration_is_pending:
<li class="item status status-pending status-registration">
<h4 class="title">Registration Request</h4>
<p class="details">Your exam registration is pending. Once your information is processed, it will be forwarded to Pearson and you will be able to schedule an exam.</p>
</li>
% endif
% if registration.registration_is_accepted:
<li class="item status status-processed status-registration">
<h4 class="title">Registration Request</h4>
<p class="details">Your exam registration has been processed and has been forwarded to Pearson. <strong>You are now able to <a href="${registration.registration_signup_url}" class="exam-link">schedule a Pearson exam</a></strong>.</p>
</li>
% endif
% if registration.registration_is_rejected:
<li class="item status status-rejected status-registration">
<h4 class="title">Registration Request</h4>
<p class="details">Your exam registration has been rejected by Pearson. <strong>You currently cannot schedule an exam</strong>. The errors found include:</p>
<ul class="error-list">
<li class="item">${registration.upload_error_message}</li>
</ul>
<p class="action">Please <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a>.</p>
</li>
% endif
</ol>
</div>
% endif
<div class="details details-course">
<h4>About ${get_course_about_section(course, 'university')} ${course.number}</h4>
<p>
% if course.has_ended():
<span class="label">Course Completed:</span> <span class="value">${course.end_date_text}</span>
% elif course.has_started():
<span class="label">Course Started:</span> <span class="value">${course.start_date_text}</span>
% else: # hasn't started yet
<span class="label">Course Starts:</span> <span class="value">${course.start_date_text}</span>
% endif
</p>
</div>
<div class="details details-registration">
<h4>Pearson VUE Test Details</h4>
% if exam_info is not None:
<ul>
<li>
<span class="label">Exam Name:</span> <span class="value">${exam_info.display_name}</span>
</li>
<li>
<span class="label">First Eligible Appointment Date:</span> <span class="value">${exam_info.first_eligible_appointment_date_text}</span>
</li>
<li>
<span class="label">Last Eligible Appointment Date:</span> <span class="value">${exam_info.last_eligible_appointment_date_text}</span>
</li>
<li>
<span class="label">Registration End Date:</span> <span class="value">${exam_info.registration_end_date_text}</span>
</li>
</ul>
% endif
</div>
<div class="details details-contact">
<h4>Questions</h4>
<p>If you have a specific question pertaining to your registration, you may contact <a class="contact-link" href="${exam_help_href}">exam-help@edx.org</a>.</p>
</div>
</aside>
</section>
......@@ -45,6 +45,9 @@ urlpatterns = ('',
url(r'^create_account$', 'student.views.create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
url(r'^begin_exam_registration/(?P<course_id>[^/]+/[^/]+/[^/]+)$', 'student.views.begin_exam_registration', name="begin_exam_registration"),
url(r'^create_exam_registration$', 'student.views.create_exam_registration'),
url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
## Obsolete Django views for password resets
## TODO: Replace with Mako-ized views
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment