Merged to lastest master.
This commit is contained in:
25
apt-packages.txt
Normal file
25
apt-packages.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
python-software-properties
|
||||
pkg-config
|
||||
curl
|
||||
git
|
||||
python-virtualenv
|
||||
build-essential
|
||||
python-dev
|
||||
gfortran
|
||||
liblapack-dev
|
||||
libfreetype6-dev
|
||||
libpng12-dev
|
||||
libxml2-dev
|
||||
libxslt-dev
|
||||
yui-compressor
|
||||
graphviz
|
||||
graphviz-dev
|
||||
mysql-server
|
||||
libmysqlclient-dev
|
||||
libgeos-dev
|
||||
libreadline6
|
||||
libreadline6-dev
|
||||
mongodb
|
||||
nodejs
|
||||
npm
|
||||
coffeescript
|
||||
3
apt-repos.txt
Normal file
3
apt-repos.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
ppa:chris-lea/node.js
|
||||
ppa:chris-lea/node.js-libs
|
||||
ppa:chris-lea/libjs-underscore
|
||||
@@ -1,10 +1,12 @@
|
||||
readline
|
||||
sqlite
|
||||
gdbm
|
||||
pkg-config
|
||||
gfortran
|
||||
python
|
||||
yuicompressor
|
||||
readline
|
||||
sqlite
|
||||
gdbm
|
||||
pkg-config
|
||||
gfortran
|
||||
python
|
||||
yuicompressor
|
||||
node
|
||||
graphviz
|
||||
mysql
|
||||
geos
|
||||
mongodb
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
[run]
|
||||
data_file = reports/cms/.coverage
|
||||
source = cms
|
||||
omit = cms/envs/*, cms/manage.py
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = CMS Python Test Coverage Report
|
||||
directory = reports/cms/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("mitx." + __name__)
|
||||
|
||||
from django.template import Context
|
||||
from django.http import HttpResponse
|
||||
|
||||
@@ -42,7 +38,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
context_dictionary.update(context)
|
||||
# fetch and render template
|
||||
template = middleware.lookup[namespace].get_template(template_name)
|
||||
return template.render(**context_dictionary)
|
||||
return template.render_unicode(**context_dictionary)
|
||||
|
||||
|
||||
def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs):
|
||||
|
||||
@@ -54,5 +54,4 @@ class Template(MakoTemplate):
|
||||
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
|
||||
context_dictionary['django_context'] = context_instance
|
||||
|
||||
return super(Template, self).render(**context_dictionary)
|
||||
|
||||
return super(Template, self).render_unicode(**context_dictionary)
|
||||
|
||||
@@ -12,6 +12,8 @@ admin.site.register(UserTestGroup)
|
||||
|
||||
admin.site.register(CourseEnrollment)
|
||||
|
||||
admin.site.register(CourseEnrollmentAllowed)
|
||||
|
||||
admin.site.register(Registration)
|
||||
|
||||
admin.site.register(PendingNameChange)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -1,150 +1,93 @@
|
||||
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()
|
||||
|
||||
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 = join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
|
||||
else:
|
||||
destfile = dest
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
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."
|
||||
|
||||
|
||||
@@ -1,35 +1,53 @@
|
||||
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 + (
|
||||
make_option(
|
||||
'--client_candidate_id',
|
||||
action='store',
|
||||
dest='client_candidate_id',
|
||||
help='ID we assign a user to identify them to Pearson'
|
||||
),
|
||||
# demographics:
|
||||
make_option(
|
||||
'--first_name',
|
||||
action='store',
|
||||
dest='first_name',
|
||||
),
|
||||
make_option(
|
||||
'--middle_name',
|
||||
action='store',
|
||||
dest='middle_name',
|
||||
),
|
||||
make_option(
|
||||
'--last_name',
|
||||
action='store',
|
||||
dest='last_name',
|
||||
),
|
||||
make_option(
|
||||
'--suffix',
|
||||
action='store',
|
||||
dest='suffix',
|
||||
),
|
||||
make_option(
|
||||
'--salutation',
|
||||
action='store',
|
||||
dest='salutation',
|
||||
),
|
||||
make_option(
|
||||
'--address_1',
|
||||
action='store',
|
||||
dest='address_1',
|
||||
),
|
||||
make_option(
|
||||
'--address_2',
|
||||
action='store',
|
||||
dest='address_2',
|
||||
),
|
||||
make_option(
|
||||
'--address_3',
|
||||
action='store',
|
||||
dest='address_3',
|
||||
),
|
||||
make_option(
|
||||
'--city',
|
||||
action='store',
|
||||
@@ -58,15 +76,56 @@ class Command(BaseCommand):
|
||||
dest='phone',
|
||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||
),
|
||||
make_option(
|
||||
'--extension',
|
||||
action='store',
|
||||
dest='extension',
|
||||
),
|
||||
make_option(
|
||||
'--phone_country_code',
|
||||
action='store',
|
||||
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.")
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- 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 'CourseEnrollmentAllowed'
|
||||
db.create_table('student_courseenrollmentallowed', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollmentAllowed'])
|
||||
|
||||
# Adding unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id']
|
||||
db.create_unique('student_courseenrollmentallowed', ['email', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id']
|
||||
db.delete_unique('student_courseenrollmentallowed', ['email', 'course_id'])
|
||||
|
||||
# Deleting model 'CourseEnrollmentAllowed'
|
||||
db.delete_table('student_courseenrollmentallowed')
|
||||
|
||||
|
||||
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.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', [], {'max_length': '50', 'db_index': 'True'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': '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'}),
|
||||
'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'}),
|
||||
'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']
|
||||
@@ -0,0 +1,241 @@
|
||||
# -*- 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']
|
||||
@@ -36,10 +36,12 @@ file and check it in at the same time as your model changes. To do that,
|
||||
3. Add the migration file created in mitx/common/djangoapps/student/migrations/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from hashlib import sha1
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from random import randint
|
||||
from time import strftime
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
@@ -47,10 +49,9 @@ 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
|
||||
from django_comment_client.models import Role
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,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:
|
||||
@@ -141,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)
|
||||
@@ -151,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)
|
||||
@@ -187,24 +190,376 @@ 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.upload_status = ''
|
||||
new_user.save()
|
||||
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.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.upload_status = ''
|
||||
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.
|
||||
|
||||
Currently happens to be implemented as a sha1 hash of the username
|
||||
(and thus assumes that usernames don't change).
|
||||
"""
|
||||
# Using the user id as the salt because it's sort of random, and is already
|
||||
# in the db.
|
||||
salt = str(user.id)
|
||||
return sha1(salt + user.username).hexdigest()
|
||||
# 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)
|
||||
h.update(str(user.id))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
## TODO: Should be renamed to generic UserGroup, and possibly
|
||||
@@ -263,15 +618,22 @@ class CourseEnrollment(models.Model):
|
||||
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
class CourseEnrollmentAllowed(models.Model):
|
||||
"""
|
||||
Table of users (specified by email address strings) who are allowed to enroll in a specified course.
|
||||
The user may or may not (yet) exist. Enrollment by users listed in this table is allowed
|
||||
even if the enrollment time window is past.
|
||||
"""
|
||||
email = models.CharField(max_length=255, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('email', 'course_id'), )
|
||||
|
||||
def __unicode__(self):
|
||||
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
|
||||
|
||||
#cache_relation(User.profile)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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,21 +27,22 @@ 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_by_university
|
||||
from courseware.courses import get_courses
|
||||
from courseware.access import has_access
|
||||
|
||||
from statsd import statsd
|
||||
@@ -74,16 +76,21 @@ def index(request, extra_context={}, user=None):
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
if domain==False: # do explicit check, because domain=None is valid
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
universities = get_courses_by_university(None,
|
||||
domain=domain)
|
||||
|
||||
courses = get_courses(None, domain=domain)
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
|
||||
context = {'universities': universities, 'news': top_news}
|
||||
context = {'courses': courses, 'news': top_news}
|
||||
context.update(extra_context)
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
|
||||
def course_from_id(course_id):
|
||||
"""Return the CourseDescriptor corresponding to this course_id"""
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
@@ -204,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.
|
||||
@@ -234,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)
|
||||
|
||||
@@ -244,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)
|
||||
@@ -295,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.'}
|
||||
|
||||
@@ -333,6 +343,14 @@ def change_enrollment(request):
|
||||
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def accounts_login(request, error=""):
|
||||
|
||||
|
||||
return render_to_response('accounts_login.html', { 'error': error })
|
||||
|
||||
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
@@ -453,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:
|
||||
@@ -586,6 +605,172 @@ 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:
|
||||
log.error("User {0} enrolled in non-existent course {1}".format(user.username, course_id))
|
||||
raise Http404
|
||||
|
||||
# get the exam to be registered for:
|
||||
# (For now, we just assume there is one at most.)
|
||||
# if there is no exam now (because someone bookmarked this stupid page),
|
||||
# then return a 404:
|
||||
exam_info = course.current_test_center_exam
|
||||
if exam_info is None:
|
||||
raise Http404
|
||||
|
||||
# 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....
|
||||
|
||||
# make sure that any demographic data values received from the page have been stripped.
|
||||
# Whitespace is not an acceptable response for any of these values
|
||||
demographic_data = {}
|
||||
for fieldname in TestCenterUser.user_provided_fields():
|
||||
if fieldname in post_vars:
|
||||
demographic_data[fieldname] = (post_vars[fieldname]).strip()
|
||||
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=user)
|
||||
needs_updating = testcenter_user.needs_update(demographic_data)
|
||||
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=demographic_data)
|
||||
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():
|
||||
"""
|
||||
@@ -641,7 +826,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
|
||||
|
||||
48
common/djangoapps/track/migrations/0001_initial.py
Normal file
48
common/djangoapps/track/migrations/0001_initial.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- 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 'TrackingLog'
|
||||
db.create_table('track_trackinglog', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('dtcreated', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('username', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('ip', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('event_source', self.gf('django.db.models.fields.CharField')(max_length=32)),
|
||||
('event_type', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('event', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
('agent', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)),
|
||||
('page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
|
||||
('time', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
))
|
||||
db.send_create_signal('track', ['TrackingLog'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'TrackingLog'
|
||||
db.delete_table('track_trackinglog')
|
||||
|
||||
|
||||
models = {
|
||||
'track.trackinglog': {
|
||||
'Meta': {'object_name': 'TrackingLog'},
|
||||
'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
|
||||
'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'event_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'page': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
|
||||
'time': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- 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 field 'TrackingLog.host'
|
||||
db.add_column('track_trackinglog', 'host',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
# Changing field 'TrackingLog.event_type'
|
||||
db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=512))
|
||||
|
||||
# Changing field 'TrackingLog.page'
|
||||
db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=512, null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'TrackingLog.host'
|
||||
db.delete_column('track_trackinglog', 'host')
|
||||
|
||||
|
||||
# Changing field 'TrackingLog.event_type'
|
||||
db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=32))
|
||||
|
||||
# Changing field 'TrackingLog.page'
|
||||
db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
|
||||
|
||||
models = {
|
||||
'track.trackinglog': {
|
||||
'Meta': {'object_name': 'TrackingLog'},
|
||||
'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
|
||||
'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'event_type': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'host': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'page': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
|
||||
'time': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
@@ -7,11 +7,12 @@ class TrackingLog(models.Model):
|
||||
username = models.CharField(max_length=32,blank=True)
|
||||
ip = models.CharField(max_length=32,blank=True)
|
||||
event_source = models.CharField(max_length=32)
|
||||
event_type = models.CharField(max_length=32,blank=True)
|
||||
event_type = models.CharField(max_length=512,blank=True)
|
||||
event = models.TextField(blank=True)
|
||||
agent = models.CharField(max_length=256,blank=True)
|
||||
page = models.CharField(max_length=32,blank=True,null=True)
|
||||
page = models.CharField(max_length=512,blank=True,null=True)
|
||||
time = models.DateTimeField('event time')
|
||||
host = models.CharField(max_length=64,blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source,
|
||||
|
||||
@@ -17,7 +17,7 @@ from track.models import TrackingLog
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time']
|
||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host']
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
@@ -58,6 +58,7 @@ def user_track(request):
|
||||
"agent": agent,
|
||||
"page": request.GET['page'],
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
"host": request.META['SERVER_NAME'],
|
||||
}
|
||||
log_event(event)
|
||||
return HttpResponse('success')
|
||||
@@ -83,6 +84,7 @@ def server_track(request, event_type, event, page=None):
|
||||
"agent": agent,
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
"host": request.META['SERVER_NAME'],
|
||||
}
|
||||
|
||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||
|
||||
@@ -4,6 +4,11 @@ import json
|
||||
|
||||
|
||||
def expect_json(view_function):
|
||||
"""
|
||||
View decorator for simplifying handing of requests that expect json. If the request's
|
||||
CONTENT_TYPE is application/json, parses the json dict from request.body, and updates
|
||||
request.POST with the contents.
|
||||
"""
|
||||
@wraps(view_function)
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
|
||||
|
||||
@@ -7,6 +7,7 @@ source = common/lib/capa
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = Capa Python Test Coverage Report
|
||||
directory = reports/common/lib/capa/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -33,6 +33,7 @@ from xml.sax.saxutils import unescape
|
||||
import chem
|
||||
import chem.chemcalc
|
||||
import chem.chemtools
|
||||
import chem.miller
|
||||
|
||||
import calc
|
||||
from correctmap import CorrectMap
|
||||
@@ -52,7 +53,7 @@ response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
solution_tags = ['solution']
|
||||
|
||||
# these get captured as student responses
|
||||
response_properties = ["codeparam", "responseparam", "answer"]
|
||||
response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
|
||||
|
||||
# special problem tags which should be turned into innocuous HTML
|
||||
html_transforms = {'problem': {'tag': 'div'},
|
||||
@@ -67,10 +68,11 @@ global_context = {'random': random,
|
||||
'calc': calc,
|
||||
'eia': eia,
|
||||
'chemcalc': chem.chemcalc,
|
||||
'chemtools': chem.chemtools}
|
||||
'chemtools': chem.chemtools,
|
||||
'miller': chem.miller}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
267
common/lib/capa/capa/chem/miller.py
Normal file
267
common/lib/capa/capa/chem/miller.py
Normal file
@@ -0,0 +1,267 @@
|
||||
""" Calculation of Miller indices """
|
||||
|
||||
import numpy as np
|
||||
import math
|
||||
import fractions as fr
|
||||
import decimal
|
||||
import json
|
||||
|
||||
|
||||
def lcm(a, b):
|
||||
"""
|
||||
Returns least common multiple of a, b
|
||||
|
||||
Args:
|
||||
a, b: floats
|
||||
|
||||
Returns:
|
||||
float
|
||||
"""
|
||||
return a * b / fr.gcd(a, b)
|
||||
|
||||
|
||||
def segment_to_fraction(distance):
|
||||
"""
|
||||
Converts lengths of which the plane cuts the axes to fraction.
|
||||
|
||||
Tries convert distance to closest nicest fraction with denominator less or
|
||||
equal than 10. It is
|
||||
purely for simplicity and clearance of learning purposes. Jenny: 'In typical
|
||||
courses students usually do not encounter indices any higher than 6'.
|
||||
|
||||
If distance is not a number (numpy nan), it means that plane is parallel to
|
||||
axis or contains it. Inverted fraction to nan (nan is 1/0) = 0 / 1 is
|
||||
returned
|
||||
|
||||
Generally (special cases):
|
||||
|
||||
a) if distance is smaller than some constant, i.g. 0.01011,
|
||||
than fraction's denominator usually much greater than 10.
|
||||
|
||||
b) Also, if student will set point on 0.66 -> 1/3, so it is 333 plane,
|
||||
But if he will slightly move the mouse and click on 0.65 -> it will be
|
||||
(16,15,16) plane. That's why we are doing adjustments for points coordinates,
|
||||
to the closest tick, tick + tick / 2 value. And now UI sends to server only
|
||||
values multiple to 0.05 (half of tick). Same rounding is implemented for
|
||||
unittests.
|
||||
|
||||
But if one will want to calculate miller indices with exact coordinates and
|
||||
with nice fractions (which produce small Miller indices), he may want shift
|
||||
to new origin if segments are like S = (0.015, > 0.05, >0.05) - close to zero
|
||||
in one coordinate. He may update S to (0, >0.05, >0.05) and shift origin.
|
||||
In this way he can recieve nice small fractions. Also there is can be
|
||||
degenerated case when S = (0.015, 0.012, >0.05) - if update S to (0, 0, >0.05) -
|
||||
it is a line. This case should be considered separately. Small nice Miller
|
||||
numbers and possibility to create very small segments can not be implemented
|
||||
at same time).
|
||||
|
||||
|
||||
Args:
|
||||
distance: float distance that plane cuts on axis, it must not be 0.
|
||||
Distance is multiple of 0.05.
|
||||
|
||||
Returns:
|
||||
Inverted fraction.
|
||||
0 / 1 if distance is nan
|
||||
|
||||
"""
|
||||
if np.isnan(distance):
|
||||
return fr.Fraction(0, 1)
|
||||
else:
|
||||
fract = fr.Fraction(distance).limit_denominator(10)
|
||||
return fr.Fraction(fract.denominator, fract.numerator)
|
||||
|
||||
|
||||
def sub_miller(segments):
|
||||
'''
|
||||
Calculates Miller indices from segments.
|
||||
|
||||
Algorithm:
|
||||
|
||||
1. Obtain inverted fraction from segments
|
||||
|
||||
2. Find common denominator of inverted fractions
|
||||
|
||||
3. Lead fractions to common denominator and throws denominator away.
|
||||
|
||||
4. Return obtained values.
|
||||
|
||||
Args:
|
||||
List of 3 floats, meaning distances that plane cuts on x, y, z axes.
|
||||
Any float not equals zero, it means that plane does not intersect origin,
|
||||
i. e. shift of origin has already been done.
|
||||
|
||||
Returns:
|
||||
String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2)
|
||||
'''
|
||||
fracts = [segment_to_fraction(segment) for segment in segments]
|
||||
common_denominator = reduce(lcm, [fract.denominator for fract in fracts])
|
||||
miller = ([fract.numerator * math.fabs(common_denominator) /
|
||||
fract.denominator for fract in fracts])
|
||||
return'(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')'
|
||||
|
||||
|
||||
def miller(points):
|
||||
"""
|
||||
Calculates Miller indices from points.
|
||||
|
||||
Algorithm:
|
||||
|
||||
1. Calculate normal vector to a plane that goes trough all points.
|
||||
|
||||
2. Set origin.
|
||||
|
||||
3. Create Cartesian coordinate system (Ccs).
|
||||
|
||||
4. Find the lengths of segments of which the plane cuts the axes. Equation
|
||||
of a line for axes: Origin + (Coordinate_vector - Origin) * parameter.
|
||||
|
||||
5. If plane goes trough Origin:
|
||||
|
||||
a) Find new random origin: find unit cube vertex, not crossed by a plane.
|
||||
|
||||
b) Repeat 2-4.
|
||||
|
||||
c) Fix signs of segments after Origin shift. This means to consider
|
||||
original directions of axes. I.g.: Origin was 0,0,0 and became
|
||||
new_origin. If new_origin has same Y coordinate as Origin, then segment
|
||||
does not change its sign. But if new_origin has another Y coordinate than
|
||||
origin (was 0, became 1), than segment has to change its sign (it now
|
||||
lies on negative side of Y axis). New Origin 0 value of X or Y or Z
|
||||
coordinate means that segment does not change sign, 1 value -> does
|
||||
change. So new sign is (1 - 2 * new_origin): 0 -> 1, 1 -> -1
|
||||
|
||||
6. Run function that calculates miller indices from segments.
|
||||
|
||||
Args:
|
||||
List of points. Each point is list of float coordinates. Order of
|
||||
coordinates in point's list: x, y, z. Points are different!
|
||||
|
||||
Returns:
|
||||
String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2)
|
||||
"""
|
||||
|
||||
N = np.cross(points[1] - points[0], points[2] - points[0])
|
||||
O = np.array([0, 0, 0])
|
||||
P = points[0] # point of plane
|
||||
Ccs = map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]])
|
||||
segments = ([np.dot(P - O, N) / np.dot(ort, N) if np.dot(ort, N) != 0 else
|
||||
np.nan for ort in Ccs])
|
||||
if any(x == 0 for x in segments): # Plane goes through origin.
|
||||
vertices = [ # top:
|
||||
np.array([1.0, 1.0, 1.0]),
|
||||
np.array([0.0, 0.0, 1.0]),
|
||||
np.array([1.0, 0.0, 1.0]),
|
||||
np.array([0.0, 1.0, 1.0]),
|
||||
# bottom, except 0,0,0:
|
||||
np.array([1.0, 0.0, 0.0]),
|
||||
np.array([0.0, 1.0, 0.0]),
|
||||
np.array([1.0, 1.0, 1.0]),
|
||||
]
|
||||
for vertex in vertices:
|
||||
if np.dot(vertex - O, N) != 0: # vertex not in plane
|
||||
new_origin = vertex
|
||||
break
|
||||
# obtain new axes with center in new origin
|
||||
X = np.array([1 - new_origin[0], new_origin[1], new_origin[2]])
|
||||
Y = np.array([new_origin[0], 1 - new_origin[1], new_origin[2]])
|
||||
Z = np.array([new_origin[0], new_origin[1], 1 - new_origin[2]])
|
||||
new_Ccs = [X - new_origin, Y - new_origin, Z - new_origin]
|
||||
segments = ([np.dot(P - new_origin, N) / np.dot(ort, N) if
|
||||
np.dot(ort, N) != 0 else np.nan for ort in new_Ccs])
|
||||
# fix signs of indices: 0 -> 1, 1 -> -1 (
|
||||
segments = (1 - 2 * new_origin) * segments
|
||||
|
||||
return sub_miller(segments)
|
||||
|
||||
|
||||
def grade(user_input, correct_answer):
|
||||
'''
|
||||
Grade crystallography problem.
|
||||
|
||||
Returns true if lattices are the same and Miller indices are same or minus
|
||||
same. E.g. (2,2,2) = (2, 2, 2) or (-2, -2, -2). Because sign depends only
|
||||
on student's selection of origin.
|
||||
|
||||
Args:
|
||||
user_input, correct_answer: json. Format:
|
||||
|
||||
user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"],
|
||||
["0.78","1.00","0.00"],["0.00","1.00","0.72"]]}
|
||||
|
||||
correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'}
|
||||
|
||||
"lattice" is one of: "", "sc", "bcc", "fcc"
|
||||
|
||||
Returns:
|
||||
True or false.
|
||||
'''
|
||||
def negative(m):
|
||||
"""
|
||||
Change sign of Miller indices.
|
||||
|
||||
Args:
|
||||
m: string with meaning of Miller indices. E.g.:
|
||||
(-6,3,-6) -> (6, -3, 6)
|
||||
|
||||
Returns:
|
||||
String with changed signs.
|
||||
"""
|
||||
output = ''
|
||||
i = 1
|
||||
while i in range(1, len(m) - 1):
|
||||
if m[i] in (',', ' '):
|
||||
output += m[i]
|
||||
elif m[i] not in ('-', '0'):
|
||||
output += '-' + m[i]
|
||||
elif m[i] == '0':
|
||||
output += m[i]
|
||||
else:
|
||||
i += 1
|
||||
output += m[i]
|
||||
i += 1
|
||||
return '(' + output + ')'
|
||||
|
||||
def round0_25(point):
|
||||
"""
|
||||
Rounds point coordinates to closest 0.5 value.
|
||||
|
||||
Args:
|
||||
point: list of float coordinates. Order of coordinates: x, y, z.
|
||||
|
||||
Returns:
|
||||
list of coordinates rounded to closes 0.5 value
|
||||
"""
|
||||
rounded_points = []
|
||||
for coord in point:
|
||||
base = math.floor(coord * 10)
|
||||
fractional_part = (coord * 10 - base)
|
||||
aliquot0_25 = math.floor(fractional_part / 0.25)
|
||||
if aliquot0_25 == 0.0:
|
||||
rounded_points.append(base / 10)
|
||||
if aliquot0_25 in (1.0, 2.0):
|
||||
rounded_points.append(base / 10 + 0.05)
|
||||
if aliquot0_25 == 3.0:
|
||||
rounded_points.append(base / 10 + 0.1)
|
||||
return rounded_points
|
||||
|
||||
user_answer = json.loads(user_input)
|
||||
|
||||
if user_answer['lattice'] != correct_answer['lattice']:
|
||||
return False
|
||||
|
||||
points = [map(float, p) for p in user_answer['points']]
|
||||
|
||||
if len(points) < 3:
|
||||
return False
|
||||
|
||||
# round point to closes 0.05 value
|
||||
points = [round0_25(point) for point in points]
|
||||
|
||||
points = [np.array(point) for point in points]
|
||||
# print miller(points), (correct_answer['miller'].replace(' ', ''),
|
||||
# negative(correct_answer['miller']).replace(' ', ''))
|
||||
if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,13 +1,15 @@
|
||||
import codecs
|
||||
from fractions import Fraction
|
||||
from pyparsing import ParseException
|
||||
import unittest
|
||||
|
||||
from chemcalc import (compare_chemical_expression, divide_chemical_expression,
|
||||
render_to_html, chemical_equations_equal)
|
||||
|
||||
import miller
|
||||
|
||||
local_debug = None
|
||||
|
||||
|
||||
def log(s, output_type=None):
|
||||
if local_debug:
|
||||
print s
|
||||
@@ -37,7 +39,6 @@ class Test_Compare_Equations(unittest.TestCase):
|
||||
self.assertFalse(chemical_equations_equal('2H2 + O2 -> H2O2',
|
||||
'2O2 + 2H2 -> 2H2O2'))
|
||||
|
||||
|
||||
def test_different_arrows(self):
|
||||
self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2',
|
||||
'2O2 + 2H2 -> 2H2O2'))
|
||||
@@ -56,7 +57,6 @@ class Test_Compare_Equations(unittest.TestCase):
|
||||
self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2',
|
||||
'O2 + H2 -> H2O2', exact=True))
|
||||
|
||||
|
||||
def test_syntax_errors(self):
|
||||
self.assertFalse(chemical_equations_equal('H2 + O2 a-> H2O2',
|
||||
'2O2 + 2H2 -> 2H2O2'))
|
||||
@@ -311,7 +311,6 @@ class Test_Render_Equations(unittest.TestCase):
|
||||
log(out + ' ------- ' + correct, 'html')
|
||||
self.assertEqual(out, correct)
|
||||
|
||||
|
||||
def test_render_eq3(self):
|
||||
s = "H^+ + OH^- <= H2O" # unsupported arrow
|
||||
out = render_to_html(s)
|
||||
@@ -320,10 +319,148 @@ class Test_Render_Equations(unittest.TestCase):
|
||||
self.assertEqual(out, correct)
|
||||
|
||||
|
||||
class Test_Crystallography_Miller(unittest.TestCase):
|
||||
''' Tests for crystallography grade function.'''
|
||||
|
||||
def test_empty_points(self):
|
||||
user_input = '{"lattice": "bcc", "points": []}'
|
||||
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_only_one_point(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}'
|
||||
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_only_two_points(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}'
|
||||
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_1(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_2(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_3(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_4(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_5(self):
|
||||
""" return true only in case points coordinates are exact.
|
||||
But if they transform to closest 0.05 value it is not true"""
|
||||
user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}'
|
||||
self.assertFalse(miller.grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_6(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_7(self): # goes throug origin
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_8(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_9(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_10(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_11(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_12(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_13(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_14(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_15(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_16(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_17(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_18(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_19(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_20(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_21(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_22(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_23(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_24(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'}))
|
||||
|
||||
def test_25(self):
|
||||
user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''}))
|
||||
|
||||
def test_26(self):
|
||||
user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''}))
|
||||
|
||||
def test_27(self):
|
||||
""" rounding to 0.35"""
|
||||
user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': ''}))
|
||||
|
||||
def test_28(self):
|
||||
""" rounding to 0.30"""
|
||||
user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}'
|
||||
self.assertTrue(miller.grade(user_input, {'miller': '(10,10,10)', 'lattice': ''}))
|
||||
|
||||
def test_wrong_lattice(self):
|
||||
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
|
||||
self.assertFalse(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'}))
|
||||
|
||||
|
||||
def suite():
|
||||
|
||||
testcases = [Test_Compare_Expressions, Test_Divide_Expressions, Test_Render_Equations]
|
||||
testcases = [Test_Compare_Expressions,
|
||||
Test_Divide_Expressions,
|
||||
Test_Render_Equations,
|
||||
Test_Crystallography_Miller]
|
||||
suites = []
|
||||
for testcase in testcases:
|
||||
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))
|
||||
|
||||
@@ -671,18 +671,15 @@ class Crystallography(InputTypeBase):
|
||||
"""
|
||||
Note: height, width are required.
|
||||
"""
|
||||
return [Attribute('size', None),
|
||||
Attribute('height'),
|
||||
return [Attribute('height'),
|
||||
Attribute('width'),
|
||||
|
||||
# can probably be removed (textline should prob be always-hidden)
|
||||
Attribute('hidden', ''),
|
||||
]
|
||||
|
||||
registry.register(Crystallography)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
class VseprInput(InputTypeBase):
|
||||
"""
|
||||
Input for molecular geometry--show possible structures, let student
|
||||
@@ -736,3 +733,5 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js',}
|
||||
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -8,22 +8,25 @@ Used by capa_problem.py
|
||||
'''
|
||||
|
||||
# standard library imports
|
||||
import abc
|
||||
import cgi
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import numbers
|
||||
import numpy
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
import hashlib
|
||||
import abc
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
from collections import namedtuple
|
||||
from shapely.geometry import Point, MultiPoint
|
||||
|
||||
# specific library imports
|
||||
from calc import evaluator, UndefinedVariable
|
||||
from correctmap import CorrectMap
|
||||
@@ -626,7 +629,7 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
|
||||
self.correct_choices = [choice.get('name') for choice in cxml]
|
||||
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
@@ -720,7 +723,7 @@ class OptionResponse(LoncapaResponse):
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'), af.get('correct')) for af in self.answer_fields])
|
||||
amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields])
|
||||
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
|
||||
return amap
|
||||
|
||||
@@ -1100,6 +1103,15 @@ class SymbolicResponse(CustomResponse):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
valid: Flag indicating valid score_msg format (Boolean)
|
||||
correct: Correctness of submission (Boolean)
|
||||
score: Points to be assigned (numeric, can be float)
|
||||
msg: Message from grader to display to student (string)
|
||||
"""
|
||||
ScoreMessage = namedtuple('ScoreMessage',
|
||||
['valid', 'correct', 'points', 'msg'])
|
||||
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
"""
|
||||
@@ -1139,7 +1151,7 @@ class CodeResponse(LoncapaResponse):
|
||||
else:
|
||||
self._parse_coderesponse_xml(codeparam)
|
||||
|
||||
def _parse_coderesponse_xml(self,codeparam):
|
||||
def _parse_coderesponse_xml(self, codeparam):
|
||||
'''
|
||||
Parse the new CodeResponse XML format. When successful, sets:
|
||||
self.initial_display
|
||||
@@ -1151,17 +1163,9 @@ class CodeResponse(LoncapaResponse):
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
self.payload = {'grader_payload': grader_payload}
|
||||
|
||||
answer_display = codeparam.find('answer_display')
|
||||
if answer_display is not None:
|
||||
self.answer = answer_display.text
|
||||
else:
|
||||
self.answer = 'No answer provided.'
|
||||
|
||||
initial_display = codeparam.find('initial_display')
|
||||
if initial_display is not None:
|
||||
self.initial_display = initial_display.text
|
||||
else:
|
||||
self.initial_display = ''
|
||||
self.initial_display = find_with_default(codeparam, 'initial_display', '')
|
||||
self.answer = find_with_default(codeparam, 'answer_display',
|
||||
'No answer provided.')
|
||||
|
||||
def _parse_externalresponse_xml(self):
|
||||
'''
|
||||
@@ -1308,8 +1312,6 @@ class CodeResponse(LoncapaResponse):
|
||||
# Sanity check on returned points
|
||||
if points < 0:
|
||||
points = 0
|
||||
elif points > self.maxpoints[self.answer_id]:
|
||||
points = self.maxpoints[self.answer_id]
|
||||
# Queuestate is consumed
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
|
||||
msg=msg.replace(' ', ' '), queuestate=None)
|
||||
@@ -1717,15 +1719,38 @@ class ImageResponse(LoncapaResponse):
|
||||
which produces an [x,y] coordinate pair. The click is correct if it falls
|
||||
within a region specified. This region is a union of rectangles.
|
||||
|
||||
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it.
|
||||
That doesn't make sense to me (Ike). Instead, let's have it such that
|
||||
<imageresponse> should contain one or more <imageinput> stanzas.
|
||||
Each <imageinput> should specify a rectangle(s) or region(s), given as an
|
||||
attribute, defining the correct answer.
|
||||
|
||||
<imageinput src="/static/images/Lecture2/S2_p04.png" width="811" height="610"
|
||||
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
|
||||
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
|
||||
|
||||
Regions is list of lists [region1, region2, region3, ...] where regionN
|
||||
is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]].
|
||||
|
||||
If there is only one region in the list, simpler notation can be used:
|
||||
regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly
|
||||
setting outer list)
|
||||
|
||||
Returns:
|
||||
True, if click is inside any region or rectangle. Otherwise False.
|
||||
"""
|
||||
snippets = [{'snippet': '''<imageresponse>
|
||||
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
|
||||
<imageinput src="image1.jpg" width="200" height="100"
|
||||
rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130"
|
||||
rectangle="(12,12)-(40,60)" />
|
||||
<imageinput src="image3.jpg" width="210" height="130"
|
||||
rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
|
||||
<imageinput src="image4.jpg" width="811" height="610"
|
||||
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
|
||||
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
|
||||
<imageinput src="image5.jpg" width="200" height="200"
|
||||
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
|
||||
</imageresponse>'''}]
|
||||
|
||||
response_tag = 'imageresponse'
|
||||
@@ -1733,19 +1758,17 @@ class ImageResponse(LoncapaResponse):
|
||||
|
||||
def setup_response(self):
|
||||
self.ielements = self.inputfields
|
||||
self.answer_ids = [ie.get('id') for ie in self.ielements]
|
||||
self.answer_ids = [ie.get('id') for ie in self.ielements]
|
||||
|
||||
def get_score(self, student_answers):
|
||||
correct_map = CorrectMap()
|
||||
expectedset = self.get_answers()
|
||||
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
|
||||
given = student_answers[aid] # this should be a string of the form '[x,y]'
|
||||
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput>
|
||||
# fields in our stanza
|
||||
given = student_answers[aid] # this should be a string of the form '[x,y]'
|
||||
correct_map.set(aid, 'incorrect')
|
||||
if not given: # No answer to parse. Mark as incorrect and move on
|
||||
if not given: # No answer to parse. Mark as incorrect and move on
|
||||
continue
|
||||
|
||||
# parse given answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
|
||||
if not m:
|
||||
@@ -1753,29 +1776,44 @@ class ImageResponse(LoncapaResponse):
|
||||
'error grading %s (input=%s)' % (aid, given))
|
||||
(gx, gy) = [int(x) for x in m.groups()]
|
||||
|
||||
# Check whether given point lies in any of the solution rectangles
|
||||
solution_rectangles = expectedset[aid].split(';')
|
||||
for solution_rectangle in solution_rectangles:
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
|
||||
solution_rectangle.strip().replace(' ', ''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
|
||||
etree.tostring(self.ielements[aid], pretty_print=True))
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
|
||||
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
if (llx <= gx <= urx) and (lly <= gy <= ury):
|
||||
correct_map.set(aid, 'correct')
|
||||
break
|
||||
rectangles, regions = expectedset
|
||||
if rectangles[aid]: # rectangles part - for backward compatibility
|
||||
# Check whether given point lies in any of the solution rectangles
|
||||
solution_rectangles = rectangles[aid].split(';')
|
||||
for solution_rectangle in solution_rectangles:
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
|
||||
solution_rectangle.strip().replace(' ', ''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
|
||||
etree.tostring(self.ielements[aid], pretty_print=True))
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
|
||||
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
if (llx <= gx <= urx) and (lly <= gy <= ury):
|
||||
correct_map.set(aid, 'correct')
|
||||
break
|
||||
if correct_map[aid]['correctness'] != 'correct' and regions[aid]:
|
||||
parsed_region = json.loads(regions[aid])
|
||||
if parsed_region:
|
||||
if type(parsed_region[0][0]) != list:
|
||||
# we have [[1,2],[3,4],[5,6]] - single region
|
||||
# instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]]
|
||||
# or [[[1,2],[3,4],[5,6]]] - multiple regions syntax
|
||||
parsed_region = [parsed_region]
|
||||
for region in parsed_region:
|
||||
polygon = MultiPoint(region).convex_hull
|
||||
if (polygon.type == 'Polygon' and
|
||||
polygon.contains(Point(gx, gy))):
|
||||
correct_map.set(aid, 'correct')
|
||||
break
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements])
|
||||
|
||||
return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]),
|
||||
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
|
||||
#-----------------------------------------------------------------------------
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
<section id="inputtype_${id}" class="capa_inputtype" >
|
||||
<div id="holder" style="width:${width};height:${height}"></div>
|
||||
<div class="crystalography_problem" style="width:${width};height:${height}"></div>
|
||||
|
||||
<div class="input_lattice">
|
||||
Lattice: <select></select>
|
||||
</div>
|
||||
|
||||
<div class="script_placeholder" data-src="/static/js/raphael.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
|
||||
|
||||
|
||||
% if status == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
<div class="unanswered" id="status_${id}">
|
||||
% elif status == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
<div class="correct" id="status_${id}">
|
||||
% elif status == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% elif status == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
|
||||
<p class="status">
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" style="display:none;"/>
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif status == 'correct':
|
||||
@@ -38,14 +32,15 @@
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
@@ -18,4 +18,23 @@ Hello</p></text>
|
||||
<text><p>Use conservation of energy.</p></text>
|
||||
</hintgroup>
|
||||
</imageresponse>
|
||||
|
||||
|
||||
<imageresponse max="1" loncapaid="12">
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions='[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]'/>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
|
||||
<text>Click on either of the two positions as discussed previously.</text>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]]]"/>
|
||||
<text>Click on either of the two positions as discussed previously.</text>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [15, 15]]"/>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [10, 30], [30, 10]]"/>
|
||||
<text>Click on either of the two positions as discussed previously.</text>
|
||||
<hintgroup showoncorrect="no">
|
||||
<text><p>Use conservation of energy.</p></text>
|
||||
</hintgroup>
|
||||
</imageresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
|
||||
@@ -407,13 +407,11 @@ class CrystallographyTest(unittest.TestCase):
|
||||
def test_rendering(self):
|
||||
height = '12'
|
||||
width = '33'
|
||||
size = '10'
|
||||
|
||||
xml_str = """<crystallography id="prob_1_2"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
size="{s}"
|
||||
/>""".format(h=height, w=width, s=size)
|
||||
/>""".format(h=height, w=width)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
@@ -428,9 +426,7 @@ class CrystallographyTest(unittest.TestCase):
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'status': 'unsubmitted',
|
||||
'size': size,
|
||||
'msg': '',
|
||||
'hidden': '',
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
|
||||
@@ -52,24 +52,57 @@ class ImageResponseTest(unittest.TestCase):
|
||||
def test_ir_grade(self):
|
||||
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
|
||||
correct_answers = {'1_2_1': '(490,11)-(556,98)',
|
||||
'1_2_2': '(242,202)-(296,276)',
|
||||
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
# testing regions only
|
||||
correct_answers = {
|
||||
#regions
|
||||
'1_2_1': '(490,11)-(556,98)',
|
||||
'1_2_2': '(242,202)-(296,276)',
|
||||
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
|
||||
#testing regions and rectanges
|
||||
'1_3_1': 'rectangle="(490,11)-(556,98)" \
|
||||
regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
|
||||
'1_3_2': 'rectangle="(490,11)-(556,98)" \
|
||||
regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
|
||||
'1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
|
||||
'1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
|
||||
'1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"',
|
||||
'1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"',
|
||||
'1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"',
|
||||
}
|
||||
test_answers = {'1_2_1': '[500,20]',
|
||||
'1_2_2': '[250,300]',
|
||||
'1_2_3': '[500,20]',
|
||||
'1_2_4': '[250,250]',
|
||||
'1_2_5': '[10,10]',
|
||||
test_answers = {
|
||||
'1_2_1': '[500,20]',
|
||||
'1_2_2': '[250,300]',
|
||||
'1_2_3': '[500,20]',
|
||||
'1_2_4': '[250,250]',
|
||||
'1_2_5': '[10,10]',
|
||||
|
||||
'1_3_1': '[500,20]',
|
||||
'1_3_2': '[15,15]',
|
||||
'1_3_3': '[500,20]',
|
||||
'1_3_4': '[115,115]',
|
||||
'1_3_5': '[15,15]',
|
||||
'1_3_6': '[20,20]',
|
||||
'1_3_7': '[20,15]',
|
||||
}
|
||||
|
||||
# regions
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect')
|
||||
|
||||
# regions and rectangles
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_2'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_3'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_4'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_5'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_6'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_7'), 'correct')
|
||||
|
||||
|
||||
class SymbolicResponseTest(unittest.TestCase):
|
||||
def test_sr_grade(self):
|
||||
|
||||
@@ -65,3 +65,25 @@ def is_file(file_to_test):
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
'''
|
||||
return all(hasattr(file_to_test, method) for method in ['read', 'name'])
|
||||
|
||||
|
||||
def find_with_default(node, path, default):
|
||||
"""
|
||||
Look for a child of node using , and return its text if found.
|
||||
Otherwise returns default.
|
||||
|
||||
Arguments:
|
||||
node: lxml node
|
||||
path: xpath search expression
|
||||
default: value to return if nothing found
|
||||
|
||||
Returns:
|
||||
node.find(path).text if the find succeeds, default otherwise.
|
||||
|
||||
"""
|
||||
v = node.find(path)
|
||||
if v is not None:
|
||||
return v.text
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ def parse_xreply(xreply):
|
||||
|
||||
return_code = xreply['return_code']
|
||||
content = xreply['content']
|
||||
|
||||
return (return_code, content)
|
||||
|
||||
|
||||
@@ -80,7 +81,11 @@ class XQueueInterface(object):
|
||||
|
||||
# Log in, then try again
|
||||
if error and (msg == 'login_required'):
|
||||
self._login()
|
||||
(error, content) = self._login()
|
||||
if error != 0:
|
||||
# when the login fails
|
||||
log.debug("Failed to login to queue: %s", content)
|
||||
return (error, content)
|
||||
if files_to_upload is not None:
|
||||
# Need to rewind file pointers
|
||||
for f in files_to_upload:
|
||||
|
||||
@@ -7,6 +7,7 @@ source = common/lib/xmodule
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = XModule Python Test Coverage Report
|
||||
directory = reports/common/lib/xmodule/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -19,6 +19,7 @@ setup(
|
||||
"abtest = xmodule.abtest_module:ABTestDescriptor",
|
||||
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"chapter = xmodule.seq_module:SequenceDescriptor",
|
||||
"combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor",
|
||||
"course = xmodule.course_module:CourseDescriptor",
|
||||
"customtag = xmodule.template_module:CustomTagDescriptor",
|
||||
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
@@ -28,7 +29,6 @@ setup(
|
||||
"problem = xmodule.capa_module:CapaDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"selfassessment = xmodule.self_assessment_module:SelfAssessmentDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
@@ -36,6 +36,7 @@ setup(
|
||||
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"videosequence = xmodule.seq_module:SequenceDescriptor",
|
||||
"discussion = xmodule.discussion_module:DiscussionDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -146,6 +146,11 @@ class CapaModule(XModule):
|
||||
else:
|
||||
self.seed = None
|
||||
|
||||
# Need the problem location in openendedresponse to send out. Adding
|
||||
# it to the system here seems like the least clunky way to get it
|
||||
# there.
|
||||
self.system.set('location', self.location.url())
|
||||
|
||||
try:
|
||||
# TODO (vshnayder): move as much as possible of this work and error
|
||||
# checking to descriptor load time
|
||||
@@ -425,6 +430,7 @@ class CapaModule(XModule):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def update_score(self, get):
|
||||
"""
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
|
||||
598
common/lib/xmodule/xmodule/combined_open_ended_module.py
Normal file
598
common/lib/xmodule/xmodule/combined_open_ended_module.py
Normal file
@@ -0,0 +1,598 @@
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .capa_module import only_one, ComplexEncoder
|
||||
from .editing_module import EditingDescriptor
|
||||
from .html_checker import check_html
|
||||
from progress import Progress
|
||||
from .stringify import stringify_children
|
||||
from .x_module import XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
import self_assessment_module
|
||||
import open_ended_module
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
# Set the default number of max attempts. Should be 1 for production
|
||||
# Set higher for debugging/testing
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 10000
|
||||
|
||||
# Set maximum available number of points.
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
class CombinedOpenEndedModule(XModule):
|
||||
"""
|
||||
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
|
||||
It transitions between problems, and support arbitrary ordering.
|
||||
Each combined open ended module contains one or multiple "child" modules.
|
||||
Child modules track their own state, and can transition between states. They also implement get_html and
|
||||
handle_ajax.
|
||||
The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess
|
||||
ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem)
|
||||
ajax actions implemented by all children are:
|
||||
'save_answer' -- Saves the student answer
|
||||
'save_assessment' -- Saves the student assessment (or external grader assessment)
|
||||
'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc)
|
||||
ajax actions implemented by combined open ended module are:
|
||||
'reset' -- resets the whole combined open ended module and returns to the first child module
|
||||
'next_problem' -- moves to the next child module
|
||||
'get_results' -- gets results from a given child module
|
||||
|
||||
Types of children. Task is synonymous with child module, so each combined open ended module
|
||||
incorporates multiple children (tasks):
|
||||
openendedmodule
|
||||
selfassessmentmodule
|
||||
"""
|
||||
STATE_VERSION = 1
|
||||
|
||||
# states
|
||||
INITIAL = 'initial'
|
||||
ASSESSING = 'assessing'
|
||||
INTERMEDIATE_DONE = 'intermediate_done'
|
||||
DONE = 'done'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
]}
|
||||
js_module_name = "CombinedOpenEnded"
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
"""
|
||||
Definition file should have one or many task blocks, a rubric block, and a prompt block:
|
||||
|
||||
Sample file:
|
||||
<combinedopenended attempts="10000" max_score="1">
|
||||
<rubric>
|
||||
Blah blah rubric.
|
||||
</rubric>
|
||||
<prompt>
|
||||
Some prompt.
|
||||
</prompt>
|
||||
<task>
|
||||
<selfassessment>
|
||||
<hintprompt>
|
||||
What hint about this problem would you give to someone?
|
||||
</hintprompt>
|
||||
<submitmessage>
|
||||
Save Succcesful. Thanks for participating!
|
||||
</submitmessage>
|
||||
</selfassessment>
|
||||
</task>
|
||||
<task>
|
||||
<openended min_score_to_attempt="1" max_score_to_attempt="1">
|
||||
<openendedparam>
|
||||
<initial_display>Enter essay here.</initial_display>
|
||||
<answer_display>This is the answer.</answer_display>
|
||||
<grader_payload>{"grader_settings" : "ml_grading.conf",
|
||||
"problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
|
||||
</openendedparam>
|
||||
</openended>
|
||||
</task>
|
||||
</combinedopenended>
|
||||
|
||||
"""
|
||||
|
||||
# Load instance state
|
||||
if instance_state is not None:
|
||||
instance_state = json.loads(instance_state)
|
||||
else:
|
||||
instance_state = {}
|
||||
|
||||
#We need to set the location here so the child modules can use it
|
||||
system.set('location', location)
|
||||
|
||||
#Tells the system which xml definition to load
|
||||
self.current_task_number = instance_state.get('current_task_number', 0)
|
||||
#This loads the states of the individual children
|
||||
self.task_states = instance_state.get('task_states', [])
|
||||
#Overall state of the combined open ended module
|
||||
self.state = instance_state.get('state', self.INITIAL)
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
|
||||
#Allow reset is true if student has failed the criteria to move to the next child task
|
||||
self.allow_reset = instance_state.get('ready_to_reset', False)
|
||||
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
|
||||
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
|
||||
|
||||
#Static data is passed to the child modules to render
|
||||
self.static_data = {
|
||||
'max_score': self._max_score,
|
||||
'max_attempts': self.max_attempts,
|
||||
'prompt': definition['prompt'],
|
||||
'rubric': definition['rubric']
|
||||
}
|
||||
|
||||
self.task_xml = definition['task_xml']
|
||||
self.setup_next_task()
|
||||
|
||||
def get_tag_name(self, xml):
|
||||
"""
|
||||
Gets the tag name of a given xml block.
|
||||
Input: XML string
|
||||
Output: The name of the root tag
|
||||
"""
|
||||
tag = etree.fromstring(xml).tag
|
||||
return tag
|
||||
|
||||
def overwrite_state(self, current_task_state):
|
||||
"""
|
||||
Overwrites an instance state and sets the latest response to the current response. This is used
|
||||
to ensure that the student response is carried over from the first child to the rest.
|
||||
Input: Task state json string
|
||||
Output: Task state json string
|
||||
"""
|
||||
last_response_data = self.get_last_response(self.current_task_number - 1)
|
||||
last_response = last_response_data['response']
|
||||
|
||||
loaded_task_state = json.loads(current_task_state)
|
||||
if loaded_task_state['state'] == self.INITIAL:
|
||||
loaded_task_state['state'] = self.ASSESSING
|
||||
loaded_task_state['created'] = True
|
||||
loaded_task_state['history'].append({'answer': last_response})
|
||||
current_task_state = json.dumps(loaded_task_state)
|
||||
return current_task_state
|
||||
|
||||
def child_modules(self):
|
||||
"""
|
||||
Returns the constructors associated with the child modules in a dictionary. This makes writing functions
|
||||
simpler (saves code duplication)
|
||||
Input: None
|
||||
Output: A dictionary of dictionaries containing the descriptor functions and module functions
|
||||
"""
|
||||
child_modules = {
|
||||
'openended': open_ended_module.OpenEndedModule,
|
||||
'selfassessment': self_assessment_module.SelfAssessmentModule,
|
||||
}
|
||||
child_descriptors = {
|
||||
'openended': open_ended_module.OpenEndedDescriptor,
|
||||
'selfassessment': self_assessment_module.SelfAssessmentDescriptor,
|
||||
}
|
||||
children = {
|
||||
'modules': child_modules,
|
||||
'descriptors': child_descriptors,
|
||||
}
|
||||
return children
|
||||
|
||||
def setup_next_task(self, reset=False):
|
||||
"""
|
||||
Sets up the next task for the module. Creates an instance state if none exists, carries over the answer
|
||||
from the last instance state to the next if needed.
|
||||
Input: A boolean indicating whether or not the reset function is calling.
|
||||
Output: Boolean True (not useful right now)
|
||||
"""
|
||||
current_task_state = None
|
||||
if len(self.task_states) > self.current_task_number:
|
||||
current_task_state = self.task_states[self.current_task_number]
|
||||
|
||||
self.current_task_xml = self.task_xml[self.current_task_number]
|
||||
|
||||
if self.current_task_number > 0:
|
||||
self.allow_reset = self.check_allow_reset()
|
||||
if self.allow_reset:
|
||||
self.current_task_number = self.current_task_number - 1
|
||||
|
||||
current_task_type = self.get_tag_name(self.current_task_xml)
|
||||
|
||||
children = self.child_modules()
|
||||
child_task_module = children['modules'][current_task_type]
|
||||
|
||||
self.current_task_descriptor = children['descriptors'][current_task_type](self.system)
|
||||
|
||||
#This is the xml object created from the xml definition of the current task
|
||||
etree_xml = etree.fromstring(self.current_task_xml)
|
||||
|
||||
#This sends the etree_xml object through the descriptor module of the current task, and
|
||||
#returns the xml parsed by the descriptor
|
||||
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system)
|
||||
if current_task_state is None and self.current_task_number == 0:
|
||||
self.current_task = child_task_module(self.system, self.location,
|
||||
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data)
|
||||
self.task_states.append(self.current_task.get_instance_state())
|
||||
self.state = self.ASSESSING
|
||||
elif current_task_state is None and self.current_task_number > 0:
|
||||
last_response_data = self.get_last_response(self.current_task_number - 1)
|
||||
last_response = last_response_data['response']
|
||||
current_task_state=json.dumps({
|
||||
'state' : self.ASSESSING,
|
||||
'version' : self.STATE_VERSION,
|
||||
'max_score' : self._max_score,
|
||||
'attempts' : 0,
|
||||
'created' : True,
|
||||
'history' : [{'answer' : str(last_response)}],
|
||||
})
|
||||
self.current_task = child_task_module(self.system, self.location,
|
||||
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
|
||||
instance_state=current_task_state)
|
||||
self.task_states.append(self.current_task.get_instance_state())
|
||||
self.state = self.ASSESSING
|
||||
else:
|
||||
if self.current_task_number > 0 and not reset:
|
||||
current_task_state = self.overwrite_state(current_task_state)
|
||||
self.current_task = child_task_module(self.system, self.location,
|
||||
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
|
||||
instance_state=current_task_state)
|
||||
|
||||
log.debug(current_task_state)
|
||||
return True
|
||||
|
||||
def check_allow_reset(self):
|
||||
"""
|
||||
Checks to see if the student has passed the criteria to move to the next module. If not, sets
|
||||
allow_reset to true and halts the student progress through the tasks.
|
||||
Input: None
|
||||
Output: the allow_reset attribute of the current module.
|
||||
"""
|
||||
if not self.allow_reset:
|
||||
if self.current_task_number > 0:
|
||||
last_response_data = self.get_last_response(self.current_task_number - 1)
|
||||
current_response_data = self.get_current_attributes(self.current_task_number)
|
||||
|
||||
if(current_response_data['min_score_to_attempt'] > last_response_data['score']
|
||||
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
|
||||
self.state = self.DONE
|
||||
self.allow_reset = True
|
||||
|
||||
return self.allow_reset
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
Generates a context dictionary that is used to render html.
|
||||
Input: None
|
||||
Output: A dictionary that can be rendered into the combined open ended template.
|
||||
"""
|
||||
task_html = self.get_html_base()
|
||||
#set context variables and render template
|
||||
|
||||
context = {
|
||||
'items': [{'content': task_html}],
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'allow_reset': self.allow_reset,
|
||||
'state': self.state,
|
||||
'task_count': len(self.task_xml),
|
||||
'task_number': self.current_task_number + 1,
|
||||
'status': self.get_status(),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Gets HTML for rendering.
|
||||
Input: None
|
||||
Output: rendered html
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = self.system.render_template('combined_open_ended.html', context)
|
||||
return html
|
||||
|
||||
def get_html_nonsystem(self):
|
||||
"""
|
||||
Gets HTML for rendering via AJAX. Does not use system, because system contains some additional
|
||||
html, which is not appropriate for returning via ajax calls.
|
||||
Input: None
|
||||
Output: HTML rendered directly via Mako
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = render_to_string('combined_open_ended.html', context)
|
||||
return html
|
||||
|
||||
def get_html_base(self):
|
||||
"""
|
||||
Gets the HTML associated with the current child task
|
||||
Input: None
|
||||
Output: Child task HTML
|
||||
"""
|
||||
self.update_task_states()
|
||||
html = self.current_task.get_html(self.system)
|
||||
return_html = rewrite_links(html, self.rewrite_content_links)
|
||||
return return_html
|
||||
|
||||
def get_current_attributes(self, task_number):
|
||||
"""
|
||||
Gets the min and max score to attempt attributes of the specified task.
|
||||
Input: The number of the task.
|
||||
Output: The minimum and maximum scores needed to move on to the specified task.
|
||||
"""
|
||||
task_xml = self.task_xml[task_number]
|
||||
etree_xml = etree.fromstring(task_xml)
|
||||
min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0))
|
||||
max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score))
|
||||
return {'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt}
|
||||
|
||||
def get_last_response(self, task_number):
|
||||
"""
|
||||
Returns data associated with the specified task number, such as the last response, score, etc.
|
||||
Input: The number of the task.
|
||||
Output: A dictionary that contains information about the specified task.
|
||||
"""
|
||||
last_response = ""
|
||||
task_state = self.task_states[task_number]
|
||||
task_xml = self.task_xml[task_number]
|
||||
task_type = self.get_tag_name(task_xml)
|
||||
|
||||
children = self.child_modules()
|
||||
|
||||
task_descriptor = children['descriptors'][task_type](self.system)
|
||||
etree_xml = etree.fromstring(task_xml)
|
||||
|
||||
min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0))
|
||||
max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score))
|
||||
|
||||
task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system)
|
||||
task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor,
|
||||
self.static_data, instance_state=task_state)
|
||||
last_response = task.latest_answer()
|
||||
last_score = task.latest_score()
|
||||
last_post_assessment = task.latest_post_assessment()
|
||||
last_post_feedback = ""
|
||||
if task_type == "openended":
|
||||
last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False)
|
||||
if isinstance(last_post_assessment, list):
|
||||
eval_list = []
|
||||
for i in xrange(0, len(last_post_assessment)):
|
||||
eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i]))
|
||||
last_post_evaluation = "".join(eval_list)
|
||||
else:
|
||||
last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment)
|
||||
last_post_assessment = last_post_evaluation
|
||||
last_correctness = task.is_last_response_correct()
|
||||
max_score = task.max_score()
|
||||
state = task.state
|
||||
last_response_dict = {
|
||||
'response': last_response,
|
||||
'score': last_score,
|
||||
'post_assessment': last_post_assessment,
|
||||
'type': task_type,
|
||||
'max_score': max_score,
|
||||
'state': state,
|
||||
'human_state': task.HUMAN_NAMES[state],
|
||||
'correct': last_correctness,
|
||||
'min_score_to_attempt': min_score_to_attempt,
|
||||
'max_score_to_attempt': max_score_to_attempt,
|
||||
}
|
||||
|
||||
return last_response_dict
|
||||
|
||||
def update_task_states(self):
|
||||
"""
|
||||
Updates the task state of the combined open ended module with the task state of the current child module.
|
||||
Input: None
|
||||
Output: boolean indicating whether or not the task state changed.
|
||||
"""
|
||||
changed = False
|
||||
if not self.allow_reset:
|
||||
self.task_states[self.current_task_number] = self.current_task.get_instance_state()
|
||||
current_task_state = json.loads(self.task_states[self.current_task_number])
|
||||
if current_task_state['state'] == self.DONE:
|
||||
self.current_task_number += 1
|
||||
if self.current_task_number >= (len(self.task_xml)):
|
||||
self.state = self.DONE
|
||||
self.current_task_number = len(self.task_xml) - 1
|
||||
else:
|
||||
self.state = self.INITIAL
|
||||
changed = True
|
||||
self.setup_next_task()
|
||||
return changed
|
||||
|
||||
def update_task_states_ajax(self, return_html):
|
||||
"""
|
||||
Runs the update task states function for ajax calls. Currently the same as update_task_states
|
||||
Input: The html returned by the handle_ajax function of the child
|
||||
Output: New html that should be rendered
|
||||
"""
|
||||
changed = self.update_task_states()
|
||||
if changed:
|
||||
#return_html=self.get_html()
|
||||
pass
|
||||
return return_html
|
||||
|
||||
def get_results(self, get):
|
||||
"""
|
||||
Gets the results of a given grader via ajax.
|
||||
Input: AJAX get dictionary
|
||||
Output: Dictionary to be rendered via ajax that contains the result html.
|
||||
"""
|
||||
task_number = int(get['task_number'])
|
||||
self.update_task_states()
|
||||
response_dict = self.get_last_response(task_number)
|
||||
context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1}
|
||||
html = render_to_string('combined_open_ended_results.html', context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress': 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
"""
|
||||
|
||||
handlers = {
|
||||
'next_problem': self.next_problem,
|
||||
'reset': self.reset,
|
||||
'get_results': self.get_results
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
return_html = self.current_task.handle_ajax(dispatch, get, self.system)
|
||||
return self.update_task_states_ajax(return_html)
|
||||
|
||||
d = handlers[dispatch](get)
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def next_problem(self, get):
|
||||
"""
|
||||
Called via ajax to advance to the next problem.
|
||||
Input: AJAX get request.
|
||||
Output: Dictionary to be rendered
|
||||
"""
|
||||
self.update_task_states()
|
||||
return {'success': True, 'html': self.get_html_nonsystem(), 'allow_reset': self.allow_reset}
|
||||
|
||||
def reset(self, get):
|
||||
"""
|
||||
If resetting is allowed, reset the state of the combined open ended module.
|
||||
Input: AJAX get dictionary
|
||||
Output: AJAX dictionary to tbe rendered
|
||||
"""
|
||||
if self.state != self.DONE:
|
||||
if not self.allow_reset:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
if self.attempts > self.max_attempts:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Too many attempts.'
|
||||
}
|
||||
self.state = self.INITIAL
|
||||
self.allow_reset = False
|
||||
for i in xrange(0, len(self.task_xml)):
|
||||
self.current_task_number = i
|
||||
self.setup_next_task(reset=True)
|
||||
self.current_task.reset(self.system)
|
||||
self.task_states[self.current_task_number] = self.current_task.get_instance_state()
|
||||
self.current_task_number = 0
|
||||
self.allow_reset = False
|
||||
self.setup_next_task()
|
||||
return {'success': True, 'html': self.get_html_nonsystem()}
|
||||
|
||||
def get_instance_state(self):
|
||||
"""
|
||||
Returns the current instance state. The module can be recreated from the instance state.
|
||||
Input: None
|
||||
Output: A dictionary containing the instance state.
|
||||
"""
|
||||
|
||||
state = {
|
||||
'version': self.STATE_VERSION,
|
||||
'current_task_number': self.current_task_number,
|
||||
'state': self.state,
|
||||
'task_states': self.task_states,
|
||||
'attempts': self.attempts,
|
||||
'ready_to_reset': self.allow_reset,
|
||||
}
|
||||
|
||||
return json.dumps(state)
|
||||
|
||||
def get_status(self):
|
||||
"""
|
||||
Gets the status panel to be displayed at the top right.
|
||||
Input: None
|
||||
Output: The status html to be rendered
|
||||
"""
|
||||
status = []
|
||||
for i in xrange(0, self.current_task_number + 1):
|
||||
task_data = self.get_last_response(i)
|
||||
task_data.update({'task_number': i + 1})
|
||||
status.append(task_data)
|
||||
context = {'status_list': status}
|
||||
status_html = self.system.render_template("combined_open_ended_status.html", context)
|
||||
|
||||
return status_html
|
||||
|
||||
|
||||
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding combined open ended questions
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = CombinedOpenEndedModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Pull out the individual tasks, the rubric, and the prompt, and parse
|
||||
|
||||
Returns:
|
||||
{
|
||||
'rubric': 'some-html',
|
||||
'prompt': 'some-html',
|
||||
'task_xml': dictionary of xml strings,
|
||||
}
|
||||
"""
|
||||
expected_children = ['task', 'rubric', 'prompt']
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) == 0:
|
||||
raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child))
|
||||
|
||||
def parse_task(k):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))]
|
||||
|
||||
def parse(k):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return xml_object.xpath(k)[0]
|
||||
|
||||
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
|
||||
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('combinedopenended')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['task']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
129
common/lib/xmodule/xmodule/combined_open_ended_rubric.py
Normal file
129
common/lib/xmodule/xmodule/combined_open_ended_rubric.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
log=logging.getLogger(__name__)
|
||||
|
||||
class CombinedOpenEndedRubric:
|
||||
|
||||
@staticmethod
|
||||
def render_rubric(rubric_xml):
|
||||
try:
|
||||
rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml)
|
||||
html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories})
|
||||
except:
|
||||
log.exception("Could not parse the rubric.")
|
||||
html = rubric_xml
|
||||
return html
|
||||
|
||||
@staticmethod
|
||||
def extract_rubric_categories(element):
|
||||
'''
|
||||
Contstruct a list of categories such that the structure looks like:
|
||||
[ { category: "Category 1 Name",
|
||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||
},
|
||||
{ category: "Category 2 Name",
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
{text: "Option 3 Name", points: 2]}]
|
||||
|
||||
'''
|
||||
element = etree.fromstring(element)
|
||||
categories = []
|
||||
for category in element:
|
||||
if category.tag != 'category':
|
||||
raise Exception("[capa.inputtypes.extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
|
||||
else:
|
||||
categories.append(CombinedOpenEndedRubric.extract_category(category))
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def extract_category(category):
|
||||
'''
|
||||
construct an individual category
|
||||
{category: "Category 1 Name",
|
||||
options: [{text: "Option 1 text", points: 1},
|
||||
{text: "Option 2 text", points: 2}]}
|
||||
|
||||
all sorting and auto-point generation occurs in this function
|
||||
'''
|
||||
|
||||
has_score=False
|
||||
descriptionxml = category[0]
|
||||
scorexml = category[1]
|
||||
if scorexml.tag == "option":
|
||||
optionsxml = category[1:]
|
||||
else:
|
||||
optionsxml = category[2:]
|
||||
has_score=True
|
||||
|
||||
# parse description
|
||||
if descriptionxml.tag != 'description':
|
||||
raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag))
|
||||
|
||||
if has_score:
|
||||
if scorexml.tag != 'score':
|
||||
raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag))
|
||||
|
||||
for option in optionsxml:
|
||||
if option.tag != "option":
|
||||
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
|
||||
description = descriptionxml.text
|
||||
|
||||
if has_score:
|
||||
score = int(scorexml.text)
|
||||
else:
|
||||
score = 0
|
||||
|
||||
cur_points = 0
|
||||
options = []
|
||||
autonumbering = True
|
||||
# parse options
|
||||
for option in optionsxml:
|
||||
if option.tag != 'option':
|
||||
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
else:
|
||||
pointstr = option.get("points")
|
||||
if pointstr:
|
||||
autonumbering = False
|
||||
# try to parse this into an int
|
||||
try:
|
||||
points = int(pointstr)
|
||||
except ValueError:
|
||||
raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr))
|
||||
elif autonumbering:
|
||||
# use the generated one if we're in the right mode
|
||||
points = cur_points
|
||||
cur_points = cur_points + 1
|
||||
else:
|
||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.")
|
||||
optiontext = option.text
|
||||
selected = False
|
||||
if has_score:
|
||||
if points == score:
|
||||
selected = True
|
||||
options.append({'text': option.text, 'points': points, 'selected' : selected})
|
||||
|
||||
# sort and check for duplicates
|
||||
options = sorted(options, key=lambda option: option['points'])
|
||||
CombinedOpenEndedRubric.validate_options(options)
|
||||
|
||||
return {'description': description, 'options': options, 'score' : score, 'has_score' : has_score}
|
||||
|
||||
@staticmethod
|
||||
def validate_options(options):
|
||||
'''
|
||||
Validates a set of options. This can and should be extended to filter out other bad edge cases
|
||||
'''
|
||||
if len(options) == 0:
|
||||
raise Exception("[extract_category]: no options associated with this category")
|
||||
if len(options) == 1:
|
||||
return
|
||||
prev = options[0]['points']
|
||||
for option in options[1:]:
|
||||
if prev == option['points']:
|
||||
raise Exception("[extract_category]: found duplicate point values between two different options")
|
||||
else:
|
||||
prev = option['points']
|
||||
@@ -1,9 +1,9 @@
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import logging
|
||||
from lxml import etree
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import load_grading_policy
|
||||
@@ -13,6 +13,7 @@ from xmodule.timeparse import parse_time, stringify_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
@@ -96,11 +97,27 @@ 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:
|
||||
self._grading_policy = load_grading_policy(policy_str)
|
||||
except:
|
||||
except Exception, err:
|
||||
log.exception('Failed to load grading policy:')
|
||||
self.system.error_tracker("Failed to load grading policy")
|
||||
# Setting this to an empty dictionary will lead to errors when
|
||||
# grading needs to happen, but should allow course staff to see
|
||||
@@ -149,6 +166,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def grade_cutoffs(self):
|
||||
return self._grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
@property
|
||||
def lowest_passing_grade(self):
|
||||
return min(self._grading_policy['GRADE_CUTOFFS'].values())
|
||||
|
||||
@property
|
||||
def tabs(self):
|
||||
"""
|
||||
@@ -160,6 +181,38 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def show_calculator(self):
|
||||
return self.metadata.get("show_calculator", None) == "Yes"
|
||||
|
||||
@property
|
||||
def is_new(self):
|
||||
# The course is "new" if either if the metadata flag is_new is
|
||||
# true or if the course has not started yet
|
||||
flag = self.metadata.get('is_new', None)
|
||||
if flag is None:
|
||||
return self.days_until_start > 1
|
||||
elif isinstance(flag, basestring):
|
||||
return flag.lower() in ['true', 'yes', 'y']
|
||||
else:
|
||||
return bool(flag)
|
||||
|
||||
@property
|
||||
def days_until_start(self):
|
||||
def convert_to_datetime(timestamp):
|
||||
return datetime.fromtimestamp(time.mktime(timestamp))
|
||||
|
||||
start_date = convert_to_datetime(self.start)
|
||||
|
||||
# Try to use course advertised date if we can parse it
|
||||
advertised_start = self.metadata.get('advertised_start', None)
|
||||
if advertised_start:
|
||||
try:
|
||||
start_date = datetime.strptime(advertised_start,
|
||||
"%Y-%m-%dT%H:%M")
|
||||
except ValueError:
|
||||
pass # Invalid date, keep using 'start''
|
||||
|
||||
now = convert_to_datetime(time.gmtime())
|
||||
days_until_start = (start_date - now).days
|
||||
return days_until_start
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
"""
|
||||
@@ -239,7 +292,6 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
raise ValueError("{0} is not a course location".format(loc))
|
||||
return "/".join([loc.org, loc.course, loc.name])
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Return the course_id for this course"""
|
||||
@@ -247,7 +299,20 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
displayed_start = self._try_parse_time('advertised_start') or self.start
|
||||
parsed_advertised_start = self._try_parse_time('advertised_start')
|
||||
|
||||
# If the advertised start isn't a real date string, we assume it's free
|
||||
# form text...
|
||||
if parsed_advertised_start is None and \
|
||||
('advertised_start' in self.metadata):
|
||||
return self.metadata['advertised_start']
|
||||
|
||||
displayed_start = parsed_advertised_start or self.start
|
||||
|
||||
# If we have neither an advertised start or a real start, just return TBD
|
||||
if not displayed_start:
|
||||
return "TBD"
|
||||
|
||||
return time.strftime("%b %d, %Y", displayed_start)
|
||||
|
||||
@property
|
||||
@@ -292,7 +357,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
return False
|
||||
except:
|
||||
log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
|
||||
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -312,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
|
||||
@@ -323,4 +470,3 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
@@ -121,16 +121,6 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
&.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
@@ -250,6 +240,13 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
.reload
|
||||
{
|
||||
float:right;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.grader-status {
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
@@ -266,6 +263,13 @@ section.problem {
|
||||
margin: -7px 7px 0 0;
|
||||
}
|
||||
|
||||
.grading {
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
padding-left: 25px;
|
||||
text-indent: 0px;
|
||||
margin: 0px 7px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
text-transform: capitalize;
|
||||
@@ -293,6 +297,51 @@ section.problem {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.evaluation {
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.feedback-on-feedback {
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.evaluation-response {
|
||||
header {
|
||||
text-align: right;
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.evaluation-scoring {
|
||||
.scoring-list {
|
||||
list-style-type: none;
|
||||
margin-left: 3px;
|
||||
|
||||
li {
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
display:inline;
|
||||
margin-left: 50px;
|
||||
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.submit-message-container {
|
||||
margin: 10px 0px ;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,6 +679,10 @@ section.problem {
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
header {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.shortform {
|
||||
font-weight: bold;
|
||||
@@ -685,6 +738,21 @@ section.problem {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
|
||||
.markup-text{
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
|
||||
bs {
|
||||
color: #BB0000;
|
||||
}
|
||||
|
||||
bg {
|
||||
color: #BDA046;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
626
common/lib/xmodule/xmodule/css/combinedopenended/display.scss
Normal file
626
common/lib/xmodule/xmodule/css/combinedopenended/display.scss
Normal file
@@ -0,0 +1,626 @@
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.problem-header {
|
||||
section.staff {
|
||||
margin-top: 30px;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
color: darken($error-red, 10%);
|
||||
}
|
||||
|
||||
section.combined-open-ended {
|
||||
@include clearfix;
|
||||
.status-container
|
||||
{
|
||||
float:right;
|
||||
width:40%;
|
||||
}
|
||||
.item-container
|
||||
{
|
||||
float:left;
|
||||
width: 53%;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.result-container
|
||||
{
|
||||
float:left;
|
||||
width: 93%;
|
||||
position:relative;
|
||||
}
|
||||
}
|
||||
|
||||
section.combined-open-ended-status {
|
||||
|
||||
.statusitem {
|
||||
background-color: #FAFAFA;
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.statusitem-current {
|
||||
background-color: #BEBEBE;
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.correct {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 25px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.incorrect {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.result-container {
|
||||
|
||||
.evaluation {
|
||||
p {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-on-feedback {
|
||||
height: 100px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.evaluation-response {
|
||||
header {
|
||||
text-align: right;
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.evaluation-scoring {
|
||||
.scoring-list {
|
||||
list-style-type: none;
|
||||
margin-left: 3px;
|
||||
|
||||
li {
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
display:inline;
|
||||
margin-left: 0px;
|
||||
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.submit-message-container {
|
||||
margin: 10px 0px ;
|
||||
}
|
||||
|
||||
.external-grader-message {
|
||||
section {
|
||||
padding-left: 20px;
|
||||
background-color: #FAFAFA;
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
header {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.shortform {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.longform {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
|
||||
.result-errors {
|
||||
margin: 5px;
|
||||
padding: 10px 10px 10px 40px;
|
||||
background: url('../images/incorrect-icon.png') center left no-repeat;
|
||||
li {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
|
||||
.result-output {
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
|
||||
h4 {
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 24pt;
|
||||
}
|
||||
}
|
||||
|
||||
.result-correct {
|
||||
background: url('../images/correct-icon.png') left 20px no-repeat;
|
||||
.result-actual-output {
|
||||
color: #090;
|
||||
}
|
||||
}
|
||||
|
||||
.result-incorrect {
|
||||
background: url('../images/incorrect-icon.png') left 20px no-repeat;
|
||||
.result-actual-output {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
|
||||
.markup-text{
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
|
||||
bs {
|
||||
color: #BB0000;
|
||||
}
|
||||
|
||||
bg {
|
||||
color: #BDA046;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.result-container, section.open-ended-child {
|
||||
.rubric {
|
||||
tr {
|
||||
margin:10px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
td {
|
||||
padding: 20px 0px;
|
||||
margin: 10px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
th {
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
label,
|
||||
.view-only {
|
||||
margin:10px;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
width: 200px;
|
||||
height:100%;
|
||||
display: inline-block;
|
||||
min-height: 50px;
|
||||
min-width: 50px;
|
||||
background-color: #CCC;
|
||||
font-size: 1em;
|
||||
}
|
||||
.grade {
|
||||
position: absolute;
|
||||
bottom:0px;
|
||||
right:0px;
|
||||
margin:10px;
|
||||
}
|
||||
.selected-grade {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white; }
|
||||
input[class='score-selection'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.open-ended-child {
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
|
||||
canvas, img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
ol.enumerate {
|
||||
li {
|
||||
&:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.solution-span {
|
||||
> span {
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
padding: 9px 15px 20px;
|
||||
background: #FFF;
|
||||
position: relative;
|
||||
@include box-shadow(inset 0 0 0 1px #eee);
|
||||
@include border-radius(3px);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
&.answer {
|
||||
margin-top: -2px;
|
||||
}
|
||||
&.status {
|
||||
text-indent: -9999px;
|
||||
margin: 8px 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div.unanswered {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
div.correct, div.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: green;
|
||||
}
|
||||
}
|
||||
|
||||
div.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
div.incorrect, div.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
p.answer {
|
||||
@include inline-block();
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
|
||||
&:before {
|
||||
content: "Answer: ";
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
|
||||
}
|
||||
&:empty {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered, &.ui-icon-bullet {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&.processing, &.ui-icon-processing {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.reload
|
||||
{
|
||||
float:right;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.grader-status {
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0;
|
||||
margin-bottom: 20px;
|
||||
@include clearfix;
|
||||
|
||||
span {
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: -7px 7px 0 0;
|
||||
}
|
||||
|
||||
.grading {
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
padding-left: 25px;
|
||||
text-indent: 0px;
|
||||
margin: 0px 7px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&.file {
|
||||
background: #FFF;
|
||||
margin-top: 20px;
|
||||
padding: 20px 0 0 0;
|
||||
|
||||
border: {
|
||||
top: 1px solid #eee;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
p.debug {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
form.option-input {
|
||||
margin: -10px 0 20px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
select {
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
dl {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: .5em;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 1.4em;
|
||||
margin-bottom: lh(.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: lh();
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #ddd;
|
||||
border: none;
|
||||
clear: both;
|
||||
color: #ddd;
|
||||
float: none;
|
||||
height: 1px;
|
||||
margin: 0 0 .75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#{$all-text-inputs} {
|
||||
display: inline;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
section.action {
|
||||
margin-top: 20px;
|
||||
|
||||
input.save {
|
||||
@extend .blue-button;
|
||||
}
|
||||
|
||||
.submission_feedback {
|
||||
// background: #F3F3F3;
|
||||
// border: 1px solid #ddd;
|
||||
// @include border-radius(3px);
|
||||
// padding: 8px 12px;
|
||||
// margin-top: 10px;
|
||||
@include inline-block;
|
||||
font-style: italic;
|
||||
margin: 8px 0 0 10px;
|
||||
color: #777;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-solution {
|
||||
> p:first-child {
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.open-ended-alert {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #EBE8BF;
|
||||
border-radius: 3px;
|
||||
background: #FFFCDD;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.capa_reset {
|
||||
padding: 25px;
|
||||
border: 1px solid $error-red;
|
||||
background-color: lighten($error-red, 25%);
|
||||
border-radius: 3px;
|
||||
font-size: 1em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.capa_reset>h2 {
|
||||
color: #AA0000;
|
||||
}
|
||||
.capa_reset li {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -149,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
'''
|
||||
try:
|
||||
xml = etree.fromstring(self.definition['data']['contents'])
|
||||
return etree.tostring(xml)
|
||||
return etree.tostring(xml, encoding='unicode')
|
||||
except etree.XMLSyntaxError:
|
||||
# still not valid.
|
||||
root = etree.Element('error')
|
||||
root.text = self.definition['data']['contents']
|
||||
err_node = etree.SubElement(root, 'error_msg')
|
||||
err_node.text = self.definition['data']['error_msg']
|
||||
return etree.tostring(root)
|
||||
return etree.tostring(root, encoding='unicode')
|
||||
|
||||
|
||||
class NonStaffErrorDescriptor(ErrorDescriptor):
|
||||
|
||||
@@ -316,7 +316,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
min_count = 2 would produce the labels "Assignment 3", "Assignment 4"
|
||||
|
||||
"""
|
||||
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None, show_only_average=False, starting_index=1):
|
||||
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None, show_only_average=False, hide_average=False, starting_index=1):
|
||||
self.type = type
|
||||
self.min_count = min_count
|
||||
self.drop_count = drop_count
|
||||
@@ -325,6 +325,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
self.short_label = short_label or self.type
|
||||
self.show_only_average = show_only_average
|
||||
self.starting_index = starting_index
|
||||
self.hide_average = hide_average
|
||||
|
||||
def grade(self, grade_sheet, generate_random_scores=False):
|
||||
def totalWithDrops(breakdown, drop_count):
|
||||
@@ -385,7 +386,8 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
if self.show_only_average:
|
||||
breakdown = []
|
||||
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
if not self.hide_average:
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': breakdown,
|
||||
|
||||
194
common/lib/xmodule/xmodule/gst_module.py
Normal file
194
common/lib/xmodule/xmodule/gst_module.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Graphical slider tool module is ungraded xmodule used by students to
|
||||
understand functional dependencies.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
from lxml import html
|
||||
import xmltodict
|
||||
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.stringify import stringify_children
|
||||
from pkg_resources import resource_string
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphicalSliderToolModule(XModule):
|
||||
''' Graphical-Slider-Tool Module
|
||||
'''
|
||||
|
||||
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/gst_main.js'),
|
||||
resource_string(__name__, 'js/src/graphical_slider_tool/state.js'),
|
||||
resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'),
|
||||
resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'),
|
||||
resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'),
|
||||
resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'),
|
||||
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"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor, instance_state=None,
|
||||
shared_state=None, **kwargs):
|
||||
"""
|
||||
For XML file format please look at documentation. TODO - receive
|
||||
information where to store XML documentation.
|
||||
"""
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
def get_html(self):
|
||||
""" Renders parameters to template. """
|
||||
|
||||
# these 3 will be used in class methods
|
||||
self.html_id = self.location.html_id()
|
||||
self.html_class = self.location.category
|
||||
self.configuration_json = self.build_configuration_json()
|
||||
params = {
|
||||
'gst_html': self.substitute_controls(self.definition['render']),
|
||||
'element_id': self.html_id,
|
||||
'element_class': self.html_class,
|
||||
'configuration_json': self.configuration_json
|
||||
}
|
||||
self.content = self.system.render_template(
|
||||
'graphical_slider_tool.html', params)
|
||||
return self.content
|
||||
|
||||
def substitute_controls(self, html_string):
|
||||
""" Substitutes control elements (slider, textbox and plot) in
|
||||
html_string with their divs. Html_string is content of <render> tag
|
||||
inside <graphical_slider_tool> tag. Documentation on how information in
|
||||
<render> tag is organized and processed is located in:
|
||||
mitx/docs/build/html/graphical_slider_tool.html.
|
||||
|
||||
Args:
|
||||
html_string: content of <render> tag, with controls as xml tags,
|
||||
e.g. <slider var="a"/>.
|
||||
|
||||
Returns:
|
||||
html_string with control tags replaced by proper divs
|
||||
(<slider var="a"/> -> <div class="....slider" > </div>)
|
||||
"""
|
||||
|
||||
xml = html.fromstring(html_string)
|
||||
|
||||
#substitute plot, if presented
|
||||
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
|
||||
style="{style}"></div>'
|
||||
plot_el = xml.xpath('//plot')
|
||||
if plot_el:
|
||||
plot_el = plot_el[0]
|
||||
plot_el.getparent().replace(plot_el, html.fromstring(
|
||||
plot_div.format(element_class=self.html_class,
|
||||
element_id=self.html_id,
|
||||
style=plot_el.get('style', ""))))
|
||||
|
||||
#substitute sliders
|
||||
slider_div = '<div class="{element_class}_slider" \
|
||||
id="{element_id}_slider_{var}" \
|
||||
data-var="{var}" \
|
||||
style="{style}">\
|
||||
</div>'
|
||||
slider_els = xml.xpath('//slider')
|
||||
for slider_el in slider_els:
|
||||
slider_el.getparent().replace(slider_el, html.fromstring(
|
||||
slider_div.format(element_class=self.html_class,
|
||||
element_id=self.html_id,
|
||||
var=slider_el.get('var', ""),
|
||||
style=slider_el.get('style', ""))))
|
||||
|
||||
# substitute inputs aka textboxes
|
||||
input_div = '<input class="{element_class}_input" \
|
||||
id="{element_id}_input_{var}_{input_index}" \
|
||||
data-var="{var}" style="{style}"/>'
|
||||
input_els = xml.xpath('//textbox')
|
||||
for input_index, input_el in enumerate(input_els):
|
||||
input_el.getparent().replace(input_el, html.fromstring(
|
||||
input_div.format(element_class=self.html_class,
|
||||
element_id=self.html_id,
|
||||
var=input_el.get('var', ""),
|
||||
style=input_el.get('style', ""),
|
||||
input_index=input_index)))
|
||||
|
||||
return html.tostring(xml)
|
||||
|
||||
def build_configuration_json(self):
|
||||
"""Creates json element from xml element (with aim to transfer later
|
||||
directly to javascript via hidden field in template). Steps:
|
||||
|
||||
1. Convert xml tree to python dict.
|
||||
|
||||
2. Dump dict to json.
|
||||
|
||||
"""
|
||||
# <root> added for interface compatibility with xmltodict.parse
|
||||
# class added for javascript's part purposes
|
||||
return json.dumps(xmltodict.parse('<root class="' + self.html_class +
|
||||
'">' + self.definition['configuration'] + '</root>'))
|
||||
|
||||
|
||||
class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
module_class = GraphicalSliderToolModule
|
||||
template_dir_name = 'graphical_slider_tool'
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Pull out the data into dictionary.
|
||||
|
||||
Args:
|
||||
xml_object: xml from file.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
# check for presense of required tags in xml
|
||||
expected_children_level_0 = ['render', 'configuration']
|
||||
for child in expected_children_level_0:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
raise ValueError("Graphical Slider Tool definition must include \
|
||||
exactly one '{0}' tag".format(child))
|
||||
|
||||
expected_children_level_1 = ['functions']
|
||||
for child in expected_children_level_1:
|
||||
if len(xml_object.xpath('configuration')[0].xpath(child)) != 1:
|
||||
raise ValueError("Graphical Slider Tool definition must include \
|
||||
exactly one '{0}' tag".format(child))
|
||||
# finished
|
||||
|
||||
def parse(k):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return stringify_children(xml_object.xpath(k)[0])
|
||||
return {
|
||||
'render': parse('render'),
|
||||
'configuration': parse('configuration')
|
||||
}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
xml_object = etree.Element('graphical_slider_tool')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
xml_object.append(child_node)
|
||||
|
||||
for child in ['render', 'configuration']:
|
||||
add_child(child)
|
||||
|
||||
return xml_object
|
||||
@@ -7,15 +7,14 @@ from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
|
||||
from .x_module import XModule
|
||||
from pkg_resources import resource_string
|
||||
from .xml_module import XmlDescriptor, name_to_pathname
|
||||
from .editing_module import EditingDescriptor
|
||||
from .stringify import stringify_children
|
||||
from .html_checker import check_html
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.html_checker import check_html
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.stringify import stringify_children
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor, name_to_pathname
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -123,7 +122,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
html = file.read()
|
||||
html = file.read().decode('utf-8')
|
||||
# Log a warning if we can't parse the file, but don't error
|
||||
if not check_html(html):
|
||||
msg = "Couldn't parse html in {0}.".format(filepath)
|
||||
@@ -164,7 +163,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(self.definition['data'])
|
||||
file.write(self.definition['data'].encode('utf-8'))
|
||||
|
||||
# write out the relative name
|
||||
relname = path(pathname).basename()
|
||||
|
||||
@@ -1953,7 +1953,7 @@ cktsim = (function() {
|
||||
var module = {
|
||||
'Circuit': Circuit,
|
||||
'parse_number': parse_number,
|
||||
'parse_source': parse_source,
|
||||
'parse_source': parse_source
|
||||
}
|
||||
return module;
|
||||
}());
|
||||
@@ -2068,7 +2068,7 @@ schematic = (function() {
|
||||
'n': [NFet, 'NFet'],
|
||||
'p': [PFet, 'PFet'],
|
||||
's': [Probe, 'Voltage Probe'],
|
||||
'a': [Ammeter, 'Current Probe'],
|
||||
'a': [Ammeter, 'Current Probe']
|
||||
};
|
||||
|
||||
// global clipboard
|
||||
@@ -5502,7 +5502,7 @@ schematic = (function() {
|
||||
'magenta' : 'rgb(255,64,255)',
|
||||
'yellow': 'rgb(255,255,64)',
|
||||
'black': 'rgb(0,0,0)',
|
||||
'x-axis': undefined,
|
||||
'x-axis': undefined
|
||||
};
|
||||
|
||||
function Probe(x,y,rotation,color,offset) {
|
||||
@@ -6100,7 +6100,7 @@ schematic = (function() {
|
||||
'Amplitude',
|
||||
'Frequency (Hz)',
|
||||
'Delay until sin starts (secs)',
|
||||
'Phase offset (degrees)'],
|
||||
'Phase offset (degrees)']
|
||||
}
|
||||
|
||||
// build property editor div
|
||||
@@ -6300,7 +6300,7 @@ schematic = (function() {
|
||||
|
||||
var module = {
|
||||
'Schematic': Schematic,
|
||||
'component_slider': component_slider,
|
||||
'component_slider': component_slider
|
||||
}
|
||||
return module;
|
||||
}());
|
||||
|
||||
@@ -22,7 +22,7 @@ class @Collapsible
|
||||
if $(event.target).text() == 'See full output'
|
||||
new_text = 'Hide output'
|
||||
else
|
||||
new_text = 'See full ouput'
|
||||
new_text = 'See full output'
|
||||
$(event.target).text(new_text)
|
||||
|
||||
@toggleHint: (event) =>
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
class @CombinedOpenEnded
|
||||
constructor: (element) ->
|
||||
@element=element
|
||||
@reinitialize(element)
|
||||
|
||||
reinitialize: (element) ->
|
||||
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
|
||||
@el = $(element).find('section.combined-open-ended')
|
||||
@combined_open_ended=$(element).find('section.combined-open-ended')
|
||||
@id = @el.data('id')
|
||||
@ajax_url = @el.data('ajax-url')
|
||||
@state = @el.data('state')
|
||||
@task_count = @el.data('task-count')
|
||||
@task_number = @el.data('task-number')
|
||||
|
||||
@allow_reset = @el.data('allow_reset')
|
||||
@reset_button = @$('.reset-button')
|
||||
@reset_button.click @reset
|
||||
@next_problem_button = @$('.next-step-button')
|
||||
@next_problem_button.click @next_problem
|
||||
|
||||
@show_results_button=@$('.show-results-button')
|
||||
@show_results_button.click @show_results
|
||||
|
||||
# valid states: 'initial', 'assessing', 'post_assessment', 'done'
|
||||
Collapsible.setCollapsibles(@el)
|
||||
@submit_evaluation_button = $('.submit-evaluation-button')
|
||||
@submit_evaluation_button.click @message_post
|
||||
|
||||
@results_container = $('.result-container')
|
||||
|
||||
# Where to put the rubric once we load it
|
||||
@el = $(element).find('section.open-ended-child')
|
||||
@errors_area = @$('.error')
|
||||
@answer_area = @$('textarea.answer')
|
||||
|
||||
@rubric_wrapper = @$('.rubric-wrapper')
|
||||
@hint_wrapper = @$('.hint-wrapper')
|
||||
@message_wrapper = @$('.message-wrapper')
|
||||
@submit_button = @$('.submit-button')
|
||||
@child_state = @el.data('state')
|
||||
@child_type = @el.data('child-type')
|
||||
if @child_type=="openended"
|
||||
@skip_button = @$('.skip-button')
|
||||
@skip_button.click @skip_post_assessment
|
||||
|
||||
@open_ended_child= @$('.open-ended-child')
|
||||
|
||||
@find_assessment_elements()
|
||||
@find_hint_elements()
|
||||
|
||||
@rebind()
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
show_results: (event) =>
|
||||
status_item = $(event.target).parent().parent()
|
||||
status_number = status_item.data('status-number')
|
||||
data = {'task_number' : status_number}
|
||||
$.postWithPrefix "#{@ajax_url}/get_results", data, (response) =>
|
||||
if response.success
|
||||
@results_container.after(response.html).remove()
|
||||
@results_container = $('div.result-container')
|
||||
@submit_evaluation_button = $('.submit-evaluation-button')
|
||||
@submit_evaluation_button.click @message_post
|
||||
Collapsible.setCollapsibles(@results_container)
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
|
||||
message_post: (event)=>
|
||||
Logger.log 'message_post', @answers
|
||||
external_grader_message=$(event.target).parent().parent().parent()
|
||||
evaluation_scoring = $(event.target).parent()
|
||||
|
||||
fd = new FormData()
|
||||
feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value
|
||||
submission_id = external_grader_message.find('input.submission_id')[0].value
|
||||
grader_id = external_grader_message.find('input.grader_id')[0].value
|
||||
score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val()
|
||||
|
||||
fd.append('feedback', feedback)
|
||||
fd.append('submission_id', submission_id)
|
||||
fd.append('grader_id', grader_id)
|
||||
if(!score)
|
||||
@gentle_alert "You need to pick a rating before you can submit."
|
||||
return
|
||||
else
|
||||
fd.append('score', score)
|
||||
|
||||
settings =
|
||||
type: "POST"
|
||||
data: fd
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (response) =>
|
||||
@gentle_alert response.msg
|
||||
$('section.evaluation').slideToggle()
|
||||
@message_wrapper.html(response.message_html)
|
||||
|
||||
$.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings)
|
||||
|
||||
|
||||
rebind: () =>
|
||||
# rebind to the appropriate function for the current state
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.show()
|
||||
@reset_button.hide()
|
||||
@next_problem_button.hide()
|
||||
@hint_area.attr('disabled', false)
|
||||
|
||||
if @child_type=="openended"
|
||||
@skip_button.hide()
|
||||
if @allow_reset=="True"
|
||||
@reset_button.show()
|
||||
@submit_button.hide()
|
||||
@answer_area.attr("disabled", true)
|
||||
@hint_area.attr('disabled', true)
|
||||
else if @child_state == 'initial'
|
||||
@answer_area.attr("disabled", false)
|
||||
@submit_button.prop('value', 'Submit')
|
||||
@submit_button.click @save_answer
|
||||
else if @child_state == 'assessing'
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit assessment')
|
||||
@submit_button.click @save_assessment
|
||||
if @child_type == "openended"
|
||||
@submit_button.hide()
|
||||
@queueing()
|
||||
else if @child_state == 'post_assessment'
|
||||
if @child_type=="openended"
|
||||
@skip_button.show()
|
||||
@skip_post_assessment()
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit post-assessment')
|
||||
if @child_type=="selfassessment"
|
||||
@submit_button.click @save_hint
|
||||
else
|
||||
@submit_button.click @message_post
|
||||
else if @child_state == 'done'
|
||||
@answer_area.attr("disabled", true)
|
||||
@hint_area.attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
if @child_type=="openended"
|
||||
@skip_button.hide()
|
||||
if @task_number<@task_count
|
||||
@next_problem()
|
||||
else
|
||||
@reset_button.show()
|
||||
|
||||
|
||||
find_assessment_elements: ->
|
||||
@assessment = @$('select.assessment')
|
||||
|
||||
find_hint_elements: ->
|
||||
@hint_area = @$('textarea.post_assessment')
|
||||
|
||||
save_answer: (event) =>
|
||||
event.preventDefault()
|
||||
if @child_state == 'initial'
|
||||
data = {'student_answer' : @answer_area.val()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
|
||||
if response.success
|
||||
@rubric_wrapper.html(response.rubric_html)
|
||||
@child_state = 'assessing'
|
||||
@find_assessment_elements()
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
save_assessment: (event) =>
|
||||
event.preventDefault()
|
||||
if @child_state == 'assessing'
|
||||
data = {'assessment' : @assessment.find(':selected').text()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
|
||||
if response.success
|
||||
@child_state = response.state
|
||||
|
||||
if @child_state == 'post_assessment'
|
||||
@hint_wrapper.html(response.hint_html)
|
||||
@find_hint_elements()
|
||||
else if @child_state == 'done'
|
||||
@message_wrapper.html(response.message_html)
|
||||
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
save_hint: (event) =>
|
||||
event.preventDefault()
|
||||
if @child_state == 'post_assessment'
|
||||
data = {'hint' : @hint_area.val()}
|
||||
|
||||
$.postWithPrefix "#{@ajax_url}/save_post_assessment", data, (response) =>
|
||||
if response.success
|
||||
@message_wrapper.html(response.message_html)
|
||||
@child_state = 'done'
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
skip_post_assessment: =>
|
||||
if @child_state == 'post_assessment'
|
||||
|
||||
$.postWithPrefix "#{@ajax_url}/skip_post_assessment", {}, (response) =>
|
||||
if response.success
|
||||
@child_state = 'done'
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
reset: (event) =>
|
||||
event.preventDefault()
|
||||
if @child_state == 'done' or @allow_reset=="True"
|
||||
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
|
||||
if response.success
|
||||
@answer_area.val('')
|
||||
@rubric_wrapper.html('')
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@child_state = 'initial'
|
||||
@combined_open_ended.after(response.html).remove()
|
||||
@allow_reset="False"
|
||||
@reinitialize(@element)
|
||||
@rebind()
|
||||
@reset_button.hide()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
next_problem: =>
|
||||
if @child_state == 'done'
|
||||
$.postWithPrefix "#{@ajax_url}/next_problem", {}, (response) =>
|
||||
if response.success
|
||||
@answer_area.val('')
|
||||
@rubric_wrapper.html('')
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@child_state = 'initial'
|
||||
@combined_open_ended.after(response.html).remove()
|
||||
@reinitialize(@element)
|
||||
@rebind()
|
||||
@next_problem_button.hide()
|
||||
if !response.allow_reset
|
||||
@gentle_alert "Moved to next step."
|
||||
else
|
||||
@gentle_alert "Your score did not meet the criteria to move to the next step."
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
gentle_alert: (msg) =>
|
||||
if @el.find('.open-ended-alert').length
|
||||
@el.find('.open-ended-alert').remove()
|
||||
alert_elem = "<div class='open-ended-alert'>" + msg + "</div>"
|
||||
@el.find('.open-ended-action').after(alert_elem)
|
||||
@el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700)
|
||||
|
||||
queueing: =>
|
||||
if @child_state=="assessing" and @child_type=="openended"
|
||||
if window.queuePollerID # Only one poller 'thread' per Problem
|
||||
window.clearTimeout(window.queuePollerID)
|
||||
window.queuePollerID = window.setTimeout(@poll, 10000)
|
||||
|
||||
poll: =>
|
||||
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
|
||||
if response.state == "done" or response.state=="post_assessment"
|
||||
delete window.queuePollerID
|
||||
location.reload()
|
||||
else
|
||||
window.queuePollerID = window.setTimeout(@poll, 10000)
|
||||
@@ -0,0 +1,139 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('ElOutput', ['logme'], function (logme) {
|
||||
|
||||
return ElOutput;
|
||||
|
||||
function ElOutput(config, state) {
|
||||
|
||||
if ($.isPlainObject(config.functions.function)) {
|
||||
processFuncObj(config.functions.function);
|
||||
} else if ($.isArray(config.functions.function)) {
|
||||
(function (c1) {
|
||||
while (c1 < config.functions.function.length) {
|
||||
if ($.isPlainObject(config.functions.function[c1])) {
|
||||
processFuncObj(config.functions.function[c1]);
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
function processFuncObj(obj) {
|
||||
var paramNames, funcString, func, el, disableAutoReturn, updateOnEvent;
|
||||
|
||||
// We are only interested in functions that are meant for output to an
|
||||
// element.
|
||||
if (
|
||||
(typeof obj['@output'] !== 'string') ||
|
||||
((obj['@output'].toLowerCase() !== 'element') && (obj['@output'].toLowerCase() !== 'none'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj['@el_id'] !== 'string') {
|
||||
logme('ERROR: You specified "output" as "element", but did not spify "el_id".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj['#text'] !== 'string') {
|
||||
logme('ERROR: Function body is not defined.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateOnEvent = 'slide';
|
||||
if (
|
||||
(obj.hasOwnProperty('@update_on') === true) &&
|
||||
(typeof obj['@update_on'] === 'string') &&
|
||||
((obj['@update_on'].toLowerCase() === 'slide') || (obj['@update_on'].toLowerCase() === 'change'))
|
||||
) {
|
||||
updateOnEvent = obj['@update_on'].toLowerCase();
|
||||
}
|
||||
|
||||
disableAutoReturn = obj['@disable_auto_return'];
|
||||
|
||||
funcString = obj['#text'];
|
||||
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that all HTML entities are converted to their proper
|
||||
// ASCII text equivalents.
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
paramNames = state.getAllParameterNames();
|
||||
paramNames.push(funcString);
|
||||
|
||||
try {
|
||||
func = Function.apply(null, paramNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: The function body "' +
|
||||
funcString +
|
||||
'" was not converted by the Function constructor.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
if (obj['@output'].toLowerCase() !== 'none') {
|
||||
el = $('#' + obj['@el_id']);
|
||||
|
||||
if (el.length !== 1) {
|
||||
logme(
|
||||
'ERROR: DOM element with ID "' + obj['@el_id'] + '" ' +
|
||||
'not found. Dynamic element not created.'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
el.html(func.apply(window, state.getAllParameterValues()));
|
||||
} else {
|
||||
el = null;
|
||||
func.apply(window, state.getAllParameterValues());
|
||||
}
|
||||
|
||||
state.addDynamicEl(el, func, obj['@el_id'], updateOnEvent);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -0,0 +1,113 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('GLabelElOutput', ['logme'], function (logme) {
|
||||
return GLabelElOutput;
|
||||
|
||||
function GLabelElOutput(config, state) {
|
||||
if ($.isPlainObject(config.functions.function)) {
|
||||
processFuncObj(config.functions.function);
|
||||
} else if ($.isArray(config.functions.function)) {
|
||||
(function (c1) {
|
||||
while (c1 < config.functions.function.length) {
|
||||
if ($.isPlainObject(config.functions.function[c1])) {
|
||||
processFuncObj(config.functions.function[c1]);
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
function processFuncObj(obj) {
|
||||
var paramNames, funcString, func, disableAutoReturn;
|
||||
|
||||
// We are only interested in functions that are meant for output to an
|
||||
// element.
|
||||
if (
|
||||
(typeof obj['@output'] !== 'string') ||
|
||||
(obj['@output'].toLowerCase() !== 'plot_label')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj['@el_id'] !== 'string') {
|
||||
logme('ERROR: You specified "output" as "plot_label", but did not spify "el_id".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj['#text'] !== 'string') {
|
||||
logme('ERROR: Function body is not defined.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
disableAutoReturn = obj['@disable_auto_return'];
|
||||
|
||||
funcString = obj['#text'];
|
||||
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that all HTML entities are converted to their proper
|
||||
// ASCII text equivalents.
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
paramNames = state.getAllParameterNames();
|
||||
paramNames.push(funcString);
|
||||
|
||||
try {
|
||||
func = Function.apply(null, paramNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: The function body "' +
|
||||
funcString +
|
||||
'" was not converted by the Function constructor.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
state.plde.push({
|
||||
'elId': obj['@el_id'],
|
||||
'func': func
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -0,0 +1,23 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('GeneralMethods', [], function () {
|
||||
if (!String.prototype.trim) {
|
||||
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
|
||||
String.prototype.trim = function trim(str) {
|
||||
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'module_name': 'GeneralMethods',
|
||||
'module_status': 'OK'
|
||||
};
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
1496
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js
Normal file
1496
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js
Normal file
@@ -0,0 +1,1496 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('Graph', ['logme'], function (logme) {
|
||||
|
||||
return Graph;
|
||||
|
||||
function Graph(gstId, config, state) {
|
||||
var plotDiv, dataSeries, functions, xaxis, yaxis, numPoints, xrange,
|
||||
asymptotes, movingLabels, xTicksNames, yTicksNames, graphBarWidth, graphBarAlign;
|
||||
|
||||
// We need plot configuration settings. Without them we can't continue.
|
||||
if ($.isPlainObject(config.plot) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We must have a graph container DIV element available in order to
|
||||
// proceed.
|
||||
plotDiv = $('#' + gstId + '_plot');
|
||||
if (plotDiv.length === 0) {
|
||||
logme('ERROR: Could not find the plot DIV with ID "' + gstId + '_plot".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (plotDiv.width() === 0) {
|
||||
plotDiv.width(300);
|
||||
}
|
||||
|
||||
// Sometimes, when height is not explicitly set via CSS (or by some
|
||||
// other means), it is 0 pixels by default. When Flot will try to plot
|
||||
// a graph in this DIV with 0 height, then it will raise an error. To
|
||||
// prevent this, we will set it to be equal to the width.
|
||||
if (plotDiv.height() === 0) {
|
||||
plotDiv.height(plotDiv.width());
|
||||
}
|
||||
|
||||
plotDiv.css('position', 'relative');
|
||||
|
||||
// Configure some settings for the graph.
|
||||
if (setGraphXRange() === false) {
|
||||
logme('ERROR: Could not configure the xrange. Will not continue.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (setGraphAxes() === false) {
|
||||
logme('ERROR: Could not process configuration for the axes.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
graphBarWidth = 1;
|
||||
graphBarAlign = null;
|
||||
|
||||
getBarWidth();
|
||||
getBarAlign();
|
||||
|
||||
// Get the user defined functions. If there aren't any, don't do
|
||||
// anything else.
|
||||
createFunctions();
|
||||
|
||||
if (functions.length === 0) {
|
||||
logme('ERROR: No functions were specified, or something went wrong.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (createMarkingsFunctions() === false) {
|
||||
return;
|
||||
}
|
||||
if (createMovingLabelFunctions() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the initial graph and plot it for the user to see.
|
||||
if (generateData() === true) {
|
||||
updatePlot();
|
||||
}
|
||||
|
||||
// Bind an event. Whenever some constant changes, the graph will be
|
||||
// redrawn
|
||||
state.bindUpdatePlotEvent(plotDiv, onUpdatePlot);
|
||||
|
||||
return;
|
||||
|
||||
function getBarWidth() {
|
||||
if (config.plot.hasOwnProperty('bar_width') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config.plot.bar_width !== 'string') {
|
||||
logme('ERROR: The parameter config.plot.bar_width must be a string.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFinite(graphBarWidth = parseFloat(config.plot.bar_width)) === false) {
|
||||
logme('ERROR: The parameter config.plot.bar_width is not a valid floating number.');
|
||||
graphBarWidth = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function getBarAlign() {
|
||||
if (config.plot.hasOwnProperty('bar_align') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config.plot.bar_align !== 'string') {
|
||||
logme('ERROR: The parameter config.plot.bar_align must be a string.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(config.plot.bar_align.toLowerCase() !== 'left') &&
|
||||
(config.plot.bar_align.toLowerCase() !== 'center')
|
||||
) {
|
||||
logme('ERROR: Property config.plot.bar_align can be one of "left", or "center".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
graphBarAlign = config.plot.bar_align.toLowerCase();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function createMovingLabelFunctions() {
|
||||
var c1, returnStatus;
|
||||
|
||||
returnStatus = true;
|
||||
movingLabels = [];
|
||||
|
||||
if (config.plot.hasOwnProperty('moving_label') !== true) {
|
||||
returnStatus = true;
|
||||
} else if ($.isPlainObject(config.plot.moving_label) === true) {
|
||||
if (processMovingLabel(config.plot.moving_label) === false) {
|
||||
returnStatus = false;
|
||||
}
|
||||
} else if ($.isArray(config.plot.moving_label) === true) {
|
||||
for (c1 = 0; c1 < config.plot.moving_label.length; c1++) {
|
||||
if (processMovingLabel(config.plot.moving_label[c1]) === false) {
|
||||
returnStatus = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnStatus;
|
||||
}
|
||||
|
||||
function processMovingLabel(obj) {
|
||||
var labelText, funcString, disableAutoReturn, paramNames, func,
|
||||
fontWeight, fontColor;
|
||||
|
||||
if (obj.hasOwnProperty('@text') === false) {
|
||||
logme('ERROR: You did not define a "text" attribute for the moving_label.');
|
||||
|
||||
return false;
|
||||
}
|
||||
if (typeof obj['@text'] !== 'string') {
|
||||
logme('ERROR: "text" attribute is not a string.');
|
||||
|
||||
return false;
|
||||
}
|
||||
labelText = obj['@text'];
|
||||
|
||||
if (obj.hasOwnProperty('#text') === false) {
|
||||
logme('ERROR: moving_label is missing function declaration.');
|
||||
|
||||
return false;
|
||||
}
|
||||
if (typeof obj['#text'] !== 'string') {
|
||||
logme('ERROR: Function declaration is not a string.');
|
||||
|
||||
return false;
|
||||
}
|
||||
funcString = obj['#text'];
|
||||
|
||||
fontColor = 'black';
|
||||
if (
|
||||
(obj.hasOwnProperty('@color') === true) &&
|
||||
(typeof obj['@color'] === 'string')
|
||||
) {
|
||||
fontColor = obj['@color'];
|
||||
}
|
||||
|
||||
fontWeight = 'normal';
|
||||
if (
|
||||
(obj.hasOwnProperty('@weight') === true) &&
|
||||
(typeof obj['@weight'] === 'string')
|
||||
) {
|
||||
if (
|
||||
(obj['@weight'].toLowerCase() === 'normal') ||
|
||||
(obj['@weight'].toLowerCase() === 'bold')
|
||||
) {
|
||||
fontWeight = obj['@weight'];
|
||||
} else {
|
||||
logme('ERROR: Moving label can have a weight property of "normal" or "bold".');
|
||||
}
|
||||
}
|
||||
|
||||
disableAutoReturn = obj['@disable_auto_return'];
|
||||
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
paramNames = state.getAllParameterNames();
|
||||
paramNames.push(funcString);
|
||||
|
||||
try {
|
||||
func = Function.apply(null, paramNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: The function body "' +
|
||||
funcString +
|
||||
'" was not converted by the Function constructor.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '"');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
movingLabels.push({
|
||||
'labelText': labelText,
|
||||
'func': func,
|
||||
'el': null,
|
||||
'fontColor': fontColor,
|
||||
'fontWeight': fontWeight
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function createMarkingsFunctions() {
|
||||
var c1, paramNames, returnStatus;
|
||||
|
||||
returnStatus = true;
|
||||
|
||||
asymptotes = [];
|
||||
paramNames = state.getAllParameterNames();
|
||||
|
||||
if ($.isPlainObject(config.plot.asymptote)) {
|
||||
if (processAsymptote(config.plot.asymptote) === false) {
|
||||
returnStatus = false;
|
||||
}
|
||||
} else if ($.isArray(config.plot.asymptote)) {
|
||||
for (c1 = 0; c1 < config.plot.asymptote.length; c1 += 1) {
|
||||
if (processAsymptote(config.plot.asymptote[c1]) === false) {
|
||||
returnStatus = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnStatus;
|
||||
|
||||
// Read configuration options for asymptotes, and store them as
|
||||
// an array of objects. Each object will have 3 properties:
|
||||
//
|
||||
// - color: the color of the asymptote line
|
||||
// - type: 'x' (vertical), or 'y' (horizontal)
|
||||
// - func: the function that will generate the value at which
|
||||
// the asymptote will be plotted; i.e. x = func(), or
|
||||
// y = func(); for now only horizontal and vertical
|
||||
// asymptotes are supported
|
||||
//
|
||||
// Since each asymptote can have a variable function - function
|
||||
// that relies on some parameter specified in the config - we will
|
||||
// generate each asymptote just before we draw the graph. See:
|
||||
//
|
||||
// function updatePlot()
|
||||
// function generateMarkings()
|
||||
//
|
||||
// Asymptotes are really thin rectangles implemented via the Flot's
|
||||
// markings option.
|
||||
function processAsymptote(asyObj) {
|
||||
var newAsyObj, funcString, func;
|
||||
|
||||
newAsyObj = {};
|
||||
|
||||
if (typeof asyObj['@type'] === 'string') {
|
||||
if (asyObj['@type'].toLowerCase() === 'x') {
|
||||
newAsyObj.type = 'x';
|
||||
} else if (asyObj['@type'].toLowerCase() === 'y') {
|
||||
newAsyObj.type = 'y';
|
||||
} else {
|
||||
logme('ERROR: Attribute "type" for asymptote can be "x" or "y".');
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: Attribute "type" for asymptote is not specified.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof asyObj['#text'] === 'string') {
|
||||
funcString = asyObj['#text'];
|
||||
} else {
|
||||
logme('ERROR: Function body for asymptote is not specified.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
newAsyObj.color = '#000';
|
||||
if (typeof asyObj['@color'] === 'string') {
|
||||
newAsyObj.color = asyObj['@color'];
|
||||
}
|
||||
|
||||
newAsyObj.label = false;
|
||||
if (
|
||||
(asyObj.hasOwnProperty('@label') === true) &&
|
||||
(typeof asyObj['@label'] === 'string')
|
||||
) {
|
||||
newAsyObj.label = asyObj['@label'];
|
||||
}
|
||||
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
disableAutoReturn = asyObj['@disable_auto_return'];
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
paramNames.push(funcString);
|
||||
|
||||
try {
|
||||
func = Function.apply(null, paramNames);
|
||||
} catch (err) {
|
||||
logme('ERROR: Asymptote function body could not be converted to function object.');
|
||||
logme('Error message: "".' + err.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
paramNames.pop();
|
||||
|
||||
newAsyObj.func = func;
|
||||
asymptotes.push(newAsyObj);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function setGraphAxes() {
|
||||
xaxis = {
|
||||
'tickFormatter': null
|
||||
};
|
||||
|
||||
if (typeof config.plot['xticks'] === 'string') {
|
||||
if (processTicks(config.plot['xticks'], xaxis, 'xunits') === false) {
|
||||
logme('ERROR: Could not process the ticks for x-axis.');
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// logme('MESSAGE: "xticks" were not specified. Using defaults.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
yaxis = {
|
||||
'tickFormatter': null
|
||||
};
|
||||
if (typeof config.plot['yticks'] === 'string') {
|
||||
if (processTicks(config.plot['yticks'], yaxis, 'yunits') === false) {
|
||||
logme('ERROR: Could not process the ticks for y-axis.');
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// logme('MESSAGE: "yticks" were not specified. Using defaults.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
xTicksNames = null;
|
||||
yTicksNames = null;
|
||||
|
||||
if (checkForTicksNames('x') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkForTicksNames('y') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
//
|
||||
// function checkForTicksNames(axisName)
|
||||
//
|
||||
// The parameter "axisName" can be either "x" or "y" (string). Depending on it, the function
|
||||
// will set "xTicksNames" or "yTicksNames" private variable.
|
||||
//
|
||||
// This function does not return anything. It sets the private variable "xTicksNames" ("yTicksNames")
|
||||
// to the object converted by JSON.parse from the XML parameter "plot.xticks_names" ("plot.yticks_names").
|
||||
// If the "plot.xticks_names" ("plot.yticks_names") is missing or it is not a valid JSON string, then
|
||||
// "xTicksNames" ("yTicksNames") will be set to "null".
|
||||
//
|
||||
// Depending on the "xTicksNames" ("yTicksNames") being "null" or an object, the plot will either draw
|
||||
// number ticks, or use the names specified by the opbject.
|
||||
//
|
||||
function checkForTicksNames(axisName) {
|
||||
var tmpObj;
|
||||
|
||||
if ((axisName !== 'x') && (axisName !== 'y')) {
|
||||
// This is not an error. This funcion should simply stop executing.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
(config.plot.hasOwnProperty(axisName + 'ticks_names') === true) ||
|
||||
(typeof config.plot[axisName + 'ticks_names'] === 'string')
|
||||
) {
|
||||
try {
|
||||
tmpObj = JSON.parse(config.plot[axisName + 'ticks_names']);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: plot.' + axisName + 'ticks_names is not a valid JSON string.',
|
||||
'Error message: "' + err.message + '".'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (axisName === 'x') {
|
||||
xTicksNames = tmpObj;
|
||||
xaxis.tickFormatter = xAxisTickFormatter;
|
||||
}
|
||||
// At this point, we are certain that axisName = 'y'.
|
||||
else {
|
||||
yTicksNames = tmpObj;
|
||||
yaxis.tickFormatter = yAxisTickFormatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processTicks(ticksStr, ticksObj, unitsType) {
|
||||
var ticksBlobs, tempFloat, tempTicks, c1, c2;
|
||||
|
||||
// The 'ticks' setting is a string containing 3 floating-point
|
||||
// numbers.
|
||||
ticksBlobs = ticksStr.split(',');
|
||||
|
||||
if (ticksBlobs.length !== 3) {
|
||||
logme('ERROR: Did not get 3 blobs from ticksStr = "' + ticksStr + '".');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
tempFloat = parseFloat(ticksBlobs[0]);
|
||||
if (isNaN(tempFloat) === false) {
|
||||
ticksObj.min = tempFloat;
|
||||
} else {
|
||||
logme('ERROR: Invalid "min". ticksBlobs[0] = ', ticksBlobs[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
tempFloat = parseFloat(ticksBlobs[1]);
|
||||
if (isNaN(tempFloat) === false) {
|
||||
ticksObj.tickSize = tempFloat;
|
||||
} else {
|
||||
logme('ERROR: Invalid "tickSize". ticksBlobs[1] = ', ticksBlobs[1]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
tempFloat = parseFloat(ticksBlobs[2]);
|
||||
if (isNaN(tempFloat) === false) {
|
||||
ticksObj.max = tempFloat;
|
||||
} else {
|
||||
logme('ERROR: Invalid "max". ticksBlobs[2] = ', ticksBlobs[2]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the starting tick to the left of the ending tick (on the
|
||||
// x-axis)? If not, set default starting and ending tick.
|
||||
if (ticksObj.min >= ticksObj.max) {
|
||||
logme('ERROR: Ticks min >= max.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the range makes sense - i.e. that there are at
|
||||
// least 3 ticks. If not, set a tickSize which will produce
|
||||
// 11 ticks. tickSize is the spacing between the ticks.
|
||||
if (ticksObj.tickSize > ticksObj.max - ticksObj.min) {
|
||||
logme('ERROR: tickSize > max - min.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// units: change last tick to units
|
||||
if (typeof config.plot[unitsType] === 'string') {
|
||||
tempTicks = [];
|
||||
|
||||
for (c1 = ticksObj.min; c1 <= ticksObj.max; c1 += ticksObj.tickSize) {
|
||||
c2 = roundToPrec(c1, ticksObj.tickSize);
|
||||
tempTicks.push([c2, c2]);
|
||||
}
|
||||
|
||||
tempTicks.pop();
|
||||
tempTicks.push([
|
||||
roundToPrec(ticksObj.max, ticksObj.tickSize),
|
||||
config.plot[unitsType]
|
||||
]);
|
||||
|
||||
ticksObj.tickSize = null;
|
||||
ticksObj.ticks = tempTicks;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
function roundToPrec(num, prec) {
|
||||
var c1, tn1, tn2, digitsBefore, digitsAfter;
|
||||
|
||||
tn1 = Math.abs(num);
|
||||
tn2 = Math.abs(prec);
|
||||
|
||||
// Find out number of digits BEFORE the decimal point.
|
||||
c1 = 0;
|
||||
tn1 = Math.abs(num);
|
||||
while (tn1 >= 1) {
|
||||
c1 += 1;
|
||||
|
||||
tn1 /= 10;
|
||||
}
|
||||
digitsBefore = c1;
|
||||
|
||||
// Find out number of digits AFTER the decimal point.
|
||||
c1 = 0;
|
||||
tn1 = Math.abs(num);
|
||||
while (Math.round(tn1) !== tn1) {
|
||||
c1 += 1;
|
||||
|
||||
tn1 *= 10;
|
||||
}
|
||||
digitsAfter = c1;
|
||||
|
||||
// For precision, find out number of digits AFTER the
|
||||
// decimal point.
|
||||
c1 = 0;
|
||||
while (Math.round(tn2) !== tn2) {
|
||||
c1 += 1;
|
||||
|
||||
tn2 *= 10;
|
||||
}
|
||||
|
||||
// If precision is more than 1 (no digits after decimal
|
||||
// points).
|
||||
if (c1 === 0) {
|
||||
return num;
|
||||
}
|
||||
|
||||
// If the precision contains digits after the decimal
|
||||
// point, we apply special rules.
|
||||
else {
|
||||
tn1 = Math.abs(num);
|
||||
|
||||
// if (digitsAfter > c1) {
|
||||
tn1 = tn1.toFixed(c1);
|
||||
// } else {
|
||||
// tn1 = tn1.toPrecision(digitsBefore + digitsAfter);
|
||||
// }
|
||||
}
|
||||
|
||||
if (num < 0) {
|
||||
return -tn1;
|
||||
}
|
||||
|
||||
return tn1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setGraphXRange() {
|
||||
var xRangeStr, xRangeBlobs, tempNum, allParamNames, funcString,
|
||||
disableAutoReturn;
|
||||
|
||||
xrange = {};
|
||||
|
||||
if ($.isPlainObject(config.plot.xrange) === false) {
|
||||
logme(
|
||||
'ERROR: Expected config.plot.xrange to be an object. ' +
|
||||
'It is not.'
|
||||
);
|
||||
logme('config.plot.xrange = ', config.plot.xrange);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.plot.xrange.hasOwnProperty('min') === false) {
|
||||
logme(
|
||||
'ERROR: Expected config.plot.xrange.min to be ' +
|
||||
'present. It is not.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
disableAutoReturn = false;
|
||||
if (typeof config.plot.xrange.min === 'string') {
|
||||
funcString = config.plot.xrange.min;
|
||||
} else if (
|
||||
($.isPlainObject(config.plot.xrange.min) === true) &&
|
||||
(config.plot.xrange.min.hasOwnProperty('#text') === true) &&
|
||||
(typeof config.plot.xrange.min['#text'] === 'string')
|
||||
) {
|
||||
funcString = config.plot.xrange.min['#text'];
|
||||
|
||||
disableAutoReturn =
|
||||
config.plot.xrange.min['@disable_auto_return'];
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
disableAutoReturn = false;
|
||||
} else {
|
||||
disableAutoReturn = true;
|
||||
}
|
||||
} else {
|
||||
logme(
|
||||
'ERROR: Could not get a function definition for ' +
|
||||
'xrange.min property.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
if (disableAutoReturn === false) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
allParamNames = state.getAllParameterNames();
|
||||
|
||||
allParamNames.push(funcString);
|
||||
try {
|
||||
xrange.min = Function.apply(null, allParamNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: could not create a function from the string "' +
|
||||
funcString + '" for xrange.min.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '"');
|
||||
|
||||
$('#' + gstId).html(
|
||||
'<div style="color: red;">' + 'ERROR IN ' +
|
||||
'XML: Could not create a function from the string "' +
|
||||
funcString + '" for xrange.min.' + '</div>'
|
||||
);
|
||||
$('#' + gstId).append(
|
||||
'<div style="color: red;">' + 'Error ' +
|
||||
'message: "' + err.message + '".' + '</div>'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
allParamNames.pop();
|
||||
|
||||
if (config.plot.xrange.hasOwnProperty('max') === false) {
|
||||
logme(
|
||||
'ERROR: Expected config.plot.xrange.max to be ' +
|
||||
'present. It is not.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
disableAutoReturn = false;
|
||||
if (typeof config.plot.xrange.max === 'string') {
|
||||
funcString = config.plot.xrange.max;
|
||||
} else if (
|
||||
($.isPlainObject(config.plot.xrange.max) === true) &&
|
||||
(config.plot.xrange.max.hasOwnProperty('#text') === true) &&
|
||||
(typeof config.plot.xrange.max['#text'] === 'string')
|
||||
) {
|
||||
funcString = config.plot.xrange.max['#text'];
|
||||
|
||||
disableAutoReturn =
|
||||
config.plot.xrange.max['@disable_auto_return'];
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
disableAutoReturn = false;
|
||||
} else {
|
||||
disableAutoReturn = true;
|
||||
}
|
||||
} else {
|
||||
logme(
|
||||
'ERROR: Could not get a function definition for ' +
|
||||
'xrange.max property.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
if (disableAutoReturn === false) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
allParamNames.push(funcString);
|
||||
try {
|
||||
xrange.max = Function.apply(null, allParamNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: could not create a function from the string "' +
|
||||
funcString + '" for xrange.max.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '"');
|
||||
|
||||
$('#' + gstId).html(
|
||||
'<div style="color: red;">' + 'ERROR IN ' +
|
||||
'XML: Could not create a function from the string "' +
|
||||
funcString + '" for xrange.max.' + '</div>'
|
||||
);
|
||||
$('#' + gstId).append(
|
||||
'<div style="color: red;">' + 'Error message: "' +
|
||||
err.message + '".' + '</div>'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
allParamNames.pop();
|
||||
|
||||
tempNum = parseInt(config.plot.num_points, 10);
|
||||
if (isFinite(tempNum) === false) {
|
||||
tempNum = plotDiv.width() / 5.0;
|
||||
}
|
||||
|
||||
if (
|
||||
(tempNum < 2) &&
|
||||
(tempNum > 1000)
|
||||
) {
|
||||
logme(
|
||||
'ERROR: Number of points is outside the allowed range ' +
|
||||
'[2, 1000]'
|
||||
);
|
||||
logme('config.plot.num_points = ' + tempNum);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
numPoints = tempNum;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function createFunctions() {
|
||||
var c1;
|
||||
|
||||
functions = [];
|
||||
|
||||
if (typeof config.functions === 'undefined') {
|
||||
logme('ERROR: config.functions is undefined.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config.functions.function === 'string') {
|
||||
|
||||
// If just one function string is present.
|
||||
addFunction(config.functions.function);
|
||||
|
||||
} else if ($.isPlainObject(config.functions.function) === true) {
|
||||
|
||||
// If a function is present, but it also has properties
|
||||
// defined.
|
||||
callAddFunction(config.functions.function);
|
||||
|
||||
} else if ($.isArray(config.functions.function)) {
|
||||
|
||||
// If more than one function is defined.
|
||||
for (c1 = 0; c1 < config.functions.function.length; c1 += 1) {
|
||||
|
||||
// For each definition, we must check if it is a simple
|
||||
// string definition, or a complex one with properties.
|
||||
if (typeof config.functions.function[c1] === 'string') {
|
||||
|
||||
// Simple string.
|
||||
addFunction(config.functions.function[c1]);
|
||||
|
||||
} else if ($.isPlainObject(config.functions.function[c1])) {
|
||||
|
||||
// Properties are present.
|
||||
callAddFunction(config.functions.function[c1]);
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: config.functions.function is of an unsupported type.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// This function will reduce code duplication. We have to call
|
||||
// the function addFunction() several times passing object
|
||||
// properties as parameters. Rather than writing them out every
|
||||
// time, we will have a single place where it is done.
|
||||
function callAddFunction(obj) {
|
||||
if (
|
||||
(obj.hasOwnProperty('@output')) &&
|
||||
(typeof obj['@output'] === 'string')
|
||||
) {
|
||||
|
||||
// If this function is meant to be calculated for an
|
||||
// element then skip it.
|
||||
if ((obj['@output'].toLowerCase() === 'element') ||
|
||||
(obj['@output'].toLowerCase() === 'none')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this function is meant to be calculated for a
|
||||
// dynamic element in a label then skip it.
|
||||
else if (obj['@output'].toLowerCase() === 'plot_label') {
|
||||
return;
|
||||
}
|
||||
|
||||
// It is an error if '@output' is not 'element',
|
||||
// 'plot_label', or 'graph'. However, if the '@output'
|
||||
// attribute is omitted, we will not have reached this.
|
||||
else if (obj['@output'].toLowerCase() !== 'graph') {
|
||||
logme(
|
||||
'ERROR: Function "output" attribute can be ' +
|
||||
'either "element", "plot_label", "none" or "graph".'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The user did not specify an "output" attribute, or it is
|
||||
// "graph".
|
||||
addFunction(
|
||||
obj['#text'],
|
||||
obj['@color'],
|
||||
obj['@line'],
|
||||
obj['@dot'],
|
||||
obj['@label'],
|
||||
obj['@point_size'],
|
||||
obj['@fill_area'],
|
||||
obj['@bar'],
|
||||
obj['@disable_auto_return']
|
||||
);
|
||||
}
|
||||
|
||||
function addFunction(funcString, color, line, dot, label,
|
||||
pointSize, fillArea, bar, disableAutoReturn) {
|
||||
|
||||
var newFunctionObject, func, paramNames, c1, rgxp;
|
||||
|
||||
// The main requirement is function string. Without it we can't
|
||||
// create a function, and the series cannot be calculated.
|
||||
if (typeof funcString !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that any HTML entities that were escaped will be
|
||||
// unescaped. This is done because if a string with escaped
|
||||
// HTML entities is passed to the Function() constructor, it
|
||||
// will break.
|
||||
funcString = $('<div>').html(funcString).text();
|
||||
|
||||
// If the user did not specifically turn off this feature,
|
||||
// check if the function string contains a 'return', and
|
||||
// prepend a 'return ' to the string if one, or more, is not
|
||||
// found.
|
||||
if (
|
||||
(disableAutoReturn === undefined) ||
|
||||
(
|
||||
(typeof disableAutoReturn === 'string') &&
|
||||
(disableAutoReturn.toLowerCase() !== 'true')
|
||||
)
|
||||
) {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
funcString = 'return ' + funcString;
|
||||
}
|
||||
} else {
|
||||
if (funcString.search(/return/i) === -1) {
|
||||
logme(
|
||||
'ERROR: You have specified a JavaScript ' +
|
||||
'function without a "return" statemnt. Your ' +
|
||||
'function will return "undefined" by default.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Some defaults. If no options are set for the graph, we will
|
||||
// make sure that at least a line is drawn for a function.
|
||||
newFunctionObject = {
|
||||
'line': true,
|
||||
'dot': false,
|
||||
'bars': false
|
||||
};
|
||||
|
||||
// Get all of the parameter names defined by the user in the
|
||||
// XML.
|
||||
paramNames = state.getAllParameterNames();
|
||||
|
||||
// The 'x' is always one of the function parameters.
|
||||
paramNames.push('x');
|
||||
|
||||
// Must make sure that the function body also gets passed to
|
||||
// the Function constructor.
|
||||
paramNames.push(funcString);
|
||||
|
||||
// Create the function from the function string, and all of the
|
||||
// available parameters AND the 'x' variable as it's parameters.
|
||||
// For this we will use the built-in Function object
|
||||
// constructor.
|
||||
//
|
||||
// If something goes wrong during this step, most
|
||||
// likely the user supplied an invalid JavaScript function body
|
||||
// string. In this case we will not proceed.
|
||||
try {
|
||||
func = Function.apply(null, paramNames);
|
||||
} catch (err) {
|
||||
logme(
|
||||
'ERROR: The function body "' +
|
||||
funcString +
|
||||
'" was not converted by the Function constructor.'
|
||||
);
|
||||
logme('Error message: "' + err.message + '"');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
paramNames.pop();
|
||||
paramNames.pop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the array back to original state. Remember that it is
|
||||
// a pointer to original array which is stored in state object.
|
||||
paramNames.pop();
|
||||
paramNames.pop();
|
||||
|
||||
newFunctionObject['func'] = func;
|
||||
|
||||
if (typeof color === 'string') {
|
||||
newFunctionObject['color'] = color;
|
||||
}
|
||||
|
||||
if (typeof line === 'string') {
|
||||
if (line.toLowerCase() === 'true') {
|
||||
newFunctionObject['line'] = true;
|
||||
} else if (line.toLowerCase() === 'false') {
|
||||
newFunctionObject['line'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof dot === 'string') {
|
||||
if (dot.toLowerCase() === 'true') {
|
||||
newFunctionObject['dot'] = true;
|
||||
} else if (dot.toLowerCase() === 'false') {
|
||||
newFunctionObject['dot'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof pointSize === 'string') {
|
||||
newFunctionObject['pointSize'] = pointSize;
|
||||
}
|
||||
|
||||
if (typeof bar === 'string') {
|
||||
if (bar.toLowerCase() === 'true') {
|
||||
newFunctionObject['bars'] = true;
|
||||
} else if (bar.toLowerCase() === 'false') {
|
||||
newFunctionObject['bars'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (newFunctionObject['bars'] === true) {
|
||||
newFunctionObject['line'] = false;
|
||||
newFunctionObject['dot'] = false;
|
||||
// To do: See if need to do anything here.
|
||||
} else if (
|
||||
(newFunctionObject['dot'] === false) &&
|
||||
(newFunctionObject['line'] === false)
|
||||
) {
|
||||
newFunctionObject['line'] = true;
|
||||
}
|
||||
|
||||
if (newFunctionObject['line'] === true) {
|
||||
if (typeof fillArea === 'string') {
|
||||
if (fillArea.toLowerCase() === 'true') {
|
||||
newFunctionObject['fillArea'] = true;
|
||||
} else if (fillArea.toLowerCase() === 'false') {
|
||||
newFunctionObject['fillArea'] = false;
|
||||
} else {
|
||||
logme('ERROR: The attribute fill_area should be either "true" or "false".');
|
||||
logme('fill_area = "' + fillArea + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof label === 'string') {
|
||||
|
||||
newFunctionObject.specialLabel = false;
|
||||
newFunctionObject.pldeHash = [];
|
||||
|
||||
// Let's check the label against all of the plde objects.
|
||||
// plde is an abbreviation for Plot Label Dynamic Elements.
|
||||
for (c1 = 0; c1 < state.plde.length; c1 += 1) {
|
||||
rgxp = new RegExp(state.plde[c1].elId, 'g');
|
||||
|
||||
// If we find a dynamic element in the label, we will
|
||||
// hash the current plde object, and indicate that this
|
||||
// is a special label.
|
||||
if (rgxp.test(label) === true) {
|
||||
newFunctionObject.specialLabel = true;
|
||||
newFunctionObject.pldeHash.push(state.plde[c1]);
|
||||
}
|
||||
}
|
||||
|
||||
newFunctionObject.label = label;
|
||||
} else {
|
||||
newFunctionObject.label = false;
|
||||
}
|
||||
|
||||
functions.push(newFunctionObject);
|
||||
}
|
||||
}
|
||||
|
||||
// The callback that will be called whenever a constant changes (gets
|
||||
// updated via a slider or a text input).
|
||||
function onUpdatePlot(event) {
|
||||
if (generateData() === true) {
|
||||
updatePlot();
|
||||
}
|
||||
}
|
||||
|
||||
function generateData() {
|
||||
var c0, c1, c3, functionObj, seriesObj, dataPoints, paramValues, x, y,
|
||||
start, end, step, numNotUndefined;
|
||||
|
||||
paramValues = state.getAllParameterValues();
|
||||
|
||||
dataSeries = [];
|
||||
|
||||
for (c0 = 0; c0 < functions.length; c0 += 1) {
|
||||
functionObj = functions[c0];
|
||||
|
||||
try {
|
||||
start = xrange.min.apply(window, paramValues);
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not determine xrange start.');
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
end = xrange.max.apply(window, paramValues);
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not determine xrange end.');
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
seriesObj = {};
|
||||
dataPoints = [];
|
||||
|
||||
// For counting number of points added. In the end we will
|
||||
// compare this number to 'numPoints' specified in the config
|
||||
// JSON.
|
||||
c1 = 0;
|
||||
|
||||
step = (end - start) / (numPoints - 1);
|
||||
|
||||
// Generate the data points.
|
||||
for (x = start; x <= end; x += step) {
|
||||
|
||||
// Push the 'x' variable to the end of the parameter array.
|
||||
paramValues.push(x);
|
||||
|
||||
// We call the user defined function, passing all of the
|
||||
// available parameter values. Inside this function they
|
||||
// will be accessible by their names.
|
||||
try {
|
||||
y = functionObj.func.apply(window, paramValues);
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not generate data.');
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from defined function.' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the paramValues array to how it was before we
|
||||
// added 'x' variable to the end of it.
|
||||
paramValues.pop();
|
||||
|
||||
// Add the generated point to the data points set.
|
||||
dataPoints.push([x, y]);
|
||||
|
||||
c1 += 1;
|
||||
|
||||
}
|
||||
|
||||
// If the last point did not get included because of rounding
|
||||
// of floating-point number addition, then we will include it
|
||||
// manually.
|
||||
if (c1 != numPoints) {
|
||||
x = end;
|
||||
paramValues.push(x);
|
||||
try {
|
||||
y = functionObj.func.apply(window, paramValues);
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not generate data.');
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from function.' + '</div>');
|
||||
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
|
||||
|
||||
return false;
|
||||
}
|
||||
paramValues.pop();
|
||||
dataPoints.push([x, y]);
|
||||
}
|
||||
|
||||
// Put the entire data points set into the series object.
|
||||
seriesObj.data = dataPoints;
|
||||
|
||||
// See if user defined a specific color for this function.
|
||||
if (functionObj.hasOwnProperty('color') === true) {
|
||||
seriesObj.color = functionObj.color;
|
||||
}
|
||||
|
||||
// See if a user defined a label for this function.
|
||||
if (functionObj.label !== false) {
|
||||
if (functionObj.specialLabel === true) {
|
||||
(function (c1) {
|
||||
var tempLabel;
|
||||
|
||||
tempLabel = functionObj.label;
|
||||
|
||||
while (c1 < functionObj.pldeHash.length) {
|
||||
tempLabel = tempLabel.replace(
|
||||
functionObj.pldeHash[c1].elId,
|
||||
functionObj.pldeHash[c1].func.apply(
|
||||
window,
|
||||
state.getAllParameterValues()
|
||||
)
|
||||
);
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
|
||||
seriesObj.label = tempLabel;
|
||||
}(0));
|
||||
} else {
|
||||
seriesObj.label = functionObj.label;
|
||||
}
|
||||
}
|
||||
|
||||
// Should the data points be connected by a line?
|
||||
seriesObj.lines = {
|
||||
'show': functionObj.line
|
||||
};
|
||||
|
||||
if (functionObj.hasOwnProperty('fillArea') === true) {
|
||||
seriesObj.lines.fill = functionObj.fillArea;
|
||||
}
|
||||
|
||||
// Should each data point be represented by a point on the
|
||||
// graph?
|
||||
seriesObj.points = {
|
||||
'show': functionObj.dot
|
||||
};
|
||||
|
||||
seriesObj.bars = {
|
||||
'show': functionObj.bars,
|
||||
'barWidth': graphBarWidth
|
||||
};
|
||||
|
||||
if (graphBarAlign !== null) {
|
||||
seriesObj.bars.align = graphBarAlign;
|
||||
}
|
||||
|
||||
if (functionObj.hasOwnProperty('pointSize')) {
|
||||
seriesObj.points.radius = functionObj.pointSize;
|
||||
}
|
||||
|
||||
// Add the newly created series object to the series set which
|
||||
// will be plotted by Flot.
|
||||
dataSeries.push(seriesObj);
|
||||
}
|
||||
|
||||
if (graphBarAlign === null) {
|
||||
for (c0 = 0; c0 < numPoints; c0 += 1) {
|
||||
// Number of points that have a value other than 'undefined' (undefined).
|
||||
numNotUndefined = 0;
|
||||
|
||||
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
|
||||
if (dataSeries[c1].bars.show === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
|
||||
numNotUndefined += 1;
|
||||
}
|
||||
}
|
||||
|
||||
c3 = 0;
|
||||
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
|
||||
if (dataSeries[c1].bars.show === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dataSeries[c1].data[c0][0] -= graphBarWidth * (0.5 * numNotUndefined - c3);
|
||||
|
||||
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
|
||||
c3 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (c0 = 0; c0 < asymptotes.length; c0 += 1) {
|
||||
|
||||
// If the user defined a label for this asympote, then the
|
||||
// property 'label' will be a string (in the other case it is
|
||||
// a boolean value 'false'). We will create an empty data set,
|
||||
// and add to it a label. This solution is a bit _wrong_ , but
|
||||
// it will have to do for now. Flot JS does not provide a way
|
||||
// to add labels to markings, and we use markings to generate
|
||||
// asymptotes.
|
||||
if (asymptotes[c0].label !== false) {
|
||||
dataSeries.push({
|
||||
'data': [],
|
||||
'label': asymptotes[c0].label,
|
||||
'color': asymptotes[c0].color
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
} // End-of: function generateData
|
||||
|
||||
function updatePlot() {
|
||||
var paramValues, plotObj;
|
||||
|
||||
paramValues = state.getAllParameterValues();
|
||||
|
||||
if (xaxis.tickFormatter !== null) {
|
||||
xaxis.ticks = null;
|
||||
}
|
||||
|
||||
if (yaxis.tickFormatter !== null) {
|
||||
yaxis.ticks = null;
|
||||
}
|
||||
|
||||
// Tell Flot to draw the graph to our specification.
|
||||
plotObj = $.plot(
|
||||
plotDiv,
|
||||
dataSeries,
|
||||
{
|
||||
'xaxis': xaxis,
|
||||
'yaxis': yaxis,
|
||||
'legend': {
|
||||
|
||||
// To show the legend or not. Note, even if 'show' is
|
||||
// 'true', the legend will only show if labels are
|
||||
// provided for at least one of the series that are
|
||||
// going to be plotted.
|
||||
'show': true,
|
||||
|
||||
// A floating point number in the range [0, 1]. The
|
||||
// smaller the number, the more transparent will the
|
||||
// legend background become.
|
||||
'backgroundOpacity': 0
|
||||
|
||||
},
|
||||
'grid': {
|
||||
'markings': generateMarkings()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
updateMovingLabels();
|
||||
|
||||
// The first time that the graph gets added to the page, the legend
|
||||
// is created from scratch. When it appears, MathJax works some
|
||||
// magic, and all of the specially marked TeX gets rendered nicely.
|
||||
// The next time when we update the graph, no such thing happens.
|
||||
// We must ask MathJax to typeset the legend again (well, we will
|
||||
// ask it to look at our entire graph DIV), the next time it's
|
||||
// worker queue is available.
|
||||
MathJax.Hub.Queue([
|
||||
'Typeset',
|
||||
MathJax.Hub,
|
||||
plotDiv.attr('id')
|
||||
]);
|
||||
|
||||
return;
|
||||
|
||||
function updateMovingLabels() {
|
||||
var c1, labelCoord, pointOffset;
|
||||
|
||||
for (c1 = 0; c1 < movingLabels.length; c1 += 1) {
|
||||
if (movingLabels[c1].el === null) {
|
||||
movingLabels[c1].el = $(
|
||||
'<div>' +
|
||||
movingLabels[c1].labelText +
|
||||
'</div>'
|
||||
);
|
||||
movingLabels[c1].el.css('position', 'absolute');
|
||||
movingLabels[c1].el.css('color', movingLabels[c1].fontColor);
|
||||
movingLabels[c1].el.css('font-weight', movingLabels[c1].fontWeight);
|
||||
movingLabels[c1].el.appendTo(plotDiv);
|
||||
|
||||
movingLabels[c1].elWidth = movingLabels[c1].el.width();
|
||||
movingLabels[c1].elHeight = movingLabels[c1].el.height();
|
||||
} else {
|
||||
movingLabels[c1].el.detach();
|
||||
movingLabels[c1].el.appendTo(plotDiv);
|
||||
}
|
||||
|
||||
labelCoord = movingLabels[c1].func.apply(window, paramValues);
|
||||
|
||||
pointOffset = plotObj.pointOffset({'x': labelCoord.x, 'y': labelCoord.y});
|
||||
|
||||
movingLabels[c1].el.css('left', pointOffset.left - 0.5 * movingLabels[c1].elWidth);
|
||||
movingLabels[c1].el.css('top', pointOffset.top - 0.5 * movingLabels[c1].elHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate markings to represent asymptotes defined by the user.
|
||||
// See the following function for more details:
|
||||
//
|
||||
// function processAsymptote()
|
||||
//
|
||||
function generateMarkings() {
|
||||
var c1, asymptote, markings, val;
|
||||
|
||||
markings = [];
|
||||
|
||||
for (c1 = 0; c1 < asymptotes.length; c1 += 1) {
|
||||
asymptote = asymptotes[c1];
|
||||
|
||||
try {
|
||||
val = asymptote.func.apply(window, paramValues);
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not generate value from asymptote function.');
|
||||
logme('Error message: ', err.message);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (asymptote.type === 'x') {
|
||||
markings.push({
|
||||
'color': asymptote.color,
|
||||
'lineWidth': 2,
|
||||
'xaxis': {
|
||||
'from': val,
|
||||
'to': val
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markings.push({
|
||||
'color': asymptote.color,
|
||||
'lineWidth': 2,
|
||||
'yaxis': {
|
||||
'from': val,
|
||||
'to': val
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return markings;
|
||||
}
|
||||
}
|
||||
|
||||
function xAxisTickFormatter(val, axis) {
|
||||
if (xTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
|
||||
return xTicksNames[val.toFixed(axis.tickDecimals)];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function yAxisTickFormatter(val, axis) {
|
||||
if (yTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
|
||||
return yTicksNames[val.toFixed(axis.tickDecimals)];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* We will add a function that will be called for all GraphicalSliderTool
|
||||
* xmodule module instances. It must be available globally by design of
|
||||
* xmodule.
|
||||
*/
|
||||
window.GraphicalSliderTool = function (el) {
|
||||
// All the work will be performed by the GstMain module. We will get access
|
||||
// to it, and all it's dependencies, via Require JS. Currently Require JS
|
||||
// is namespaced and is available via a global object RequireJS.
|
||||
RequireJS.require(['GstMain'], function (GstMain) {
|
||||
// The GstMain module expects the DOM ID of a Graphical Slider Tool
|
||||
// element. Since we are given a <section> element which might in
|
||||
// theory contain multiple graphical_slider_tool <div> elements (each
|
||||
// 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) {
|
||||
JavascriptLoader.executeModuleScripts($(value), function(){
|
||||
GstMain($(value).attr('id'));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(
|
||||
'GstMain',
|
||||
|
||||
// Even though it is not explicitly in this module, we have to specify
|
||||
// 'GeneralMethods' as a dependency. It expands some of the core JS objects
|
||||
// with additional useful methods that are used in other modules.
|
||||
['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'ElOutput', 'GLabelElOutput', 'logme'],
|
||||
function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, GLabelElOutput, logme) {
|
||||
|
||||
return GstMain;
|
||||
|
||||
function GstMain(gstId) {
|
||||
var config, gstClass, state;
|
||||
|
||||
if ($('#' + gstId).attr('data-processed') !== 'processed') {
|
||||
$('#' + gstId).attr('data-processed', 'processed');
|
||||
} else {
|
||||
// logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the JSON configuration, parse it, and store as an object.
|
||||
try {
|
||||
config = JSON.parse($('#' + gstId + '_json').html()).root;
|
||||
} catch (err) {
|
||||
logme('ERROR: could not parse config JSON.');
|
||||
logme('$("#" + gstId + "_json").html() = ', $('#' + gstId + '_json').html());
|
||||
logme('JSON.parse(...) = ', JSON.parse($('#' + gstId + '_json').html()));
|
||||
logme('config = ', config);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the class name of the GST. All elements are assigned a class
|
||||
// name that is based on the class name of the GST. For example, inputs
|
||||
// are assigned a class name '{GST class name}_input'.
|
||||
if (typeof config['@class'] !== 'string') {
|
||||
logme('ERROR: Could not get the class name of GST.');
|
||||
logme('config["@class"] = ', config['@class']);
|
||||
|
||||
return;
|
||||
}
|
||||
gstClass = config['@class'];
|
||||
|
||||
// Parse the configuration settings for parameters, and store them in a
|
||||
// state object.
|
||||
state = State(gstId, config);
|
||||
|
||||
// It is possible that something goes wrong while extracting parameters
|
||||
// from the JSON config object. In this case, we will not continue.
|
||||
if (state === undefined) {
|
||||
logme('ERROR: The state object was not initialized properly.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the sliders and the text inputs, attaching them to
|
||||
// appropriate parameters.
|
||||
Sliders(gstId, state);
|
||||
Inputs(gstId, gstClass, state);
|
||||
|
||||
// Configure functions that output to an element instead of the graph.
|
||||
ElOutput(config, state);
|
||||
|
||||
// Configure functions that output to an element instead of the graph
|
||||
// label.
|
||||
GLabelElOutput(config, state);
|
||||
|
||||
// Configure and display the graph. Attach event for the graph to be
|
||||
// updated on any change of a slider or a text input.
|
||||
Graph(gstId, config, state);
|
||||
}
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -0,0 +1,88 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('Inputs', ['logme'], function (logme) {
|
||||
return Inputs;
|
||||
|
||||
function Inputs(gstId, gstClass, state) {
|
||||
var c1, paramName, allParamNames;
|
||||
|
||||
allParamNames = state.getAllParameterNames();
|
||||
|
||||
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
|
||||
$('#' + gstId).find('.' + gstClass + '_input').each(function (index, value) {
|
||||
var inputDiv, paramName;
|
||||
|
||||
paramName = allParamNames[c1];
|
||||
inputDiv = $(value);
|
||||
|
||||
if (paramName === inputDiv.data('var')) {
|
||||
createInput(inputDiv, paramName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
function createInput(inputDiv, paramName) {
|
||||
var paramObj;
|
||||
|
||||
paramObj = state.getParamObj(paramName);
|
||||
|
||||
// Check that the retrieval went OK.
|
||||
if (paramObj === undefined) {
|
||||
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind a function to the 'change' event. Whenever the user changes
|
||||
// the value of this text input, and presses 'enter' (or clicks
|
||||
// somewhere else on the page), this event will be triggered, and
|
||||
// our callback will be called.
|
||||
inputDiv.bind('change', inputOnChange);
|
||||
|
||||
inputDiv.val(paramObj.value);
|
||||
|
||||
// Lets style the input element nicely. We will use the button()
|
||||
// widget for this since there is no native widget for the text
|
||||
// input.
|
||||
inputDiv.button().css({
|
||||
'font': 'inherit',
|
||||
'color': 'inherit',
|
||||
'text-align': 'left',
|
||||
'outline': 'none',
|
||||
'cursor': 'text',
|
||||
'height': '15px'
|
||||
});
|
||||
|
||||
// Tell the parameter object from state that we are attaching a
|
||||
// text input to it. Next time the parameter will be updated with
|
||||
// a new value, tis input will also be updated.
|
||||
paramObj.inputDivs.push(inputDiv);
|
||||
|
||||
return;
|
||||
|
||||
// Update the 'state' - i.e. set the value of the parameter this
|
||||
// input is attached to to a new value.
|
||||
//
|
||||
// This will cause the plot to be redrawn each time after the user
|
||||
// changes the value in the input. Note that he has to either press
|
||||
// 'Enter', or click somewhere else on the page in order for the
|
||||
// 'change' event to be tiggered.
|
||||
function inputOnChange(event) {
|
||||
var inputDiv;
|
||||
|
||||
inputDiv = $(this);
|
||||
state.setParameterValue(paramName, inputDiv.val(), inputDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
236
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/jstat-1.0.0.min.js
vendored
Normal file
236
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/jstat-1.0.0.min.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
function jstat(){}
|
||||
j=jstat;(function(){var initializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(prop){var _super=this.prototype;initializing=true;var prototype=new this();initializing=false;for(var name in prop){prototype[name]=typeof prop[name]=="function"&&typeof _super[name]=="function"&&fnTest.test(prop[name])?(function(name,fn){return function(){var tmp=this._super;this._super=_super[name];var ret=fn.apply(this,arguments);this._super=tmp;return ret;};})(name,prop[name]):prop[name];}
|
||||
function Class(){if(!initializing&&this.init)
|
||||
this.init.apply(this,arguments);}
|
||||
Class.prototype=prototype;Class.constructor=Class;Class.extend=arguments.callee;return Class;};})();jstat.ONE_SQRT_2PI=0.3989422804014327;jstat.LN_SQRT_2PI=0.9189385332046727417803297;jstat.LN_SQRT_PId2=0.225791352644727432363097614947;jstat.DBL_MIN=2.22507e-308;jstat.DBL_EPSILON=2.220446049250313e-16;jstat.SQRT_32=5.656854249492380195206754896838;jstat.TWO_PI=6.283185307179586;jstat.DBL_MIN_EXP=-999;jstat.SQRT_2dPI=0.79788456080287;jstat.LN_SQRT_PI=0.5723649429247;jstat.seq=function(min,max,length){var r=new Range(min,max,length);return r.getPoints();}
|
||||
jstat.dnorm=function(x,mean,sd,log){if(mean==null)mean=0;if(sd==null)sd=1;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.pnorm=function(q,mean,sd,lower_tail,log){if(mean==null)mean=0;if(sd==null)sd=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.dlnorm=function(x,meanlog,sdlog,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.plnorm=function(q,meanlog,sdlog,lower_tail,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}
|
||||
else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.dbeta=function(x,alpha,beta,ncp,log){if(ncp==null)ncp=0;if(log==null)log=false;var b=new BetaDistribution(alpha,beta);if(!isNaN(x)){return b._pdf(x,log);}
|
||||
else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(b._pdf(x[i],log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.pbeta=function(q,alpha,beta,ncp,lower_tail,log){if(ncp==null)ncp=0;if(log==null)log=false;if(lower_tail==null)lower_tail=true;var b=new BetaDistribution(alpha,beta);if(!isNaN(q)){return b._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(b._cdf(q[i],lower_tail,log));}
|
||||
return res;}
|
||||
else{throw"Illegal argument: x";}}
|
||||
jstat.dgamma=function(x,shape,rate,scale,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(x)){return g._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(g._pdf(x[i],log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.pgamma=function(q,shape,rate,scale,lower_tail,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(q)){return g._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(g._cdf(q[i],lower_tail,log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.dt=function(x,df,ncp,log){if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(x)){return t._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(t._pdf(x[i],log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.pt=function(q,df,ncp,lower_tail,log){if(lower_tail==null)lower_tail=true;if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(q)){return t._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(t._cdf(q[i],lower_tail,log));}
|
||||
return res;}else{throw"Illegal argument: x";}}
|
||||
jstat.plot=function(x,y,options){if(x==null){throw"x is undefined in jstat.plot";}
|
||||
if(y==null){throw"y is undefined in jstat.plot";}
|
||||
if(x.length!=y.length){throw"x and y lengths differ in jstat.plot";}
|
||||
var flotOpt={series:{lines:{},points:{}}};var series=[];if(x.length==undefined){series.push([x,y]);flotOpt.series.points.show=true;}else{for(var i=0;i<x.length;i++){series.push([x[i],y[i]]);}}
|
||||
var title='jstat graph';if(options!=null){if(options.type!=null){if(options.type=='l'){flotOpt.series.lines.show=true;}else if(options.type=='p'){flotOpt.series.lines.show=false;flotOpt.series.points.show=true;}}
|
||||
if(options.hover!=null){flotOpt.grid={hoverable:options.hover}}
|
||||
if(options.main!=null){title=options.main;}}
|
||||
var now=new Date();var hash=now.getMilliseconds()*now.getMinutes()+now.getSeconds();$('body').append('<div title="'+title+'" style="display: none;" id="'+hash+'"><div id="graph-'+hash+'" style="width:95%; height: 95%"></div></div>');$('#'+hash).dialog({modal:false,width:475,height:475,resizable:true,resize:function(){$.plot($('#graph-'+hash),[series],flotOpt);},open:function(event,ui){var id='#graph-'+hash;$.plot($('#graph-'+hash),[series],flotOpt);}})}
|
||||
jstat.log10=function(arg){return Math.log(arg)/Math.LN10;}
|
||||
jstat.toSigFig=function(num,n){if(num==0){return 0;}
|
||||
var d=Math.ceil(jstat.log10(num<0?-num:num));var power=n-parseInt(d);var magnitude=Math.pow(10,power);var shifted=Math.round(num*magnitude);return shifted/magnitude;}
|
||||
jstat.trunc=function(x){return(x>0)?Math.floor(x):Math.ceil(x);}
|
||||
jstat.isFinite=function(x){return(!isNaN(x)&&(x!=Number.POSITIVE_INFINITY)&&(x!=Number.NEGATIVE_INFINITY));}
|
||||
jstat.dopois_raw=function(x,lambda,give_log){if(lambda==0){if(x==0){return(give_log)?0.0:1.0;}
|
||||
return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(!jstat.isFinite(lambda))return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<0)return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<=lambda*jstat.DBL_MIN){return(give_log)?-lambda:Math.exp(-lambda);}
|
||||
if(lambda<x*jstat.DBL_MIN){var param=-lambda+x*Math.log(lambda)-jstat.lgamma(x+1);return(give_log)?param:Math.exp(param);}
|
||||
var param1=jstat.TWO_PI*x;var param2=-jstat.stirlerr(x)-jstat.bd0(x,lambda);return(give_log)?-0.5*Math.log(param1)+param2:Math.exp(param2)/Math.sqrt(param1);}
|
||||
jstat.bd0=function(x,np){var ej,s,s1,v,j;if(!jstat.isFinite(x)||!jstat.isFinite(np)||np==0.0)throw"illegal parameter in jstat.bd0";if(Math.abs(x-np)>0.1*(x+np)){v=(x-np)/(x+np);s=(x-np)*v;ej=2*x*v;v=v*v;for(j=1;;j++){ej*=v;s1=s+ej/((j<<1)+1);if(s1==s)
|
||||
return(s1);s=s1;}}
|
||||
return(x*Math.log(x/np)+np-x);}
|
||||
jstat.stirlerr=function(n){var S0=0.083333333333333333333;var S1=0.00277777777777777777778;var S2=0.00079365079365079365079365;var S3=0.000595238095238095238095238;var S4=0.0008417508417508417508417508;var sferr_halves=[0.0,0.1534264097200273452913848,0.0810614667953272582196702,0.0548141210519176538961390,0.0413406959554092940938221,0.03316287351993628748511048,0.02767792568499833914878929,0.02374616365629749597132920,0.02079067210376509311152277,0.01848845053267318523077934,0.01664469118982119216319487,0.01513497322191737887351255,0.01387612882307074799874573,0.01281046524292022692424986,0.01189670994589177009505572,0.01110455975820691732662991,0.010411265261972096497478567,0.009799416126158803298389475,0.009255462182712732917728637,0.008768700134139385462952823,0.008330563433362871256469318,0.007934114564314020547248100,0.007573675487951840794972024,0.007244554301320383179543912,0.006942840107209529865664152,0.006665247032707682442354394,0.006408994188004207068439631,0.006171712263039457647532867,0.005951370112758847735624416,0.005746216513010115682023589,0.005554733551962801371038690];var nn;if(n<=15.0){nn=n+n;if(nn==parseInt(nn))return(sferr_halves[parseInt(nn)]);return(jstat.lgamma(n+1.0)-(n+0.5)*Math.log(n)+n-jstat.LN_SQRT_2PI);}
|
||||
nn=n*n;if(n>500)return((S0-S1/nn)/n);if(n>80)return((S0-(S1-S2/nn)/nn)/n);if(n>35)return((S0-(S1-(S2-S3/nn)/nn)/nn)/n);return((S0-(S1-(S2-(S3-S4/nn)/nn)/nn)/nn)/n);}
|
||||
jstat.lgamma=function(x){function lgammafn_sign(x,sgn){var ans,y,sinpiy;var xmax=2.5327372760800758e+305;var dxrel=1.490116119384765696e-8;if(sgn!=null)sgn=1;if(isNaN(x))return x;if(x<0&&(Math.floor(-x)%2.0)==0)
|
||||
if(sgn!=null)sgn=-1;if(x<=0&&x==jstat.trunc(x)){console.warn("Negative integer argument in lgammafn_sign");return Number.POSITIVE_INFINITY;}
|
||||
y=Math.abs(x);if(y<=10)return Math.log(Math.abs(jstat.gamma(x)));if(y>xmax){console.warn("Illegal arguement passed to lgammafn_sign");return Number.POSITIVE_INFINITY;}
|
||||
if(x>0){if(x>1e17){return(x*(Math.log(x)-1.0));}else if(x>4934720.0){return(jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x);}else{return jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x+jstat.lgammacor(x);}}
|
||||
sinpiy=Math.abs(Math.sin(Math.PI*y));if(sinpiy==0){throw"Should never happen!!";}
|
||||
ans=jstat.LN_SQRT_PId2+(x-0.5)*Math.log(y)-x-Math.log(sinpiy)-jstat.lgammacor(y);if(Math.abs((x-jstat.trunc(x-0.5))*ans/x)<dxrel){throw"The answer is less than half the precision argument too close to a negative integer";}
|
||||
return ans;}
|
||||
return lgammafn_sign(x,null);}
|
||||
jstat.gamma=function(x){var xbig=171.624;var p=[-1.71618513886549492533811,24.7656508055759199108314,-379.804256470945635097577,629.331155312818442661052,866.966202790413211295064,-31451.2729688483675254357,-36144.4134186911729807069,66456.1438202405440627855];var q=[-30.8402300119738975254353,315.350626979604161529144,-1015.15636749021914166146,-3107.77167157231109440444,22538.1184209801510330112,4755.84627752788110767815,-134659.959864969306392456,-115132.259675553483497211];var c=[-.001910444077728,8.4171387781295e-4,-5.952379913043012e-4,7.93650793500350248e-4,-.002777777777777681622553,.08333333333333333331554247,.0057083835261];var i,n,parity,fact,xden,xnum,y,z,yi,res,sum,ysq;parity=(0);fact=1.0;n=0;y=x;if(y<=0.0){y=-x;yi=jstat.trunc(y);res=y-yi;if(res!=0.0){if(yi!=jstat.trunc(yi*0.5)*2.0)
|
||||
parity=(1);fact=-Math.PI/Math.sin(Math.PI*res);y+=1.0;}else{return(Number.POSITIVE_INFINITY);}}
|
||||
if(y<jstat.DBL_EPSILON){if(y>=jstat.DBL_MIN){res=1.0/y;}else{return(Number.POSITIVE_INFINITY);}}else if(y<12.0){yi=y;if(y<1.0){z=y;y+=1.0;}else{n=parseInt(y)-1;y-=parseFloat(n);z=y-1.0;}
|
||||
xnum=0.0;xden=1.0;for(i=0;i<8;++i){xnum=(xnum+p[i])*z;xden=xden*z+q[i];}
|
||||
res=xnum/xden+1.0;if(yi<y){res/=yi;}else if(yi>y){for(i=0;i<n;++i){res*=y;y+=1.0;}}}else{if(y<=xbig){ysq=y*y;sum=c[6];for(i=0;i<6;++i){sum=sum/ysq+c[i];}
|
||||
sum=sum/y-y+jstat.LN_SQRT_2PI;sum+=(y-0.5)*Math.log(y);res=Math.exp(sum);}else{return(Number.POSITIVE_INFINITY);}}
|
||||
if(parity)
|
||||
res=-res;if(fact!=1.0)
|
||||
res=fact/res;return res;}
|
||||
jstat.lgammacor=function(x){var algmcs=[+.1666389480451863247205729650822e+0,-.1384948176067563840732986059135e-4,+.9810825646924729426157171547487e-8,-.1809129475572494194263306266719e-10,+.6221098041892605227126015543416e-13,-.3399615005417721944303330599666e-15,+.2683181998482698748957538846666e-17,-.2868042435334643284144622399999e-19,+.3962837061046434803679306666666e-21,-.6831888753985766870111999999999e-23,+.1429227355942498147573333333333e-24,-.3547598158101070547199999999999e-26,+.1025680058010470912000000000000e-27,-.3401102254316748799999999999999e-29,+.1276642195630062933333333333333e-30];var tmp;var nalgm=5;var xbig=94906265.62425156;var xmax=3.745194030963158e306;if(x<10){return Number.NaN;}else if(x>=xmax){throw"Underflow error in lgammacor";}else if(x<xbig){tmp=10/x;return jstat.chebyshev(tmp*tmp*2-1,algmcs,nalgm)/x;}
|
||||
return 1/(x*12);}
|
||||
jstat.incompleteBeta=function(a,b,x){function betacf(a,b,x){var MAXIT=100;var EPS=3.0e-12;var FPMIN=1.0e-30;var m,m2,aa,c,d,del,h,qab,qam,qap;qab=a+b;qap=a+1.0;qam=a-1.0;c=1.0;d=1.0-qab*x/qap;if(Math.abs(d)<FPMIN){d=FPMIN;}
|
||||
d=1.0/d;h=d;for(m=1;m<=MAXIT;m++){m2=2*m;aa=m*(b-m)*x/((qam+m2)*(a+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
|
||||
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
|
||||
d=1.0/d;h*=d*c;aa=-(a+m)*(qab+m)*x/((a+m2)*(qap+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
|
||||
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
|
||||
d=1.0/d;del=d*c;h*=del;if(Math.abs(del-1.0)<EPS){break;}}
|
||||
if(m>MAXIT){console.warn("a or b too big, or MAXIT too small in betacf: "+a+", "+b+", "+x+", "+h);return h;}
|
||||
if(isNaN(h)){console.warn(a+", "+b+", "+x);}
|
||||
return h;}
|
||||
var bt;if(x<0.0||x>1.0){throw"bad x in routine incompleteBeta";}
|
||||
if(x==0.0||x==1.0){bt=0.0;}else{bt=Math.exp(jstat.lgamma(a+b)-jstat.lgamma(a)-jstat.lgamma(b)+a*Math.log(x)+b*Math.log(1.0-x));}
|
||||
if(x<(a+1.0)/(a+b+2.0)){return bt*betacf(a,b,x)/a;}else{return 1.0-bt*betacf(b,a,1.0-x)/b;}}
|
||||
jstat.chebyshev=function(x,a,n){var b0,b1,b2,twox;var i;if(n<1||n>1000)return Number.NaN;if(x<-1.1||x>1.1)return Number.NaN;twox=x*2;b2=b1=0;b0=0;for(i=1;i<=n;i++){b2=b1;b1=b0;b0=twox*b1-b2+a[n-i];}
|
||||
return(b0-b2)*0.5;}
|
||||
jstat.fmin2=function(x,y){return(x<y)?x:y;}
|
||||
jstat.log1p=function(x){var ret=0,n=50;if(x<=-1){return Number.NEGATIVE_INFINITY;}
|
||||
if(x<0||x>1){return Math.log(1+x);}
|
||||
for(var i=1;i<n;i++){if((i%2)===0){ret-=Math.pow(x,i)/i;}else{ret+=Math.pow(x,i)/i;}}
|
||||
return ret;}
|
||||
jstat.expm1=function(x){var y,a=Math.abs(x);if(a<jstat.DBL_EPSILON)return x;if(a>0.697)return Math.exp(x)-1;if(a>1e-8){y=Math.exp(x)-1;}else{y=(x/2+1)*x;}
|
||||
y-=(1+y)*(jstat.log1p(y)-x);return y;}
|
||||
jstat.logBeta=function(a,b){var corr,p,q;p=q=a;if(b<p)p=b;if(b>q)q=b;if(p<0){console.warn('Both arguements must be >= 0');return Number.NaN;}
|
||||
else if(p==0){return Number.POSITIVE_INFINITY;}
|
||||
else if(!jstat.isFinite(q)){return Number.NEGATIVE_INFINITY;}
|
||||
if(p>=10){corr=jstat.lgammacor(p)+jstat.lgammacor(q)-jstat.lgammacor(p+q);return Math.log(q)*-0.5+jstat.LN_SQRT_2PI+corr
|
||||
+(p-0.5)*Math.log(p/(p+q))+q*jstat.log1p(-p/(p+q));}
|
||||
else if(q>=10){corr=jstat.lgammacor(q)-jstat.lgammacor(p+q);return jstat.lgamma(p)+corr+p-p*Math.log(p+q)
|
||||
+(q-0.5)*jstat.log1p(-p/(p+q));}
|
||||
else
|
||||
return Math.log(jstat.gamma(p)*(jstat.gamma(q)/jstat.gamma(p+q)));}
|
||||
jstat.dbinom_raw=function(x,n,p,q,give_log){if(give_log==null)give_log=false;var lf,lc;if(p==0){if(x==0){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
|
||||
if(q==0){if(x==n){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
|
||||
if(x==0){if(n==0)return(give_log)?0.0:1.0;lc=(p<0.1)?-jstat.bd0(n,n*q)-n*p:n*Math.log(q);return(give_log)?lc:Math.exp(lc);}
|
||||
if(x==n){lc=(q<0.1)?-jstat.bd0(n,n*p)-n*q:n*Math.log(p);return(give_log)?lc:Math.exp(lc);}
|
||||
if(x<0||x>n)return(give_log)?Number.NEGATIVE_INFINITY:0.0;lc=jstat.stirlerr(n)-jstat.stirlerr(x)-jstat.stirlerr(n-x)-jstat.bd0(x,n*p)-jstat.bd0(n-x,n*q);lf=Math.log(jstat.TWO_PI)+Math.log(x)+jstat.log1p(-x/n);return(give_log)?lc-0.5*lf:Math.exp(lc-0.5*lf);}
|
||||
jstat.max=function(values){var max=Number.NEGATIVE_INFINITY;for(var i=0;i<values.length;i++){if(values[i]>max){max=values[i];}}
|
||||
return max;}
|
||||
var Range=Class.extend({init:function(min,max,numPoints){this._minimum=parseFloat(min);this._maximum=parseFloat(max);this._numPoints=parseFloat(numPoints);},getMinimum:function(){return this._minimum;},getMaximum:function(){return this._maximum;},getNumPoints:function(){return this._numPoints;},getPoints:function(){var results=[];var x=this._minimum;var step=(this._maximum-this._minimum)/(this._numPoints-1);for(var i=0;i<this._numPoints;i++){results[i]=parseFloat(x.toFixed(6));x+=step;}
|
||||
return results;}});Range.validate=function(range){if(!range instanceof Range){return false;}
|
||||
if(isNaN(range.getMinimum())||isNaN(range.getMaximum())||isNaN(range.getNumPoints())||range.getMaximum()<range.getMinimum()||range.getNumPoints()<=0){return false;}
|
||||
return true;}
|
||||
var ContinuousDistribution=Class.extend({init:function(name){this._name=name;},toString:function(){return this._string;},getName:function(){return this._name;},getClassName:function(){return this._name+'Distribution';},density:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._pdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._pdf(points[i]));}
|
||||
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".density()";}},cumulativeDensity:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._cdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._cdf(points[i]));}
|
||||
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".cumulativeDensity()";}},getRange:function(standardDeviations,numPoints){if(standardDeviations==null){standardDeviations=5;}
|
||||
if(numPoints==null){numPoints=100;}
|
||||
var min=this.getMean()-standardDeviations*Math.sqrt(this.getVariance());var max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());if(this.getClassName()=='GammaDistribution'||this.getClassName()=='LogNormalDistribution'){min=0.0;max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());}else if(this.getClassName()=='BetaDistribution'){min=0.0;max=1.0;}
|
||||
var range=new Range(min,max,numPoints);return range;},getVariance:function(){},getMean:function(){},getQuantile:function(p){var self=this;function findClosestMatch(range,p){var ERR=1.0e-5;var xs=range.getPoints();var closestIndex=0;var closestDistance=999;for(var i=0;i<xs.length;i++){var pp=self.cumulativeDensity(xs[i]);var distance=Math.abs(pp-p);if(distance<closestDistance){closestIndex=i;closestDistance=distance;}}
|
||||
if(closestDistance<=ERR){return xs[closestIndex];}else{var newRange=new Range(xs[closestIndex-1],xs[closestIndex+1],20);return findClosestMatch(newRange,p);}}
|
||||
var range=this.getRange(5,20);return findClosestMatch(range,p);}});var NormalDistribution=ContinuousDistribution.extend({init:function(mean,sigma){this._super('Normal');this._mean=parseFloat(mean);this._sigma=parseFloat(sigma);this._string="Normal ("+this._mean.toFixed(2)+", "+this._sigma.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null){give_log=false;}
|
||||
var sigma=this._sigma;var mu=this._mean;if(!jstat.isFinite(sigma)){return(give_log)?Number.NEGATIVE_INFINITY:0.0}
|
||||
if(!jstat.isFinite(x)&&mu==x){return Number.NaN;}
|
||||
if(sigma<=0){if(sigma<0){throw"invalid sigma in _pdf";}
|
||||
return(x==mu)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
x=(x-mu)/sigma;if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
return(give_log?-(jstat.LN_SQRT_2PI+0.5*x*x+Math.log(sigma)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*x*x)/sigma);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;function pnorm_both(x,cum,ccum,i_tail,log_p){var a=[2.2352520354606839287,161.02823106855587881,1067.6894854603709582,18154.981253343561249,0.065682337918207449113];var b=[47.20258190468824187,976.09855173777669322,10260.932208618978205,45507.789335026729956];var c=[0.39894151208813466764,8.8831497943883759412,93.506656132177855979,597.27027639480026226,2494.5375852903726711,6848.1904505362823326,11602.651437647350124,9842.7148383839780218,1.0765576773720192317e-8];var d=[22.266688044328115691,235.38790178262499861,1519.377599407554805,6485.558298266760755,18615.571640885098091,34900.952721145977266,38912.003286093271411,19685.429676859990727];var p=[0.21589853405795699,0.1274011611602473639,0.022235277870649807,0.001421619193227893466,2.9112874951168792e-5,0.02307344176494017303];var q=[1.28426009614491121,0.468238212480865118,0.0659881378689285515,0.00378239633202758244,7.29751555083966205e-5];var xden,xnum,temp,del,eps,xsq,y,i,lower,upper;eps=jstat.DBL_EPSILON*0.5;lower=i_tail!=1;upper=i_tail!=0;y=Math.abs(x);if(y<=0.67448975){if(y>eps){xsq=x*x;xnum=a[4]*xsq;xden=xsq;for(i=0;i<3;++i){xnum=(xnum+a[i])*xsq;xden=(xden+b[i])*xsq;}}else{xnum=xden=0.0;}
|
||||
temp=x*(xnum+a[3])/(xden+b[3]);if(lower)cum=0.5+temp;if(upper)ccum=0.5-temp;if(log_p){if(lower)cum=Math.log(cum);if(upper)ccum=Math.log(ccum);}}else if(y<=jstat.SQRT_32){xnum=c[8]*y;xden=y;for(i=0;i<7;++i){xnum=(xnum+c[i])*y;xden=(xden+d[i])*y;}
|
||||
temp=(xnum+c[7])/(xden+d[7]);xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
|
||||
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
|
||||
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
|
||||
if(x>0.0){temp=cum;if(lower){cum=ccum;}
|
||||
ccum=temp;}}
|
||||
else if((log_p&&y<1e170)||(lower&&-37.5193<x&&x<8.2924)||(upper&&-8.2924<x&&x<37.5193)){xsq=1.0/(x*x);xnum=p[5]*xsq;xden=xsq;for(i=0;i<4;++i){xnum=(xnum+p[i])*xsq;xden=(xden+q[i])*xsq;}
|
||||
temp=xsq*(xnum+p[4])/(xden+q[4]);temp=(jstat.ONE_SQRT_2PI-temp)/y;xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
|
||||
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
|
||||
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
|
||||
if(x>0.0){temp=cum;if(lower){cum=ccum;}
|
||||
ccum=temp;}}else{if(x>0){cum=(log_p)?0.0:1.0;ccum=(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{cum=(log_p)?Number.NEGATIVE_INFINITY:0.0;ccum=(log_p)?0.0:1.0;}}
|
||||
return[cum,ccum];}
|
||||
var p,cp;var mu=this._mean;var sigma=this._sigma;var R_DT_0,R_DT_1;if(lower_tail){if(log_p){R_DT_0=Number.NEGATIVE_INFINITY;R_DT_1=0.0;}else{R_DT_0=0.0;R_DT_1=1.0;}}else{if(log_p){R_DT_0=0.0;R_DT_1=Number.NEGATIVE_INFINITY;}else{R_DT_0=1.0;R_DT_1=0.0;}}
|
||||
if(!jstat.isFinite(x)&&mu==x)return Number.NaN;if(sigma<=0){if(sigma<0){console.warn("Sigma is less than 0");return Number.NaN;}
|
||||
return(x<mu)?R_DT_0:R_DT_1;}
|
||||
p=(x-mu)/sigma;if(!jstat.isFinite(p)){return(x<mu)?R_DT_0:R_DT_1;}
|
||||
x=p;var result=pnorm_both(x,p,cp,(lower_tail?false:true),log_p);return(lower_tail?result[0]:result[1]);},getMean:function(){return this._mean;},getSigma:function(){return this._sigma;},getVariance:function(){return this._sigma*this._sigma;}});var LogNormalDistribution=ContinuousDistribution.extend({init:function(location,scale){this._super('LogNormal')
|
||||
this._location=parseFloat(location);this._scale=parseFloat(scale);this._string="LogNormal ("+this._location.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var y;var sdlog=this._scale;var meanlog=this._location;if(give_log==null){give_log=false;}
|
||||
if(sdlog<=0)throw"Illegal parameter in _pdf";if(x<=0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
y=(Math.log(x)-meanlog)/sdlog;return(give_log?-(jstat.LN_SQRT_2PI+0.5*y*y+Math.log(x*sdlog)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*y*y)/(x*sdlog));},_cdf:function(x,lower_tail,log_p){var sdlog=this._scale;var meanlog=this._location;if(lower_tail==null){lower_tail=true;}
|
||||
if(log_p==null){log_p=false;}
|
||||
if(sdlog<=0){throw"illegal std in _cdf";}
|
||||
if(x>0){var nd=new NormalDistribution(meanlog,sdlog);return nd._cdf(Math.log(x),lower_tail,log_p);}
|
||||
if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}},getLocation:function(){return this._location;},getScale:function(){return this._scale;},getMean:function(){return Math.exp((this._location+this._scale)/2);},getVariance:function(){var ans=(Math.exp(this._scale)-1)*Math.exp(2*this._location+this._scale);return ans;}});var GammaDistribution=ContinuousDistribution.extend({init:function(shape,scale){this._super('Gamma');this._shape=parseFloat(shape);this._scale=parseFloat(scale);this._string="Gamma ("+this._shape.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var pr;var shape=this._shape;var scale=this._scale;if(give_log==null){give_log=false;}
|
||||
if(shape<0||scale<=0){throw"Illegal argument in _pdf";}
|
||||
if(x<0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(shape==0){return(x==0)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(x==0){if(shape<1)return Number.POSITIVE_INFINITY;if(shape>1)return(give_log)?Number.NEGATIVE_INFINITY:0.0;return(give_log)?-Math.log(scale):1/scale;}
|
||||
if(shape<1){pr=jstat.dopois_raw(shape,x/scale,give_log);return give_log?pr+Math.log(shape/x):pr*shape/x;}
|
||||
pr=jstat.dopois_raw(shape-1,x/scale,give_log);return give_log?pr-Math.log(scale):pr/scale;},_cdf:function(x,lower_tail,log_p){function USE_PNORM(){pn1=Math.sqrt(alph)*3.0*(Math.pow(x/alph,1.0/3.0)+1.0/(9.0*alph)-1.0);var norm_dist=new NormalDistribution(0.0,1.0);return norm_dist._cdf(pn1,lower_tail,log_p);}
|
||||
if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var alph=this._shape;var scale=this._scale;var xbig=1.0e+8;var xlarge=1.0e+37;var alphlimit=1e5;var pn1,pn2,pn3,pn4,pn5,pn6,arg,a,b,c,an,osum,sum,n,pearson;if(alph<=0.||scale<=0.){console.warn('Invalid gamma params in _cdf');return Number.NaN;}
|
||||
x/=scale;if(isNaN(x))return x;if(x<=0.0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}}
|
||||
if(alph>alphlimit){return USE_PNORM();}
|
||||
if(x>xbig*alph){if(x>jstat.DBL_MAX*alph){if(lower_tail){return(log_p)?0.0:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}else{return USE_PNORM();}}
|
||||
if(x<=1.0||x<alph){pearson=1;arg=alph*Math.log(x)-x-jstat.lgamma(alph+1.0);c=1.0;sum=1.0;a=alph;do{a+=1.0;c*=x/a;sum+=c;}while(c>jstat.DBL_EPSILON*sum);}else{pearson=0;arg=alph*Math.log(x)-x-jstat.lgamma(alph);a=1.-alph;b=a+x+1.;pn1=1.;pn2=x;pn3=x+1.;pn4=x*b;sum=pn3/pn4;for(n=1;;n++){a+=1.;b+=2.;an=a*n;pn5=b*pn3-an*pn1;pn6=b*pn4-an*pn2;if(Math.abs(pn6)>0.){osum=sum;sum=pn5/pn6;if(Math.abs(osum-sum)<=jstat.DBL_EPSILON*jstat.fmin2(1.0,sum))
|
||||
break;}
|
||||
pn1=pn3;pn2=pn4;pn3=pn5;pn4=pn6;if(Math.abs(pn5)>=xlarge){pn1/=xlarge;pn2/=xlarge;pn3/=xlarge;pn4/=xlarge;}}}
|
||||
arg+=Math.log(sum);lower_tail=(lower_tail==pearson);if(log_p&&lower_tail)
|
||||
return(arg);if(lower_tail){return Math.exp(arg);}else{if(log_p){return(arg>-Math.LN2)?Math.log(-jstat.expm1(arg)):jstat.log1p(-Math.exp(arg));}else{return-jstat.expm1(arg);}}},getShape:function(){return this._shape;},getScale:function(){return this._scale;},getMean:function(){return this._shape*this._scale;},getVariance:function(){return this._shape*Math.pow(this._scale,2);}});var BetaDistribution=ContinuousDistribution.extend({init:function(alpha,beta){this._super('Beta');this._alpha=parseFloat(alpha);this._beta=parseFloat(beta);this._string="Beta ("+this._alpha.toFixed(2)+", "+this._beta.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null)give_log=false;var a=this._alpha;var b=this._beta;var lval;if(a<=0||b<=0){console.warn('Illegal arguments in _pdf');return Number.NaN;}
|
||||
if(x<0||x>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(x==0){if(a>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(a<1){return Number.POSITIVE_INFINITY;}
|
||||
return(give_log)?Math.log(b):b;}
|
||||
if(x==1){if(b>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(b<1){return Number.POSITIVE_INFINITY;}
|
||||
return(give_log)?Math.log(a):a;}
|
||||
if(a<=2||b<=2){lval=(a-1)*Math.log(x)+(b-1)*jstat.log1p(-x)-jstat.logBeta(a,b);}else{lval=Math.log(a+b-1)+jstat.dbinom_raw(a-1,a+b-2,x,1-x,true);}
|
||||
return(give_log)?lval:Math.exp(lval);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var pin=this._alpha;var qin=this._beta;if(pin<=0||qin<=0){console.warn('Invalid argument in _cdf');return Number.NaN;}
|
||||
if(x<=0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.1:1.0;}}
|
||||
if(x>=1){if(lower_tail){return(log_p)?0.1:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}
|
||||
return jstat.incompleteBeta(pin,qin,x);},getAlpha:function(){return this._alpha;},getBeta:function(){return this._beta;},getMean:function(){return this._alpha/(this._alpha+this._beta);},getVariance:function(){var ans=(this._alpha*this._beta)/(Math.pow(this._alpha+this._beta,2)*(this._alpha+this._beta+1));return ans;}});var StudentTDistribution=ContinuousDistribution.extend({init:function(degreesOfFreedom,mu){this._super('StudentT');this._dof=parseFloat(degreesOfFreedom);if(mu!=null){this._mu=parseFloat(mu);this._string="StudentT ("+this._dof.toFixed(2)+", "+this._mu.toFixed(2)+")";}else{this._mu=0.0;this._string="StudentT ("+this._dof.toFixed(2)+")";}},_pdf:function(x,give_log){if(give_log==null)give_log=false;if(this._mu==null){return this._dt(x,give_log);}else{var y=this._dnt(x,give_log);if(y>1){console.warn('x:'+x+', y: '+y);}
|
||||
return y;}},_cdf:function(x,lower_tail,give_log){if(lower_tail==null)lower_tail=true;if(give_log==null)give_log=false;if(this._mu==null){return this._pt(x,lower_tail,give_log);}else{return this._pnt(x,lower_tail,give_log);}},_dt:function(x,give_log){var t,u;var n=this._dof;if(n<=0){console.warn('Invalid parameters in _dt');return Number.NaN;}
|
||||
if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
|
||||
if(!jstat.isFinite(n)){var norm=new NormalDistribution(0.0,1.0);return norm.density(x,give_log);}
|
||||
t=-jstat.bd0(n/2.0,(n+1)/2.0)+jstat.stirlerr((n+1)/2.0)-jstat.stirlerr(n/2.0);if(x*x>0.2*n)
|
||||
u=Math.log(1+x*x/n)*n/2;else
|
||||
u=-jstat.bd0(n/2.0,(n+x*x)/2.0)+x*x/2.0;var p1=jstat.TWO_PI*(1+x*x/n);var p2=t-u;return(give_log)?-0.5*Math.log(p1)+p2:Math.exp(p2)/Math.sqrt(p1);},_dnt:function(x,give_log){if(give_log==null)give_log=false;var df=this._dof;var ncp=this._mu;var u;if(df<=0.0){console.warn("Illegal arguments _dnf");return Number.NaN;}
|
||||
if(ncp==0.0){return this._dt(x,give_log);}
|
||||
if(!jstat.isFinite(x)){if(give_log){return Number.NEGATIVE_INFINITY;}else{return 0.0;}}
|
||||
if(!isFinite(df)||df>1e8){var dist=new NormalDistribution(ncp,1.);return dist.density(x,give_log);}
|
||||
if(Math.abs(x)>Math.sqrt(df*jstat.DBL_EPSILON)){var newT=new StudentTDistribution(df+2,ncp);u=Math.log(df)-Math.log(Math.abs(x))+
|
||||
Math.log(Math.abs(newT._pnt(x*Math.sqrt((df+2)/df),true,false)-
|
||||
this._pnt(x,true,false)));}
|
||||
else{u=jstat.lgamma((df+1)/2)-jstat.lgamma(df/2)
|
||||
-.5*(Math.log(Math.PI)+Math.log(df)+ncp*ncp);}
|
||||
return(give_log?u:Math.exp(u));},_pt:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var val,nx;var n=this._dof;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
|
||||
if(n<=0.0){console.warn("Invalid T distribution _pt");return Number.NaN;}
|
||||
var norm=new NormalDistribution(0,1);if(!jstat.isFinite(x)){return(x<0)?DT_0:DT_1;}
|
||||
if(!jstat.isFinite(n)){return norm._cdf(x,lower_tail,log_p);}
|
||||
if(n>4e5){val=1./(4.*n);return norm._cdf(x*(1.-val)/sqrt(1.+x*x*2.*val),lower_tail,log_p);}
|
||||
nx=1+(x/n)*x;if(nx>1e100){var lval;lval=-0.5*n*(2*Math.log(Math.abs(x))-Math.log(n))
|
||||
-jstat.logBeta(0.5*n,0.5)-Math.log(0.5*n);val=log_p?lval:Math.exp(lval);}else{if(n>x*x){var beta=new BetaDistribution(0.5,n/2.);return beta._cdf(x*x/(n+x*x),false,log_p);}else{beta=new BetaDistribution(n/2.,0.5);return beta._cdf(1./nx,true,log_p);}}
|
||||
if(x<=0.)
|
||||
lower_tail=!lower_tail;if(log_p){if(lower_tail)return jstat.log1p(-0.5*Math.exp(val));else return val-M_LN2;}
|
||||
else{val/=2.;if(lower_tail){return(0.5-val+0.5);}else{return val;}}},_pnt:function(t,lower_tail,log_p){var dof=this._dof;var ncp=this._mu;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
|
||||
var albeta,a,b,del,errbd,lambda,rxb,tt,x;var geven,godd,p,q,s,tnc,xeven,xodd;var it,negdel;var ITRMAX=1000;var ERRMAX=1.e-7;if(dof<=0.0){return Number.NaN;}else if(dof==0.0){return this._pt(t);}
|
||||
if(!jstat.isFinite(t)){return(t<0)?DT_0:DT_1;}
|
||||
if(t>=0.){negdel=false;tt=t;del=ncp;}else{if(ncp>=40&&(!log_p||!lower_tail)){return DT_0;}
|
||||
negdel=true;tt=-t;del=-ncp;}
|
||||
if(dof>4e5||del*del>2*Math.LN2*(-(jstat.DBL_MIN_EXP))){s=1./(4.*dof);var norm=new NormalDistribution(del,Math.sqrt(1.+tt*tt*2.*s));var result=norm._cdf(tt*(1.-s),lower_tail!=negdel,log_p);return result;}
|
||||
x=t*t;rxb=dof/(x+dof);x=x/(x+dof);if(x>0.){lambda=del*del;p=.5*Math.exp(-.5*lambda);if(p==0.){console.warn("underflow in _pnt");return DT_0;}
|
||||
q=jstat.SQRT_2dPI*p*del;s=.5-p;if(s<1e-7){s=-0.5*jstat.expm1(-0.5*lambda);}
|
||||
a=.5;b=.5*dof;rxb=Math.pow(rxb,b);albeta=jstat.LN_SQRT_PI+jstat.lgamma(b)-jstat.lgamma(.5+b);xodd=jstat.incompleteBeta(a,b,x);godd=2.*rxb*Math.exp(a*Math.log(x)-albeta);tnc=b*x;xeven=(tnc<jstat.DBL_EPSILON)?tnc:1.-rxb;geven=tnc*rxb;tnc=p*xodd+q*xeven;for(it=1;it<=ITRMAX;it++){a+=1.;xodd-=godd;xeven-=geven;godd*=x*(a+b-1.)/a;geven*=x*(a+b-.5)/(a+.5);p*=lambda/(2*it);q*=lambda/(2*it+1);tnc+=p*xodd+q*xeven;s-=p;if(s<-1.e-10){console.write("precision error _pnt");break;}
|
||||
if(s<=0&&it>1)break;errbd=2.*s*(xodd-godd);if(Math.abs(errbd)<ERRMAX)break;}
|
||||
if(it==ITRMAX){throw"Non-convergence _pnt";}}else{tnc=0.;}
|
||||
norm=new NormalDistribution(0,1);tnc+=norm._cdf(-del,true,false);lower_tail=lower_tail!=negdel;if(tnc>1-1e-10&&lower_tail){console.warn("precision error _pnt");}
|
||||
var res=jstat.fmin2(tnc,1.);if(lower_tail){if(log_p){return Math.log(res);}else{return res;}}else{if(log_p){return jstat.log1p(-(res));}else{return(0.5-(res)+0.5);}}},getDegreesOfFreedom:function(){return this._dof;},getNonCentralityParameter:function(){return this._mu;},getMean:function(){if(this._dof>1){var ans=(1/2)*Math.log(this._dof/2)+jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)
|
||||
return Math.exp(ans)*this._mu;}else{return Number.NaN;}},getVariance:function(){if(this._dof>2){var ans=this._dof*(1+this._mu*this._mu)/(this._dof-2)-(((this._mu*this._mu*this._dof)/2)*Math.pow(Math.exp(jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)),2));return ans;}else{return Number.NaN;}}});var Plot=Class.extend({init:function(id,options){this._container='#'+String(id);this._plots=[];this._flotObj=null;this._locked=false;if(options!=null){this._options=options;}else{this._options={};}},getContainer:function(){return this._container;},getGraph:function(){return this._flotObj;},setData:function(data){this._plots=data;},clear:function(){this._plots=[];},showLegend:function(){this._options.legend={show:true}
|
||||
this.render();},hideLegend:function(){this._options.legend={show:false}
|
||||
this.render();},render:function(){this._flotObj=null;this._flotObj=$.plot($(this._container),this._plots,this._options);}});var DistributionPlot=Plot.extend({init:function(id,distribution,range,options){this._super(id,options);this._showPDF=true;this._showCDF=false;this._pdfValues=[];this._cdfValues=[];this._maxY=1;this._plotType='line';this._fill=false;this._distribution=distribution;if(range!=null&&Range.validate(range)){this._range=range;}else{this._range=this._distribution.getRange();}
|
||||
if(this._distribution!=null){this._maxY=this._generateValues();}else{this._options.xaxis={min:range.getMinimum(),max:range.getMaximum()}
|
||||
this._options.yaxis={max:1}}
|
||||
this.render();},setHover:function(bool){if(bool){if(this._options.grid==null){this._options.grid={hoverable:true,mouseActiveRadius:25}}else{this._options.grid.hoverable=true,this._options.grid.mouseActiveRadius=25}
|
||||
function showTooltip(x,y,contents,color){$('<div id="jstat_tooltip">'+contents+'</div>').css({position:'absolute',display:'none',top:y+15,'font-size':'small',left:x+5,border:'1px solid '+color[1],color:color[2],padding:'5px','background-color':color[0],opacity:0.80}).appendTo("body").show();}
|
||||
var previousPoint=null;$(this._container).bind("plothover",function(event,pos,item){$("#x").text(pos.x.toFixed(2));$("#y").text(pos.y.toFixed(2));if(item){if(previousPoint!=item.datapoint){previousPoint=item.datapoint;$("#jstat_tooltip").remove();var x=jstat.toSigFig(item.datapoint[0],2),y=jstat.toSigFig(item.datapoint[1],2);var text=null;var color=item.series.color;if(item.series.label=='PDF'){text="P("+x+") = "+y;color=["#fee","#fdd","#C05F5F"];}else{text="F("+x+") = "+y;color=["#eef","#ddf","#4A4AC0"];}
|
||||
showTooltip(item.pageX,item.pageY,text,color);}}
|
||||
else{$("#jstat_tooltip").remove();previousPoint=null;}});$(this._container).bind("mouseleave",function(){if($('#jstat_tooltip').is(':visible')){$('#jstat_tooltip').remove();previousPoint=null;}});}else{if(this._options.grid==null){this._options.grid={hoverable:false}}else{this._options.grid.hoverable=false}
|
||||
$(this._container).unbind("plothover");}
|
||||
this.render();},setType:function(type){this._plotType=type;var lines={};var points={};if(this._plotType=='line'){lines.show=true;points.show=false;}else if(this._plotType=='points'){lines.show=false;points.show=true;}else if(this._plotType=='both'){lines.show=true;points.show=true;}
|
||||
if(this._options.series==null){this._options.series={lines:lines,points:points}}else{if(this._options.series.lines==null){this._options.series.lines=lines;}else{this._options.series.lines.show=lines.show;}
|
||||
if(this._options.series.points==null){this._options.series.points=points;}else{this._options.series.points.show=points.show;}}
|
||||
this.render();},setFill:function(bool){this._fill=bool;if(this._options.series==null){this._options.series={lines:{fill:bool}}}else{if(this._options.series.lines==null){this._options.series.lines={fill:bool}}else{this._options.series.lines.fill=bool;}}
|
||||
this.render();},clear:function(){this._super();this._distribution=null;this._pdfValues=[];this._cdfValues=[];this.render();},_generateValues:function(){this._cdfValues=[];this._pdfValues=[];var xs=this._range.getPoints();this._options.xaxis={min:xs[0],max:xs[xs.length-1]}
|
||||
var pdfs=this._distribution.density(this._range);var cdfs=this._distribution.cumulativeDensity(this._range);for(var i=0;i<xs.length;i++){if(pdfs[i]==Number.POSITIVE_INFINITY||pdfs[i]==Number.NEGATIVE_INFINITY){pdfs[i]=null;}
|
||||
if(cdfs[i]==Number.POSITIVE_INFINITY||cdfs[i]==Number.NEGATIVE_INFINITY){cdfs[i]=null;}
|
||||
this._pdfValues.push([xs[i],pdfs[i]]);this._cdfValues.push([xs[i],cdfs[i]]);}
|
||||
return jstat.max(pdfs);},showPDF:function(){this._showPDF=true;this.render();},hidePDF:function(){this._showPDF=false;this.render();},showCDF:function(){this._showCDF=true;this.render();},hideCDF:function(){this._showCDF=false;this.render();},setDistribution:function(distribution,range){this._distribution=distribution;if(range!=null){this._range=range;}else{this._range=distribution.getRange();}
|
||||
this._maxY=this._generateValues();this._options.yaxis={max:this._maxY*1.1}
|
||||
this.render();},getDistribution:function(){return this._distribution;},getRange:function(){return this._range;},setRange:function(range){this._range=range;this._generateValues();this.render();},render:function(){if(this._distribution!=null){if(this._showPDF&&this._showCDF){this.setData([{yaxis:1,data:this._pdfValues,color:'rgb(237,194,64)',clickable:false,hoverable:true,label:"PDF"},{yaxis:2,data:this._cdfValues,clickable:false,color:'rgb(175,216,248)',hoverable:true,label:"CDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showPDF){this.setData([{data:this._pdfValues,hoverable:true,color:'rgb(237,194,64)',clickable:false,label:"PDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showCDF){this.setData([{data:this._cdfValues,hoverable:true,color:'rgb(175,216,248)',clickable:false,label:"CDF"}]);this._options.yaxis={max:1.1}}}else{this.setData([]);}
|
||||
this._super();}});var DistributionFactory={};DistributionFactory.build=function(json){if(json.NormalDistribution){if(json.NormalDistribution.mean!=null&&json.NormalDistribution.standardDeviation!=null){return new NormalDistribution(json.NormalDistribution.mean[0],json.NormalDistribution.standardDeviation[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.LogNormalDistribution){if(json.LogNormalDistribution.location!=null&&json.LogNormalDistribution.scale!=null){return new LogNormalDistribution(json.LogNormalDistribution.location[0],json.LogNormalDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.BetaDistribution){if(json.BetaDistribution.alpha!=null&&json.BetaDistribution.beta!=null){return new BetaDistribution(json.BetaDistribution.alpha[0],json.BetaDistribution.beta[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.GammaDistribution){if(json.GammaDistribution.shape!=null&&json.GammaDistribution.scale!=null){return new GammaDistribution(json.GammaDistribution.shape[0],json.GammaDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.StudentTDistribution){if(json.StudentTDistribution.degreesOfFreedom!=null&&json.StudentTDistribution.nonCentralityParameter!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0],json.StudentTDistribution.nonCentralityParameter[0]);}else if(json.StudentTDistribution.degreesOfFreedom!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('logme', [], function () {
|
||||
var debugMode;
|
||||
|
||||
// debugMode can be one of the following:
|
||||
//
|
||||
// true - All messages passed to logme will be written to the internal
|
||||
// browser console.
|
||||
// false - Suppress all output to the internal browser console.
|
||||
//
|
||||
// Obviously, if anywhere there is a direct console.log() call, we can't do
|
||||
// anything about it. That's why use logme() - it will allow to turn off
|
||||
// the output of debug information with a single change to a variable.
|
||||
debugMode = true;
|
||||
|
||||
return logme;
|
||||
|
||||
/*
|
||||
* function: logme
|
||||
*
|
||||
* A helper function that provides logging facilities. We don't want
|
||||
* to call console.log() directly, because sometimes it is not supported
|
||||
* by the browser. Also when everything is routed through this function.
|
||||
* the logging output can be easily turned off.
|
||||
*
|
||||
* logme() supports multiple parameters. Each parameter will be passed to
|
||||
* console.log() function separately.
|
||||
*
|
||||
*/
|
||||
function logme() {
|
||||
var i;
|
||||
|
||||
if (
|
||||
(typeof debugMode === 'undefined') ||
|
||||
(debugMode !== true) ||
|
||||
(typeof window.console === 'undefined')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < arguments.length; i++) {
|
||||
window.console.log(arguments[i]);
|
||||
}
|
||||
} // End-of: function logme
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -0,0 +1,89 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('Sliders', ['logme'], function (logme) {
|
||||
return Sliders;
|
||||
|
||||
function Sliders(gstId, state) {
|
||||
var c1, paramName, allParamNames, sliderDiv;
|
||||
|
||||
allParamNames = state.getAllParameterNames();
|
||||
|
||||
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
|
||||
paramName = allParamNames[c1];
|
||||
|
||||
sliderDiv = $('#' + gstId + '_slider_' + paramName);
|
||||
|
||||
if (sliderDiv.length === 1) {
|
||||
createSlider(sliderDiv, paramName);
|
||||
} 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 + '".');
|
||||
// }
|
||||
}
|
||||
|
||||
function createSlider(sliderDiv, paramName) {
|
||||
var paramObj;
|
||||
|
||||
paramObj = state.getParamObj(paramName);
|
||||
|
||||
// Check that the retrieval went OK.
|
||||
if (paramObj === undefined) {
|
||||
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a jQuery UI slider from the slider DIV. We will set
|
||||
// starting parameters, and will also attach a handler to update
|
||||
// the 'state' on the 'slide' event.
|
||||
sliderDiv.slider({
|
||||
'min': paramObj.min,
|
||||
'max': paramObj.max,
|
||||
'value': paramObj.value,
|
||||
'step': paramObj.step
|
||||
});
|
||||
|
||||
// Tell the parameter object stored in state that we have a slider
|
||||
// that is attached to it. Next time when the parameter changes, it
|
||||
// will also update the value of this slider.
|
||||
paramObj.sliderDiv = sliderDiv;
|
||||
|
||||
// Atach callbacks to update the slider's parameter.
|
||||
paramObj.sliderDiv.on('slide', sliderOnSlide);
|
||||
paramObj.sliderDiv.on('slidechange', sliderOnChange);
|
||||
|
||||
return;
|
||||
|
||||
// Update the 'state' - i.e. set the value of the parameter this
|
||||
// slider is attached to to a new value.
|
||||
//
|
||||
// This will cause the plot to be redrawn each time after the user
|
||||
// drags the slider handle and releases it.
|
||||
function sliderOnSlide(event, ui) {
|
||||
// Last parameter passed to setParameterValue() will be 'true'
|
||||
// so that the function knows we are a slider, and it can
|
||||
// change the our value back in the case when the new value is
|
||||
// invalid for some reason.
|
||||
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'slide') === undefined) {
|
||||
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
|
||||
}
|
||||
}
|
||||
|
||||
function sliderOnChange(event, ui) {
|
||||
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'change') === undefined) {
|
||||
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
395
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js
Normal file
395
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js
Normal file
@@ -0,0 +1,395 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define('State', ['logme'], function (logme) {
|
||||
var stateInst;
|
||||
|
||||
// Since there will be (can be) multiple GST on a page, and each will have
|
||||
// a separate state, we will create a factory constructor function. The
|
||||
// constructor will expect the ID of the DIV with the GST contents, and the
|
||||
// configuration object (parsed from a JSON string). It will return an
|
||||
// object containing methods to set and get the private state properties.
|
||||
|
||||
stateInst = 0;
|
||||
|
||||
// This module defines and returns a factory constructor.
|
||||
return State;
|
||||
|
||||
function State(gstId, config) {
|
||||
var parameters, allParameterNames, allParameterValues,
|
||||
plotDiv, dynamicEl, dynamicElByElId;
|
||||
|
||||
dynamicEl = [];
|
||||
dynamicElByElId = {};
|
||||
|
||||
stateInst += 1;
|
||||
// logme('MESSAGE: Creating state instance # ' + stateInst + '.');
|
||||
|
||||
// Initially, there are no parameters to track. So, we will instantiate
|
||||
// an empty object.
|
||||
//
|
||||
// As we parse the JSON config object, we will add parameters as
|
||||
// named properties. For example
|
||||
//
|
||||
// parameters.a = {...};
|
||||
//
|
||||
// will be created for the parameter 'a'.
|
||||
parameters = {};
|
||||
|
||||
// Check that the required parameters config object is available.
|
||||
if ($.isPlainObject(config.parameters) === false) {
|
||||
logme('ERROR: Expected config.parameters to be an object. It is not.');
|
||||
logme('config.parameters = ', config.parameters);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If config.parameters.param is an array, pass it to the processor
|
||||
// element by element.
|
||||
if ($.isArray(config.parameters.param) === true) {
|
||||
(function (c1) {
|
||||
while (c1 < config.parameters.param.length) {
|
||||
processParameter(config.parameters.param[c1]);
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
// If config.parameters.param is an object, pass this object to the
|
||||
// processor directly.
|
||||
else if ($.isPlainObject(config.parameters.param) === true) {
|
||||
processParameter(config.parameters.param);
|
||||
}
|
||||
|
||||
// If config.parameters.param is some other type, report an error and
|
||||
// do not continue.
|
||||
else {
|
||||
logme('ERROR: config.parameters.param is of an unsupported type.');
|
||||
logme('config.parameters.param = ', config.parameters.param);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Instead of building these arrays every time when some component
|
||||
// requests them, we will create them in the beginning, and then update
|
||||
// each element individually when some parameter's value changes.
|
||||
//
|
||||
// Then we can just return the required array, instead of iterating
|
||||
// over all of the properties of the 'parameters' object, and
|
||||
// extracting their names/values one by one.
|
||||
allParameterNames = [];
|
||||
allParameterValues = [];
|
||||
|
||||
// Populate 'allParameterNames', and 'allParameterValues' with data.
|
||||
generateHelperArrays();
|
||||
|
||||
// The constructor will return an object with methods to operate on
|
||||
// it's private properties.
|
||||
return {
|
||||
'getParameterValue': getParameterValue,
|
||||
'setParameterValue': setParameterValue,
|
||||
|
||||
'getParamObj': getParamObj,
|
||||
|
||||
'getAllParameterNames': getAllParameterNames,
|
||||
'getAllParameterValues': getAllParameterValues,
|
||||
|
||||
'bindUpdatePlotEvent': bindUpdatePlotEvent,
|
||||
'addDynamicEl': addDynamicEl,
|
||||
|
||||
// plde is an abbreviation for Plot Label Dynamic Elements.
|
||||
plde: []
|
||||
};
|
||||
|
||||
function getAllParameterNames() {
|
||||
return allParameterNames;
|
||||
}
|
||||
|
||||
function getAllParameterValues() {
|
||||
return allParameterValues;
|
||||
}
|
||||
|
||||
function getParamObj(paramName) {
|
||||
if (parameters.hasOwnProperty(paramName) === false) {
|
||||
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return parameters[paramName];
|
||||
}
|
||||
|
||||
function bindUpdatePlotEvent(newPlotDiv, callback) {
|
||||
plotDiv = newPlotDiv;
|
||||
|
||||
plotDiv.bind('update_plot', callback);
|
||||
}
|
||||
|
||||
function addDynamicEl(el, func, elId, updateOnEvent) {
|
||||
var newLength;
|
||||
|
||||
newLength = dynamicEl.push({
|
||||
'el': el,
|
||||
'func': func,
|
||||
'elId': elId,
|
||||
'updateOnEvent': updateOnEvent
|
||||
});
|
||||
|
||||
if (typeof dynamicElByElId[elId] !== 'undefined') {
|
||||
logme(
|
||||
'ERROR: Duplicate dynamic element ID "' + elId + '" found.'
|
||||
);
|
||||
} else {
|
||||
dynamicElByElId[elId] = dynamicEl[newLength - 1];
|
||||
}
|
||||
}
|
||||
|
||||
function getParameterValue(paramName) {
|
||||
|
||||
// If the name of the constant is not tracked by state, return an
|
||||
// 'undefined' value.
|
||||
if (parameters.hasOwnProperty(paramName) === false) {
|
||||
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return parameters[paramname].value;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
//
|
||||
// Function: setParameterValue(paramName, paramValue, element)
|
||||
// --------------------------------------------------
|
||||
//
|
||||
//
|
||||
// This function can be called from a callback, registered by a slider
|
||||
// or a text input, when specific events ('slide' or 'change') are
|
||||
// triggered.
|
||||
//
|
||||
// The 'paramName' is the name of the parameter in 'parameters' object
|
||||
// whose value must be updated to the new value of 'paramValue'.
|
||||
//
|
||||
// Before we update the value, we must check that:
|
||||
//
|
||||
// 1.) the parameter named as 'paramName' actually exists in the
|
||||
// 'parameters' object;
|
||||
// 2.) the value 'paramValue' is a valid floating-point number, and
|
||||
// it lies within the range specified by the 'min' and 'max'
|
||||
// properties of the stored parameter object.
|
||||
//
|
||||
// If 'paramName' and 'paramValue' turn out to be valid, we will update
|
||||
// the stored value in the parameter with the new value, and also
|
||||
// update all of the text inputs and the slider that correspond to this
|
||||
// parameter (if any), so that they reflect the new parameter's value.
|
||||
// Finally, the helper array 'allParameterValues' will also be updated
|
||||
// to reflect the change.
|
||||
//
|
||||
// If something went wrong (for example the new value is outside the
|
||||
// allowed range), then we will reset the 'element' to display the
|
||||
// original value.
|
||||
//
|
||||
// ####################################################################
|
||||
function setParameterValue(paramName, paramValue, element, slider, updateOnEvent) {
|
||||
var paramValueNum, c1;
|
||||
|
||||
// If a parameter with the name specified by the 'paramName'
|
||||
// parameter is not tracked by state, do not do anything.
|
||||
if (parameters.hasOwnProperty(paramName) === false) {
|
||||
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to convert the passed value to a valid floating-point
|
||||
// number.
|
||||
paramValueNum = parseFloat(paramValue);
|
||||
|
||||
// We are interested only in valid float values. NaN, -INF,
|
||||
// +INF we will disregard.
|
||||
if (isFinite(paramValueNum) === false) {
|
||||
logme('ERROR: New parameter value is not a floating-point number.');
|
||||
logme('paramValue = ', paramValue);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (paramValueNum < parameters[paramName].min) {
|
||||
paramValueNum = parameters[paramName].min;
|
||||
} else if (paramValueNum > parameters[paramName].max) {
|
||||
paramValueNum = parameters[paramName].max;
|
||||
}
|
||||
|
||||
parameters[paramName].value = paramValueNum;
|
||||
|
||||
// Update all text inputs with the new parameter's value.
|
||||
for (c1 = 0; c1 < parameters[paramName].inputDivs.length; c1 += 1) {
|
||||
parameters[paramName].inputDivs[c1].val(paramValueNum);
|
||||
}
|
||||
|
||||
// Update the single slider with the new parameter's value.
|
||||
if ((slider === false) && (parameters[paramName].sliderDiv !== null)) {
|
||||
parameters[paramName].sliderDiv.slider('value', paramValueNum);
|
||||
}
|
||||
|
||||
// Update the helper array with the new parameter's value.
|
||||
allParameterValues[parameters[paramName].helperArrayIndex] = paramValueNum;
|
||||
|
||||
for (c1 = 0; c1 < dynamicEl.length; c1++) {
|
||||
if (
|
||||
((updateOnEvent !== undefined) && (dynamicEl[c1].updateOnEvent === updateOnEvent)) ||
|
||||
(updateOnEvent === undefined)
|
||||
) {
|
||||
// If we have a DOM element, call the function "paste" the answer into the DIV.
|
||||
if (dynamicEl[c1].el !== null) {
|
||||
dynamicEl[c1].el.html(dynamicEl[c1].func.apply(window, allParameterValues));
|
||||
}
|
||||
// If we DO NOT have an element, simply call the function. The function can then
|
||||
// manipulate all the DOM elements it wants, without the fear of them being overwritten
|
||||
// by us afterwards.
|
||||
else {
|
||||
dynamicEl[c1].func.apply(window, allParameterValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a plot DIV to work with, tell to update.
|
||||
if (plotDiv !== undefined) {
|
||||
plotDiv.trigger('update_plot');
|
||||
}
|
||||
|
||||
return true;
|
||||
} // End-of: function setParameterValue
|
||||
|
||||
// ####################################################################
|
||||
//
|
||||
// Function: processParameter(obj)
|
||||
// -------------------------------
|
||||
//
|
||||
//
|
||||
// This function will be run once for each instance of a GST when
|
||||
// parsing the JSON config object.
|
||||
//
|
||||
// 'newParamObj' must be empty from the start for each invocation of
|
||||
// this function, that's why we will declare it locally.
|
||||
//
|
||||
// We will parse the passed object 'obj' and populate the 'newParamObj'
|
||||
// object with required properties.
|
||||
//
|
||||
// Since there will be many properties that are of type floating-point
|
||||
// number, we will have a separate function for parsing them.
|
||||
//
|
||||
// processParameter() will fail right away if 'obj' does not have a
|
||||
// '@var' property which represents the name of the parameter we want
|
||||
// to process.
|
||||
//
|
||||
// If, after all of the properties have been processed, we reached the
|
||||
// end of the function successfully, the 'newParamObj' will be added to
|
||||
// the 'parameters' object (that is defined in the scope of State()
|
||||
// function) as a property named as the name of the parameter.
|
||||
//
|
||||
// If at least one of the properties from 'obj' does not get correctly
|
||||
// parsed, then the parameter represented by 'obj' will be disregarded.
|
||||
// It will not be available to user-defined plotting functions, and
|
||||
// things will most likely break. We will notify the user about this.
|
||||
//
|
||||
// ####################################################################
|
||||
function processParameter(obj) {
|
||||
var paramName, newParamObj;
|
||||
|
||||
if (typeof obj['@var'] !== 'string') {
|
||||
logme('ERROR: Expected obj["@var"] to be a string. It is not.');
|
||||
logme('obj["@var"] = ', obj['@var']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
paramName = obj['@var'];
|
||||
newParamObj = {};
|
||||
|
||||
if (
|
||||
(processFloat('@min', 'min') === false) ||
|
||||
(processFloat('@max', 'max') === false) ||
|
||||
(processFloat('@step', 'step') === false) ||
|
||||
(processFloat('@initial', 'value') === false)
|
||||
) {
|
||||
logme('ERROR: A required property is missing. Not creating parameter "' + paramName + '"');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pointers to text input and slider DIV elements that this
|
||||
// parameter will be attached to. Initially there are none. When we
|
||||
// will create text inputs and sliders, we will update these
|
||||
// properties.
|
||||
newParamObj.inputDivs = [];
|
||||
newParamObj.sliderDiv = null;
|
||||
|
||||
// Everything went well, so save the new parameter object.
|
||||
parameters[paramName] = newParamObj;
|
||||
|
||||
return;
|
||||
|
||||
function processFloat(attrName, newAttrName) {
|
||||
var attrValue;
|
||||
|
||||
if (typeof obj[attrName] !== 'string') {
|
||||
logme('ERROR: Expected obj["' + attrName + '"] to be a string. It is not.');
|
||||
logme('obj["' + attrName + '"] = ', obj[attrName]);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
attrValue = parseFloat(obj[attrName]);
|
||||
|
||||
if (isFinite(attrValue) === false) {
|
||||
logme('ERROR: Expected obj["' + attrName + '"] to be a valid floating-point number. It is not.');
|
||||
logme('obj["' + attrName + '"] = ', obj[attrName]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
newParamObj[newAttrName] = attrValue;
|
||||
|
||||
return true;
|
||||
} // End-of: function processFloat
|
||||
} // End-of: function processParameter
|
||||
|
||||
// ####################################################################
|
||||
//
|
||||
// Function: generateHelperArrays()
|
||||
// -------------------------------
|
||||
//
|
||||
//
|
||||
// Populate 'allParameterNames' and 'allParameterValues' with data.
|
||||
// Link each parameter object with the corresponding helper array via
|
||||
// an index 'helperArrayIndex'. It will be the same for both of the
|
||||
// arrays.
|
||||
//
|
||||
// NOTE: It is important to remember to update these helper arrays
|
||||
// whenever a new parameter is added (or one is removed), or when a
|
||||
// parameter's value changes.
|
||||
//
|
||||
// ####################################################################
|
||||
function generateHelperArrays() {
|
||||
var paramName, c1;
|
||||
|
||||
c1 = 0;
|
||||
for (paramName in parameters) {
|
||||
allParameterNames.push(paramName);
|
||||
allParameterValues.push(parameters[paramName].value);
|
||||
|
||||
parameters[paramName].helperArrayIndex = c1;
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}
|
||||
} // End-of: function State
|
||||
});
|
||||
|
||||
// End of wrapper for RequireJS. As you can see, we are passing
|
||||
// namespaced Require JS variables to an anonymous function. Within
|
||||
// it, you can use the standard requirejs(), require(), and define()
|
||||
// functions as if they were in the global namespace.
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
|
||||
@@ -1,133 +0,0 @@
|
||||
class @SelfAssessment
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('section.self-assessment')
|
||||
@id = @el.data('id')
|
||||
@ajax_url = @el.data('ajax-url')
|
||||
@state = @el.data('state')
|
||||
@allow_reset = @el.data('allow_reset')
|
||||
# valid states: 'initial', 'assessing', 'request_hint', 'done'
|
||||
|
||||
# Where to put the rubric once we load it
|
||||
@errors_area = @$('.error')
|
||||
@answer_area = @$('textarea.answer')
|
||||
|
||||
@rubric_wrapper = @$('.rubric-wrapper')
|
||||
@hint_wrapper = @$('.hint-wrapper')
|
||||
@message_wrapper = @$('.message-wrapper')
|
||||
@submit_button = @$('.submit-button')
|
||||
@reset_button = @$('.reset-button')
|
||||
@reset_button.click @reset
|
||||
|
||||
@find_assessment_elements()
|
||||
@find_hint_elements()
|
||||
|
||||
@rebind()
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
rebind: () =>
|
||||
# rebind to the appropriate function for the current state
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.show()
|
||||
@reset_button.hide()
|
||||
@hint_area.attr('disabled', false)
|
||||
if @state == 'initial'
|
||||
@answer_area.attr("disabled", false)
|
||||
@submit_button.prop('value', 'Submit')
|
||||
@submit_button.click @save_answer
|
||||
else if @state == 'assessing'
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit assessment')
|
||||
@submit_button.click @save_assessment
|
||||
else if @state == 'request_hint'
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit hint')
|
||||
@submit_button.click @save_hint
|
||||
else if @state == 'done'
|
||||
@answer_area.attr("disabled", true)
|
||||
@hint_area.attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
if @allow_reset
|
||||
@reset_button.show()
|
||||
else
|
||||
@reset_button.hide()
|
||||
|
||||
|
||||
find_assessment_elements: ->
|
||||
@assessment = @$('select.assessment')
|
||||
|
||||
find_hint_elements: ->
|
||||
@hint_area = @$('textarea.hint')
|
||||
|
||||
save_answer: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'initial'
|
||||
data = {'student_answer' : @answer_area.val()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
|
||||
if response.success
|
||||
@rubric_wrapper.html(response.rubric_html)
|
||||
@state = 'assessing'
|
||||
@find_assessment_elements()
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
save_assessment: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'assessing'
|
||||
data = {'assessment' : @assessment.find(':selected').text()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
|
||||
if response.success
|
||||
@state = response.state
|
||||
|
||||
if @state == 'request_hint'
|
||||
@hint_wrapper.html(response.hint_html)
|
||||
@find_hint_elements()
|
||||
else if @state == 'done'
|
||||
@message_wrapper.html(response.message_html)
|
||||
@allow_reset = response.allow_reset
|
||||
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
|
||||
save_hint: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'request_hint'
|
||||
data = {'hint' : @hint_area.val()}
|
||||
|
||||
$.postWithPrefix "#{@ajax_url}/save_hint", data, (response) =>
|
||||
if response.success
|
||||
@message_wrapper.html(response.message_html)
|
||||
@state = 'done'
|
||||
@allow_reset = response.allow_reset
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
|
||||
reset: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'done'
|
||||
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
|
||||
if response.success
|
||||
@answer_area.html('')
|
||||
@rubric_wrapper.html('')
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@state = 'initial'
|
||||
@rebind()
|
||||
@reset_button.hide()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
@@ -2,6 +2,8 @@ class @Video
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@start = @el.data('start')
|
||||
@end = @el.data('end')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@show_captions = @el.data('show-captions') == "true"
|
||||
window.player = null
|
||||
|
||||
@@ -36,14 +36,21 @@ class @VideoPlayer extends Subview
|
||||
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
@progressSlider = new VideoProgressSlider el: @$('.slider')
|
||||
@playerVars =
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
if @video.start
|
||||
@playerVars.start = @video.start
|
||||
if @video.end
|
||||
# work in AS3, not HMLT5. but iframe use AS3
|
||||
@playerVars.end = @video.end
|
||||
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
playerVars: @playerVars
|
||||
videoId: @video.youtubeId()
|
||||
events:
|
||||
onReady: @onReady
|
||||
|
||||
@@ -339,9 +339,15 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_course(self, course_id):
|
||||
'''
|
||||
Look for a specific course id. Returns the course descriptor, or None if not found.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
@@ -399,3 +405,10 @@ class ModuleStoreBase(ModuleStore):
|
||||
|
||||
errorlog = self._get_errorlog(location)
|
||||
return errorlog.errors
|
||||
|
||||
def get_course(self, course_id):
|
||||
"""Default impl--linear search through course list"""
|
||||
for c in self.get_courses():
|
||||
if c.id == course_id:
|
||||
return c
|
||||
return None
|
||||
|
||||
@@ -309,9 +309,9 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
If there is no data at location in this modulestore, raise
|
||||
ItemNotFoundError.
|
||||
|
||||
@@ -64,7 +64,7 @@ def path_to_location(modulestore, course_id, location):
|
||||
# isn't found so we don't have to do it explicitly. Call this
|
||||
# first to make sure the location is there (even if it's a course, and
|
||||
# we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc)
|
||||
parents = modulestore.get_parent_locations(loc, course_id)
|
||||
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
if loc.category == "course":
|
||||
|
||||
@@ -23,12 +23,3 @@ def check_path_to_location(modulestore):
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
|
||||
|
||||
# Since our test files are valid, there shouldn't be any
|
||||
# elements with no path to them. But we can look for them in
|
||||
# another course.
|
||||
no_path = (
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, path_to_location, modulestore, course_id, location)
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
make_name_unique(xml_data)
|
||||
|
||||
descriptor = XModuleDescriptor.load_from_xml(
|
||||
etree.tostring(xml_data), self, self.org,
|
||||
etree.tostring(xml_data, encoding='unicode'), self, self.org,
|
||||
self.course, xmlstore.default_class)
|
||||
except Exception as err:
|
||||
print err, self.load_error_modules
|
||||
@@ -275,14 +275,16 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
self.parent_tracker = ParentTracker()
|
||||
self.parent_trackers = defaultdict(ParentTracker)
|
||||
|
||||
# If we are specifically asked for missing courses, that should
|
||||
# be an error. If we are asked for "all" courses, find the ones
|
||||
# that have a course.xml
|
||||
# that have a course.xml. We sort the dirs in alpha order so we always
|
||||
# read things in the same order (OS differences in load order have
|
||||
# bitten us in the past.)
|
||||
if course_dirs is None:
|
||||
course_dirs = [d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")]
|
||||
course_dirs = sorted([d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")])
|
||||
|
||||
for course_dir in course_dirs:
|
||||
self.try_load_course(course_dir)
|
||||
@@ -307,7 +309,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
if course_descriptor is not None:
|
||||
self.courses[course_dir] = course_descriptor
|
||||
self._location_errors[course_descriptor.location] = errorlog
|
||||
self.parent_tracker.make_known(course_descriptor.location)
|
||||
self.parent_trackers[course_descriptor.id].make_known(course_descriptor.location)
|
||||
else:
|
||||
# Didn't load course. Instead, save the errors elsewhere.
|
||||
self.errored_courses[course_dir] = errorlog
|
||||
@@ -432,11 +434,11 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
course_dir,
|
||||
policy,
|
||||
tracker,
|
||||
self.parent_tracker,
|
||||
self.parent_trackers[course_id],
|
||||
self.load_error_modules,
|
||||
)
|
||||
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data))
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode'))
|
||||
|
||||
# NOTE: The descriptors end up loading somewhat bottom up, which
|
||||
# breaks metadata inheritance via get_children(). Instead
|
||||
@@ -541,9 +543,9 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
If there is no data at location in this modulestore, raise
|
||||
ItemNotFoundError.
|
||||
@@ -552,7 +554,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
if not self.parent_tracker.is_known(location):
|
||||
raise ItemNotFoundError(location)
|
||||
if not self.parent_trackers[course_id].is_known(location):
|
||||
raise ItemNotFoundError("{0} not in {1}".format(location, course_id))
|
||||
|
||||
return self.parent_tracker.parents(location)
|
||||
return self.parent_trackers[course_id].parents(location)
|
||||
|
||||
660
common/lib/xmodule/xmodule/open_ended_module.py
Normal file
660
common/lib/xmodule/xmodule/open_ended_module.py
Normal file
@@ -0,0 +1,660 @@
|
||||
"""
|
||||
A Self Assessment module that allows students to write open-ended responses,
|
||||
submit, then see a rubric and rate themselves. Persists student supplied
|
||||
hints, answers, and assessment judgment (currently only correct/incorrect).
|
||||
Parses xml definition file--see below for exact format.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import capa.xqueue_interface as xqueue_interface
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .capa_module import only_one, ComplexEncoder
|
||||
from .editing_module import EditingDescriptor
|
||||
from .html_checker import check_html
|
||||
from progress import Progress
|
||||
from .stringify import stringify_children
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from capa.util import *
|
||||
import openendedchild
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from numpy import median
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from combined_open_ended_rubric import CombinedOpenEndedRubric
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
The open ended module supports all external open ended grader problems.
|
||||
Sample XML file:
|
||||
<openended min_score_to_attempt="1" max_score_to_attempt="1">
|
||||
<openendedparam>
|
||||
<initial_display>Enter essay here.</initial_display>
|
||||
<answer_display>This is the answer.</answer_display>
|
||||
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
|
||||
</openendedparam>
|
||||
</openended>
|
||||
"""
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Sets up the response type.
|
||||
@param system: Modulesystem object
|
||||
@param location: The location of the problem
|
||||
@param definition: The xml definition of the problem
|
||||
@param descriptor: The OpenEndedDescriptor associated with this
|
||||
@return: None
|
||||
"""
|
||||
oeparam = definition['oeparam']
|
||||
|
||||
self.url = definition.get('url', None)
|
||||
self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE)
|
||||
self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
|
||||
|
||||
#This is needed to attach feedback to specific responses later
|
||||
self.submission_id = None
|
||||
self.grader_id = None
|
||||
|
||||
if oeparam is None:
|
||||
raise ValueError("No oeparam found in problem xml.")
|
||||
if self.prompt is None:
|
||||
raise ValueError("No prompt found in problem xml.")
|
||||
if self.rubric is None:
|
||||
raise ValueError("No rubric found in problem xml.")
|
||||
|
||||
self._parse(oeparam, self.prompt, self.rubric, system)
|
||||
|
||||
if self.created == True and self.state == self.ASSESSING:
|
||||
self.created = False
|
||||
self.send_to_grader(self.latest_answer(), system)
|
||||
self.created = False
|
||||
|
||||
def _parse(self, oeparam, prompt, rubric, system):
|
||||
'''
|
||||
Parse OpenEndedResponse XML:
|
||||
self.initial_display
|
||||
self.payload - dict containing keys --
|
||||
'grader' : path to grader settings file, 'problem_id' : id of the problem
|
||||
|
||||
self.answer - What to display when show answer is clicked
|
||||
'''
|
||||
# Note that OpenEndedResponse is agnostic to the specific contents of grader_payload
|
||||
prompt_string = stringify_children(prompt)
|
||||
rubric_string = stringify_children(rubric)
|
||||
self.prompt = prompt_string
|
||||
self.rubric = rubric_string
|
||||
|
||||
grader_payload = oeparam.find('grader_payload')
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
|
||||
#Update grader payload with student id. If grader payload not json, error.
|
||||
try:
|
||||
parsed_grader_payload = json.loads(grader_payload)
|
||||
# NOTE: self.system.location is valid because the capa_module
|
||||
# __init__ adds it (easiest way to get problem location into
|
||||
# response types)
|
||||
except TypeError, ValueError:
|
||||
log.exception("Grader payload %r is not a json object!", grader_payload)
|
||||
|
||||
self.initial_display = find_with_default(oeparam, 'initial_display', '')
|
||||
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
|
||||
|
||||
parsed_grader_payload.update({
|
||||
'location': system.location.url(),
|
||||
'course_id': system.course_id,
|
||||
'prompt': prompt_string,
|
||||
'rubric': rubric_string,
|
||||
'initial_display': self.initial_display,
|
||||
'answer': self.answer,
|
||||
})
|
||||
updated_grader_payload = json.dumps(parsed_grader_payload)
|
||||
|
||||
self.payload = {'grader_payload': updated_grader_payload}
|
||||
|
||||
def skip_post_assessment(self, get, system):
|
||||
"""
|
||||
Ajax function that allows one to skip the post assessment phase
|
||||
@param get: AJAX dictionary
|
||||
@param system: ModuleSystem
|
||||
@return: Success indicator
|
||||
"""
|
||||
self.state = self.DONE
|
||||
return {'success': True}
|
||||
|
||||
def message_post(self, get, system):
|
||||
"""
|
||||
Handles a student message post (a reaction to the grade they received from an open ended grader type)
|
||||
Returns a boolean success/fail and an error message
|
||||
"""
|
||||
|
||||
event_info = dict()
|
||||
event_info['problem_id'] = system.location.url()
|
||||
event_info['student_id'] = system.anonymous_student_id
|
||||
event_info['survey_responses'] = get
|
||||
|
||||
survey_responses = event_info['survey_responses']
|
||||
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
|
||||
if tag not in survey_responses:
|
||||
return {'success': False, 'msg': "Could not find needed tag {0}".format(tag)}
|
||||
try:
|
||||
submission_id = int(survey_responses['submission_id'])
|
||||
grader_id = int(survey_responses['grader_id'])
|
||||
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
|
||||
score = int(survey_responses['score'])
|
||||
except:
|
||||
error_message = ("Could not parse submission id, grader id, "
|
||||
"or feedback from message_post ajax call. Here is the message data: {0}".format(
|
||||
survey_responses))
|
||||
log.exception(error_message)
|
||||
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
|
||||
|
||||
qinterface = system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
anonymous_student_id = system.anonymous_student_id
|
||||
queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
str(len(self.history)))
|
||||
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url=system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.message_queue_name
|
||||
)
|
||||
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime,
|
||||
}
|
||||
contents = {
|
||||
'feedback': feedback,
|
||||
'submission_id': submission_id,
|
||||
'grader_id': grader_id,
|
||||
'score': score,
|
||||
'student_info': json.dumps(student_info),
|
||||
}
|
||||
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
|
||||
#Convert error to a success value
|
||||
success = True
|
||||
if error:
|
||||
success = False
|
||||
|
||||
self.state = self.DONE
|
||||
|
||||
return {'success': success, 'msg': "Successfully submitted your feedback."}
|
||||
|
||||
def send_to_grader(self, submission, system):
|
||||
"""
|
||||
Send a given submission to the grader, via the xqueue
|
||||
@param submission: The student submission to send to the grader
|
||||
@param system: Modulesystem
|
||||
@return: Boolean true (not useful right now)
|
||||
"""
|
||||
|
||||
# Prepare xqueue request
|
||||
#------------------------------------------------------------
|
||||
|
||||
qinterface = system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
|
||||
anonymous_student_id = system.anonymous_student_id
|
||||
|
||||
# Generate header
|
||||
queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
str(len(self.history)))
|
||||
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
contents = self.payload.copy()
|
||||
|
||||
# Metadata related to the student submission revealed to the external grader
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime,
|
||||
}
|
||||
|
||||
#Update contents with student response and student info
|
||||
contents.update({
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': submission,
|
||||
'max_score': self.max_score(),
|
||||
})
|
||||
|
||||
# Submit request. When successful, 'msg' is the prior length of the queue
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
|
||||
# State associated with the queueing request
|
||||
queuestate = {'key': queuekey,
|
||||
'time': qtime, }
|
||||
return True
|
||||
|
||||
def _update_score(self, score_msg, queuekey, system):
|
||||
"""
|
||||
Called by xqueue to update the score
|
||||
@param score_msg: The message from xqueue
|
||||
@param queuekey: The key sent by xqueue
|
||||
@param system: Modulesystem
|
||||
@return: Boolean True (not useful currently)
|
||||
"""
|
||||
new_score_msg = self._parse_score_msg(score_msg)
|
||||
if not new_score_msg['valid']:
|
||||
score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.'
|
||||
|
||||
self.record_latest_score(new_score_msg['score'])
|
||||
self.record_latest_post_assessment(score_msg)
|
||||
self.state = self.POST_ASSESSMENT
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_answers(self):
|
||||
"""
|
||||
Gets and shows the answer for this problem.
|
||||
@return: Answer html
|
||||
"""
|
||||
anshtml = '<span class="openended-answer"><pre><code>{0}</code></pre></span>'.format(self.answer)
|
||||
return {self.answer_id: anshtml}
|
||||
|
||||
def get_initial_display(self):
|
||||
"""
|
||||
Gets and shows the initial display for the input box.
|
||||
@return: Initial display html
|
||||
"""
|
||||
return {self.answer_id: self.initial_display}
|
||||
|
||||
def _convert_longform_feedback_to_html(self, response_items):
|
||||
"""
|
||||
Take in a dictionary, and return html strings for display to student.
|
||||
Input:
|
||||
response_items: Dictionary with keys success, feedback.
|
||||
if success is True, feedback should be a dictionary, with keys for
|
||||
types of feedback, and the corresponding feedback values.
|
||||
if success is False, feedback is actually an error string.
|
||||
|
||||
NOTE: this will need to change when we integrate peer grading, because
|
||||
that will have more complex feedback.
|
||||
|
||||
Output:
|
||||
String -- html that can be displayincorrect-icon.pnged to the student.
|
||||
"""
|
||||
|
||||
# We want to display available feedback in a particular order.
|
||||
# This dictionary specifies which goes first--lower first.
|
||||
priorities = {# These go at the start of the feedback
|
||||
'spelling': 0,
|
||||
'grammar': 1,
|
||||
# needs to be after all the other feedback
|
||||
'markup_text': 3}
|
||||
|
||||
default_priority = 2
|
||||
|
||||
def get_priority(elt):
|
||||
"""
|
||||
Args:
|
||||
elt: a tuple of feedback-type, feedback
|
||||
Returns:
|
||||
the priority for this feedback type
|
||||
"""
|
||||
return priorities.get(elt[0], default_priority)
|
||||
|
||||
def encode_values(feedback_type, value):
|
||||
feedback_type = str(feedback_type).encode('ascii', 'ignore')
|
||||
if not isinstance(value, basestring):
|
||||
value = str(value)
|
||||
value = value.encode('ascii', 'ignore')
|
||||
return feedback_type, value
|
||||
|
||||
def format_feedback(feedback_type, value):
|
||||
feedback_type, value = encode_values(feedback_type, value)
|
||||
feedback = """
|
||||
<div class="{feedback_type}">
|
||||
{value}
|
||||
</div>
|
||||
""".format(feedback_type=feedback_type, value=value)
|
||||
return feedback
|
||||
|
||||
def format_feedback_hidden(feedback_type, value):
|
||||
feedback_type, value = encode_values(feedback_type, value)
|
||||
feedback = """
|
||||
<input class="{feedback_type}" type="hidden" value="{value}" />
|
||||
""".format(feedback_type=feedback_type, value=value)
|
||||
return feedback
|
||||
|
||||
# TODO (vshnayder): design and document the details of this format so
|
||||
# that we can do proper escaping here (e.g. are the graders allowed to
|
||||
# include HTML?)
|
||||
|
||||
for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
|
||||
if tag not in response_items:
|
||||
return format_feedback('errors', 'Error getting feedback')
|
||||
|
||||
feedback_items = response_items['feedback']
|
||||
try:
|
||||
feedback = json.loads(feedback_items)
|
||||
except (TypeError, ValueError):
|
||||
log.exception("feedback_items have invalid json %r", feedback_items)
|
||||
return format_feedback('errors', 'Could not parse feedback')
|
||||
|
||||
if response_items['success']:
|
||||
if len(feedback) == 0:
|
||||
return format_feedback('errors', 'No feedback available')
|
||||
|
||||
feedback_lst = sorted(feedback.items(), key=get_priority)
|
||||
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
|
||||
else:
|
||||
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
|
||||
|
||||
feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value)
|
||||
for feedback_type, value in response_items.items()
|
||||
if feedback_type in ['submission_id', 'grader_id']]))
|
||||
|
||||
return u"\n".join([feedback_list_part1, feedback_list_part2])
|
||||
|
||||
def _format_feedback(self, response_items):
|
||||
"""
|
||||
Input:
|
||||
Dictionary called feedback. Must contain keys seen below.
|
||||
Output:
|
||||
Return error message or feedback template
|
||||
"""
|
||||
|
||||
log.debug(response_items)
|
||||
rubric_feedback=""
|
||||
feedback = self._convert_longform_feedback_to_html(response_items)
|
||||
if response_items['rubric_scores_complete']==True:
|
||||
rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'])
|
||||
|
||||
if not response_items['success']:
|
||||
return system.render_template("open_ended_error.html",
|
||||
{'errors': feedback})
|
||||
|
||||
feedback_template = render_to_string("open_ended_feedback.html", {
|
||||
'grader_type': response_items['grader_type'],
|
||||
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
|
||||
'feedback': feedback,
|
||||
'rubric_feedback' : rubric_feedback
|
||||
})
|
||||
|
||||
return feedback_template
|
||||
|
||||
|
||||
def _parse_score_msg(self, score_msg, join_feedback=True):
|
||||
"""
|
||||
Grader reply is a JSON-dump of the following dict
|
||||
{ 'correct': True/False,
|
||||
'score': Numeric value (floating point is okay) to assign to answer
|
||||
'msg': grader_msg
|
||||
'feedback' : feedback from grader
|
||||
}
|
||||
|
||||
Returns (valid_score_msg, correct, score, msg):
|
||||
valid_score_msg: Flag indicating valid score_msg format (Boolean)
|
||||
correct: Correctness of submission (Boolean)
|
||||
score: Points to be assigned (numeric, can be float)
|
||||
"""
|
||||
fail = {'valid': False, 'score': 0, 'feedback': ''}
|
||||
try:
|
||||
score_result = json.loads(score_msg)
|
||||
except (TypeError, ValueError):
|
||||
error_message = ("External grader message should be a JSON-serialized dict."
|
||||
" Received score_msg = {0}".format(score_msg))
|
||||
log.error(error_message)
|
||||
fail['feedback'] = error_message
|
||||
return fail
|
||||
|
||||
if not isinstance(score_result, dict):
|
||||
error_message = ("External grader message should be a JSON-serialized dict."
|
||||
" Received score_result = {0}".format(score_result))
|
||||
log.error(error_message)
|
||||
fail['feedback'] = error_message
|
||||
return fail
|
||||
|
||||
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
|
||||
if tag not in score_result:
|
||||
error_message = ("External grader message is missing required tag: {0}"
|
||||
.format(tag))
|
||||
log.error(error_message)
|
||||
fail['feedback'] = error_message
|
||||
return fail
|
||||
#This is to support peer grading
|
||||
if isinstance(score_result['score'], list):
|
||||
feedback_items = []
|
||||
for i in xrange(0, len(score_result['score'])):
|
||||
new_score_result = {
|
||||
'score': score_result['score'][i],
|
||||
'feedback': score_result['feedback'][i],
|
||||
'grader_type': score_result['grader_type'],
|
||||
'success': score_result['success'],
|
||||
'grader_id': score_result['grader_id'][i],
|
||||
'submission_id': score_result['submission_id'],
|
||||
'rubric_scores_complete' : score_result['rubric_scores_complete'],
|
||||
'rubric_xml' : score_result['rubric_xml'],
|
||||
}
|
||||
feedback_items.append(self._format_feedback(new_score_result))
|
||||
if join_feedback:
|
||||
feedback = "".join(feedback_items)
|
||||
else:
|
||||
feedback = feedback_items
|
||||
score = int(median(score_result['score']))
|
||||
else:
|
||||
#This is for instructor and ML grading
|
||||
feedback = self._format_feedback(score_result)
|
||||
score = score_result['score']
|
||||
|
||||
self.submission_id = score_result['submission_id']
|
||||
self.grader_id = score_result['grader_id']
|
||||
|
||||
return {'valid': True, 'score': score, 'feedback': feedback}
|
||||
|
||||
def latest_post_assessment(self, short_feedback=False, join_feedback=True):
|
||||
"""
|
||||
Gets the latest feedback, parses, and returns
|
||||
@param short_feedback: If the long feedback is wanted or not
|
||||
@return: Returns formatted feedback
|
||||
"""
|
||||
if not self.history:
|
||||
return ""
|
||||
|
||||
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback)
|
||||
if not short_feedback:
|
||||
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
|
||||
if feedback_dict['valid']:
|
||||
short_feedback = self._convert_longform_feedback_to_html(
|
||||
json.loads(self.history[-1].get('post_assessment', "")))
|
||||
return short_feedback if feedback_dict['valid'] else ''
|
||||
|
||||
def format_feedback_with_evaluation(self, feedback):
|
||||
"""
|
||||
Renders a given html feedback into an evaluation template
|
||||
@param feedback: HTML feedback
|
||||
@return: Rendered html
|
||||
"""
|
||||
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
|
||||
html = render_to_string('open_ended_evaluation.html', context)
|
||||
return html
|
||||
|
||||
def handle_ajax(self, dispatch, get, system):
|
||||
'''
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress' : 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
'''
|
||||
handlers = {
|
||||
'save_answer': self.save_answer,
|
||||
'score_update': self.update_score,
|
||||
'save_post_assessment': self.message_post,
|
||||
'skip_post_assessment': self.skip_post_assessment,
|
||||
'check_for_score': self.check_for_score,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
return 'Error'
|
||||
|
||||
before = self.get_progress()
|
||||
d = handlers[dispatch](get, system)
|
||||
after = self.get_progress()
|
||||
d.update({
|
||||
'progress_changed': after != before,
|
||||
'progress_status': Progress.to_js_status_str(after),
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def check_for_score(self, get, system):
|
||||
"""
|
||||
Checks to see if a score has been received yet.
|
||||
@param get: AJAX get dictionary
|
||||
@param system: Modulesystem (needed to align with other ajax functions)
|
||||
@return: Returns the current state
|
||||
"""
|
||||
state = self.state
|
||||
return {'state': state}
|
||||
|
||||
def save_answer(self, get, system):
|
||||
"""
|
||||
Saves a student answer
|
||||
@param get: AJAX get dictionary
|
||||
@param system: modulesystem
|
||||
@return: Success indicator
|
||||
"""
|
||||
if self.attempts > self.max_attempts:
|
||||
# If too many attempts, prevent student from saving answer and
|
||||
# seeing rubric. In normal use, students shouldn't see this because
|
||||
# they won't see the reset button once they're out of attempts.
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Too many attempts.'
|
||||
}
|
||||
|
||||
if self.state != self.INITIAL:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
# add new history element with answer and empty score and hint.
|
||||
self.new_history_entry(get['student_answer'])
|
||||
self.send_to_grader(get['student_answer'], system)
|
||||
self.change_state(self.ASSESSING)
|
||||
|
||||
return {'success': True, }
|
||||
|
||||
def update_score(self, get, system):
|
||||
"""
|
||||
Updates the current score via ajax. Called by xqueue.
|
||||
Input: AJAX get dictionary, modulesystem
|
||||
Output: None
|
||||
"""
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
#TODO: Remove need for cmap
|
||||
self._update_score(score_msg, queuekey, system)
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def get_html(self, system):
|
||||
"""
|
||||
Gets the HTML for this problem and renders it
|
||||
Input: Modulesystem object
|
||||
Output: Rendered HTML
|
||||
"""
|
||||
#set context variables and render template
|
||||
if self.state != self.INITIAL:
|
||||
latest = self.latest_answer()
|
||||
previous_answer = latest if latest is not None else self.initial_display
|
||||
post_assessment = self.latest_post_assessment()
|
||||
score = self.latest_score()
|
||||
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
|
||||
else:
|
||||
post_assessment = ""
|
||||
correct = ""
|
||||
previous_answer = self.initial_display
|
||||
|
||||
context = {
|
||||
'prompt': self.prompt,
|
||||
'previous_answer': previous_answer,
|
||||
'state': self.state,
|
||||
'allow_reset': self._allow_reset(),
|
||||
'rows': 30,
|
||||
'cols': 80,
|
||||
'id': 'open_ended',
|
||||
'msg': post_assessment,
|
||||
'child_type': 'openended',
|
||||
'correct': correct,
|
||||
}
|
||||
log.debug(context)
|
||||
html = system.render_template('open_ended.html', context)
|
||||
return html
|
||||
|
||||
|
||||
class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding open ended response questions to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = OpenEndedModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "openended"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Pull out the open ended parameters into a dictionary.
|
||||
|
||||
Returns:
|
||||
{
|
||||
'oeparam': 'some-html'
|
||||
}
|
||||
"""
|
||||
for child in ['openendedparam']:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child))
|
||||
|
||||
def parse(k):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return xml_object.xpath(k)[0]
|
||||
|
||||
return {'oeparam': parse('openendedparam'), }
|
||||
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('openended')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['openendedparam']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
|
||||
|
||||
263
common/lib/xmodule/xmodule/openendedchild.py
Normal file
263
common/lib/xmodule/xmodule/openendedchild.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import capa.xqueue_interface as xqueue_interface
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .capa_module import only_one, ComplexEncoder
|
||||
from .editing_module import EditingDescriptor
|
||||
from .html_checker import check_html
|
||||
from progress import Progress
|
||||
from .stringify import stringify_children
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from capa.util import *
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
# Set the default number of max attempts. Should be 1 for production
|
||||
# Set higher for debugging/testing
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 1
|
||||
|
||||
# Set maximum available number of points.
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
class OpenEndedChild():
|
||||
"""
|
||||
States:
|
||||
|
||||
initial (prompt, textbox shown)
|
||||
|
|
||||
assessing (read-only textbox, rubric + assessment input shown for self assessment, response queued for open ended)
|
||||
|
|
||||
post_assessment (read-only textbox, read-only rubric and assessment, hint input box shown)
|
||||
|
|
||||
done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows
|
||||
a reset button that goes back to initial state. Saves previous
|
||||
submissions too.)
|
||||
"""
|
||||
|
||||
DEFAULT_QUEUE = 'open-ended'
|
||||
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
|
||||
max_inputfields = 1
|
||||
|
||||
STATE_VERSION = 1
|
||||
|
||||
# states
|
||||
INITIAL = 'initial'
|
||||
ASSESSING = 'assessing'
|
||||
POST_ASSESSMENT = 'post_assessment'
|
||||
DONE = 'done'
|
||||
|
||||
#This is used to tell students where they are at in the module
|
||||
HUMAN_NAMES = {
|
||||
'initial': 'Started',
|
||||
'assessing': 'Being scored',
|
||||
'post_assessment': 'Scoring finished',
|
||||
'done': 'Problem complete',
|
||||
}
|
||||
|
||||
def __init__(self, system, location, definition, descriptor, static_data,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
# Load instance state
|
||||
if instance_state is not None:
|
||||
instance_state = json.loads(instance_state)
|
||||
else:
|
||||
instance_state = {}
|
||||
|
||||
# History is a list of tuples of (answer, score, hint), where hint may be
|
||||
# None for any element, and score and hint can be None for the last (current)
|
||||
# element.
|
||||
# Scores are on scale from 0 to max_score
|
||||
self.history = instance_state.get('history', [])
|
||||
|
||||
self.state = instance_state.get('state', self.INITIAL)
|
||||
|
||||
self.created = instance_state.get('created', False)
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
self.max_attempts = static_data['max_attempts']
|
||||
|
||||
self.prompt = static_data['prompt']
|
||||
self.rubric = static_data['rubric']
|
||||
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
self._max_score = static_data['max_score']
|
||||
|
||||
self.setup_response(system, location, definition, descriptor)
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Needs to be implemented by the inheritors of this module. Sets up additional fields used by the child modules.
|
||||
@param system: Modulesystem
|
||||
@param location: Module location
|
||||
@param definition: XML definition
|
||||
@param descriptor: Descriptor of the module
|
||||
@return: None
|
||||
"""
|
||||
pass
|
||||
|
||||
def latest_answer(self):
|
||||
"""None if not available"""
|
||||
if not self.history:
|
||||
return ""
|
||||
return self.history[-1].get('answer', "")
|
||||
|
||||
def latest_score(self):
|
||||
"""None if not available"""
|
||||
if not self.history:
|
||||
return None
|
||||
return self.history[-1].get('score')
|
||||
|
||||
def latest_post_assessment(self):
|
||||
"""None if not available"""
|
||||
if not self.history:
|
||||
return ""
|
||||
return self.history[-1].get('post_assessment', "")
|
||||
|
||||
def new_history_entry(self, answer):
|
||||
"""
|
||||
Adds a new entry to the history dictionary
|
||||
@param answer: The student supplied answer
|
||||
@return: None
|
||||
"""
|
||||
self.history.append({'answer': answer})
|
||||
|
||||
def record_latest_score(self, score):
|
||||
"""Assumes that state is right, so we're adding a score to the latest
|
||||
history element"""
|
||||
self.history[-1]['score'] = score
|
||||
|
||||
def record_latest_post_assessment(self, post_assessment):
|
||||
"""Assumes that state is right, so we're adding a score to the latest
|
||||
history element"""
|
||||
self.history[-1]['post_assessment'] = post_assessment
|
||||
|
||||
def change_state(self, new_state):
|
||||
"""
|
||||
A centralized place for state changes--allows for hooks. If the
|
||||
current state matches the old state, don't run any hooks.
|
||||
"""
|
||||
if self.state == new_state:
|
||||
return
|
||||
|
||||
self.state = new_state
|
||||
|
||||
if self.state == self.DONE:
|
||||
self.attempts += 1
|
||||
|
||||
def get_instance_state(self):
|
||||
"""
|
||||
Get the current score and state
|
||||
"""
|
||||
|
||||
state = {
|
||||
'version': self.STATE_VERSION,
|
||||
'history': self.history,
|
||||
'state': self.state,
|
||||
'max_score': self._max_score,
|
||||
'attempts': self.attempts,
|
||||
'created': False,
|
||||
}
|
||||
return json.dumps(state)
|
||||
|
||||
def _allow_reset(self):
|
||||
"""Can the module be reset?"""
|
||||
return (self.state == self.DONE and self.attempts < self.max_attempts)
|
||||
|
||||
def max_score(self):
|
||||
"""
|
||||
Return max_score
|
||||
"""
|
||||
return self._max_score
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
Returns the last score in the list
|
||||
"""
|
||||
score = self.latest_score()
|
||||
return {'score': score if score is not None else 0,
|
||||
'total': self._max_score}
|
||||
|
||||
def reset(self, system):
|
||||
"""
|
||||
If resetting is allowed, reset the state.
|
||||
|
||||
Returns {'success': bool, 'error': msg}
|
||||
(error only present if not success)
|
||||
"""
|
||||
self.change_state(self.INITIAL)
|
||||
return {'success': True}
|
||||
|
||||
def get_progress(self):
|
||||
'''
|
||||
For now, just return last score / max_score
|
||||
'''
|
||||
if self._max_score > 0:
|
||||
try:
|
||||
return Progress(self.get_score()['score'], self._max_score)
|
||||
except Exception as err:
|
||||
log.exception("Got bad progress")
|
||||
return None
|
||||
return None
|
||||
|
||||
def out_of_sync_error(self, get, msg=''):
|
||||
"""
|
||||
return dict out-of-sync error message, and also log.
|
||||
"""
|
||||
log.warning("Assessment module state out sync. state: %r, get: %r. %s",
|
||||
self.state, get, msg)
|
||||
return {'success': False,
|
||||
'error': 'The problem state got out-of-sync'}
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Needs to be implemented by inheritors. Renders the HTML that students see.
|
||||
@return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def handle_ajax(self):
|
||||
"""
|
||||
Needs to be implemented by child modules. Handles AJAX events.
|
||||
@return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_submission_correct(self, score):
|
||||
"""
|
||||
Checks to see if a given score makes the answer correct. Very naive right now (>66% is correct)
|
||||
@param score: Numeric score.
|
||||
@return: Boolean correct.
|
||||
"""
|
||||
correct = False
|
||||
if(isinstance(score, (int, long, float, complex))):
|
||||
score_ratio = int(score) / float(self.max_score())
|
||||
correct = (score_ratio >= 0.66)
|
||||
return correct
|
||||
|
||||
def is_last_response_correct(self):
|
||||
"""
|
||||
Checks to see if the last response in the module is correct.
|
||||
@return: 'correct' if correct, otherwise 'incorrect'
|
||||
"""
|
||||
score = self.get_score()['score']
|
||||
correctness = 'correct' if self.is_submission_correct(score) else 'incorrect'
|
||||
return correctness
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
|
||||
"""
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': etree.tostring(xml_object, pretty_print=True)}
|
||||
return {'data': etree.tostring(xml_object, pretty_print=True,encoding='unicode')}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
try:
|
||||
|
||||
@@ -1,189 +1,92 @@
|
||||
"""
|
||||
A Self Assessment module that allows students to write open-ended responses,
|
||||
submit, then see a rubric and rate themselves. Persists student supplied
|
||||
hints, answers, and assessment judgment (currently only correct/incorrect).
|
||||
Parses xml definition file--see below for exact format.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
import json
|
||||
from progress import Progress
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .capa_module import only_one, ComplexEncoder
|
||||
from .editing_module import EditingDescriptor
|
||||
from .html_checker import check_html
|
||||
from progress import Progress
|
||||
from .stringify import stringify_children
|
||||
from .x_module import XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
import openendedchild
|
||||
|
||||
from combined_open_ended_rubric import CombinedOpenEndedRubric
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
# Set the default number of max attempts. Should be 1 for production
|
||||
# Set higher for debugging/testing
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 1
|
||||
|
||||
# Set maximum available number of points.
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
class SelfAssessmentModule(XModule):
|
||||
class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
States:
|
||||
A Self Assessment module that allows students to write open-ended responses,
|
||||
submit, then see a rubric and rate themselves. Persists student supplied
|
||||
hints, answers, and assessment judgment (currently only correct/incorrect).
|
||||
Parses xml definition file--see below for exact format.
|
||||
|
||||
initial (prompt, textbox shown)
|
||||
|
|
||||
assessing (read-only textbox, rubric + assessment input shown)
|
||||
|
|
||||
request_hint (read-only textbox, read-only rubric and assessment, hint input box shown)
|
||||
|
|
||||
done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows
|
||||
a reset button that goes back to initial state. Saves previous
|
||||
submissions too.)
|
||||
Sample XML format:
|
||||
<selfassessment>
|
||||
<hintprompt>
|
||||
What hint about this problem would you give to someone?
|
||||
</hintprompt>
|
||||
<submitmessage>
|
||||
Save Succcesful. Thanks for participating!
|
||||
</submitmessage>
|
||||
</selfassessment>
|
||||
"""
|
||||
|
||||
# states
|
||||
INITIAL = 'initial'
|
||||
ASSESSING = 'assessing'
|
||||
REQUEST_HINT = 'request_hint'
|
||||
DONE = 'done'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
|
||||
js_module_name = "SelfAssessment"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt,
|
||||
and two optional attributes:
|
||||
attempts, which should be an integer that defaults to 1.
|
||||
If it's > 1, the student will be able to re-submit after they see
|
||||
the rubric.
|
||||
max_score, which should be an integer that defaults to 1.
|
||||
It defines the maximum number of points a student can get. Assumed to be integer scale
|
||||
from 0 to max_score, with an interval of 1.
|
||||
|
||||
Note: all the submissions are stored.
|
||||
|
||||
Sample file:
|
||||
|
||||
<selfassessment attempts="1" max_score="1">
|
||||
<prompt>
|
||||
Insert prompt text here. (arbitrary html)
|
||||
</prompt>
|
||||
<rubric>
|
||||
Insert grading rubric here. (arbitrary html)
|
||||
</rubric>
|
||||
<hintprompt>
|
||||
Please enter a hint below: (arbitrary html)
|
||||
</hintprompt>
|
||||
<submitmessage>
|
||||
Thanks for submitting! (arbitrary html)
|
||||
</submitmessage>
|
||||
</selfassessment>
|
||||
Sets up the module
|
||||
@param system: Modulesystem
|
||||
@param location: location, to let the module know where it is.
|
||||
@param definition: XML definition of the module.
|
||||
@param descriptor: SelfAssessmentDescriptor
|
||||
@return: None
|
||||
"""
|
||||
|
||||
# Load instance state
|
||||
if instance_state is not None:
|
||||
instance_state = json.loads(instance_state)
|
||||
else:
|
||||
instance_state = {}
|
||||
|
||||
# Note: score responses are on scale from 0 to max_score
|
||||
self.student_answers = instance_state.get('student_answers', [])
|
||||
self.scores = instance_state.get('scores', [])
|
||||
self.hints = instance_state.get('hints', [])
|
||||
|
||||
self.state = instance_state.get('state', 'initial')
|
||||
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
|
||||
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
|
||||
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
|
||||
|
||||
self.rubric = definition['rubric']
|
||||
self.prompt = definition['prompt']
|
||||
self.submit_message = definition['submitmessage']
|
||||
self.hint_prompt = definition['hintprompt']
|
||||
self.prompt = stringify_children(self.prompt)
|
||||
self.rubric = stringify_children(self.rubric)
|
||||
|
||||
def _allow_reset(self):
|
||||
"""Can the module be reset?"""
|
||||
return self.state == self.DONE and self.attempts < self.max_attempts
|
||||
|
||||
def get_html(self):
|
||||
def get_html(self, system):
|
||||
"""
|
||||
Gets context and renders HTML that represents the module
|
||||
@param system: Modulesystem
|
||||
@return: Rendered HTML
|
||||
"""
|
||||
#set context variables and render template
|
||||
if self.state != self.INITIAL and self.student_answers:
|
||||
previous_answer = self.student_answers[-1]
|
||||
if self.state != self.INITIAL:
|
||||
latest = self.latest_answer()
|
||||
previous_answer = latest if latest is not None else ''
|
||||
else:
|
||||
previous_answer = ''
|
||||
|
||||
context = {
|
||||
'prompt': self.prompt,
|
||||
'previous_answer': previous_answer,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'initial_rubric': self.get_rubric_html(),
|
||||
'initial_hint': self.get_hint_html(),
|
||||
'ajax_url': system.ajax_url,
|
||||
'initial_rubric': self.get_rubric_html(system),
|
||||
'initial_hint': self.get_hint_html(system),
|
||||
'initial_message': self.get_message_html(),
|
||||
'state': self.state,
|
||||
'allow_reset': self._allow_reset(),
|
||||
'child_type': 'selfassessment',
|
||||
}
|
||||
html = self.system.render_template('self_assessment_prompt.html', context)
|
||||
|
||||
# cdodge: perform link substitutions for any references to course static content (e.g. images)
|
||||
return rewrite_links(html, self.rewrite_content_links)
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
Returns dict with 'score' key
|
||||
"""
|
||||
return {'score': self.get_last_score()}
|
||||
|
||||
def max_score(self):
|
||||
"""
|
||||
Return max_score
|
||||
"""
|
||||
return self._max_score
|
||||
|
||||
def get_last_score(self):
|
||||
"""
|
||||
Returns the last score in the list
|
||||
"""
|
||||
last_score=0
|
||||
if(len(self.scores)>0):
|
||||
last_score=self.scores[len(self.scores)-1]
|
||||
return last_score
|
||||
|
||||
def get_progress(self):
|
||||
'''
|
||||
For now, just return last score / max_score
|
||||
'''
|
||||
if self._max_score > 0:
|
||||
try:
|
||||
return Progress(self.get_last_score(), self._max_score)
|
||||
except Exception as err:
|
||||
log.exception("Got bad progress")
|
||||
return None
|
||||
return None
|
||||
html = system.render_template('self_assessment_prompt.html', context)
|
||||
return html
|
||||
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
def handle_ajax(self, dispatch, get, system):
|
||||
"""
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
@@ -197,15 +100,14 @@ class SelfAssessmentModule(XModule):
|
||||
handlers = {
|
||||
'save_answer': self.save_answer,
|
||||
'save_assessment': self.save_assessment,
|
||||
'save_hint': self.save_hint,
|
||||
'reset': self.reset,
|
||||
'save_post_assessment': self.save_hint,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
return 'Error'
|
||||
|
||||
before = self.get_progress()
|
||||
d = handlers[dispatch](get)
|
||||
d = handlers[dispatch](get, system)
|
||||
after = self.get_progress()
|
||||
d.update({
|
||||
'progress_changed': after != before,
|
||||
@@ -213,60 +115,54 @@ class SelfAssessmentModule(XModule):
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def out_of_sync_error(self, get, msg=''):
|
||||
"""
|
||||
return dict out-of-sync error message, and also log.
|
||||
"""
|
||||
log.warning("Assessment module state out sync. state: %r, get: %r. %s",
|
||||
self.state, get, msg)
|
||||
return {'success': False,
|
||||
'error': 'The problem state got out-of-sync'}
|
||||
|
||||
def get_rubric_html(self):
|
||||
def get_rubric_html(self, system):
|
||||
"""
|
||||
Return the appropriate version of the rubric, based on the state.
|
||||
"""
|
||||
if self.state == self.INITIAL:
|
||||
return ''
|
||||
|
||||
rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric)
|
||||
|
||||
# we'll render it
|
||||
context = {'rubric': self.rubric,
|
||||
'max_score' : self._max_score,
|
||||
}
|
||||
context = {'rubric': rubric_html,
|
||||
'max_score': self._max_score,
|
||||
}
|
||||
|
||||
if self.state == self.ASSESSING:
|
||||
context['read_only'] = False
|
||||
elif self.state in (self.REQUEST_HINT, self.DONE):
|
||||
elif self.state in (self.POST_ASSESSMENT, self.DONE):
|
||||
context['read_only'] = True
|
||||
else:
|
||||
raise ValueError("Illegal state '%r'" % self.state)
|
||||
|
||||
return self.system.render_template('self_assessment_rubric.html', context)
|
||||
return system.render_template('self_assessment_rubric.html', context)
|
||||
|
||||
def get_hint_html(self):
|
||||
def get_hint_html(self, system):
|
||||
"""
|
||||
Return the appropriate version of the hint view, based on state.
|
||||
"""
|
||||
if self.state in (self.INITIAL, self.ASSESSING):
|
||||
return ''
|
||||
|
||||
if self.state == self.DONE and len(self.hints) > 0:
|
||||
if self.state == self.DONE:
|
||||
# display the previous hint
|
||||
hint = self.hints[-1]
|
||||
latest = self.latest_post_assessment()
|
||||
hint = latest if latest is not None else ''
|
||||
else:
|
||||
hint = ''
|
||||
|
||||
context = {'hint_prompt': self.hint_prompt,
|
||||
'hint': hint}
|
||||
|
||||
if self.state == self.REQUEST_HINT:
|
||||
if self.state == self.POST_ASSESSMENT:
|
||||
context['read_only'] = False
|
||||
elif self.state == self.DONE:
|
||||
context['read_only'] = True
|
||||
else:
|
||||
raise ValueError("Illegal state '%r'" % self.state)
|
||||
|
||||
return self.system.render_template('self_assessment_hint.html', context)
|
||||
return system.render_template('self_assessment_hint.html', context)
|
||||
|
||||
def get_message_html(self):
|
||||
"""
|
||||
@@ -278,9 +174,17 @@ class SelfAssessmentModule(XModule):
|
||||
return """<div class="save_message">{0}</div>""".format(self.submit_message)
|
||||
|
||||
|
||||
def save_answer(self, get):
|
||||
def save_answer(self, get, system):
|
||||
"""
|
||||
After the answer is submitted, show the rubric.
|
||||
|
||||
Args:
|
||||
get: the GET dictionary passed to the ajax request. Should contain
|
||||
a key 'student_answer'
|
||||
|
||||
Returns:
|
||||
Dictionary with keys 'success' and either 'error' (if not success),
|
||||
or 'rubric_html' (if success).
|
||||
"""
|
||||
# Check to see if attempts are less than max
|
||||
if self.attempts > self.max_attempts:
|
||||
@@ -295,15 +199,16 @@ class SelfAssessmentModule(XModule):
|
||||
if self.state != self.INITIAL:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
self.student_answers.append(get['student_answer'])
|
||||
self.state = self.ASSESSING
|
||||
# add new history element with answer and empty score and hint.
|
||||
self.new_history_entry(get['student_answer'])
|
||||
self.change_state(self.ASSESSING)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'rubric_html': self.get_rubric_html()
|
||||
}
|
||||
'rubric_html': self.get_rubric_html(system)
|
||||
}
|
||||
|
||||
def save_assessment(self, get):
|
||||
def save_assessment(self, get, system):
|
||||
"""
|
||||
Save the assessment. If the student said they're right, don't ask for a
|
||||
hint, and go straight to the done state. Otherwise, do ask for a hint.
|
||||
@@ -318,34 +223,30 @@ class SelfAssessmentModule(XModule):
|
||||
'message_html' only if success is true
|
||||
"""
|
||||
|
||||
n_answers = len(self.student_answers)
|
||||
n_scores = len(self.scores)
|
||||
if (self.state != self.ASSESSING or n_answers != n_scores + 1):
|
||||
msg = "%d answers, %d scores" % (n_answers, n_scores)
|
||||
return self.out_of_sync_error(get, msg)
|
||||
if self.state != self.ASSESSING:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
try:
|
||||
score = int(get['assessment'])
|
||||
except:
|
||||
except ValueError:
|
||||
return {'success': False, 'error': "Non-integer score value"}
|
||||
|
||||
self.scores.append(score)
|
||||
self.record_latest_score(score)
|
||||
|
||||
d = {'success': True,}
|
||||
d = {'success': True, }
|
||||
|
||||
if score == self.max_score():
|
||||
self.state = self.DONE
|
||||
self.change_state(self.DONE)
|
||||
d['message_html'] = self.get_message_html()
|
||||
d['allow_reset'] = self._allow_reset()
|
||||
else:
|
||||
self.state = self.REQUEST_HINT
|
||||
d['hint_html'] = self.get_hint_html()
|
||||
self.change_state(self.POST_ASSESSMENT)
|
||||
d['hint_html'] = self.get_hint_html(system)
|
||||
|
||||
d['state'] = self.state
|
||||
return d
|
||||
|
||||
|
||||
def save_hint(self, get):
|
||||
def save_hint(self, get, system):
|
||||
'''
|
||||
Save the hint.
|
||||
Returns a dict { 'success': bool,
|
||||
@@ -355,68 +256,19 @@ class SelfAssessmentModule(XModule):
|
||||
with the error key only present if success is False and message_html
|
||||
only if True.
|
||||
'''
|
||||
if self.state != self.REQUEST_HINT:
|
||||
if self.state != self.POST_ASSESSMENT:
|
||||
# Note: because we only ask for hints on wrong answers, may not have
|
||||
# the same number of hints and answers.
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
self.hints.append(get['hint'].lower())
|
||||
self.state = self.DONE
|
||||
|
||||
# increment attempts
|
||||
self.attempts = self.attempts + 1
|
||||
|
||||
# To the tracking logs!
|
||||
event_info = {
|
||||
'selfassessment_id': self.location.url(),
|
||||
'state': {
|
||||
'student_answers': self.student_answers,
|
||||
'score': self.scores,
|
||||
'hints': self.hints,
|
||||
}
|
||||
}
|
||||
self.system.track_function('save_hint', event_info)
|
||||
self.record_latest_post_assessment(get['hint'])
|
||||
self.change_state(self.DONE)
|
||||
|
||||
return {'success': True,
|
||||
'message_html': self.get_message_html(),
|
||||
'allow_reset': self._allow_reset()}
|
||||
|
||||
|
||||
def reset(self, get):
|
||||
"""
|
||||
If resetting is allowed, reset the state.
|
||||
|
||||
Returns {'success': bool, 'error': msg}
|
||||
(error only present if not success)
|
||||
"""
|
||||
if self.state != self.DONE:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
if self.attempts > self.max_attempts:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Too many attempts.'
|
||||
}
|
||||
self.state = self.INITIAL
|
||||
return {'success': True}
|
||||
|
||||
|
||||
def get_instance_state(self):
|
||||
"""
|
||||
Get the current score and state
|
||||
"""
|
||||
|
||||
state = {
|
||||
'student_answers': self.student_answers,
|
||||
'hints': self.hints,
|
||||
'state': self.state,
|
||||
'scores': self.scores,
|
||||
'max_score': self._max_score,
|
||||
'attempts': self.attempts
|
||||
}
|
||||
return json.dumps(state)
|
||||
|
||||
|
||||
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding self assessment questions to courses
|
||||
@@ -439,13 +291,11 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
|
||||
Returns:
|
||||
{
|
||||
'rubric': 'some-html',
|
||||
'prompt': 'some-html',
|
||||
'submitmessage': 'some-html'
|
||||
'hintprompt': 'some-html'
|
||||
}
|
||||
"""
|
||||
expected_children = ['rubric', 'prompt', 'submitmessage', 'hintprompt']
|
||||
expected_children = ['submitmessage', 'hintprompt']
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child))
|
||||
@@ -454,12 +304,9 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return stringify_children(xml_object.xpath(k)[0])
|
||||
|
||||
return {'rubric': parse('rubric'),
|
||||
'prompt': parse('prompt'),
|
||||
'submitmessage': parse('submitmessage'),
|
||||
return {'submitmessage': parse('submitmessage'),
|
||||
'hintprompt': parse('hintprompt'),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
@@ -470,7 +317,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['rubric', 'prompt', 'submitmessage', 'hintprompt']:
|
||||
for child in ['submitmessage', 'hintprompt']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
|
||||
@@ -10,7 +10,7 @@ from xmodule.progress import Progress
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger("mitx.common.lib.seq_module")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
@@ -124,7 +124,7 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
children = []
|
||||
for child in xml_object:
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child)).location.url())
|
||||
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
|
||||
except:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
continue
|
||||
|
||||
@@ -22,7 +22,7 @@ def stringify_children(node):
|
||||
# next element.
|
||||
parts = [node.text]
|
||||
for c in node.getchildren():
|
||||
parts.append(etree.tostring(c, with_tail=True))
|
||||
parts.append(etree.tostring(c, with_tail=True, encoding='unicode'))
|
||||
|
||||
# filter removes possible Nones in texts and tails
|
||||
return ''.join(filter(None, parts))
|
||||
return u''.join(filter(None, parts))
|
||||
|
||||
@@ -58,7 +58,7 @@ class CustomTagDescriptor(RawDescriptor):
|
||||
params = dict(xmltree.items())
|
||||
with system.resources_fs.open('custom_tags/{name}'
|
||||
.format(name=template_name)) as template:
|
||||
return Template(template.read()).render(**params)
|
||||
return Template(template.read().decode('utf-8')).render(**params)
|
||||
|
||||
|
||||
def __init__(self, system, definition, **kwargs):
|
||||
|
||||
@@ -4,7 +4,7 @@ unittests for xmodule
|
||||
Run like this:
|
||||
|
||||
rake test_common/lib/xmodule
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -19,11 +19,12 @@ import xmodule
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from mock import Mock
|
||||
|
||||
i4xs = ModuleSystem(
|
||||
test_system = ModuleSystem(
|
||||
ajax_url='courses/course_id/modx/a_location',
|
||||
track_function=Mock(),
|
||||
get_module=Mock(),
|
||||
render_template=Mock(),
|
||||
# "render" to just the context...
|
||||
render_template=lambda template, context: str(context),
|
||||
replace_urls=Mock(),
|
||||
user=Mock(),
|
||||
filestore=Mock(),
|
||||
|
||||
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
from time import strptime, gmtime
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00')
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
|
||||
def __init__(self, load_error_modules):
|
||||
|
||||
xmlstore = XMLModuleStore("data_dir", course_dirs=[],
|
||||
load_error_modules=load_error_modules)
|
||||
course_id = "/".join([ORG, COURSE, 'test_run'])
|
||||
course_dir = "test_dir"
|
||||
policy = {}
|
||||
error_tracker = Mock()
|
||||
parent_tracker = Mock()
|
||||
|
||||
super(DummySystem, self).__init__(
|
||||
xmlstore,
|
||||
course_id,
|
||||
course_dir,
|
||||
policy,
|
||||
error_tracker,
|
||||
parent_tracker,
|
||||
load_error_modules=load_error_modules,
|
||||
)
|
||||
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
@staticmethod
|
||||
def get_dummy_course(start, is_new=None, load_error_modules=True):
|
||||
"""Get a dummy course"""
|
||||
|
||||
system = DummySystem(load_error_modules)
|
||||
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
|
||||
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}"
|
||||
{is_new}>
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new)
|
||||
|
||||
return system.process_xml(start_xml)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_non_started_yet(self, gmtime_mock):
|
||||
descriptor = self.get_dummy_course(start='2013-01-05T12:00')
|
||||
gmtime_mock.return_value = NOW
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start == 4)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_already_started(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00')
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_is_new_set(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
@@ -39,9 +39,12 @@ def strip_filenames(descriptor):
|
||||
|
||||
|
||||
class RoundTripTestCase(unittest.TestCase):
|
||||
'''Check that our test courses roundtrip properly'''
|
||||
''' Check that our test courses roundtrip properly.
|
||||
Same course imported , than exported, then imported again.
|
||||
And we compare original import with second import (after export).
|
||||
Thus we make sure that export and import work properly.
|
||||
'''
|
||||
def check_export_roundtrip(self, data_dir, course_dir):
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
print "Copying test course to temp dir {0}".format(root_dir)
|
||||
|
||||
@@ -117,3 +120,11 @@ class RoundTripTestCase(unittest.TestCase):
|
||||
def test_selfassessment_roundtrip(self):
|
||||
#Test selfassessment xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"self_assessment")
|
||||
|
||||
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")
|
||||
|
||||
@@ -352,3 +352,19 @@ class ImportTestCase(unittest.TestCase):
|
||||
sa_sample = modulestore.get_instance(sa_id, location)
|
||||
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
|
||||
self.assertEqual(sa_sample.metadata['attempts'], '10')
|
||||
|
||||
def test_graphicslidertool_import(self):
|
||||
'''
|
||||
Check to see if definition_from_xml in gst_module.py
|
||||
works properly. Pulls data from the graphic_slider_tool directory
|
||||
in the test data directory.
|
||||
'''
|
||||
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool'])
|
||||
|
||||
sa_id = "edX/gst_test/2012_Fall"
|
||||
location = Location(["i4x", "edX", "gst_test", "graphical_slider_tool", "sample_gst"])
|
||||
gst_sample = modulestore.get_instance(sa_id, location)
|
||||
render_string_from_sample_gst_xml = """
|
||||
<slider var="a" style="width:400px;float:left;"/>\
|
||||
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
|
||||
self.assertEqual(gst_sample.definition['render'], render_string_from_sample_gst_xml)
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
from xmodule.progress import Progress
|
||||
from xmodule import x_module
|
||||
|
||||
from . import i4xs
|
||||
from . import test_system
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
@@ -133,6 +133,6 @@ class ModuleProgressTest(unittest.TestCase):
|
||||
'''
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
|
||||
xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {})
|
||||
p = xm.get_progress()
|
||||
self.assertEqual(p, None)
|
||||
|
||||
54
common/lib/xmodule/xmodule/tests/test_self_assessment.py
Normal file
54
common/lib/xmodule/xmodule/tests/test_self_assessment.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
from mock import Mock
|
||||
import unittest
|
||||
|
||||
from xmodule.self_assessment_module import SelfAssessmentModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
|
||||
class SelfAssessmentTest(unittest.TestCase):
|
||||
|
||||
definition = {'rubric': 'A rubric',
|
||||
'prompt': 'Who?',
|
||||
'submitmessage': 'Shall we submit now?',
|
||||
'hintprompt': 'Consider this...',
|
||||
}
|
||||
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment",
|
||||
"SampleQuestion"])
|
||||
|
||||
metadata = {'attempts': '10'}
|
||||
|
||||
descriptor = Mock()
|
||||
|
||||
def test_import(self):
|
||||
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
|
||||
'scores': [0, 1],
|
||||
'hints': ['o hai'],
|
||||
'state': SelfAssessmentModule.ASSESSING,
|
||||
'attempts': 2})
|
||||
|
||||
module = SelfAssessmentModule(test_system, self.location,
|
||||
self.definition, self.descriptor,
|
||||
state, {}, metadata=self.metadata)
|
||||
|
||||
self.assertEqual(module.get_score()['score'], 0)
|
||||
|
||||
self.assertTrue('answer 3' in module.get_html())
|
||||
self.assertFalse('answer 2' in module.get_html())
|
||||
|
||||
module.save_assessment({'assessment': '0'})
|
||||
self.assertEqual(module.state, module.REQUEST_HINT)
|
||||
|
||||
module.save_hint({'hint': 'hint for ans 3'})
|
||||
self.assertEqual(module.state, module.DONE)
|
||||
|
||||
d = module.reset({})
|
||||
self.assertTrue(d['success'])
|
||||
self.assertEqual(module.state, module.INITIAL)
|
||||
|
||||
# if we now assess as right, skip the REQUEST_HINT state
|
||||
module.save_answer({'student_answer': 'answer 4'})
|
||||
module.save_assessment({'assessment': '1'})
|
||||
self.assertEqual(module.state, module.DONE)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ from pkg_resources import resource_string, resource_listdir
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -33,6 +36,7 @@ class VideoModule(XModule):
|
||||
self.show_captions = xmltree.get('show_captions', 'true')
|
||||
self.source = self._get_source(xmltree)
|
||||
self.track = self._get_track(xmltree)
|
||||
self.start_time, self.end_time = self._get_timeframe(xmltree)
|
||||
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
@@ -42,11 +46,11 @@ class VideoModule(XModule):
|
||||
def _get_source(self, xmltree):
|
||||
# find the first valid source
|
||||
return self._get_first_external(xmltree, 'source')
|
||||
|
||||
|
||||
def _get_track(self, xmltree):
|
||||
# find the first valid track
|
||||
return self._get_first_external(xmltree, 'track')
|
||||
|
||||
|
||||
def _get_first_external(self, xmltree, tag):
|
||||
"""
|
||||
Will return the first valid element
|
||||
@@ -61,6 +65,23 @@ class VideoModule(XModule):
|
||||
break
|
||||
return result
|
||||
|
||||
def _get_timeframe(self, xmltree):
|
||||
""" Converts 'from' and 'to' parameters in video tag to seconds.
|
||||
If there are no parameters, returns empty string. """
|
||||
|
||||
def parse_time(s):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if s is None:
|
||||
return ''
|
||||
else:
|
||||
x = time.strptime(s, '%H:%M:%S')
|
||||
return datetime.timedelta(hours=x.tm_hour,
|
||||
minutes=x.tm_min,
|
||||
seconds=x.tm_sec).total_seconds()
|
||||
|
||||
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
Handle ajax calls to this video.
|
||||
@@ -98,11 +119,13 @@ class VideoModule(XModule):
|
||||
'id': self.location.html_id(),
|
||||
'position': self.position,
|
||||
'source': self.source,
|
||||
'track' : self.track,
|
||||
'track': self.track,
|
||||
'display_name': self.display_name,
|
||||
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
||||
'data_dir': self.metadata['data_dir'],
|
||||
'show_captions': self.show_captions
|
||||
'show_captions': self.show_captions,
|
||||
'start': self.start_time,
|
||||
'end': self.end_time
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -233,17 +233,17 @@ class XModule(HTMLSnippet):
|
||||
self._loaded_children = [c for c in children if c is not None]
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
|
||||
def get_children_locations(self):
|
||||
'''
|
||||
Returns the locations of each of child modules.
|
||||
|
||||
|
||||
Overriding this changes the behavior of get_children and
|
||||
anything that uses get_children, such as get_display_items.
|
||||
|
||||
|
||||
This method will not instantiate the modules of the children
|
||||
unless absolutely necessary, so it is cheaper to call than get_children
|
||||
|
||||
|
||||
These children will be the same children returned by the
|
||||
descriptor unless descriptor.has_dynamic_children() is true.
|
||||
'''
|
||||
@@ -288,8 +288,20 @@ class XModule(HTMLSnippet):
|
||||
return '{}'
|
||||
|
||||
def get_score(self):
|
||||
''' Score the student received on the problem.
|
||||
'''
|
||||
"""
|
||||
Score the student received on the problem, or None if there is no
|
||||
score.
|
||||
|
||||
Returns:
|
||||
dictionary
|
||||
{'score': integer, from 0 to get_max_score(),
|
||||
'total': get_max_score()}
|
||||
|
||||
NOTE (vshnayder): not sure if this was the intended return value, but
|
||||
that's what it's doing now. I suspect that we really want it to just
|
||||
return a number. Would need to change (at least) capa and
|
||||
modx_dispatch to match if we did that.
|
||||
"""
|
||||
return None
|
||||
|
||||
def max_score(self):
|
||||
@@ -319,7 +331,7 @@ class XModule(HTMLSnippet):
|
||||
get is a dictionary-like object '''
|
||||
return ""
|
||||
|
||||
# cdodge: added to support dynamic substitutions of
|
||||
# cdodge: added to support dynamic substitutions of
|
||||
# links for courseware assets (e.g. images). <link> is passed through from lxml.html parser
|
||||
def rewrite_content_links(self, link):
|
||||
# see if we start with our format, e.g. 'xasset:<filename>'
|
||||
@@ -402,13 +414,17 @@ 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
|
||||
# and should not be edited by an end-user
|
||||
system_metadata_fields = [ 'data_dir' ]
|
||||
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
equality_attributes = ('definition', 'metadata', 'location',
|
||||
@@ -485,12 +501,26 @@ 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):
|
||||
"""
|
||||
@@ -562,18 +592,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
self,
|
||||
metadata=self.metadata
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def has_dynamic_children(self):
|
||||
"""
|
||||
Returns True if this descriptor has dynamic children for a given
|
||||
student when the module is created.
|
||||
|
||||
|
||||
Returns False if the children of this descriptor are the same
|
||||
children that the module will return for any student.
|
||||
children that the module will return for any student.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
@@ -703,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:
|
||||
@@ -797,7 +828,8 @@ class ModuleSystem(object):
|
||||
debug=False,
|
||||
xqueue=None,
|
||||
node_path="",
|
||||
anonymous_student_id=''):
|
||||
anonymous_student_id='',
|
||||
course_id=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -832,6 +864,8 @@ class ModuleSystem(object):
|
||||
ajax results.
|
||||
|
||||
anonymous_student_id - Used for tracking modules with student id
|
||||
|
||||
course_id - the course_id containing this module
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue = xqueue
|
||||
@@ -844,6 +878,7 @@ class ModuleSystem(object):
|
||||
self.replace_urls = replace_urls
|
||||
self.node_path = node_path
|
||||
self.anonymous_student_id = anonymous_student_id
|
||||
self.course_id = course_id
|
||||
self.user_is_staff = user is not None and user.is_staff
|
||||
|
||||
def get(self, attr):
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
from xmodule.x_module import (XModuleDescriptor, policy_key)
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
import json
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import os
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import (XModuleDescriptor, policy_key)
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# assume all XML files are persisted as utf-8.
|
||||
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True)
|
||||
remove_comments=True, remove_blank_text=True,
|
||||
encoding='utf-8')
|
||||
|
||||
def name_to_pathname(name):
|
||||
"""
|
||||
@@ -93,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
|
||||
@@ -366,7 +373,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath = self.__class__._format_filepath(self.category, url_path)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(etree.tostring(xml_object, pretty_print=True))
|
||||
file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8'))
|
||||
|
||||
# And return just a pointer with the category and filename.
|
||||
record_object = etree.Element(self.category)
|
||||
@@ -381,7 +388,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
record_object.set('org', self.location.org)
|
||||
record_object.set('course', self.location.course)
|
||||
|
||||
return etree.tostring(record_object, pretty_print=True)
|
||||
return etree.tostring(record_object, pretty_print=True, encoding='utf-8')
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
"""
|
||||
|
||||
57
common/static/js/vendor/RequireJS.js
vendored
Normal file
57
common/static/js/vendor/RequireJS.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is a wrapper for the Require JS file and module loader. Please see
|
||||
* the discussion at:
|
||||
*
|
||||
* https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
*/
|
||||
|
||||
var RequireJS = function() {
|
||||
|
||||
// Below is the unmodified minified version of Require JS. The latest can be
|
||||
// found at:
|
||||
//
|
||||
// http://requirejs.org/docs/download.html
|
||||
|
||||
/*
|
||||
RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
|
||||
Available via the MIT or new BSD license.
|
||||
see: http://github.com/jrburke/requirejs for details
|
||||
*/
|
||||
var requirejs,require,define;
|
||||
(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function M(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function r(b,c){return da.call(b,c)}function i(b,c){return r(b,c)&&b[c]}function E(b,c){for(var d in b)if(r(b,d)&&c(b[d],d))break}function Q(b,c,d,i){c&&E(c,function(c,h){if(d||!r(b,h))i&&"string"!==typeof c?(b[h]||(b[h]={}),Q(b[h],
|
||||
c,d,i)):b[h]=c});return b}function t(b,c){return function(){return c.apply(b,arguments)}}function Z(b){if(!b)return b;var c=Y;x(b.split("."),function(b){c=c[b]});return c}function J(b,c,d,i){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=i;d&&(c.originalError=d);return c}function ea(b){function c(a,g,v){var e,n,b,c,d,j,f,h=g&&g.split("/");e=h;var l=m.map,k=l&&l["*"];if(a&&"."===a.charAt(0))if(g){e=i(m.pkgs,g)?h=[g]:h.slice(0,h.length-1);g=a=e.concat(a.split("/"));
|
||||
for(e=0;g[e];e+=1)if(n=g[e],"."===n)g.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===g[2]||".."===g[0]))break;else 0<e&&(g.splice(e-1,2),e-=2);e=i(m.pkgs,g=a[0]);a=a.join("/");e&&a===g+"/"+e.main&&(a=g)}else 0===a.indexOf("./")&&(a=a.substring(2));if(v&&(h||k)&&l){g=a.split("/");for(e=g.length;0<e;e-=1){b=g.slice(0,e).join("/");if(h)for(n=h.length;0<n;n-=1)if(v=i(l,h.slice(0,n).join("/")))if(v=i(v,b)){c=v;d=e;break}if(c)break;!j&&(k&&i(k,b))&&(j=i(k,b),f=e)}!c&&j&&(c=j,d=f);c&&(g.splice(0,d,
|
||||
c),a=g.join("/"))}return a}function d(a){z&&x(document.getElementsByTagName("script"),function(g){if(g.getAttribute("data-requiremodule")===a&&g.getAttribute("data-requirecontext")===j.contextName)return g.parentNode.removeChild(g),!0})}function y(a){var g=i(m.paths,a);if(g&&I(g)&&1<g.length)return d(a),g.shift(),j.require.undef(a),j.require([a]),!0}function f(a){var g,b=a?a.indexOf("!"):-1;-1<b&&(g=a.substring(0,b),a=a.substring(b+1,a.length));return[g,a]}function h(a,g,b,e){var n,u,d=null,h=g?g.name:
|
||||
null,l=a,m=!0,k="";a||(m=!1,a="_@r"+(L+=1));a=f(a);d=a[0];a=a[1];d&&(d=c(d,h,e),u=i(p,d));a&&(d?k=u&&u.normalize?u.normalize(a,function(a){return c(a,h,e)}):c(a,h,e):(k=c(a,h,e),a=f(k),d=a[0],k=a[1],b=!0,n=j.nameToUrl(k)));b=d&&!u&&!b?"_unnormalized"+(M+=1):"";return{prefix:d,name:k,parentMap:g,unnormalized:!!b,url:n,originalName:l,isDefine:m,id:(d?d+"!"+k:k)+b}}function q(a){var g=a.id,b=i(k,g);b||(b=k[g]=new j.Module(a));return b}function s(a,g,b){var e=a.id,n=i(k,e);if(r(p,e)&&(!n||n.defineEmitComplete))"defined"===
|
||||
g&&b(p[e]);else q(a).on(g,b)}function C(a,g){var b=a.requireModules,e=!1;if(g)g(a);else if(x(b,function(g){if(g=i(k,g))g.error=a,g.events.error&&(e=!0,g.emit("error",a))}),!e)l.onError(a)}function w(){R.length&&(fa.apply(F,[F.length-1,0].concat(R)),R=[])}function A(a,g,b){var e=a.map.id;a.error?a.emit("error",a.error):(g[e]=!0,x(a.depMaps,function(e,c){var d=e.id,h=i(k,d);h&&(!a.depMatched[c]&&!b[d])&&(i(g,d)?(a.defineDep(c,p[d]),a.check()):A(h,g,b))}),b[e]=!0)}function B(){var a,g,b,e,n=(b=1E3*m.waitSeconds)&&
|
||||
j.startTime+b<(new Date).getTime(),c=[],h=[],f=!1,l=!0;if(!T){T=!0;E(k,function(b){a=b.map;g=a.id;if(b.enabled&&(a.isDefine||h.push(b),!b.error))if(!b.inited&&n)y(g)?f=e=!0:(c.push(g),d(g));else if(!b.inited&&(b.fetched&&a.isDefine)&&(f=!0,!a.prefix))return l=!1});if(n&&c.length)return b=J("timeout","Load timeout for modules: "+c,null,c),b.contextName=j.contextName,C(b);l&&x(h,function(a){A(a,{},{})});if((!n||e)&&f)if((z||$)&&!U)U=setTimeout(function(){U=0;B()},50);T=!1}}function D(a){r(p,a[0])||
|
||||
q(h(a[0],null,!0)).init(a[1],a[2])}function G(a){var a=a.currentTarget||a.srcElement,b=j.onScriptLoad;a.detachEvent&&!V?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=j.onScriptError;(!a.detachEvent||V)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function K(){var a;for(w();F.length;){a=F.shift();if(null===a[0])return C(J("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));D(a)}}var T,W,j,N,U,m={waitSeconds:7,
|
||||
baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},k={},X={},F=[],p={},S={},L=1,M=1;N={require:function(a){return a.require?a.require:a.require=j.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=p[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m.config&&i(m.config,a.map.id)||{}},exports:p[a.map.id]}}};W=function(a){this.events=i(X,a.id)||{};this.map=a;this.shim=
|
||||
i(m.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};W.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=t(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=
|
||||
b)},fetch:function(){if(!this.fetched){this.fetched=!0;j.startTime=(new Date).getTime();var a=this.map;if(this.shim)j.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],t(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,j.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory;
|
||||
if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&
|
||||
!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0,
|
||||
skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error=
|
||||
a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a,
|
||||
b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=
|
||||
this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a,
|
||||
b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments));
|
||||
return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap;
|
||||
m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k,
|
||||
a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0<j;j-=1)if(k=
|
||||
f.slice(0,j).join("/"),h=i(d,k),k=i(c,k)){I(k)&&(k=k[0]);f.splice(0,j,k);break}else if(h){c=a===h.name?h.location+"/"+h.main:h.location;f.splice(0,j,c);break}f=f.join("/");f+=b||(/\?/.test(f)?"":".js");f=("/"===f.charAt(0)||f.match(/^[\w\+\.\-]+:/)?"":m.baseUrl)+f}return m.urlArgs?f+((-1===f.indexOf("?")?"?":"&")+m.urlArgs):f},load:function(a,b){l.load(j,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ha.test((a.currentTarget||a.srcElement).readyState))P=
|
||||
null,a=G(a),j.completeLoad(a.id)},onScriptError:function(a){var b=G(a);if(!y(b.id))return C(J("scripterror","Script error",a,[b.id]))}};j.require=j.makeRequire();return j}var l,w,A,D,s,G,P,K,ba,ca,ia=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ja=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,aa=/\.js$/,ga=/^\.\//;w=Object.prototype;var L=w.toString,da=w.hasOwnProperty,fa=Array.prototype.splice,z=!!("undefined"!==typeof window&&navigator&&document),$=!z&&"undefined"!==typeof importScripts,ha=z&&
|
||||
"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,V="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),B={},q={},R=[],O=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(H(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!H(require)&&(q=require,require=void 0);l=requirejs=function(b,c,d,y){var f,h="_";!I(b)&&"string"!==typeof b&&(f=b,I(c)?(b=c,c=d,d=y):b=[]);f&&f.context&&(h=f.context);(y=i(B,h))||(y=B[h]=l.s.newContext(h));
|
||||
f&&y.configure(f);return y.require(b,c,d)};l.config=function(b){return l(b)};l.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=l);l.version="2.1.2";l.jsExtRegExp=/^\/|:|\?|\.js$/;l.isBrowser=z;w=l.s={contexts:B,newContext:ea};l({});x(["toUrl","undef","defined","specified"],function(b){l[b]=function(){var c=B._;return c.require[b].apply(c,arguments)}});if(z&&(A=w.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))A=
|
||||
w.head=D.parentNode;l.onError=function(b){throw b;};l.load=function(b,c,d){var i=b&&b.config||{},f;if(z)return f=i.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),f.type=i.scriptType||"text/javascript",f.charset="utf-8",f.async=!0,f.setAttribute("data-requirecontext",b.contextName),f.setAttribute("data-requiremodule",c),f.attachEvent&&!(f.attachEvent.toString&&0>f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange",
|
||||
b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i,
|
||||
f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd=
|
||||
{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this);
|
||||
|
||||
// The object which will be globally available via RequireJS variable.
|
||||
return {
|
||||
'requirejs': requirejs,
|
||||
'require': require,
|
||||
'define': define
|
||||
};
|
||||
}(); // End-of: var RequireJS = function()
|
||||
@@ -33,4 +33,4 @@
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
|
||||
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
|
||||
MathJax extension libraries -->
|
||||
<script type="text/javascript" src="/static/js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
|
||||
<script type="text/javascript" src="https://edx-static.s3.amazonaws.com/mathjax-MathJax-07669ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p>No - anyone and everyone is welcome to take this course.</p>
|
||||
</li>
|
||||
<li>What textbook should I buy?
|
||||
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) -- Volume II, which was written specifically for this course.</p>
|
||||
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) — Volume II, which was written specifically for this course.</p>
|
||||
</li>
|
||||
<li>Does Harvard award credentials or reports regarding my work in this course?
|
||||
<p>Princeton does not award credentials or issue reports for student work in this course. However, Coursera could maintain a record of your score on the assessments and, with your permission, verify that score for authorized parties.</p>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<video url_name="welcome"/>
|
||||
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
|
||||
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
|
||||
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab… </html>
|
||||
<html slug="html_5555" filename="html_5555"/>
|
||||
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
|
||||
</vertical>
|
||||
|
||||
@@ -1 +1 @@
|
||||
More information given in <a href="/book/${page}">the text</a>.
|
||||
More information given in… <a href="/book/${page}">the text</a>.
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag} </a>
|
||||
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag}… </a>
|
||||
@@ -1 +1 @@
|
||||
Lecture Slides Handout [<a href="">Clean </a>][<a href="">Annotated</a>]
|
||||
Lecture Slides Handout [<a href="">Clean… </a>][<a href="">Annotated…</a>]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Hint
|
||||
Hint…
|
||||
<br/><br/>
|
||||
Remember that the time evolution of any variable \(x(t)\) governed by
|
||||
a first-order system with a time-constant \(\tau\) for a time \(t) between an initial
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<section class="tutorials">
|
||||
<h2> Basic Tutorials </h2>
|
||||
<ul>
|
||||
<li><a href="/section/wk13_solder">Soldering</a> -- Steve
|
||||
<li><a href="/section/wk13_solder">Soldering</a> — Steve
|
||||
Finberg, one of the pioneers in from Draper Lab, talks about
|
||||
soldering. </li>
|
||||
</ul>
|
||||
<h2> Bonus Tutorials </h2>
|
||||
<ul>
|
||||
<li><a href="/section/wk13_FreqResp">Frequency Response
|
||||
Curves</a> -- We explain several techniques for understanding
|
||||
Curves</a> — We explain several techniques for understanding
|
||||
and approximating Bode plots. </li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<li><a href="/section/problem_1_3">OCW Problem 1-3 </a> - Reverse engineer a black-box resistor network</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. </p>
|
||||
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. Gratuitous ≥ entity.</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab. </html>
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
the Thevenin or Norton theorems to summarize the behavior at
|
||||
a pair of exposed terminals.
|
||||
</p><p>
|
||||
Sorry for the confusion of words -- natural language is like
|
||||
Sorry for the confusion of words — natural language is like
|
||||
that!
|
||||
</p>
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
the Thevenin or Norton theorems to summarize the behavior at
|
||||
a pair of exposed terminals.
|
||||
</p><p>
|
||||
Sorry for the confusion of words -- natural language is like
|
||||
Sorry for the confusion of words — natural language is like
|
||||
that!
|
||||
</p>
|
||||
|
||||
@@ -9,14 +9,14 @@ the right of the diagram area) and drag it onto the diagram. Release
|
||||
the mouse when the component is in the correct position.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- note that entities like — may be used. -->
|
||||
<tr>
|
||||
<td>Move a component</td>
|
||||
<td>Click to select a component in the diagram (it will turn green)
|
||||
and then drag it to its new location. You can use shift-click to add
|
||||
a component to the current selection. Or you can click somewhere in
|
||||
the diagram that is not on top of a component and drag out a selection
|
||||
rectangle -- components intersecting the rectangle will be added to
|
||||
rectangle — components intersecting the rectangle will be added to
|
||||
the current selection.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -63,7 +63,7 @@ engineeering notation:
|
||||
<td>Add a wire</td>
|
||||
<td>Wires start at connection points, the open circles that
|
||||
appear at the terminals of components or the ends of wires.
|
||||
Click on a connection point to start a wire -- a green wire
|
||||
Click on a connection point to start a wire — a green wire
|
||||
will appear with one end anchored at the starting point.
|
||||
Drag the mouse and release the mouse button when the other
|
||||
end of the wire is positioned as you wish. Once a wire has
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Hint
|
||||
Hint…
|
||||
<br/><br/>
|
||||
Be careful of units here. Make sure you notice multipliers such
|
||||
as u, k, m, M.
|
||||
as u (or μ), k, m, M.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user