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.")
......
......@@ -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})
......
......@@ -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;
}
}
}
}
}
}
......@@ -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'):
......
......@@ -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>
......
......@@ -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