Merge branch 'feature/cale/cms-master' into feature/christina/tiny-mce
Conflicts: cms/static/css/tiny-mce.css
This commit is contained in:
@@ -12,7 +12,7 @@ import re
|
||||
import logging
|
||||
|
||||
|
||||
class CourseDetails:
|
||||
class CourseDetails(object):
|
||||
def __init__(self, location):
|
||||
self.course_location = location # a Location obj
|
||||
self.start_date = None # 'start'
|
||||
@@ -79,8 +79,7 @@ class CourseDetails:
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
dirty = False
|
||||
|
||||
## ??? Will this comparison work?
|
||||
|
||||
if 'start_date' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['start_date'])
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from util import converters
|
||||
|
||||
|
||||
class CourseGradingModel:
|
||||
class CourseGradingModel(object):
|
||||
"""
|
||||
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
||||
"""
|
||||
|
||||
@@ -1,129 +1,130 @@
|
||||
@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local("Open Sans Bold"),local("OpenSans-Bold"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:300;src:local("Open Sans Light"),local("OpenSans-Light"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:700;src:local("Open Sans Bold Italic"),local("OpenSans-BoldItalic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:300;src:local("Open Sans Light Italic"),local("OpenSansLight-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:400;src:local("Open Sans Italic"),local("OpenSans-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format("woff")}
|
||||
|
||||
.mceContentBody {
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3c3c3c;
|
||||
scrollbar-3dlight-color: #F0F0EE;
|
||||
scrollbar-arrow-color: #676662;
|
||||
scrollbar-base-color: #F0F0EE;
|
||||
scrollbar-darkshadow-color: #DDDDDD;
|
||||
scrollbar-face-color: #E0E0DD;
|
||||
scrollbar-highlight-color: #F0F0EE;
|
||||
scrollbar-shadow-color: #F0F0EE;
|
||||
scrollbar-track-color: #F5F5F5;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3c3c3c;
|
||||
scrollbar-3dlight-color: #F0F0EE;
|
||||
scrollbar-arrow-color: #676662;
|
||||
scrollbar-base-color: #F0F0EE;
|
||||
scrollbar-darkshadow-color: #DDDDDD;
|
||||
scrollbar-face-color: #E0E0DD;
|
||||
scrollbar-highlight-color: #F0F0EE;
|
||||
scrollbar-shadow-color: #F0F0EE;
|
||||
scrollbar-track-color: #F5F5F5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #3c3c3c;
|
||||
font-weight: normal;
|
||||
font-size: 2em;
|
||||
line-height: 1.4em;
|
||||
letter-spacing: 1px;
|
||||
margin: 0 0 1.416em 0;
|
||||
color: #3c3c3c;
|
||||
font-weight: normal;
|
||||
font-size: 2em;
|
||||
line-height: 1.4em;
|
||||
letter-spacing: 1px;
|
||||
margin: 0 0 1.416em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #646464;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #646464;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: .83em;
|
||||
font-size: .83em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.75em;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.416em;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em !important;
|
||||
color: #3c3c3c;
|
||||
margin-bottom: 1.416em;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em !important;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-style: bold;
|
||||
font-style: bold;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: 1em 0;
|
||||
padding: 0 0 0 1em;
|
||||
color: #3c3c3c;
|
||||
margin: 1em 0;
|
||||
padding: 0 0 0 1em;
|
||||
color: #3c3c3c;
|
||||
|
||||
}
|
||||
|
||||
ol li, ul li {
|
||||
margin-bottom: 0.708em;
|
||||
margin-bottom: 0.708em;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
list-style: decimal outside none;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
list-style: disc outside none;
|
||||
}
|
||||
|
||||
a, a:link, a:visited, a:hover, a:active {
|
||||
color: #1d9dd9;
|
||||
}
|
||||
color: #1d9dd9;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace, serif;
|
||||
background: none;
|
||||
color: #3c3c3c;
|
||||
font-family: monospace, serif;
|
||||
background: none;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #eee;
|
||||
font-weight: bold;
|
||||
th {
|
||||
background: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table td, th {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc !important;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
table td, th {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc !important;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
}
|
||||
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@@ -118,6 +119,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-type,
|
||||
.new-component-template {
|
||||
@include clearfix;
|
||||
@@ -177,7 +196,11 @@
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.1;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -214,16 +237,6 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,4 +566,4 @@ body.unit {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class CourseRelativeMember:
|
||||
class CourseRelativeMember(object):
|
||||
def __init__(self, location, idx):
|
||||
self.course_location = location # a Location obj
|
||||
self.idx = idx # which milestone this represents. Hopefully persisted # so we don't have race conditions
|
||||
|
||||
@@ -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']
|
||||
@@ -40,6 +40,8 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from random import randint
|
||||
from time import strftime
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
@@ -47,6 +49,7 @@ 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
|
||||
|
||||
@@ -125,6 +128,9 @@ class UserProfile(models.Model):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
TEST_CENTER_STATUS_ACCEPTED = "Accepted"
|
||||
TEST_CENTER_STATUS_ERROR = "Error"
|
||||
|
||||
class TestCenterUser(models.Model):
|
||||
"""This is our representation of the User for in-person testing, and
|
||||
specifically for Pearson at this point. A few things to note:
|
||||
@@ -140,6 +146,9 @@ class TestCenterUser(models.Model):
|
||||
The field names and lengths are modeled on the conventions and constraints
|
||||
of Pearson's data import system, including oddities such as suffix having
|
||||
a limit of 255 while last_name only gets 50.
|
||||
|
||||
Also storing here the confirmation information received from Pearson (if any)
|
||||
as to the success or failure of the upload. (VCDC file)
|
||||
"""
|
||||
# Our own record keeping...
|
||||
user = models.ForeignKey(User, unique=True, default=None)
|
||||
@@ -150,12 +159,8 @@ class TestCenterUser(models.Model):
|
||||
# updated_at, this will not get incremented when we do a batch data import.
|
||||
user_updated_at = models.DateTimeField(db_index=True)
|
||||
|
||||
# Unique ID given to us for this User by the Testing Center. It's null when
|
||||
# we first create the User entry, and is assigned by Pearson later.
|
||||
candidate_id = models.IntegerField(null=True, db_index=True)
|
||||
|
||||
# Unique ID we assign our user for a the Test Center.
|
||||
client_candidate_id = models.CharField(max_length=50, db_index=True)
|
||||
# Unique ID we assign our user for the Test Center.
|
||||
client_candidate_id = models.CharField(unique=True, max_length=50, db_index=True)
|
||||
|
||||
# Name
|
||||
first_name = models.CharField(max_length=30, db_index=True)
|
||||
@@ -186,18 +191,371 @@ 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.
|
||||
"""
|
||||
# include the secret key as a salt, and to make the ids unique accross
|
||||
# include the secret key as a salt, and to make the ids unique across
|
||||
# different LMS installs.
|
||||
h = hashlib.md5()
|
||||
h.update(settings.SECRET_KEY)
|
||||
@@ -261,6 +619,22 @@ class CourseEnrollment(models.Model):
|
||||
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
from courseware.courses import get_courses, sort_by_announcement
|
||||
from courseware.access import has_access
|
||||
|
||||
from statsd import statsd
|
||||
@@ -76,10 +78,7 @@ def index(request, extra_context={}, user=None):
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
|
||||
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)
|
||||
courses = sort_by_announcement(courses)
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
@@ -239,6 +238,8 @@ def dashboard(request):
|
||||
|
||||
cert_statuses = { course.id: cert_info(request.user, course) for course in courses}
|
||||
|
||||
exam_registrations = { course.id: exam_registration_info(request.user, course) for course in courses}
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
|
||||
@@ -249,6 +250,7 @@ def dashboard(request):
|
||||
'show_courseware_links_for' : show_courseware_links_for,
|
||||
'cert_statuses': cert_statuses,
|
||||
'news': top_news,
|
||||
'exam_registrations': exam_registrations,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
@@ -300,7 +302,7 @@ def change_enrollment(request):
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.warning("User {0} tried to enroll in non-existant course {1}"
|
||||
log.warning("User {0} tried to enroll in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
@@ -466,8 +468,9 @@ def _do_create_account(post_vars):
|
||||
try:
|
||||
profile.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
profile.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
# If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
profile.year_of_birth = None
|
||||
try:
|
||||
profile.save()
|
||||
except Exception:
|
||||
@@ -599,6 +602,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():
|
||||
"""
|
||||
@@ -654,7 +823,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
|
||||
|
||||
@@ -15,7 +15,7 @@ def jsdate_to_time(field):
|
||||
"""
|
||||
if field is None:
|
||||
return field
|
||||
elif isinstance(field, unicode) or isinstance(field, str): # iso format but ignores time zone assuming it's Z
|
||||
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
|
||||
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
||||
return d.utctimetuple()
|
||||
elif isinstance(field, int) or isinstance(field, float):
|
||||
|
||||
@@ -34,6 +34,8 @@ import chem
|
||||
import chem.chemcalc
|
||||
import chem.chemtools
|
||||
import chem.miller
|
||||
import verifiers
|
||||
import verifiers.draganddrop
|
||||
|
||||
import calc
|
||||
from correctmap import CorrectMap
|
||||
@@ -69,7 +71,8 @@ global_context = {'random': random,
|
||||
'eia': eia,
|
||||
'chemcalc': chem.chemcalc,
|
||||
'chemtools': chem.chemtools,
|
||||
'miller': chem.miller}
|
||||
'miller': chem.miller,
|
||||
'draganddrop': verifiers.draganddrop}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
|
||||
@@ -186,24 +189,6 @@ class LoncapaProblem(object):
|
||||
maxscore += responder.get_max_score()
|
||||
return maxscore
|
||||
|
||||
def message_post(self,event_info):
|
||||
"""
|
||||
Handle an ajax post that contains feedback on feedback
|
||||
Returns a boolean success variable
|
||||
Note: This only allows for feedback to be posted back to the grading controller for the first
|
||||
open ended response problem on each page. Multiple problems will cause some sync issues.
|
||||
TODO: Handle multiple problems on one page sync issues.
|
||||
"""
|
||||
success=False
|
||||
message = "Could not find a valid responder."
|
||||
log.debug("in lcp")
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder, 'handle_message_post'):
|
||||
success, message = responder.handle_message_post(event_info)
|
||||
if success:
|
||||
break
|
||||
return success, message
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
Compute score for this problem. The score is the number of points awarded.
|
||||
|
||||
@@ -13,6 +13,9 @@ Module containing the problem elements which render into input objects
|
||||
- imageinput (for clickable image)
|
||||
- optioninput (for option list)
|
||||
- filesubmission (upload a file)
|
||||
- crystallography
|
||||
- vsepr_input
|
||||
- drag_and_drop
|
||||
|
||||
These are matched by *.html files templates/*.html which are mako templates with the
|
||||
actual html.
|
||||
@@ -41,6 +44,7 @@ from lxml import etree
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
import sys
|
||||
import os
|
||||
|
||||
from registry import TagRegistry
|
||||
|
||||
@@ -692,7 +696,7 @@ class VseprInput(InputTypeBase):
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Note: height, width are required.
|
||||
Note: height, width, molecules and geometries are required.
|
||||
"""
|
||||
return [Attribute('height'),
|
||||
Attribute('width'),
|
||||
@@ -736,50 +740,92 @@ registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OpenEndedInput(InputTypeBase):
|
||||
class DragAndDropInput(InputTypeBase):
|
||||
"""
|
||||
A text area input for code--uses codemirror, does syntax highlighting, special tab handling,
|
||||
etc.
|
||||
Input for drag and drop problems. Allows student to drag and drop images and
|
||||
labels to base image.
|
||||
"""
|
||||
|
||||
template = "openendedinput.html"
|
||||
tags = ['openendedinput']
|
||||
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Feedback not yet available. Reload to check again. "
|
||||
"Once the problem is graded, this message will be "
|
||||
"replaced with the grader's feedback.")
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Convert options to a convenient format.
|
||||
"""
|
||||
return [Attribute('rows', '30'),
|
||||
Attribute('cols', '80'),
|
||||
Attribute('hidden', ''),
|
||||
]
|
||||
template = 'drag_and_drop_input.html'
|
||||
tags = ['drag_and_drop_input']
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Implement special logic: handle queueing state, and default input.
|
||||
"""
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not self.value:
|
||||
self.value = self.xml.text
|
||||
|
||||
# Check if problem has been queued
|
||||
self.queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if self.status == 'incomplete':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = self.submitted_msg
|
||||
def parse(tag, tag_type):
|
||||
"""Parses <tag ... /> xml element to dictionary. Stores
|
||||
'draggable' and 'target' tags with attributes to dictionary and
|
||||
returns last.
|
||||
|
||||
def _extra_context(self):
|
||||
"""Defined queue_len, add it """
|
||||
return {'queue_len': self.queue_len,}
|
||||
Args:
|
||||
tag: xml etree element <tag...> with attributes
|
||||
|
||||
registry.register(OpenEndedInput)
|
||||
tag_type: 'draggable' or 'target'.
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
If tag_type is 'draggable' : all attributes except id
|
||||
(name or label or icon or can_reuse) are optional
|
||||
|
||||
If tag_type is 'target' all attributes (name, x, y, w, h)
|
||||
are required. (x, y) - coordinates of center of target,
|
||||
w, h - weight and height of target.
|
||||
|
||||
Returns:
|
||||
Dictionary of vaues of attributes:
|
||||
dict{'name': smth, 'label': smth, 'icon': smth,
|
||||
'can_reuse': smth}.
|
||||
"""
|
||||
tag_attrs = dict()
|
||||
tag_attrs['draggable'] = {'id': Attribute._sentinel,
|
||||
'label': "", 'icon': "",
|
||||
'can_reuse': ""}
|
||||
|
||||
tag_attrs['target'] = {'id': Attribute._sentinel,
|
||||
'x': Attribute._sentinel,
|
||||
'y': Attribute._sentinel,
|
||||
'w': Attribute._sentinel,
|
||||
'h': Attribute._sentinel}
|
||||
|
||||
dic = dict()
|
||||
|
||||
for attr_name in tag_attrs[tag_type].keys():
|
||||
dic[attr_name] = Attribute(attr_name,
|
||||
default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag)
|
||||
|
||||
if tag_type == 'draggable' and not self.no_labels:
|
||||
dic['label'] = dic['label'] or dic['id']
|
||||
|
||||
return dic
|
||||
|
||||
# add labels to images?:
|
||||
self.no_labels = Attribute('no_labels',
|
||||
default="False").parse_from_xml(self.xml)
|
||||
|
||||
to_js = dict()
|
||||
|
||||
# image drag and drop onto
|
||||
to_js['base_image'] = Attribute('img').parse_from_xml(self.xml)
|
||||
|
||||
# outline places on image where to drag adn drop
|
||||
to_js['target_outline'] = Attribute('target_outline',
|
||||
default="False").parse_from_xml(self.xml)
|
||||
# one draggable per target?
|
||||
to_js['one_per_target'] = Attribute('one_per_target',
|
||||
default="True").parse_from_xml(self.xml)
|
||||
# list of draggables
|
||||
to_js['draggables'] = [parse(draggable, 'draggable') for draggable in
|
||||
self.xml.iterchildren('draggable')]
|
||||
# list of targets
|
||||
to_js['targets'] = [parse(target, 'target') for target in
|
||||
self.xml.iterchildren('target')]
|
||||
|
||||
# custom background color for labels:
|
||||
label_bg_color = Attribute('label_bg_color',
|
||||
default=None).parse_from_xml(self.xml)
|
||||
if label_bg_color:
|
||||
to_js['label_bg_color'] = label_bg_color
|
||||
|
||||
self.loaded_attributes['drag_and_drop_json'] = json.dumps(to_js)
|
||||
self.to_render.add('drag_and_drop_json')
|
||||
|
||||
registry.register(DragAndDropInput)
|
||||
|
||||
#--------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -33,7 +33,7 @@ from correctmap import CorrectMap
|
||||
from datetime import datetime
|
||||
from util import *
|
||||
from lxml import etree
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
import xqueue_interface
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
@@ -873,7 +873,9 @@ def sympy_check2():
|
||||
|
||||
response_tag = 'customresponse'
|
||||
|
||||
allowed_inputfields = ['textline', 'textbox', 'crystallography', 'chemicalequationinput', 'vsepr_input']
|
||||
allowed_inputfields = ['textline', 'textbox', 'crystallography',
|
||||
'chemicalequationinput', 'vsepr_input',
|
||||
'drag_and_drop_input']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
@@ -1048,7 +1050,7 @@ def sympy_check2():
|
||||
pretty_print=True)
|
||||
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ', '')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
msg = re.sub('(?ms)<html>(.*)</html>', '\\1', msg)
|
||||
|
||||
messages[0] = msg
|
||||
@@ -1780,7 +1782,7 @@ class ImageResponse(LoncapaResponse):
|
||||
def get_score(self, student_answers):
|
||||
correct_map = CorrectMap()
|
||||
expectedset = self.get_answers()
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput>
|
||||
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')
|
||||
@@ -1833,443 +1835,6 @@ class ImageResponse(LoncapaResponse):
|
||||
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OpenEndedResponse(LoncapaResponse):
|
||||
"""
|
||||
Grade student open ended responses using an external grading system,
|
||||
accessed through the xqueue system.
|
||||
|
||||
Expects 'xqueue' dict in ModuleSystem with the following keys that are
|
||||
needed by OpenEndedResponse:
|
||||
|
||||
system.xqueue = { 'interface': XqueueInterface object,
|
||||
'callback_url': Per-StudentModule callback URL
|
||||
where results are posted (string),
|
||||
}
|
||||
|
||||
External requests are only submitted for student submission grading
|
||||
(i.e. and not for getting reference answers)
|
||||
|
||||
By default, uses the OpenEndedResponse.DEFAULT_QUEUE queue.
|
||||
"""
|
||||
|
||||
DEFAULT_QUEUE = 'open-ended'
|
||||
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
|
||||
response_tag = 'openendedresponse'
|
||||
allowed_inputfields = ['openendedinput']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
'''
|
||||
Configure OpenEndedResponse from XML.
|
||||
'''
|
||||
xml = self.xml
|
||||
self.url = xml.get('url', None)
|
||||
self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE)
|
||||
self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
|
||||
|
||||
# The openendedparam tag encapsulates all grader settings
|
||||
oeparam = self.xml.find('openendedparam')
|
||||
prompt = self.xml.find('prompt')
|
||||
rubric = self.xml.find('openendedrubric')
|
||||
|
||||
#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 prompt is None:
|
||||
raise ValueError("No prompt found in problem xml.")
|
||||
if rubric is None:
|
||||
raise ValueError("No rubric found in problem xml.")
|
||||
|
||||
self._parse(oeparam, prompt, rubric)
|
||||
|
||||
@staticmethod
|
||||
def stringify_children(node):
|
||||
"""
|
||||
Modify code from stringify_children in xmodule. Didn't import directly
|
||||
in order to avoid capa depending on xmodule (seems to be avoided in
|
||||
code)
|
||||
"""
|
||||
parts=[node.text if node.text is not None else '']
|
||||
for p in node.getchildren():
|
||||
parts.append(etree.tostring(p, with_tail=True, encoding='unicode'))
|
||||
|
||||
return ' '.join(parts)
|
||||
|
||||
def _parse(self, oeparam, prompt, rubric):
|
||||
'''
|
||||
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 = self.stringify_children(prompt)
|
||||
rubric_string = self.stringify_children(rubric)
|
||||
|
||||
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' : self.system.location,
|
||||
'course_id' : self.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}
|
||||
|
||||
try:
|
||||
self.max_score = int(find_with_default(oeparam, 'max_score', 1))
|
||||
except ValueError:
|
||||
self.max_score = 1
|
||||
|
||||
def handle_message_post(self,event_info):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
survey_responses=event_info['survey_responses']
|
||||
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
|
||||
if tag not in survey_responses:
|
||||
return False, "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 False, "There was an error saving your feedback. Please contact course staff."
|
||||
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.answer_id)
|
||||
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url=self.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
|
||||
|
||||
return success, "Successfully submitted your feedback."
|
||||
|
||||
|
||||
def get_score(self, student_answers):
|
||||
|
||||
try:
|
||||
submission = student_answers[self.answer_id]
|
||||
except KeyError:
|
||||
msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}'
|
||||
.format(self.answer_id, student_answers))
|
||||
log.exception(msg)
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
# Prepare xqueue request
|
||||
#------------------------------------------------------------
|
||||
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
|
||||
# Generate header
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.answer_id)
|
||||
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
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,}
|
||||
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
cmap.set(self.answer_id, queuestate=None,
|
||||
msg='Unable to deliver your submission to grader. (Reason: {0}.)'
|
||||
' Please try again later.'.format(msg))
|
||||
else:
|
||||
# Queueing mechanism flags:
|
||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
|
||||
# the problem has been queued
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down
|
||||
# through inputtypes.textbox and .filesubmission to inform the
|
||||
# browser that the submission is queued (and it could e.g. poll)
|
||||
cmap.set(self.answer_id, queuestate=queuestate,
|
||||
correctness='incomplete', msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
log.debug(score_msg)
|
||||
score_msg = self._parse_score_msg(score_msg)
|
||||
if not score_msg.valid:
|
||||
oldcmap.set(self.answer_id,
|
||||
msg = 'Invalid grader reply. Please contact the course staff.')
|
||||
return oldcmap
|
||||
|
||||
correctness = 'correct' if score_msg.correct else 'incorrect'
|
||||
|
||||
# TODO: Find out how this is used elsewhere, if any
|
||||
self.context['correct'] = correctness
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
|
||||
# does not match, we keep waiting for the score_msg whose key actually matches
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
# Sanity check on returned points
|
||||
points = score_msg.points
|
||||
if points < 0:
|
||||
points = 0
|
||||
|
||||
# Queuestate is consumed, so reset it to None
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
|
||||
msg = score_msg.msg.replace(' ', ' '), queuestate=None)
|
||||
else:
|
||||
log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format(
|
||||
queuekey, self.answer_id))
|
||||
|
||||
return oldcmap
|
||||
|
||||
def get_answers(self):
|
||||
anshtml = '<span class="openended-answer"><pre><code>{0}</code></pre></span>'.format(self.answer)
|
||||
return {self.answer_id: anshtml}
|
||||
|
||||
def get_initial_display(self):
|
||||
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 displayed 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 = """
|
||||
<div class="{feedback_type}" style="display: none;">
|
||||
{value}
|
||||
</div>
|
||||
""".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
|
||||
"""
|
||||
|
||||
feedback = self._convert_longform_feedback_to_html(response_items)
|
||||
|
||||
if not response_items['success']:
|
||||
return self.system.render_template("open_ended_error.html",
|
||||
{'errors' : feedback})
|
||||
|
||||
feedback_template = self.system.render_template("open_ended_feedback.html", {
|
||||
'grader_type': response_items['grader_type'],
|
||||
'score': "{0} / {1}".format(response_items['score'], self.max_score),
|
||||
'feedback': feedback,
|
||||
})
|
||||
|
||||
return feedback_template
|
||||
|
||||
|
||||
def _parse_score_msg(self, score_msg):
|
||||
"""
|
||||
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 = ScoreMessage(valid=False, correct=False, points=0, msg='')
|
||||
try:
|
||||
score_result = json.loads(score_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_msg = {0}".format(score_msg))
|
||||
return fail
|
||||
|
||||
if not isinstance(score_result, dict):
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_result = {0}".format(score_result))
|
||||
return fail
|
||||
|
||||
|
||||
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
|
||||
if tag not in score_result:
|
||||
log.error("External grader message is missing required tag: {0}"
|
||||
.format(tag))
|
||||
return fail
|
||||
|
||||
feedback = self._format_feedback(score_result)
|
||||
|
||||
self.submission_id=score_result['submission_id']
|
||||
self.grader_id=score_result['grader_id']
|
||||
|
||||
# HACK: for now, just assume it's correct if you got more than 2/3.
|
||||
# Also assumes that score_result['score'] is an integer.
|
||||
score_ratio = int(score_result['score']) / float(self.max_score)
|
||||
correct = (score_ratio >= 0.66)
|
||||
|
||||
#Currently ignore msg and only return feedback (which takes the place of msg)
|
||||
return ScoreMessage(valid=True, correct=correct,
|
||||
points=score_result['score'], msg=feedback)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
@@ -2286,5 +1851,4 @@ __all__ = [CodeResponse,
|
||||
ChoiceResponse,
|
||||
MultipleChoiceResponse,
|
||||
TrueFalseResponse,
|
||||
JavascriptResponse,
|
||||
OpenEndedResponse]
|
||||
JavascriptResponse]
|
||||
|
||||
46
common/lib/capa/capa/templates/drag_and_drop_input.html
Normal file
46
common/lib/capa/capa/templates/drag_and_drop_input.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<section id="inputtype_${id}" class="capa_inputtype" >
|
||||
<div class="drag_and_drop_problem_div" id="drag_and_drop_div_${id}"
|
||||
data-plain-id="${id}">
|
||||
</div>
|
||||
|
||||
<div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}"
|
||||
style="display:none;">${drag_and_drop_json}</div>
|
||||
|
||||
<div class="script_placeholder" data-src="/static/js/capa/drag_and_drop.js"></div>
|
||||
|
||||
% if status == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
% elif status == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
% elif status == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% elif status == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
|
||||
|
||||
<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':
|
||||
correct
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</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
|
||||
</section>
|
||||
@@ -1,56 +0,0 @@
|
||||
<section id="openended_${id}" class="openended">
|
||||
<textarea rows="${rows}" cols="${cols}" name="input_${id}" class="short-form-response" id="input_${id}"
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
>${value|h}</textarea>
|
||||
|
||||
<div class="grader-status">
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif status == 'queued':
|
||||
<span class="grading" id="status_${id}">Submitted for grading</span>
|
||||
% endif
|
||||
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if status == 'queued':
|
||||
<input name="reload" class="reload" type="button" value="Recheck for Feedback" onclick="document.location.reload(true);" />
|
||||
% endif
|
||||
<div class="external-grader-message">
|
||||
${msg|n}
|
||||
% if status in ['correct','incorrect']:
|
||||
<div class="collapsible evaluation-response">
|
||||
<header>
|
||||
<a href="#">Respond to Feedback</a>
|
||||
</header>
|
||||
<section id="evaluation_${id}" class="evaluation">
|
||||
<p>How accurate do you find this feedback?</p>
|
||||
<div class="evaluation-scoring">
|
||||
<ul class="scoring-list">
|
||||
<li><input type="radio" name="evaluation-score" id="evaluation-score-5" value="5" /> <label for="evaluation-score-5"> Correct</label></li>
|
||||
<li><input type="radio" name="evaluation-score" id="evaluation-score-4" value="4" /> <label for="evaluation-score-4"> Partially Correct</label></li>
|
||||
<li><input type="radio" name="evaluation-score" id="evaluation-score-3" value="3" /> <label for="evaluation-score-3"> No Opinion</label></li>
|
||||
<li><input type="radio" name="evaluation-score" id="evaluation-score-2" value="2" /> <label for="evaluation-score-2"> Partially Incorrect</label></li>
|
||||
<li><input type="radio" name="evaluation-score" id="evaluation-score-1" value="1" /> <label for="evaluation-score-1"> Incorrect</label></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Additional comments:</p>
|
||||
<textarea rows="${rows}" cols="${cols}" name="feedback_${id}" class="feedback-on-feedback" id="feedback_${id}"></textarea>
|
||||
<div class="submit-message-container">
|
||||
<input name="submit-message" class="submit-message" type="button" value="Submit your message"/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
@@ -9,13 +9,14 @@ TODO:
|
||||
- check rendering -- e.g. msg should appear in the rendered output. If possible, test that
|
||||
templates are escaping things properly.
|
||||
|
||||
|
||||
|
||||
- test unicode in values, parameters, etc.
|
||||
- test various html escapes
|
||||
- test funny xml chars -- should never get xml parse error if things are escaped properly.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from lxml import etree
|
||||
import unittest
|
||||
import xml.sax.saxutils as saxutils
|
||||
@@ -501,3 +502,70 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class DragAndDropTest(unittest.TestCase):
|
||||
'''
|
||||
Check that drag and drop inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
path_to_images = '/static/images/'
|
||||
|
||||
xml_str = """
|
||||
<drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false">
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="name_with_icon" label="cc" icon="{path}cc.jpg"/>
|
||||
<draggable id="with_icon" label="arrow-left" icon="{path}arrow-left.png" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Mute" icon="{path}mute.png" />
|
||||
<draggable id="name_label_icon3" label="spinner" icon="{path}spinner.gif" />
|
||||
<draggable id="name4" label="Star" icon="{path}volume.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
<target id="t1" x="210" y="90" w="90" h="90"/>
|
||||
<target id="t2" x="370" y="160" w="90" h="90"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
""".format(path=path_to_images)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
value = 'abc'
|
||||
state = {'value': value,
|
||||
'status': 'unsubmitted'}
|
||||
|
||||
user_input = { # order matters, for string comparison
|
||||
"target_outline": "false",
|
||||
"base_image": "/static/images/about_1.png",
|
||||
"draggables": [
|
||||
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": ""},
|
||||
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", },
|
||||
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": ""},
|
||||
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": ""},
|
||||
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": ""},
|
||||
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": ""},
|
||||
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": ""},
|
||||
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": ""}],
|
||||
"one_per_target": "True",
|
||||
"targets": [
|
||||
{"y": "90", "x": "210", "id": "t1", "w": "90", "h": "90"},
|
||||
{"y": "160", "x": "370", "id": "t2", "w": "90", "h": "90"}
|
||||
]
|
||||
}
|
||||
|
||||
the_input = lookup_tag('drag_and_drop_input')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'status': 'unsubmitted',
|
||||
'msg': '',
|
||||
'drag_and_drop_json': json.dumps(user_input)
|
||||
}
|
||||
|
||||
# as we are dumping 'draggables' dicts while dumping user_input, string
|
||||
# comparison will fail, as order of keys is random.
|
||||
self.assertEqual(json.loads(context['drag_and_drop_json']), user_input)
|
||||
context.pop('drag_and_drop_json')
|
||||
expected.pop('drag_and_drop_json')
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
0
common/lib/capa/capa/verifiers/__init__.py
Normal file
0
common/lib/capa/capa/verifiers/__init__.py
Normal file
376
common/lib/capa/capa/verifiers/draganddrop.py
Normal file
376
common/lib/capa/capa/verifiers/draganddrop.py
Normal file
@@ -0,0 +1,376 @@
|
||||
""" Grader of drag and drop input.
|
||||
|
||||
Client side behavior: user can drag and drop images from list on base image.
|
||||
|
||||
|
||||
Then json returned from client is:
|
||||
{
|
||||
"draggable": [
|
||||
{ "image1": "t1" },
|
||||
{ "ant": "t2" },
|
||||
{ "molecule": "t3" },
|
||||
]
|
||||
}
|
||||
values are target names.
|
||||
|
||||
or:
|
||||
{
|
||||
"draggable": [
|
||||
{ "image1": "[10, 20]" },
|
||||
{ "ant": "[30, 40]" },
|
||||
{ "molecule": "[100, 200]" },
|
||||
]
|
||||
}
|
||||
values are (x,y) coordinates of centers of dragged images.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class PositionsCompare(list):
|
||||
""" Class for comparing positions.
|
||||
|
||||
Args:
|
||||
list or string::
|
||||
"abc" - target
|
||||
[10, 20] - list of integers
|
||||
[[10,20], 200] list of list and integer
|
||||
|
||||
"""
|
||||
def __eq__(self, other):
|
||||
""" Compares two arguments.
|
||||
|
||||
Default lists behavior is conversion of string "abc" to list
|
||||
["a", "b", "c"]. We will use that.
|
||||
|
||||
If self or other is empty - returns False.
|
||||
|
||||
Args:
|
||||
self, other: str, unicode, list, int, float
|
||||
|
||||
Returns: bool
|
||||
"""
|
||||
# checks if self or other is not empty list (empty lists = false)
|
||||
if not self or not other:
|
||||
return False
|
||||
|
||||
if (isinstance(self[0], (list, int, float)) and
|
||||
isinstance(other[0], (list, int, float))):
|
||||
return self.coordinate_positions_compare(other)
|
||||
|
||||
elif (isinstance(self[0], (unicode, str)) and
|
||||
isinstance(other[0], (unicode, str))):
|
||||
return ''.join(self) == ''.join(other)
|
||||
else: # improper argument types: no (float / int or lists of list
|
||||
#and float / int pair) or two string / unicode lists pair
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def coordinate_positions_compare(self, other, r=10):
|
||||
""" Checks if self is equal to other inside radius of forgiveness
|
||||
(default 10 px).
|
||||
|
||||
Args:
|
||||
self, other: [x, y] or [[x, y], r], where r is radius of
|
||||
forgiveness;
|
||||
x, y, r: int
|
||||
|
||||
Returns: bool.
|
||||
"""
|
||||
# get max radius of forgiveness
|
||||
if isinstance(self[0], list): # [(x, y), r] case
|
||||
r = max(self[1], r)
|
||||
x1, y1 = self[0]
|
||||
else:
|
||||
x1, y1 = self
|
||||
|
||||
if isinstance(other[0], list): # [(x, y), r] case
|
||||
r = max(other[1], r)
|
||||
x2, y2 = other[0]
|
||||
else:
|
||||
x2, y2 = other
|
||||
|
||||
if (x2 - x1) ** 2 + (y2 - y1) ** 2 > r * r:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DragAndDrop(object):
|
||||
""" Grader class for drag and drop inputtype.
|
||||
"""
|
||||
|
||||
def grade(self):
|
||||
''' Grader user answer.
|
||||
|
||||
Checks if every draggable isplaced on proper target or on proper
|
||||
coordinates within radius of forgiveness (default is 10).
|
||||
|
||||
Returns: bool.
|
||||
'''
|
||||
for draggable in self.excess_draggables:
|
||||
if not self.excess_draggables[draggable]:
|
||||
return False # user answer has more draggables than correct answer
|
||||
|
||||
# Number of draggables in user_groups may be differ that in
|
||||
# correct_groups, that is incorrect, except special case with 'number'
|
||||
for groupname, draggable_ids in self.correct_groups.items():
|
||||
|
||||
# 'number' rule special case
|
||||
# for reusable draggables we may get in self.user_groups
|
||||
# {'1': [u'2', u'2', u'2'], '0': [u'1', u'1'], '2': [u'3']}
|
||||
# if '+number' is in rule - do not remove duplicates and strip
|
||||
# '+number' from rule
|
||||
current_rule = self.correct_positions[groupname].keys()[0]
|
||||
if 'number' in current_rule:
|
||||
rule_values = self.correct_positions[groupname][current_rule]
|
||||
# clean rule, do not do clean duplicate items
|
||||
self.correct_positions[groupname].pop(current_rule, None)
|
||||
parsed_rule = current_rule.replace('+', '').replace('number', '')
|
||||
self.correct_positions[groupname][parsed_rule] = rule_values
|
||||
else: # remove dublicates
|
||||
self.user_groups[groupname] = list(set(self.user_groups[groupname]))
|
||||
|
||||
if sorted(draggable_ids) != sorted(self.user_groups[groupname]):
|
||||
return False
|
||||
|
||||
# Check that in every group, for rule of that group, user positions of
|
||||
# every element are equal with correct positions
|
||||
for groupname in self.correct_groups:
|
||||
rules_executed = 0
|
||||
for rule in ('exact', 'anyof', 'unordered_equal'):
|
||||
# every group has only one rule
|
||||
if self.correct_positions[groupname].get(rule, None):
|
||||
rules_executed += 1
|
||||
if not self.compare_positions(
|
||||
self.correct_positions[groupname][rule],
|
||||
self.user_positions[groupname]['user'], flag=rule):
|
||||
return False
|
||||
if not rules_executed: # no correct rules for current group
|
||||
# probably xml content mistake - wrong rules names
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def compare_positions(self, correct, user, flag):
|
||||
""" Compares two lists of positions with flag rules. Order of
|
||||
correct/user arguments is matter only in 'anyof' flag.
|
||||
|
||||
Rules description:
|
||||
|
||||
'exact' means 1-1 ordered relationship::
|
||||
|
||||
[el1, el2, el3] is 'exact' equal to [el5, el6, el7] when
|
||||
el1 == el5, el2 == el6, el3 == el7.
|
||||
Equality function is custom, see below.
|
||||
|
||||
|
||||
'anyof' means subset relationship::
|
||||
|
||||
user = [el1, el2] is 'anyof' equal to correct = [el1, el2, el3]
|
||||
when
|
||||
set(user) <= set(correct).
|
||||
|
||||
'anyof' is ordered relationship. It always checks if user
|
||||
is subset of correct
|
||||
|
||||
Equality function is custom, see below.
|
||||
|
||||
Examples:
|
||||
|
||||
- many draggables per position:
|
||||
user ['1','2','2','2'] is 'anyof' equal to ['1', '2', '3']
|
||||
|
||||
- draggables can be placed in any order:
|
||||
user ['1','2','3','4'] is 'anyof' equal to ['4', '2', '1', 3']
|
||||
|
||||
'unordered_equal' is same as 'exact' but disregards on order
|
||||
|
||||
Equality functions:
|
||||
|
||||
Equality functon depends on type of element. They declared in
|
||||
PositionsCompare class. For position like targets
|
||||
ids ("t1", "t2", etc..) it is string equality function. For coordinate
|
||||
positions ([1,2] or [[1,2], 15]) it is coordinate_positions_compare
|
||||
function (see docstrings in PositionsCompare class)
|
||||
|
||||
Args:
|
||||
correst, user: lists of positions
|
||||
|
||||
Returns: True if within rule lists are equal, otherwise False.
|
||||
"""
|
||||
if flag == 'exact':
|
||||
if len(correct) != len(user):
|
||||
return False
|
||||
for el1, el2 in zip(correct, user):
|
||||
if PositionsCompare(el1) != PositionsCompare(el2):
|
||||
return False
|
||||
|
||||
if flag == 'anyof':
|
||||
for u_el in user:
|
||||
for c_el in correct:
|
||||
if PositionsCompare(u_el) == PositionsCompare(c_el):
|
||||
break
|
||||
else:
|
||||
# General: the else is executed after the for,
|
||||
# only if the for terminates normally (not by a break)
|
||||
|
||||
# In this case, 'for' is terminated normally if every element
|
||||
# from 'correct' list isn't equal to concrete element from
|
||||
# 'user' list. So as we found one element from 'user' list,
|
||||
# that not in 'correct' list - we return False
|
||||
return False
|
||||
|
||||
if flag == 'unordered_equal':
|
||||
if len(correct) != len(user):
|
||||
return False
|
||||
temp = correct[:]
|
||||
for u_el in user:
|
||||
for c_el in temp:
|
||||
if PositionsCompare(u_el) == PositionsCompare(c_el):
|
||||
temp.remove(c_el)
|
||||
break
|
||||
else:
|
||||
# same as upper - if we found element from 'user' list,
|
||||
# that not in 'correct' list - we return False.
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __init__(self, correct_answer, user_answer):
|
||||
""" Populates DragAndDrop variables from user_answer and correct_answer.
|
||||
If correct_answer is dict, converts it to list.
|
||||
Correct answer in dict form is simpe structure for fast and simple
|
||||
grading. Example of correct answer dict example::
|
||||
|
||||
correct_answer = {'name4': 't1',
|
||||
'name_with_icon': 't1',
|
||||
'5': 't2',
|
||||
'7':'t2'}
|
||||
|
||||
It is draggable_name: dragable_position mapping.
|
||||
|
||||
Advanced form converted from simple form uses 'exact' rule
|
||||
for matching.
|
||||
|
||||
Correct answer in list form is designed for advanced cases::
|
||||
|
||||
correct_answers = [
|
||||
{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'],
|
||||
'rule': 'anyof'},
|
||||
{
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
|
||||
Advanced answer in list form is list of dicts, and every dict must have
|
||||
3 keys: 'draggables', 'targets' and 'rule'. 'Draggables' value is
|
||||
list of draggables ids, 'targes' values are list of targets ids, 'rule'
|
||||
value one of 'exact', 'anyof', 'unordered_equal', 'anyof+number',
|
||||
'unordered_equal+number'
|
||||
|
||||
Advanced form uses "all dicts must match with their rule" logic.
|
||||
|
||||
Same draggable cannot appears more that in one dict.
|
||||
|
||||
Behavior is more widely explained in sphinx documentation.
|
||||
|
||||
Args:
|
||||
user_answer: json
|
||||
correct_answer: dict or list
|
||||
"""
|
||||
|
||||
self.correct_groups = dict() # correct groups from xml
|
||||
self.correct_positions = dict() # correct positions for comparing
|
||||
self.user_groups = dict() # will be populated from user answer
|
||||
self.user_positions = dict() # will be populated from user answer
|
||||
|
||||
# convert from dict answer format to list format
|
||||
if isinstance(correct_answer, dict):
|
||||
tmp = []
|
||||
for key, value in correct_answer.items():
|
||||
tmp_dict = {'draggables': [], 'targets': [], 'rule': 'exact'}
|
||||
tmp_dict['draggables'].append(key)
|
||||
tmp_dict['targets'].append(value)
|
||||
tmp.append(tmp_dict)
|
||||
correct_answer = tmp
|
||||
|
||||
user_answer = json.loads(user_answer)
|
||||
|
||||
# check if we have draggables that are not in correct answer:
|
||||
self.excess_draggables = {}
|
||||
|
||||
# create identical data structures from user answer and correct answer
|
||||
for i in xrange(0, len(correct_answer)):
|
||||
groupname = str(i)
|
||||
self.correct_groups[groupname] = correct_answer[i]['draggables']
|
||||
self.correct_positions[groupname] = {correct_answer[i]['rule']:
|
||||
correct_answer[i]['targets']}
|
||||
self.user_groups[groupname] = []
|
||||
self.user_positions[groupname] = {'user': []}
|
||||
for draggable_dict in user_answer['draggables']:
|
||||
# draggable_dict is 1-to-1 {draggable_name: position}
|
||||
draggable_name = draggable_dict.keys()[0]
|
||||
if draggable_name in self.correct_groups[groupname]:
|
||||
self.user_groups[groupname].append(draggable_name)
|
||||
self.user_positions[groupname]['user'].append(
|
||||
draggable_dict[draggable_name])
|
||||
self.excess_draggables[draggable_name] = True
|
||||
else:
|
||||
self.excess_draggables[draggable_name] = \
|
||||
self.excess_draggables.get(draggable_name, False)
|
||||
|
||||
|
||||
def grade(user_input, correct_answer):
|
||||
""" Creates DragAndDrop instance from user_input and correct_answer and
|
||||
calls DragAndDrop.grade for grading.
|
||||
|
||||
Supports two interfaces for correct_answer: dict and list.
|
||||
|
||||
Args:
|
||||
user_input: json. Format::
|
||||
|
||||
{ "draggables":
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
|
||||
or
|
||||
|
||||
{"draggables": [{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]}
|
||||
|
||||
correct_answer: dict or list.
|
||||
|
||||
Dict form::
|
||||
|
||||
{'1': 't1', 'name_with_icon': 't2'}
|
||||
|
||||
or
|
||||
|
||||
{'1': '[10, 10]', 'name_with_icon': '[[10, 10], 20]'}
|
||||
|
||||
List form::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['l3_o', 'l10_o'],
|
||||
'targets': ['t1_o', 't9_o'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['l1_c','l8_c'],
|
||||
'targets': ['t5_c','t6_c'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
|
||||
Returns: bool
|
||||
"""
|
||||
return DragAndDrop(correct_answer=correct_answer,
|
||||
user_answer=user_input).grade()
|
||||
603
common/lib/capa/capa/verifiers/tests_draganddrop.py
Normal file
603
common/lib/capa/capa/verifiers/tests_draganddrop.py
Normal file
@@ -0,0 +1,603 @@
|
||||
import unittest
|
||||
|
||||
import draganddrop
|
||||
from draganddrop import PositionsCompare
|
||||
|
||||
|
||||
class Test_PositionsCompare(unittest.TestCase):
|
||||
""" describe"""
|
||||
|
||||
def test_nested_list_and_list1(self):
|
||||
self.assertEqual(PositionsCompare([[1, 2], 40]), PositionsCompare([1, 3]))
|
||||
|
||||
def test_nested_list_and_list2(self):
|
||||
self.assertNotEqual(PositionsCompare([1, 12]), PositionsCompare([1, 1]))
|
||||
|
||||
def test_list_and_list1(self):
|
||||
self.assertNotEqual(PositionsCompare([[1, 2], 12]), PositionsCompare([1, 15]))
|
||||
|
||||
def test_list_and_list2(self):
|
||||
self.assertEqual(PositionsCompare([1, 11]), PositionsCompare([1, 1]))
|
||||
|
||||
def test_numerical_list_and_string_list(self):
|
||||
self.assertNotEqual(PositionsCompare([1, 2]), PositionsCompare(["1"]))
|
||||
|
||||
def test_string_and_string_list1(self):
|
||||
self.assertEqual(PositionsCompare("1"), PositionsCompare(["1"]))
|
||||
|
||||
def test_string_and_string_list2(self):
|
||||
self.assertEqual(PositionsCompare("abc"), PositionsCompare("abc"))
|
||||
|
||||
def test_string_and_string_list3(self):
|
||||
self.assertNotEqual(PositionsCompare("abd"), PositionsCompare("abe"))
|
||||
|
||||
def test_float_and_string(self):
|
||||
self.assertNotEqual(PositionsCompare([3.5, 5.7]), PositionsCompare(["1"]))
|
||||
|
||||
def test_floats_and_ints(self):
|
||||
self.assertEqual(PositionsCompare([3.5, 4.5]), PositionsCompare([5, 7]))
|
||||
|
||||
|
||||
class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
|
||||
def test_targets_true(self):
|
||||
user_input = '{"draggables": [{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]}'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_targets_false(self):
|
||||
user_input = '{"draggables": [{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]}'
|
||||
correct_answer = {'1': 't3', 'name_with_icon': 't2'}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_multiple_images_per_target_true(self):
|
||||
user_input = '{\
|
||||
"draggables": [{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]}'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2',
|
||||
'2': 't1'}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_multiple_images_per_target_false(self):
|
||||
user_input = '{\
|
||||
"draggables": [{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]}'
|
||||
correct_answer = {'1': 't2', 'name_with_icon': 't2',
|
||||
'2': 't1'}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_targets_and_positions(self):
|
||||
user_input = '{"draggables": [{"1": [10,10]}, \
|
||||
{"name_with_icon": [[10,10],4]}]}'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_position_and_targets(self):
|
||||
user_input = '{"draggables": [{"1": "t1"}, {"name_with_icon": "t2"}]}'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_exact(self):
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_false(self):
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_true_in_radius(self):
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_true_in_manual_radius(self):
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_false_in_manual_radius(self):
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_correct_answer_not_has_key_from_user_answer(self):
|
||||
user_input = '{"draggables": [{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]}'
|
||||
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_anywhere(self):
|
||||
"""Draggables can be places anywhere on base image.
|
||||
Place grass in the middle of the image and ant in the
|
||||
right upper corner."""
|
||||
user_input = '{"draggables": \
|
||||
[{"ant":[610.5,57.449951171875]},{"grass":[322.5,199.449951171875]}]}'
|
||||
|
||||
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_lcao_correct(self):
|
||||
"""Describe carbon molecule in LCAO-MO"""
|
||||
user_input = '{"draggables":[{"1":"s_left"}, \
|
||||
{"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \
|
||||
{"8":"p_left_2"},{"10":"p_right_1"},{"9":"p_right_2"}, \
|
||||
{"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \
|
||||
{"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]}'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_lcao_extra_element_incorrect(self):
|
||||
"""Describe carbon molecule in LCAO-MO"""
|
||||
user_input = '{"draggables":[{"1":"s_left"}, \
|
||||
{"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \
|
||||
{"8":"p_left_2"},{"17":"p_left_3"},{"10":"p_right_1"},{"9":"p_right_2"}, \
|
||||
{"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \
|
||||
{"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]}'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_draggable_no_mupliples(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '{"draggables":[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target3"},{"2":"target4"},{"2":"target5"}, \
|
||||
{"3":"target6"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_draggable_with_mupliples(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '{"draggables":[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
|
||||
{"3":"target6"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '{"draggables":[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples_wrong(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '{"draggables":[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"}, \
|
||||
{"2":"target3"}, \
|
||||
{"2":"target4"}, \
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_false(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple(self):
|
||||
"""Test reusable draggables (mupltiple draggables per target)"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple_false(self):
|
||||
"""Test reusable draggables (mupltiple draggables per target)"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused(self):
|
||||
"""Test a b c in 10 labels reused"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused_false(self):
|
||||
"""Test a b c in 10 labels reused false"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, {"a":"target8"},\
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse(self):
|
||||
"""Test reusable draggables """
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"},\
|
||||
{"a":"target5"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number(self):
|
||||
"""Test reusable draggables with number """
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number_false(self):
|
||||
"""Test reusable draggables with numbers, but wrong"""
|
||||
user_input = '{"draggables":[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]}'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target10'],
|
||||
'rule': 'anyof_number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_alternative_correct_answer(self):
|
||||
user_input = '{"draggables":[{"name_with_icon":"t1"},\
|
||||
{"name_with_icon":"t1"},{"name_with_icon":"t1"},{"name4":"t1"}, \
|
||||
{"name4":"t1"}]}'
|
||||
correct_answer = [
|
||||
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
|
||||
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
|
||||
'rule': 'exact'}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
|
||||
class Test_DragAndDrop_Populate(unittest.TestCase):
|
||||
|
||||
def test_1(self):
|
||||
correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]}
|
||||
user_input = '{"draggables": \
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
dnd = draganddrop.DragAndDrop(correct_answer, user_input)
|
||||
|
||||
correct_groups = {'1': ['name_with_icon'], '0': ['1']}
|
||||
correct_positions = {'1': {'exact': [[20, 20]]}, '0': {'exact': [[[40, 10], 29]]}}
|
||||
user_groups = {'1': [u'name_with_icon'], '0': [u'1']}
|
||||
user_positions = {'1': {'user': [[20, 20]]}, '0': {'user': [[10, 10]]}}
|
||||
|
||||
self.assertEqual(correct_groups, dnd.correct_groups)
|
||||
self.assertEqual(correct_positions, dnd.correct_positions)
|
||||
self.assertEqual(user_groups, dnd.user_groups)
|
||||
self.assertEqual(user_positions, dnd.user_positions)
|
||||
|
||||
|
||||
class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
|
||||
|
||||
def test_1(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]],
|
||||
user=[[2, 3], [1, 1]],
|
||||
flag='anyof'))
|
||||
|
||||
def test_2a(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]],
|
||||
user=[[2, 3], [1, 1]],
|
||||
flag='exact'))
|
||||
|
||||
def test_2b(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertFalse(dnd.compare_positions(correct=[[1, 1], [2, 3]],
|
||||
user=[[2, 13], [1, 1]],
|
||||
flag='exact'))
|
||||
|
||||
def test_3(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertFalse(dnd.compare_positions(correct=["a", "b"],
|
||||
user=["a", "b", "c"],
|
||||
flag='anyof'))
|
||||
|
||||
def test_4(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"],
|
||||
user=["a", "b"],
|
||||
flag='anyof'))
|
||||
|
||||
def test_5(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertFalse(dnd.compare_positions(correct=["a", "b", "c"],
|
||||
user=["a", "c", "b"],
|
||||
flag='exact'))
|
||||
|
||||
def test_6(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"],
|
||||
user=["a", "c", "b"],
|
||||
flag='anyof'))
|
||||
|
||||
def test_7(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}')
|
||||
self.assertFalse(dnd.compare_positions(correct=["a", "b", "b"],
|
||||
user=["a", "c", "b"],
|
||||
flag='anyof'))
|
||||
|
||||
|
||||
def suite():
|
||||
|
||||
testcases = [Test_PositionsCompare,
|
||||
Test_DragAndDrop_Populate,
|
||||
Test_DragAndDrop_Grade,
|
||||
Test_DraAndDrop_Compare_Positions
|
||||
]
|
||||
suites = []
|
||||
for testcase in testcases:
|
||||
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))
|
||||
return unittest.TestSuite(suites)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.TextTestRunner(verbosity=2).run(suite())
|
||||
@@ -16,6 +16,7 @@
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/CodeMirror/codemirror.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, function() {
|
||||
|
||||
@@ -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",
|
||||
@@ -39,7 +39,8 @@ setup(
|
||||
"course_info = xmodule.html_module:CourseInfoDescriptor",
|
||||
"static_tab = xmodule.html_module:StaticTabDescriptor",
|
||||
"custom_tag_template = xmodule.raw_module:RawDescriptor",
|
||||
"about = xmodule.html_module:AboutDescriptor"
|
||||
"about = xmodule.html_module:AboutDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor"
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -82,7 +82,8 @@ class CapaModule(XModule):
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
],
|
||||
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
|
||||
resource_string(__name__, 'js/src/capa/schematic.js')]}
|
||||
resource_string(__name__, 'js/src/capa/schematic.js')
|
||||
]}
|
||||
|
||||
js_module_name = "Problem"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
@@ -371,7 +372,6 @@ class CapaModule(XModule):
|
||||
'problem_save': self.save_problem,
|
||||
'problem_show': self.get_answer,
|
||||
'score_update': self.update_score,
|
||||
'message_post' : self.message_post,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -386,20 +386,6 @@ class CapaModule(XModule):
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def message_post(self, get):
|
||||
"""
|
||||
Posts a message from a form to an appropriate location
|
||||
"""
|
||||
event_info = dict()
|
||||
event_info['state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
event_info['student_id'] = self.system.anonymous_student_id
|
||||
event_info['survey_responses']= get
|
||||
|
||||
success, message = self.lcp.message_post(event_info)
|
||||
|
||||
return {'success' : success, 'message' : message}
|
||||
|
||||
def closed(self):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.attempts == self.max_attempts:
|
||||
@@ -436,6 +422,7 @@ class CapaModule(XModule):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def update_score(self, get):
|
||||
"""
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
|
||||
596
common/lib/xmodule/xmodule/combined_open_ended_module.py
Normal file
596
common/lib/xmodule/xmodule/combined_open_ended_module.py
Normal file
@@ -0,0 +1,596 @@
|
||||
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
|
||||
|
||||
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 = self.system.render_template('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(self.system)
|
||||
last_post_feedback = ""
|
||||
if task_type == "openended":
|
||||
last_post_assessment = task.latest_post_assessment(self.system, 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(self.system, last_post_assessment[i]))
|
||||
last_post_evaluation = "".join(eval_list)
|
||||
else:
|
||||
last_post_evaluation = task.format_feedback_with_evaluation(self.system, 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 = self.system.render_template('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
|
||||
128
common/lib/xmodule/xmodule/combined_open_ended_rubric.py
Normal file
128
common/lib/xmodule/xmodule/combined_open_ended_rubric.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
log=logging.getLogger(__name__)
|
||||
|
||||
class CombinedOpenEndedRubric(object):
|
||||
|
||||
@staticmethod
|
||||
def render_rubric(rubric_xml, system):
|
||||
try:
|
||||
rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml)
|
||||
html = system.render_template('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,5 +1,6 @@
|
||||
import logging
|
||||
from cStringIO import StringIO
|
||||
from math import exp, erf
|
||||
from lxml import etree
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
import requests
|
||||
@@ -128,6 +129,20 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
|
||||
self.set_grading_policy(self.definition['data'].get('grading_policy', None))
|
||||
|
||||
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 defaut_grading_policy(self):
|
||||
"""
|
||||
Return a dict which is a copy of the default grading policy
|
||||
@@ -347,35 +362,66 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@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
|
||||
"""
|
||||
Returns if the course has been flagged as new in the metadata. If
|
||||
there is no flag, return a heuristic value considering the
|
||||
announcement and the start dates.
|
||||
"""
|
||||
flag = self.metadata.get('is_new', None)
|
||||
if flag is None:
|
||||
return self.days_until_start > 1
|
||||
# Use a heuristic if the course has not been flagged
|
||||
announcement, start, now = self._sorting_dates()
|
||||
if announcement and (now - announcement).days < 30:
|
||||
# The course has been announced for less that month
|
||||
return True
|
||||
elif (now - start).days < 1:
|
||||
# The course has not started yet
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
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):
|
||||
def sorting_score(self):
|
||||
"""
|
||||
Returns a number that can be used to sort the courses according
|
||||
the how "new"" they are. The "newness"" score is computed using a
|
||||
heuristic that takes into account the announcement and
|
||||
(advertized) start dates of the course if available.
|
||||
|
||||
The lower the number the "newer" the course.
|
||||
"""
|
||||
# Make courses that have an announcement date shave a lower
|
||||
# score than courses than don't, older courses should have a
|
||||
# higher score.
|
||||
announcement, start, now = self._sorting_dates()
|
||||
scale = 300.0 # about a year
|
||||
if announcement:
|
||||
days = (now - announcement).days
|
||||
score = -exp(-days/scale)
|
||||
else:
|
||||
days = (now - start).days
|
||||
score = exp(days/scale)
|
||||
return score
|
||||
|
||||
def _sorting_dates(self):
|
||||
# utility function to get datetime objects for dates used to
|
||||
# compute the is_new flag and the sorting_score
|
||||
def to_datetime(timestamp):
|
||||
return datetime.fromtimestamp(time.mktime(timestamp))
|
||||
|
||||
start_date = convert_to_datetime(self.start)
|
||||
def get_date(field):
|
||||
timetuple = self._try_parse_time(field)
|
||||
return to_datetime(timetuple) if timetuple else None
|
||||
|
||||
# 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''
|
||||
announcement = get_date('announcement')
|
||||
start = get_date('advertised_start') or to_datetime(self.start)
|
||||
now = to_datetime(time.gmtime())
|
||||
|
||||
now = convert_to_datetime(time.gmtime())
|
||||
days_until_start = (start_date - now).days
|
||||
return days_until_start
|
||||
return announcement, start, now
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
@@ -541,6 +587,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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
@@ -25,7 +25,6 @@ class @Problem
|
||||
@$('section.action input.reset').click @reset
|
||||
@$('section.action input.show').click @show
|
||||
@$('section.action input.save').click @save
|
||||
@$('section.evaluation input.submit-message').click @message_post
|
||||
|
||||
# Collapsibles
|
||||
Collapsible.setCollapsibles(@el)
|
||||
@@ -198,35 +197,6 @@ class @Problem
|
||||
else
|
||||
@gentle_alert response.success
|
||||
|
||||
message_post: =>
|
||||
Logger.log 'message_post', @answers
|
||||
|
||||
fd = new FormData()
|
||||
feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value
|
||||
submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML
|
||||
grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML
|
||||
score = $(".evaluation-scoring 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.message
|
||||
@$('section.evaluation').slideToggle()
|
||||
|
||||
$.ajaxWithPrefix("#{@url}/message_post", settings)
|
||||
|
||||
reset: =>
|
||||
Logger.log 'problem_reset', @answers
|
||||
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>
|
||||
|
||||
@@ -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.val('')
|
||||
@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.')
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
|
||||
|
||||
var SequenceNav = function($element) {
|
||||
var _this = this;
|
||||
var $element = $element;
|
||||
@@ -44,7 +41,7 @@ var SequenceNav = function($element) {
|
||||
|
||||
var leftPercent = clamp(-left / padding, 0, 1);
|
||||
$leftShadow.css('opacity', leftPercent);
|
||||
|
||||
|
||||
var rightPercent = clamp((maxScroll + left) / padding, 0, 1);
|
||||
$rightShadow.css('opacity', rightPercent);
|
||||
};
|
||||
@@ -95,5 +92,5 @@ var SequenceNav = function($element) {
|
||||
$(window).bind('resize', updateWidths);
|
||||
setTimeout(function() {
|
||||
checkPosition();
|
||||
}, 200);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
659
common/lib/xmodule/xmodule/open_ended_module.py
Normal file
659
common/lib/xmodule/xmodule/open_ended_module.py
Normal file
@@ -0,0 +1,659 @@
|
||||
"""
|
||||
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 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, system)
|
||||
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, system):
|
||||
"""
|
||||
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'], system)
|
||||
|
||||
if not response_items['success']:
|
||||
return system.render_template("open_ended_error.html",
|
||||
{'errors': feedback})
|
||||
|
||||
feedback_template = system.render_template("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, system, 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, system))
|
||||
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, system)
|
||||
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, system, 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', ""), system, 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, system, 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 = system.render_template('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(system)
|
||||
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(object):
|
||||
"""
|
||||
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, system):
|
||||
"""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
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
"""
|
||||
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
|
||||
@@ -26,205 +19,50 @@ 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>
|
||||
"""
|
||||
|
||||
STATE_VERSION = 1
|
||||
|
||||
# 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 = {}
|
||||
|
||||
instance_state = self.convert_state_to_current_format(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', 'initial')
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
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))
|
||||
|
||||
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 latest_answer(self):
|
||||
"""None if not available"""
|
||||
if not self.history:
|
||||
return None
|
||||
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_hint(self):
|
||||
"""None if not available"""
|
||||
if not self.history:
|
||||
return None
|
||||
return self.history[-1].get('hint')
|
||||
|
||||
def new_history_entry(self, answer):
|
||||
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_hint(self, hint):
|
||||
"""Assumes that state is right, so we're adding a score to the latest
|
||||
history element"""
|
||||
self.history[-1]['hint'] = hint
|
||||
|
||||
|
||||
def change_state(self, new_state):
|
||||
def get_html(self, system):
|
||||
"""
|
||||
A centralized place for state changes--allows for hooks. If the
|
||||
current state matches the old state, don't run any hooks.
|
||||
Gets context and renders HTML that represents the module
|
||||
@param system: Modulesystem
|
||||
@return: Rendered HTML
|
||||
"""
|
||||
if self.state == new_state:
|
||||
return
|
||||
|
||||
self.state = new_state
|
||||
|
||||
if self.state == self.DONE:
|
||||
self.attempts += 1
|
||||
|
||||
@staticmethod
|
||||
def convert_state_to_current_format(old_state):
|
||||
"""
|
||||
This module used to use a problematic state representation. This method
|
||||
converts that into the new format.
|
||||
|
||||
Args:
|
||||
old_state: dict of state, as passed in. May be old.
|
||||
|
||||
Returns:
|
||||
new_state: dict of new state
|
||||
"""
|
||||
if old_state.get('version', 0) == SelfAssessmentModule.STATE_VERSION:
|
||||
# already current
|
||||
return old_state
|
||||
|
||||
# for now, there's only one older format.
|
||||
|
||||
new_state = {'version': SelfAssessmentModule.STATE_VERSION}
|
||||
|
||||
def copy_if_present(key):
|
||||
if key in old_state:
|
||||
new_state[key] = old_state[key]
|
||||
|
||||
for to_copy in ['attempts', 'state']:
|
||||
copy_if_present(to_copy)
|
||||
|
||||
# The answers, scores, and hints need to be kept together to avoid them
|
||||
# getting out of sync.
|
||||
|
||||
# NOTE: Since there's only one problem with a few hundred submissions
|
||||
# in production so far, not trying to be smart about matching up hints
|
||||
# and submissions in cases where they got out of sync.
|
||||
|
||||
student_answers = old_state.get('student_answers', [])
|
||||
scores = old_state.get('scores', [])
|
||||
hints = old_state.get('hints', [])
|
||||
|
||||
new_state['history'] = [
|
||||
{'answer': answer,
|
||||
'score': score,
|
||||
'hint': hint}
|
||||
for answer, score, hint in itertools.izip_longest(
|
||||
student_answers, scores, hints)]
|
||||
return new_state
|
||||
|
||||
|
||||
def _allow_reset(self):
|
||||
"""Can the module be reset?"""
|
||||
return self.state == self.DONE and self.attempts < self.max_attempts
|
||||
|
||||
def get_html(self):
|
||||
#set context variables and render template
|
||||
if self.state != self.INITIAL:
|
||||
latest = self.latest_answer()
|
||||
@@ -235,46 +73,20 @@ class SelfAssessmentModule(XModule):
|
||||
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 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 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
|
||||
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.
|
||||
@@ -288,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,
|
||||
@@ -304,37 +115,30 @@ 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, system)
|
||||
|
||||
# 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.
|
||||
"""
|
||||
@@ -343,7 +147,7 @@ class SelfAssessmentModule(XModule):
|
||||
|
||||
if self.state == self.DONE:
|
||||
# display the previous hint
|
||||
latest = self.latest_hint()
|
||||
latest = self.latest_post_assessment(system)
|
||||
hint = latest if latest is not None else ''
|
||||
else:
|
||||
hint = ''
|
||||
@@ -351,14 +155,14 @@ class SelfAssessmentModule(XModule):
|
||||
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):
|
||||
"""
|
||||
@@ -370,7 +174,7 @@ 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.
|
||||
|
||||
@@ -401,10 +205,10 @@ class SelfAssessmentModule(XModule):
|
||||
|
||||
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.
|
||||
@@ -429,21 +233,20 @@ class SelfAssessmentModule(XModule):
|
||||
|
||||
self.record_latest_score(score)
|
||||
|
||||
d = {'success': True,}
|
||||
d = {'success': True, }
|
||||
|
||||
if score == self.max_score():
|
||||
self.change_state(self.DONE)
|
||||
d['message_html'] = self.get_message_html()
|
||||
d['allow_reset'] = self._allow_reset()
|
||||
else:
|
||||
self.change_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,
|
||||
@@ -453,63 +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.record_latest_hint(get['hint'])
|
||||
self.record_latest_post_assessment(get['hint'])
|
||||
self.change_state(self.DONE)
|
||||
|
||||
# To the tracking logs!
|
||||
event_info = {
|
||||
'selfassessment_id': self.location.url(),
|
||||
'state': {
|
||||
'version': self.STATE_VERSION,
|
||||
'history': self.history,
|
||||
}
|
||||
}
|
||||
self.system.track_function('save_hint', event_info)
|
||||
|
||||
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.change_state(self.INITIAL)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
return json.dumps(state)
|
||||
|
||||
|
||||
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding self assessment questions to courses
|
||||
@@ -533,13 +292,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))
|
||||
@@ -548,12 +305,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.'''
|
||||
@@ -564,7 +318,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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Discussion Tag
|
||||
for: Topic-Level Student-Visible Label
|
||||
id: 6002x_group_discussion_by_this
|
||||
discussion_category: Week 1
|
||||
data: |
|
||||
<discussion for="Topic-Level Student-Visible Label" id="6002x_group_discussion_by_this" discussion_category="Week 1" />
|
||||
children: []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from time import strptime, gmtime
|
||||
from time import strptime
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
@@ -39,52 +39,81 @@ class DummySystem(ImportSystem):
|
||||
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):
|
||||
def get_dummy_course(start, announcement=None, is_new=None):
|
||||
"""Get a dummy course"""
|
||||
|
||||
system = DummySystem(load_error_modules)
|
||||
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
|
||||
system = DummySystem(load_error_modules=True)
|
||||
|
||||
def to_attrb(n, v):
|
||||
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
|
||||
|
||||
is_new = to_attrb('is_new', is_new)
|
||||
announcement = to_attrb('announcement', announcement)
|
||||
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}"
|
||||
{announcement}
|
||||
{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)
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
|
||||
announcement=announcement)
|
||||
|
||||
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')
|
||||
def test_sorting_score(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start == 4)
|
||||
dates = [('2012-10-01T12:00', '2012-09-01T12:00'), # 0
|
||||
('2012-12-01T12:00', '2012-11-01T12:00'), # 1
|
||||
('2013-02-01T12:00', '2012-12-01T12:00'), # 2
|
||||
('2013-02-01T12:00', '2012-11-10T12:00'), # 3
|
||||
('2013-02-01T12:00', None), # 4
|
||||
('2013-03-01T12:00', None), # 5
|
||||
('2013-04-01T12:00', None), # 6
|
||||
('2012-11-01T12:00', None), # 7
|
||||
('2012-09-01T12:00', None), # 8
|
||||
('1990-01-01T12:00', None), # 9
|
||||
('2013-01-02T12:00', None), # 10
|
||||
('2013-01-10T12:00', '2012-12-31T12:00'), # 11
|
||||
('2013-01-10T12:00', '2013-01-01T12:00'), # 12
|
||||
]
|
||||
|
||||
data = []
|
||||
for i, d in enumerate(dates):
|
||||
descriptor = self.get_dummy_course(start=d[0], announcement=d[1])
|
||||
score = descriptor.sorting_score
|
||||
data.append((score, i))
|
||||
|
||||
result = [d[1] for d in sorted(data)]
|
||||
assert(result == [12, 11, 2, 3, 1, 0, 6, 5, 4, 10, 7, 8, 9])
|
||||
|
||||
|
||||
@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):
|
||||
def test_is_new(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)
|
||||
assert(descriptor.is_new is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
assert(descriptor.is_new is False)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
assert(descriptor.is_new is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-01-15T12:00')
|
||||
assert(descriptor.is_new is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-03-00T12:00')
|
||||
assert(descriptor.is_new is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-10-15T12:00')
|
||||
assert(descriptor.is_new is False)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-31T12:00')
|
||||
assert(descriptor.is_new is True)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -339,16 +339,19 @@ class ImportTestCase(unittest.TestCase):
|
||||
|
||||
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
|
||||
|
||||
def test_selfassessment_import(self):
|
||||
'''
|
||||
Check to see if definition_from_xml in self_assessment_module.py
|
||||
works properly. Pulls data from the self_assessment directory in the test data directory.
|
||||
'''
|
||||
|
||||
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['self_assessment'])
|
||||
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/sa_test/2012_Fall"
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
|
||||
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')
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
|
||||
from xmodule.self_assessment_module import SelfAssessmentModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
|
||||
@@ -26,22 +27,37 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
|
||||
'scores': [0, 1],
|
||||
'hints': ['o hai'],
|
||||
'state': SelfAssessmentModule.ASSESSING,
|
||||
'state': SelfAssessmentModule.INITIAL,
|
||||
'attempts': 2})
|
||||
|
||||
rubric = '''<rubric><rubric>
|
||||
<category>
|
||||
<description>Response Quality</description>
|
||||
<option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
|
||||
</category>
|
||||
</rubric></rubric>'''
|
||||
|
||||
prompt = etree.XML("<prompt>Text</prompt>")
|
||||
static_data = {
|
||||
'max_attempts': 10,
|
||||
'rubric': etree.XML(rubric),
|
||||
'prompt': prompt,
|
||||
'max_score': 1
|
||||
}
|
||||
|
||||
module = SelfAssessmentModule(test_system, self.location,
|
||||
self.definition, self.descriptor,
|
||||
state, {}, metadata=self.metadata)
|
||||
static_data, 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_answer({'student_answer': "I am an answer"}, test_system)
|
||||
self.assertEqual(module.state, module.ASSESSING)
|
||||
|
||||
module.save_hint({'hint': 'hint for ans 3'})
|
||||
module.save_assessment({'assessment': '0'}, test_system)
|
||||
self.assertEqual(module.state, module.POST_ASSESSMENT)
|
||||
module.save_hint({'hint': 'this is a hint'}, test_system)
|
||||
self.assertEqual(module.state, module.DONE)
|
||||
|
||||
d = module.reset({})
|
||||
@@ -49,6 +65,6 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
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'})
|
||||
module.save_answer({'student_answer': 'answer 4'}, test_system)
|
||||
module.save_assessment({'assessment': '1'}, test_system)
|
||||
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)
|
||||
|
||||
|
||||
@@ -440,7 +440,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
'xqa_key',
|
||||
# TODO: This is used by the XMLModuleStore to provide for locations for
|
||||
# static files, and will need to be removed when that code is removed
|
||||
'data_dir'
|
||||
'data_dir',
|
||||
# How many days early to show a course element to beta testers (float)
|
||||
# intended to be set per-course, but can be overridden in for specific
|
||||
# elements. Can be a float.
|
||||
'days_early_for_beta'
|
||||
)
|
||||
|
||||
# cdodge: this is a list of metadata names which are 'system' metadata
|
||||
@@ -523,7 +527,8 @@ 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
|
||||
@@ -534,6 +539,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
if isinstance(value, time.struct_time):
|
||||
self.metadata['start'] = stringify_time(value)
|
||||
|
||||
@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):
|
||||
"""
|
||||
@@ -750,7 +768,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
|
||||
Returns a time_struct, or None if metadata key is not present or is invalid.
|
||||
"""
|
||||
if key in self.metadata:
|
||||
try:
|
||||
|
||||
@@ -94,13 +94,16 @@ 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',
|
||||
# cdodge: @TODO: We need to figure out a way to export out 'tabs' and 'grading_policy' which is on the course
|
||||
'tabs', 'grading_policy', 'is_draft', 'published_by', 'published_date',
|
||||
'discussion_blackouts',
|
||||
'discussion_blackouts', 'testcenter_info',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename')
|
||||
|
||||
|
||||
BIN
common/static/images/arrow-left.png
Normal file
BIN
common/static/images/arrow-left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
common/static/images/arrow-right.png
Normal file
BIN
common/static/images/arrow-right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
25
common/static/js/capa/drag_and_drop.js
Normal file
25
common/static/js/capa/drag_and_drop.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
requirejs.config({
|
||||
'baseUrl': '/static/js/capa/drag_and_drop/'
|
||||
});
|
||||
|
||||
// The current JS file will be loaded and run each time. It will require a
|
||||
// single dependency which will be loaded and stored by RequireJS. On
|
||||
// subsequent runs, RequireJS will return the dependency from memory, rather
|
||||
// than loading it again from the server. For that reason, it is a good idea to
|
||||
// keep the current JS file as small as possible, and move everything else into
|
||||
// RequireJS module dependencies.
|
||||
requirejs(['main'], function (Main) {
|
||||
Main();
|
||||
});
|
||||
|
||||
// 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)
|
||||
59
common/static/js/capa/drag_and_drop/base_image.js
Normal file
59
common/static/js/capa/drag_and_drop/base_image.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return BaseImage;
|
||||
|
||||
function BaseImage(state) {
|
||||
var baseImageElContainer;
|
||||
|
||||
baseImageElContainer = $(
|
||||
'<div ' +
|
||||
'class="base_image_container" ' +
|
||||
'style=" ' +
|
||||
'position: relative; ' +
|
||||
'margin-bottom: 25px; ' +
|
||||
'margin-left: auto; ' +
|
||||
'margin-right: auto; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
|
||||
state.baseImageEl = $('<img />');
|
||||
|
||||
state.baseImageEl.attr('src', state.config.baseImage);
|
||||
state.baseImageEl.load(function () {
|
||||
baseImageElContainer.css({
|
||||
'width': this.width,
|
||||
'height': this.height
|
||||
});
|
||||
|
||||
state.baseImageEl.appendTo(baseImageElContainer);
|
||||
baseImageElContainer.appendTo(state.containerEl);
|
||||
|
||||
state.baseImageEl.mousedown(function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
state.baseImageLoaded = true;
|
||||
});
|
||||
state.baseImageEl.error(function () {
|
||||
logme('ERROR: Image "' + state.config.baseImage + '" was not found!');
|
||||
baseImageElContainer.html(
|
||||
'<span style="color: red;">' +
|
||||
'ERROR: Image "' + state.config.baseImage + '" was not found!' +
|
||||
'</span>'
|
||||
);
|
||||
baseImageElContainer.appendTo(state.containerEl);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
259
common/static/js/capa/drag_and_drop/config_parser.js
Normal file
259
common/static/js/capa/drag_and_drop/config_parser.js
Normal file
@@ -0,0 +1,259 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return configParser;
|
||||
|
||||
function configParser(state, config) {
|
||||
state.config = {
|
||||
'draggables': [],
|
||||
'baseImage': '',
|
||||
'targets': [],
|
||||
'onePerTarget': null, // Specified by user. No default.
|
||||
'targetOutline': true,
|
||||
'labelBgColor': '#d6d6d6',
|
||||
'individualTargets': null, // Depends on 'targets'.
|
||||
'errors': 0 // Number of errors found while parsing config.
|
||||
};
|
||||
|
||||
getDraggables(state, config);
|
||||
getBaseImage(state, config);
|
||||
getTargets(state, config);
|
||||
getOnePerTarget(state, config);
|
||||
getTargetOutline(state, config);
|
||||
getLabelBgColor(state, config);
|
||||
|
||||
setIndividualTargets(state);
|
||||
|
||||
if (state.config.errors !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getDraggables(state, config) {
|
||||
if (config.hasOwnProperty('draggables') === false) {
|
||||
logme('ERROR: "config" does not have a property "draggables".');
|
||||
state.config.errors += 1;
|
||||
} else if ($.isArray(config.draggables) === true) {
|
||||
(function (i) {
|
||||
while (i < config.draggables.length) {
|
||||
if (processDraggable(state, config.draggables[i]) !== true) {
|
||||
state.config.errors += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}(0));
|
||||
} else if ($.isPlainObject(config.draggables) === true) {
|
||||
if (processDraggable(state, config.draggables) !== true) {
|
||||
state.config.errors += 1;
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: The type of config.draggables is no supported.');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseImage(state, config) {
|
||||
if (config.hasOwnProperty('base_image') === false) {
|
||||
logme('ERROR: "config" does not have a property "base_image".');
|
||||
state.config.errors += 1;
|
||||
} else if (typeof config.base_image === 'string') {
|
||||
state.config.baseImage = config.base_image;
|
||||
} else {
|
||||
logme('ERROR: Property config.base_image is not of type "string".');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getTargets(state, config) {
|
||||
if (config.hasOwnProperty('targets') === false) {
|
||||
// It is possible that no "targets" were specified. This is not an error.
|
||||
// In this case the default value of "[]" (empty array) will be used.
|
||||
// Draggables can be positioned anywhere on the image, and the server will
|
||||
// get an answer in the form of (x, y) coordinates for each draggable.
|
||||
} else if ($.isArray(config.targets) === true) {
|
||||
(function (i) {
|
||||
while (i < config.targets.length) {
|
||||
if (processTarget(state, config.targets[i]) !== true) {
|
||||
state.config.errors += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}(0));
|
||||
} else if ($.isPlainObject(config.targets) === true) {
|
||||
if (processTarget(state, config.targets) !== true) {
|
||||
state.config.errors += 1;
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: Property config.targets is not of a supported type.');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getOnePerTarget(state, config) {
|
||||
if (config.hasOwnProperty('one_per_target') === false) {
|
||||
logme('ERROR: "config" does not have a property "one_per_target".');
|
||||
state.config.errors += 1;
|
||||
} else if (typeof config.one_per_target === 'string') {
|
||||
if (config.one_per_target.toLowerCase() === 'true') {
|
||||
state.config.onePerTarget = true;
|
||||
} else if (config.one_per_target.toLowerCase() === 'false') {
|
||||
state.config.onePerTarget = false;
|
||||
} else {
|
||||
logme('ERROR: Property config.one_per_target can either be "true", or "false".');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: Property config.one_per_target is not of a supported type.');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getTargetOutline(state, config) {
|
||||
if (config.hasOwnProperty('target_outline') === false) {
|
||||
// It is possible that no "target_outline" was specified. This is not an error.
|
||||
// In this case the default value of 'true' (boolean) will be used.
|
||||
} else if (typeof config.target_outline === 'string') {
|
||||
if (config.target_outline.toLowerCase() === 'true') {
|
||||
state.config.targetOutline = true;
|
||||
} else if (config.target_outline.toLowerCase() === 'false') {
|
||||
state.config.targetOutline = false;
|
||||
} else {
|
||||
logme('ERROR: Property config.target_outline can either be "true", or "false".');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
} else {
|
||||
logme('ERROR: Property config.target_outline is not of a supported type.');
|
||||
state.config.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelBgColor(state, config) {
|
||||
if (config.hasOwnProperty('label_bg_color') === false) {
|
||||
// It is possible that no "label_bg_color" was specified. This is not an error.
|
||||
// In this case the default value of '#d6d6d6' (string) will be used.
|
||||
} else if (typeof config.label_bg_color === 'string') {
|
||||
state.config.labelBgColor = config.label_bg_color;
|
||||
} else {
|
||||
logme('ERROR: Property config.label_bg_color is not of a supported type.');
|
||||
returnStatus = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setIndividualTargets(state) {
|
||||
if (state.config.targets.length === 0) {
|
||||
state.config.individualTargets = false;
|
||||
} else {
|
||||
state.config.individualTargets = true;
|
||||
}
|
||||
}
|
||||
|
||||
function processDraggable(state, obj) {
|
||||
if (
|
||||
(attrIsString(obj, 'id') === false) ||
|
||||
(attrIsString(obj, 'icon') === false) ||
|
||||
(attrIsString(obj, 'label') === false) ||
|
||||
|
||||
(attrIsBoolean(obj, 'can_reuse', false) === false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.config.draggables.push(obj);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function processTarget(state, obj) {
|
||||
if (
|
||||
(attrIsString(obj, 'id') === false) ||
|
||||
|
||||
(attrIsInteger(obj, 'w') === false) ||
|
||||
(attrIsInteger(obj, 'h') === false) ||
|
||||
|
||||
(attrIsInteger(obj, 'x') === false) ||
|
||||
(attrIsInteger(obj, 'y') === false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.config.targets.push(obj);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function attrIsString(obj, attr) {
|
||||
if (obj.hasOwnProperty(attr) === false) {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
|
||||
|
||||
return false;
|
||||
} else if (typeof obj[attr] !== 'string') {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not a string.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function attrIsInteger(obj, attr) {
|
||||
var tempInt;
|
||||
|
||||
if (obj.hasOwnProperty(attr) === false) {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
tempInt = parseInt(obj[attr], 10);
|
||||
|
||||
if (isFinite(tempInt) === false) {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not an integer.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
obj[attr] = tempInt;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function attrIsBoolean(obj, attr, defaultVal) {
|
||||
if (obj.hasOwnProperty(attr) === false) {
|
||||
if (defaultVal === undefined) {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
|
||||
|
||||
return false;
|
||||
} else {
|
||||
obj[attr] = defaultVal;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj[attr] === '') {
|
||||
obj[attr] = defaultVal;
|
||||
} else if ((obj[attr] === 'false') || (obj[attr] === false)) {
|
||||
obj[attr] = false;
|
||||
} else if ((obj[attr] === 'true') || (obj[attr] === true)) {
|
||||
obj[attr] = true;
|
||||
} else {
|
||||
logme('ERROR: Attribute "obj.' + attr + '" is not a boolean.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
30
common/static/js/capa/drag_and_drop/container.js
Normal file
30
common/static/js/capa/drag_and_drop/container.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return Container;
|
||||
|
||||
function Container(state) {
|
||||
state.containerEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'clear: both; ' +
|
||||
'width: 665px; ' +
|
||||
'margin-left: auto; ' +
|
||||
'margin-right: auto; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
|
||||
$('#inputtype_' + state.problemId).before(state.containerEl);
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
740
common/static/js/capa/drag_and_drop/draggables.js
Normal file
740
common/static/js/capa/drag_and_drop/draggables.js
Normal file
@@ -0,0 +1,740 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme', 'update_input'], function (logme, updateInput) {
|
||||
return {
|
||||
'init': init
|
||||
};
|
||||
|
||||
function init(state) {
|
||||
(function (c1) {
|
||||
while (c1 < state.config.draggables.length) {
|
||||
processDraggable(state, state.config.draggables[c1]);
|
||||
c1 += 1
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
function makeDraggableCopy(callbackFunc) {
|
||||
var draggableObj, property;
|
||||
|
||||
// Make a full proper copy of the draggable object, with some modifications.
|
||||
draggableObj = {};
|
||||
for (property in this) {
|
||||
if (this.hasOwnProperty(property) === true) {
|
||||
draggableObj[property] = this[property];
|
||||
}
|
||||
}
|
||||
// The modifications to the draggable copy.
|
||||
draggableObj.isOriginal = false; // This new draggable is a copy.
|
||||
draggableObj.uniqueId = draggableObj.state.getUniqueId(); // Is newly set.
|
||||
draggableObj.stateDraggablesIndex = null; // Will be set.
|
||||
draggableObj.containerEl = null; // Not needed, since a copy will never return to a container element.
|
||||
draggableObj.iconEl = null; // Will be created.
|
||||
draggableObj.labelEl = null; // Will be created.
|
||||
|
||||
// Create DOM elements and attach events.
|
||||
if (draggableObj.originalConfigObj.icon.length > 0) {
|
||||
draggableObj.iconEl = $('<img />');
|
||||
draggableObj.iconEl.attr('src', draggableObj.originalConfigObj.icon);
|
||||
draggableObj.iconEl.load(function () {
|
||||
draggableObj.iconEl.css({
|
||||
'position': 'absolute',
|
||||
'width': draggableObj.iconWidthSmall,
|
||||
'height': draggableObj.iconHeightSmall,
|
||||
'left': 50 - draggableObj.iconWidthSmall * 0.5,
|
||||
'top': ((draggableObj.originalConfigObj.label.length > 0) ? 5 : 50 - draggableObj.iconHeightSmall * 0.5)
|
||||
});
|
||||
|
||||
if (draggableObj.originalConfigObj.label.length > 0) {
|
||||
draggableObj.labelEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'position: absolute; ' +
|
||||
'color: black; ' +
|
||||
'font-size: 0.95em; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
draggableObj.originalConfigObj.label +
|
||||
'</div>'
|
||||
);
|
||||
draggableObj.labelEl.css({
|
||||
'left': 50 - draggableObj.labelWidth * 0.5,
|
||||
'top': 5 + draggableObj.iconHeightSmall + 5
|
||||
});
|
||||
|
||||
draggableObj.attachMouseEventsTo('labelEl');
|
||||
}
|
||||
|
||||
draggableObj.attachMouseEventsTo('iconEl');
|
||||
|
||||
draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj);
|
||||
|
||||
setTimeout(function () {
|
||||
callbackFunc(draggableObj);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
if (draggableObj.originalConfigObj.label.length > 0) {
|
||||
draggableObj.iconEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'position: absolute; ' +
|
||||
'color: black; ' +
|
||||
'font-size: 0.95em; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
draggableObj.originalConfigObj.label +
|
||||
'</div>'
|
||||
);
|
||||
draggableObj.iconEl.css({
|
||||
'left': 50 - draggableObj.iconWidthSmall * 0.5,
|
||||
'top': 50 - draggableObj.iconHeightSmall * 0.5
|
||||
});
|
||||
|
||||
draggableObj.attachMouseEventsTo('iconEl');
|
||||
|
||||
draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj);
|
||||
|
||||
setTimeout(function () {
|
||||
callbackFunc(draggableObj);
|
||||
}, 0);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachMouseEventsTo(element) {
|
||||
var self;
|
||||
|
||||
self = this;
|
||||
|
||||
this[element].mousedown(function (event) {
|
||||
self.mouseDown(event);
|
||||
});
|
||||
this[element].mouseup(function (event) {
|
||||
self.mouseUp(event);
|
||||
});
|
||||
this[element].mousemove(function (event) {
|
||||
self.mouseMove(event);
|
||||
});
|
||||
}
|
||||
|
||||
function moveDraggableTo(moveType, target) {
|
||||
var self, offset;
|
||||
|
||||
if (this.hasLoaded === false) {
|
||||
self = this;
|
||||
|
||||
setTimeout(function () {
|
||||
self.moveDraggableTo(moveType, target);
|
||||
}, 50);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this.isReusable === true) && (this.isOriginal === true)) {
|
||||
this.makeDraggableCopy(function (draggableCopy) {
|
||||
draggableCopy.moveDraggableTo(moveType, target);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
if (this.state.config.targetOutline === true) {
|
||||
offset = 1;
|
||||
}
|
||||
|
||||
this.inContainer = false;
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.containerEl.hide();
|
||||
this.iconEl.detach();
|
||||
}
|
||||
this.iconEl.css({
|
||||
'background-color': this.iconElBGColor,
|
||||
'padding-left': this.iconElPadding,
|
||||
'padding-right': this.iconElPadding,
|
||||
'border': this.iconElBorder,
|
||||
'width': this.iconWidth,
|
||||
'height': this.iconHeight
|
||||
});
|
||||
if (moveType === 'target') {
|
||||
this.iconEl.css({
|
||||
'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
|
||||
'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset
|
||||
});
|
||||
} else {
|
||||
this.iconEl.css({
|
||||
'left': target.x - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
|
||||
'top': target.y - this.iconHeight * 0.5 + offset
|
||||
});
|
||||
}
|
||||
this.iconEl.appendTo(this.state.baseImageEl.parent());
|
||||
|
||||
if (this.labelEl !== null) {
|
||||
if (this.isOriginal === true) {
|
||||
this.labelEl.detach();
|
||||
}
|
||||
this.labelEl.css({
|
||||
'background-color': this.state.config.labelBgColor,
|
||||
'padding-left': 8,
|
||||
'padding-right': 8,
|
||||
'border': '1px solid black'
|
||||
});
|
||||
if (moveType === 'target') {
|
||||
this.labelEl.css({
|
||||
'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Account for padding, border.
|
||||
'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset
|
||||
});
|
||||
} else {
|
||||
this.labelEl.css({
|
||||
'left': target.x - this.labelWidth * 0.5 + offset - 9, // Account for padding, border.
|
||||
'top': target.y - this.iconHeight * 0.5 + this.iconHeight + 5 + offset
|
||||
});
|
||||
}
|
||||
this.labelEl.appendTo(this.state.baseImageEl.parent());
|
||||
}
|
||||
|
||||
if (moveType === 'target') {
|
||||
target.addDraggable(this);
|
||||
} else {
|
||||
this.x = target.x;
|
||||
this.y = target.y;
|
||||
}
|
||||
|
||||
this.zIndex = 1000;
|
||||
this.correctZIndexes();
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.state.numDraggablesInSlider -= 1;
|
||||
this.state.updateArrowOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
function processDraggable(state, obj) {
|
||||
var draggableObj;
|
||||
|
||||
draggableObj = {
|
||||
'uniqueId': state.getUniqueId(),
|
||||
'originalConfigObj': obj,
|
||||
'stateDraggablesIndex': null,
|
||||
'id': obj.id,
|
||||
'isReusable': obj.can_reuse,
|
||||
'isOriginal': true,
|
||||
'x': -1,
|
||||
'y': -1,
|
||||
'zIndex': 1,
|
||||
'containerEl': null,
|
||||
'iconEl': null,
|
||||
'iconElBGColor': null,
|
||||
'iconElPadding': null,
|
||||
'iconElBorder': null,
|
||||
'iconElLeftOffset': null,
|
||||
'iconWidth': null,
|
||||
'iconHeight': null,
|
||||
'iconWidthSmall': null,
|
||||
'iconHeightSmall': null,
|
||||
'labelEl': null,
|
||||
'labelWidth': null,
|
||||
'hasLoaded': false,
|
||||
'inContainer': true,
|
||||
'mousePressed': false,
|
||||
'onTarget': null,
|
||||
'onTargetIndex': null,
|
||||
'state': state,
|
||||
|
||||
'mouseDown': mouseDown,
|
||||
'mouseUp': mouseUp,
|
||||
'mouseMove': mouseMove,
|
||||
'checkLandingElement': checkLandingElement,
|
||||
'checkIfOnTarget': checkIfOnTarget,
|
||||
'snapToTarget': snapToTarget,
|
||||
'correctZIndexes': correctZIndexes,
|
||||
'moveBackToSlider': moveBackToSlider,
|
||||
'moveDraggableTo': moveDraggableTo,
|
||||
'makeDraggableCopy': makeDraggableCopy,
|
||||
'attachMouseEventsTo': attachMouseEventsTo
|
||||
};
|
||||
|
||||
draggableObj.containerEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 100px; ' +
|
||||
'height: 100px; ' +
|
||||
'display: inline; ' +
|
||||
'float: left; ' +
|
||||
'overflow: hidden; ' +
|
||||
'border-left: 1px solid #CCC; ' +
|
||||
'border-right: 1px solid #CCC; ' +
|
||||
'text-align: center; ' +
|
||||
'position: relative; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
|
||||
draggableObj.containerEl.appendTo(state.sliderEl);
|
||||
|
||||
if (obj.icon.length > 0) {
|
||||
draggableObj.iconElBGColor = 'transparent';
|
||||
draggableObj.iconElPadding = 0;
|
||||
draggableObj.iconElBorder = 'none';
|
||||
draggableObj.iconElLeftOffset = 0;
|
||||
|
||||
draggableObj.iconEl = $('<img />');
|
||||
draggableObj.iconEl.attr('src', obj.icon);
|
||||
draggableObj.iconEl.load(function () {
|
||||
draggableObj.iconWidth = this.width;
|
||||
draggableObj.iconHeight = this.height;
|
||||
|
||||
if (draggableObj.iconWidth >= draggableObj.iconHeight) {
|
||||
draggableObj.iconWidthSmall = 60;
|
||||
draggableObj.iconHeightSmall = draggableObj.iconWidthSmall * (draggableObj.iconHeight / draggableObj.iconWidth);
|
||||
} else {
|
||||
draggableObj.iconHeightSmall = 60;
|
||||
draggableObj.iconWidthSmall = draggableObj.iconHeightSmall * (draggableObj.iconWidth / draggableObj.iconHeight);
|
||||
}
|
||||
|
||||
draggableObj.iconEl.css({
|
||||
'position': 'absolute',
|
||||
'width': draggableObj.iconWidthSmall,
|
||||
'height': draggableObj.iconHeightSmall,
|
||||
'left': 50 - draggableObj.iconWidthSmall * 0.5,
|
||||
'top': ((obj.label.length > 0) ? 5 : 50 - draggableObj.iconHeightSmall * 0.5)
|
||||
});
|
||||
draggableObj.iconEl.appendTo(draggableObj.containerEl);
|
||||
|
||||
if (obj.label.length > 0) {
|
||||
draggableObj.labelEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'position: absolute; ' +
|
||||
'color: black; ' +
|
||||
'font-size: 0.95em; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
obj.label +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
draggableObj.labelEl.appendTo(draggableObj.containerEl);
|
||||
draggableObj.labelWidth = draggableObj.labelEl.width();
|
||||
draggableObj.labelEl.css({
|
||||
'left': 50 - draggableObj.labelWidth * 0.5,
|
||||
'top': 5 + draggableObj.iconHeightSmall + 5
|
||||
});
|
||||
|
||||
draggableObj.attachMouseEventsTo('labelEl');
|
||||
}
|
||||
|
||||
draggableObj.hasLoaded = true;
|
||||
});
|
||||
} else {
|
||||
// To make life easier, if there is no icon, but there is a
|
||||
// label, we will create a label and store it as if it was an
|
||||
// icon. All the existing code will work, and the user will
|
||||
// see a label instead of an icon.
|
||||
if (obj.label.length > 0) {
|
||||
draggableObj.iconElBGColor = state.config.labelBgColor;
|
||||
draggableObj.iconElPadding = 8;
|
||||
draggableObj.iconElBorder = '1px solid black';
|
||||
draggableObj.iconElLeftOffset = 9;
|
||||
|
||||
draggableObj.iconEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'position: absolute; ' +
|
||||
'color: black; ' +
|
||||
'font-size: 0.95em; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
obj.label +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
draggableObj.iconEl.appendTo(draggableObj.containerEl);
|
||||
|
||||
draggableObj.iconWidth = draggableObj.iconEl.width();
|
||||
draggableObj.iconHeight = draggableObj.iconEl.height();
|
||||
draggableObj.iconWidthSmall = draggableObj.iconWidth;
|
||||
draggableObj.iconHeightSmall = draggableObj.iconHeight;
|
||||
|
||||
draggableObj.iconEl.css({
|
||||
'left': 50 - draggableObj.iconWidthSmall * 0.5,
|
||||
'top': 50 - draggableObj.iconHeightSmall * 0.5
|
||||
});
|
||||
|
||||
draggableObj.hasLoaded = true;
|
||||
} else {
|
||||
// If no icon and no label, don't create a draggable.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
draggableObj.attachMouseEventsTo('iconEl');
|
||||
draggableObj.attachMouseEventsTo('containerEl');
|
||||
|
||||
state.numDraggablesInSlider += 1;
|
||||
draggableObj.stateDraggablesIndex = state.draggables.push(draggableObj) - 1;
|
||||
}
|
||||
|
||||
function mouseDown(event) {
|
||||
if (this.mousePressed === false) {
|
||||
// So that the browser does not perform a default drag.
|
||||
// If we don't do this, each drag operation will
|
||||
// potentially cause the highlghting of the dragged element.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// If this draggable is just being dragged out of the
|
||||
// container, we must perform some additional tasks.
|
||||
if (this.inContainer === true) {
|
||||
if ((this.isReusable === true) && (this.isOriginal === true)) {
|
||||
this.makeDraggableCopy(function (draggableCopy) {
|
||||
draggableCopy.mouseDown(event);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.containerEl.hide();
|
||||
this.iconEl.detach();
|
||||
}
|
||||
this.iconEl.css({
|
||||
'background-color': this.iconElBGColor,
|
||||
'padding-left': this.iconElPadding,
|
||||
'padding-right': this.iconElPadding,
|
||||
'border': this.iconElBorder,
|
||||
'width': this.iconWidth,
|
||||
'height': this.iconHeight,
|
||||
'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
|
||||
'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
|
||||
});
|
||||
this.iconEl.appendTo(this.state.baseImageEl.parent());
|
||||
|
||||
if (this.labelEl !== null) {
|
||||
if (this.isOriginal === true) {
|
||||
this.labelEl.detach();
|
||||
}
|
||||
this.labelEl.css({
|
||||
'background-color': this.state.config.labelBgColor,
|
||||
'padding-left': 8,
|
||||
'padding-right': 8,
|
||||
'border': '1px solid black',
|
||||
'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Account for padding, border.
|
||||
'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
|
||||
});
|
||||
this.labelEl.appendTo(this.state.baseImageEl.parent());
|
||||
}
|
||||
|
||||
this.inContainer = false;
|
||||
if (this.isOriginal === true) {
|
||||
this.state.numDraggablesInSlider -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.zIndex = 1000;
|
||||
this.iconEl.css('z-index', '1000');
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.css('z-index', '1000');
|
||||
}
|
||||
|
||||
this.mousePressed = true;
|
||||
this.state.currentMovingDraggable = this;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseUp() {
|
||||
if (this.mousePressed === true) {
|
||||
this.state.currentMovingDraggable = null;
|
||||
|
||||
this.checkLandingElement();
|
||||
}
|
||||
}
|
||||
|
||||
function mouseMove(event) {
|
||||
if (this.mousePressed === true) {
|
||||
// Because we have also attached a 'mousemove' event to the
|
||||
// 'document' (that will do the same thing), let's tell the
|
||||
// browser not to bubble up this event. The attached event
|
||||
// on the 'document' will only be triggered when the mouse
|
||||
// pointer leaves the draggable while it is in the middle
|
||||
// of a drag operation (user moves the mouse very quickly).
|
||||
event.stopPropagation();
|
||||
|
||||
this.iconEl.css({
|
||||
'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
|
||||
'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
|
||||
});
|
||||
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.css({
|
||||
'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Acoount for padding, border.
|
||||
'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point the mouse was realeased, and we need to check
|
||||
// where the draggable eneded up. Based on several things, we
|
||||
// will either move the draggable back to the slider, or update
|
||||
// the input with the user's answer (X-Y position of the draggable,
|
||||
// or the ID of the target where it landed.
|
||||
function checkLandingElement() {
|
||||
var positionIE;
|
||||
|
||||
this.mousePressed = false;
|
||||
positionIE = this.iconEl.position();
|
||||
|
||||
if (this.state.config.individualTargets === true) {
|
||||
if (this.checkIfOnTarget(positionIE) === true) {
|
||||
this.correctZIndexes();
|
||||
} else {
|
||||
if (this.onTarget !== null) {
|
||||
this.onTarget.removeDraggable(this);
|
||||
}
|
||||
|
||||
this.moveBackToSlider();
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.state.numDraggablesInSlider += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
(positionIE.left < 0) ||
|
||||
(positionIE.left + this.iconWidth > this.state.baseImageEl.width()) ||
|
||||
(positionIE.top < 0) ||
|
||||
(positionIE.top + this.iconHeight > this.state.baseImageEl.height())
|
||||
) {
|
||||
this.moveBackToSlider();
|
||||
|
||||
this.x = -1;
|
||||
this.y = -1;
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.state.numDraggablesInSlider += 1;
|
||||
}
|
||||
} else {
|
||||
this.correctZIndexes();
|
||||
|
||||
this.x = positionIE.left + this.iconWidth * 0.5;
|
||||
this.y = positionIE.top + this.iconHeight * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isOriginal === true) {
|
||||
this.state.updateArrowOpacity();
|
||||
}
|
||||
updateInput.update(this.state);
|
||||
}
|
||||
|
||||
// Determine if a draggable, after it was relased, ends up on a
|
||||
// target. We do this by iterating over all of the targets, and
|
||||
// for each one we check whether the draggable's center is
|
||||
// within the target's dimensions.
|
||||
//
|
||||
// positionIE is the object as returned by
|
||||
//
|
||||
// this.iconEl.position()
|
||||
function checkIfOnTarget(positionIE) {
|
||||
var c1, target;
|
||||
|
||||
for (c1 = 0; c1 < this.state.targets.length; c1 += 1) {
|
||||
target = this.state.targets[c1];
|
||||
|
||||
// If only one draggable per target is allowed, and
|
||||
// the current target already has a draggable on it
|
||||
// (with an ID different from the one we are checking
|
||||
// against), then go to next target.
|
||||
if (
|
||||
(this.state.config.onePerTarget === true) &&
|
||||
(target.draggableList.length === 1) &&
|
||||
(target.draggableList[0].uniqueId !== this.uniqueId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the draggable's center coordinate is within
|
||||
// the target's dimensions. If not, go to next target.
|
||||
if (
|
||||
(positionIE.top + this.iconHeight * 0.5 < target.offset.top) ||
|
||||
(positionIE.top + this.iconHeight * 0.5 > target.offset.top + target.h) ||
|
||||
(positionIE.left + this.iconWidth * 0.5 < target.offset.left) ||
|
||||
(positionIE.left + this.iconWidth * 0.5 > target.offset.left + target.w)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the draggable was moved from one target to
|
||||
// another, then we need to remove it from the
|
||||
// previous target's draggables list, and add it to the
|
||||
// new target's draggables list.
|
||||
if ((this.onTarget !== null) && (this.onTarget.id !== target.id)) {
|
||||
this.onTarget.removeDraggable(this);
|
||||
target.addDraggable(this);
|
||||
}
|
||||
// If the draggable was moved from the slider to a
|
||||
// target, remember the target, and add ID to the
|
||||
// target's draggables list.
|
||||
else if (this.onTarget === null) {
|
||||
target.addDraggable(this);
|
||||
}
|
||||
|
||||
// Reposition the draggable so that it's center
|
||||
// coincides with the center of the target.
|
||||
this.snapToTarget(target);
|
||||
|
||||
// Target was found.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Target was not found.
|
||||
return false;
|
||||
}
|
||||
|
||||
function snapToTarget(target) {
|
||||
var offset;
|
||||
|
||||
offset = 0;
|
||||
if (this.state.config.targetOutline === true) {
|
||||
offset = 1;
|
||||
}
|
||||
|
||||
this.iconEl.css({
|
||||
'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
|
||||
'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset
|
||||
});
|
||||
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.css({
|
||||
'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Acoount for padding, border.
|
||||
'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Go through all of the draggables subtract 1 from the z-index
|
||||
// of all whose z-index is higher than the old z-index of the
|
||||
// current element. After, set the z-index of the current
|
||||
// element to 1 + N (where N is the number of draggables - i.e.
|
||||
// the highest z-index possible).
|
||||
//
|
||||
// This will make sure that after releasing a draggable, it
|
||||
// will be on top of all of the other draggables. Also, the
|
||||
// ordering of the visibility (z-index) of the other draggables
|
||||
// will not change.
|
||||
function correctZIndexes() {
|
||||
var c1, highestZIndex;
|
||||
|
||||
highestZIndex = -10000;
|
||||
|
||||
if (this.state.config.individualTargets === true) {
|
||||
if (this.onTarget.draggableList.length > 0) {
|
||||
for (c1 = 0; c1 < this.onTarget.draggableList.length; c1 += 1) {
|
||||
if (
|
||||
(this.onTarget.draggableList[c1].zIndex > highestZIndex) &&
|
||||
(this.onTarget.draggableList[c1].zIndex !== 1000)
|
||||
) {
|
||||
highestZIndex = this.onTarget.draggableList[c1].zIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
highestZIndex = 0;
|
||||
}
|
||||
} else {
|
||||
for (c1 = 0; c1 < this.state.draggables.length; c1++) {
|
||||
if (this.inContainer === false) {
|
||||
if (
|
||||
(this.state.draggables[c1].zIndex > highestZIndex) &&
|
||||
(this.state.draggables[c1].zIndex !== 1000)
|
||||
) {
|
||||
highestZIndex = this.state.draggables[c1].zIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highestZIndex === -10000) {
|
||||
highestZIndex = 0;
|
||||
}
|
||||
|
||||
this.zIndex = highestZIndex + 1;
|
||||
|
||||
this.iconEl.css('z-index', this.zIndex);
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.css('z-index', this.zIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// If a draggable was released in a wrong positione, we will
|
||||
// move it back to the slider, placing it in the same position
|
||||
// that it was dragged out of.
|
||||
function moveBackToSlider() {
|
||||
var c1;
|
||||
|
||||
if (this.isOriginal === false) {
|
||||
this.iconEl.remove();
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.remove();
|
||||
}
|
||||
this.state.draggables.splice(this.stateDraggablesIndex, 1);
|
||||
|
||||
for (c1 = 0; c1 < this.state.draggables; c1 += 1) {
|
||||
if (this.state.draggables[c1].stateDraggablesIndex > this.stateDraggablesIndex) {
|
||||
this.state.draggables[c1].stateDraggablesIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.containerEl.show();
|
||||
this.zIndex = 1;
|
||||
|
||||
this.iconEl.detach();
|
||||
this.iconEl.css({
|
||||
'border': 'none',
|
||||
'background-color': 'transparent',
|
||||
'padding-left': 0,
|
||||
'padding-right': 0,
|
||||
'z-index': this.zIndex,
|
||||
'width': this.iconWidthSmall,
|
||||
'height': this.iconHeightSmall,
|
||||
'left': 50 - this.iconWidthSmall * 0.5,
|
||||
'top': ((this.labelEl !== null) ? 5 : 50 - this.iconHeightSmall * 0.5)
|
||||
});
|
||||
this.iconEl.appendTo(this.containerEl);
|
||||
|
||||
if (this.labelEl !== null) {
|
||||
this.labelEl.detach();
|
||||
this.labelEl.css({
|
||||
'border': 'none',
|
||||
'background-color': 'transparent',
|
||||
'padding-left': 0,
|
||||
'padding-right': 0,
|
||||
'z-index': this.zIndex,
|
||||
'left': 50 - this.labelWidth * 0.5,
|
||||
'top': 5 + this.iconHeightSmall + 5
|
||||
});
|
||||
this.labelEl.appendTo(this.containerEl);
|
||||
}
|
||||
|
||||
this.inContainer = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
36
common/static/js/capa/drag_and_drop/logme.js
Normal file
36
common/static/js/capa/drag_and_drop/logme.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define([], function () {
|
||||
var debugMode;
|
||||
|
||||
debugMode = true;
|
||||
|
||||
return logme;
|
||||
|
||||
function logme() {
|
||||
var i;
|
||||
|
||||
if (
|
||||
(debugMode !== true) ||
|
||||
(typeof window.console === 'undefined')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < arguments.length) {
|
||||
window.console.log(arguments[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
81
common/static/js/capa/drag_and_drop/main.js
Normal file
81
common/static/js/capa/drag_and_drop/main.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(
|
||||
['logme', 'state', 'config_parser', 'container', 'base_image', 'scroller', 'draggables', 'targets', 'update_input'],
|
||||
function (logme, State, configParser, Container, BaseImage, Scroller, Draggables, Targets, updateInput) {
|
||||
return Main;
|
||||
|
||||
function Main() {
|
||||
$('.drag_and_drop_problem_div').each(processProblem);
|
||||
}
|
||||
|
||||
// $(value) - get the element of the entire problem
|
||||
function processProblem(index, value) {
|
||||
var problemId, config, state;
|
||||
|
||||
if ($(value).attr('data-problem-processed') === 'true') {
|
||||
// This problem was already processed by us before, so we will
|
||||
// skip it.
|
||||
|
||||
return;
|
||||
}
|
||||
$(value).attr('data-problem-processed', 'true');
|
||||
|
||||
problemId = $(value).attr('data-plain-id');
|
||||
if (typeof problemId !== 'string') {
|
||||
logme('ERROR: Could not find the ID of the problem DOM element.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
config = JSON.parse($('#drag_and_drop_json_' + problemId).html());
|
||||
} catch (err) {
|
||||
logme('ERROR: Could not parse the JSON configuration options.');
|
||||
logme('Error message: "' + err.message + '".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state = State(problemId);
|
||||
|
||||
if (configParser(state, config) !== true) {
|
||||
logme('ERROR: Could not make sense of the JSON configuration options.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Container(state);
|
||||
BaseImage(state);
|
||||
|
||||
(function addContent() {
|
||||
if (state.baseImageLoaded !== true) {
|
||||
setTimeout(addContent, 50);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Targets(state);
|
||||
Scroller(state);
|
||||
Draggables.init(state);
|
||||
|
||||
state.updateArrowOpacity();
|
||||
|
||||
// Update the input element, checking first that it is not filled with
|
||||
// an answer from the server.
|
||||
if (updateInput.check(state) === false) {
|
||||
updateInput.update(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)
|
||||
215
common/static/js/capa/drag_and_drop/scroller.js
Normal file
215
common/static/js/capa/drag_and_drop/scroller.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return Scroller;
|
||||
|
||||
function Scroller(state) {
|
||||
var parentEl, moveLeftEl, showEl, moveRightEl, showElLeftMargin;
|
||||
|
||||
parentEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 665px; ' +
|
||||
'height: 102px; ' +
|
||||
'margin-left: auto; ' +
|
||||
'margin-right: auto; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
|
||||
moveLeftEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 40px; ' +
|
||||
'height: 102px; ' +
|
||||
'display: inline; ' +
|
||||
'float: left; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 38px; ' +
|
||||
'height: 100px; '+
|
||||
|
||||
'border: 1px solid #CCC; ' +
|
||||
'background-color: #EEE; ' +
|
||||
'background-image: -webkit-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -moz-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -ms-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -o-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: linear-gradient(top, #EEE, #DDD); ' +
|
||||
'-webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
|
||||
'box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
|
||||
|
||||
'background-image: url(\'/static/images/arrow-left.png\'); ' +
|
||||
'background-position: center center; ' +
|
||||
'background-repeat: no-repeat; ' +
|
||||
'" ' +
|
||||
'></div>' +
|
||||
'</div>'
|
||||
);
|
||||
moveLeftEl.appendTo(parentEl);
|
||||
|
||||
// The below is necessary to prevent the browser thinking that we want
|
||||
// to perform a drag operation, or a highlight operation. If we don't
|
||||
// do this, the browser will then highlight with a gray shade the
|
||||
// element.
|
||||
moveLeftEl.mousemove(function (event) { event.preventDefault(); });
|
||||
moveLeftEl.mousedown(function (event) { event.preventDefault(); });
|
||||
|
||||
// This event will be responsible for moving the scroller left.
|
||||
// Hidden draggables will be shown.
|
||||
moveLeftEl.mouseup(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// When there are no more hidden draggables, prevent from
|
||||
// scrolling infinitely.
|
||||
if (showElLeftMargin > -102) {
|
||||
return;
|
||||
}
|
||||
|
||||
showElLeftMargin += 102;
|
||||
|
||||
// We scroll by changing the 'margin-left' CSS property smoothly.
|
||||
state.sliderEl.animate({
|
||||
'margin-left': showElLeftMargin + 'px'
|
||||
}, 100, function () {
|
||||
updateArrowOpacity();
|
||||
});
|
||||
});
|
||||
|
||||
showEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 585px; ' +
|
||||
'height: 102px; ' +
|
||||
'overflow: hidden; ' +
|
||||
'display: inline; ' +
|
||||
'float: left; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
showEl.appendTo(parentEl);
|
||||
|
||||
showElLeftMargin = 0;
|
||||
|
||||
// Element where the draggables will be contained. It is very long
|
||||
// so that any SANE number of draggables will fit in a single row. It
|
||||
// will be contained in a parent element whose 'overflow' CSS value
|
||||
// will be hidden, preventing the long row from fully being visible.
|
||||
state.sliderEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 20000px; ' +
|
||||
'height: 100px; ' +
|
||||
'border-top: 1px solid #CCC; ' +
|
||||
'border-bottom: 1px solid #CCC; ' +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
state.sliderEl.appendTo(showEl);
|
||||
|
||||
state.sliderEl.mousedown(function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
moveRightEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 40px; ' +
|
||||
'height: 102px; ' +
|
||||
'display: inline; ' +
|
||||
'float: left; ' +
|
||||
'" ' +
|
||||
'>' +
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'width: 38px; ' +
|
||||
'height: 100px; '+
|
||||
|
||||
'border: 1px solid #CCC; ' +
|
||||
'background-color: #EEE; ' +
|
||||
'background-image: -webkit-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -moz-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -ms-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: -o-linear-gradient(top, #EEE, #DDD); ' +
|
||||
'background-image: linear-gradient(top, #EEE, #DDD); ' +
|
||||
'-webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
|
||||
'box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
|
||||
|
||||
'background-image: url(\'/static/images/arrow-right.png\'); ' +
|
||||
'background-position: center center; ' +
|
||||
'background-repeat: no-repeat; ' +
|
||||
'" ' +
|
||||
'></div>' +
|
||||
'</div>'
|
||||
);
|
||||
moveRightEl.appendTo(parentEl);
|
||||
|
||||
// The below is necessary to prevent the browser thinking that we want
|
||||
// to perform a drag operation, or a highlight operation. If we don't
|
||||
// do this, the browser will then highlight with a gray shade the
|
||||
// element.
|
||||
moveRightEl.mousemove(function (event) { event.preventDefault(); });
|
||||
moveRightEl.mousedown(function (event) { event.preventDefault(); });
|
||||
|
||||
// This event will be responsible for moving the scroller right.
|
||||
// Hidden draggables will be shown.
|
||||
moveRightEl.mouseup(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// When there are no more hidden draggables, prevent from
|
||||
// scrolling infinitely.
|
||||
if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showElLeftMargin -= 102;
|
||||
|
||||
// We scroll by changing the 'margin-left' CSS property smoothly.
|
||||
state.sliderEl.animate({
|
||||
'margin-left': showElLeftMargin + 'px'
|
||||
}, 100, function () {
|
||||
updateArrowOpacity();
|
||||
});
|
||||
});
|
||||
|
||||
parentEl.appendTo(state.containerEl);
|
||||
|
||||
// Make the function available throughout the application. We need to
|
||||
// call it in several places:
|
||||
//
|
||||
// 1.) When initially reading answer from server, if draggables will be
|
||||
// positioned on the base image, the scroller's right and left arrows
|
||||
// opacity must be updated.
|
||||
//
|
||||
// 2.) When creating draggable elements, the scroller's right and left
|
||||
// arrows opacity must be updated according to the number of
|
||||
// draggables.
|
||||
state.updateArrowOpacity = updateArrowOpacity;
|
||||
|
||||
return;
|
||||
|
||||
function updateArrowOpacity() {
|
||||
moveLeftEl.children('div').css('opacity', '1');
|
||||
moveRightEl.children('div').css('opacity', '1');
|
||||
|
||||
if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
|
||||
moveRightEl.children('div').css('opacity', '.4');
|
||||
}
|
||||
if (showElLeftMargin > -102) {
|
||||
moveLeftEl.children('div').css('opacity', '.4');
|
||||
}
|
||||
}
|
||||
} // End-of: function Scroller(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)
|
||||
105
common/static/js/capa/drag_and_drop/state.js
Normal file
105
common/static/js/capa/drag_and_drop/state.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define([], function () {
|
||||
return State;
|
||||
|
||||
function State(problemId) {
|
||||
var state;
|
||||
|
||||
state = {
|
||||
'config': null,
|
||||
|
||||
'baseImageEl': null,
|
||||
'baseImageLoaded': false,
|
||||
|
||||
'containerEl': null,
|
||||
|
||||
'sliderEl': null,
|
||||
|
||||
'problemId': problemId,
|
||||
|
||||
'draggables': [],
|
||||
'numDraggablesInSlider': 0,
|
||||
'currentMovingDraggable': null,
|
||||
|
||||
'targets': [],
|
||||
|
||||
'updateArrowOpacity': null,
|
||||
|
||||
'uniqueId': 0,
|
||||
'salt': makeSalt(),
|
||||
|
||||
'getUniqueId': getUniqueId
|
||||
};
|
||||
|
||||
$(document).mousemove(function (event) {
|
||||
documentMouseMove(state, event);
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function getUniqueId() {
|
||||
this.uniqueId += 1;
|
||||
|
||||
return this.salt + '_' + this.uniqueId.toFixed(0);
|
||||
}
|
||||
|
||||
function makeSalt() {
|
||||
var text, possible, i;
|
||||
|
||||
text = '';
|
||||
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for(i = 0; i < 5; i += 1) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function documentMouseMove(state, event) {
|
||||
if (state.currentMovingDraggable !== null) {
|
||||
state.currentMovingDraggable.iconEl.css(
|
||||
'left',
|
||||
event.pageX -
|
||||
state.baseImageEl.offset().left -
|
||||
state.currentMovingDraggable.iconWidth * 0.5
|
||||
- state.currentMovingDraggable.iconElLeftOffset
|
||||
);
|
||||
state.currentMovingDraggable.iconEl.css(
|
||||
'top',
|
||||
event.pageY -
|
||||
state.baseImageEl.offset().top -
|
||||
state.currentMovingDraggable.iconHeight * 0.5
|
||||
);
|
||||
|
||||
if (state.currentMovingDraggable.labelEl !== null) {
|
||||
state.currentMovingDraggable.labelEl.css(
|
||||
'left',
|
||||
event.pageX -
|
||||
state.baseImageEl.offset().left -
|
||||
state.currentMovingDraggable.labelWidth * 0.5
|
||||
- 9 // Account for padding, border.
|
||||
);
|
||||
state.currentMovingDraggable.labelEl.css(
|
||||
'top',
|
||||
event.pageY -
|
||||
state.baseImageEl.offset().top +
|
||||
state.currentMovingDraggable.iconHeight * 0.5 +
|
||||
5
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
192
common/static/js/capa/drag_and_drop/targets.js
Normal file
192
common/static/js/capa/drag_and_drop/targets.js
Normal file
@@ -0,0 +1,192 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return Targets;
|
||||
|
||||
function Targets(state) {
|
||||
(function (c1) {
|
||||
while (c1 < state.config.targets.length) {
|
||||
processTarget(state, state.config.targets[c1]);
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
function processTarget(state, obj) {
|
||||
var targetEl, borderCss, numTextEl, targetObj;
|
||||
|
||||
borderCss = '';
|
||||
if (state.config.targetOutline === true) {
|
||||
borderCss = 'border: 1px dashed gray; ';
|
||||
}
|
||||
|
||||
targetEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'display: block; ' +
|
||||
'position: absolute; ' +
|
||||
'width: ' + obj.w + 'px; ' +
|
||||
'height: ' + obj.h + 'px; ' +
|
||||
'top: ' + obj.y + 'px; ' +
|
||||
'left: ' + obj.x + 'px; ' +
|
||||
borderCss +
|
||||
'" ' +
|
||||
'></div>'
|
||||
);
|
||||
targetEl.appendTo(state.baseImageEl.parent());
|
||||
targetEl.mousedown(function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
if (state.config.onePerTarget === false) {
|
||||
numTextEl = $(
|
||||
'<div ' +
|
||||
'style=" ' +
|
||||
'display: block; ' +
|
||||
'position: absolute; ' +
|
||||
'width: 24px; ' +
|
||||
'height: 24px; ' +
|
||||
'top: ' + obj.y + 'px; ' +
|
||||
'left: ' + (obj.x + obj.w - 24) + 'px; ' +
|
||||
'border: 1px solid black; ' +
|
||||
'text-align: center; ' +
|
||||
'z-index: 500; ' +
|
||||
'background-color: white; ' +
|
||||
'font-size: 0.95em; ' +
|
||||
'color: #009fe2; ' +
|
||||
'cursor: pointer; ' +
|
||||
'" ' +
|
||||
'>0</div>'
|
||||
);
|
||||
} else {
|
||||
numTextEl = null;
|
||||
}
|
||||
|
||||
targetObj = {
|
||||
'id': obj.id,
|
||||
|
||||
'w': obj.w,
|
||||
'h': obj.h,
|
||||
|
||||
'el': targetEl,
|
||||
'offset': targetEl.position(),
|
||||
|
||||
'draggableList': [],
|
||||
|
||||
'state': state,
|
||||
|
||||
'targetEl': targetEl,
|
||||
|
||||
'numTextEl': numTextEl,
|
||||
'updateNumTextEl': updateNumTextEl,
|
||||
|
||||
'removeDraggable': removeDraggable,
|
||||
'addDraggable': addDraggable
|
||||
};
|
||||
|
||||
if (state.config.onePerTarget === false) {
|
||||
numTextEl.appendTo(state.baseImageEl.parent());
|
||||
numTextEl.mousedown(function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
numTextEl.mouseup(function () {
|
||||
cycleDraggableOrder.call(targetObj)
|
||||
});
|
||||
}
|
||||
|
||||
state.targets.push(targetObj);
|
||||
}
|
||||
|
||||
function removeDraggable(draggable) {
|
||||
var c1;
|
||||
|
||||
this.draggableList.splice(draggable.onTargetIndex, 1);
|
||||
|
||||
// An item from the array was removed. We need to updated all indexes accordingly.
|
||||
// Shift all indexes down by one if they are higher than the index of the removed item.
|
||||
c1 = 0;
|
||||
while (c1 < this.draggableList.length) {
|
||||
if (this.draggableList[c1].onTargetIndex > draggable.onTargetIndex) {
|
||||
this.draggableList[c1].onTargetIndex -= 1;
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
|
||||
draggable.onTarget = null;
|
||||
draggable.onTargetIndex = null;
|
||||
|
||||
this.updateNumTextEl();
|
||||
}
|
||||
|
||||
function addDraggable(draggable) {
|
||||
draggable.onTarget = this;
|
||||
draggable.onTargetIndex = this.draggableList.push(draggable) - 1;
|
||||
|
||||
this.updateNumTextEl();
|
||||
}
|
||||
|
||||
/*
|
||||
* function cycleDraggableOrder
|
||||
*
|
||||
* Parameters:
|
||||
* none - This function does not expect any parameters.
|
||||
*
|
||||
* Returns:
|
||||
* undefined - The return value of this function is not used.
|
||||
*
|
||||
* Description:
|
||||
* Go through all draggables that are on the current target, and decrease their
|
||||
* z-index by 1, making sure that the bottom-most draggable ends up on the top.
|
||||
*/
|
||||
function cycleDraggableOrder() {
|
||||
var c1, lowestZIndex, highestZIndex;
|
||||
|
||||
if (this.draggableList.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestZIndex = -10000;
|
||||
lowestZIndex = 10000;
|
||||
|
||||
for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
|
||||
if (this.draggableList[c1].zIndex < lowestZIndex) {
|
||||
lowestZIndex = this.draggableList[c1].zIndex;
|
||||
}
|
||||
|
||||
if (this.draggableList[c1].zIndex > highestZIndex) {
|
||||
highestZIndex = this.draggableList[c1].zIndex;
|
||||
}
|
||||
}
|
||||
|
||||
for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
|
||||
if (this.draggableList[c1].zIndex === lowestZIndex) {
|
||||
this.draggableList[c1].zIndex = highestZIndex;
|
||||
} else {
|
||||
this.draggableList[c1].zIndex -= 1;
|
||||
}
|
||||
|
||||
this.draggableList[c1].iconEl.css('z-index', this.draggableList[c1].zIndex);
|
||||
if (this.draggableList[c1].labelEl !== null) {
|
||||
this.draggableList[c1].labelEl.css('z-index', this.draggableList[c1].zIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateNumTextEl() {
|
||||
if (this.numTextEl !== null) {
|
||||
this.numTextEl.html(this.draggableList.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
227
common/static/js/capa/drag_and_drop/update_input.js
Normal file
227
common/static/js/capa/drag_and_drop/update_input.js
Normal file
@@ -0,0 +1,227 @@
|
||||
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
|
||||
// define() functions from Require JS available inside the anonymous function.
|
||||
//
|
||||
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
define(['logme'], function (logme) {
|
||||
return {
|
||||
'check': check,
|
||||
'update': update
|
||||
};
|
||||
|
||||
function update(state) {
|
||||
var draggables, tempObj;
|
||||
|
||||
draggables = [];
|
||||
|
||||
if (state.config.individualTargets === false) {
|
||||
(function (c1) {
|
||||
while (c1 < state.draggables.length) {
|
||||
if (state.draggables[c1].x !== -1) {
|
||||
tempObj = {};
|
||||
tempObj[state.draggables[c1].id] = [
|
||||
state.draggables[c1].x,
|
||||
state.draggables[c1].y
|
||||
];
|
||||
draggables.push(tempObj);
|
||||
tempObj = null;
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
} else {
|
||||
(function (c1) {
|
||||
while (c1 < state.targets.length) {
|
||||
(function (c2) {
|
||||
while (c2 < state.targets[c1].draggableList.length) {
|
||||
tempObj = {};
|
||||
tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id;
|
||||
draggables.push(tempObj);
|
||||
tempObj = null;
|
||||
|
||||
c2 += 1;
|
||||
}
|
||||
}(0));
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
$('#input_' + state.problemId).val(JSON.stringify({'draggables': draggables}));
|
||||
}
|
||||
|
||||
// Check if input has an answer from server. If yes, then position
|
||||
// all draggables according to answer.
|
||||
function check(state) {
|
||||
var inputElVal;
|
||||
|
||||
inputElVal = $('#input_' + state.problemId).val();
|
||||
if (inputElVal.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
repositionDraggables(state, JSON.parse(inputElVal));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getUseTargets(answer) {
|
||||
if ($.isArray(answer.draggables) === false) {
|
||||
logme('ERROR: answer.draggables is not an array.');
|
||||
|
||||
return;
|
||||
} else if (answer.draggables.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($.isPlainObject(answer.draggables[0]) === false) {
|
||||
logme('ERROR: answer.draggables array does not contain objects.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (c1 in answer.draggables[0]) {
|
||||
if (answer.draggables[0].hasOwnProperty(c1) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof answer.draggables[0][c1] === 'string') {
|
||||
// use_targets = true;
|
||||
|
||||
return true;
|
||||
} else if (
|
||||
($.isArray(answer.draggables[0][c1]) === true) &&
|
||||
(answer.draggables[0][c1].length === 2)
|
||||
) {
|
||||
// use_targets = false;
|
||||
|
||||
return false;
|
||||
} else {
|
||||
logme('ERROR: answer.draggables[0] is inconsidtent.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logme('ERROR: answer.draggables[0] is an empty object.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function processAnswerTargets(state, answer) {
|
||||
var draggableId, draggable, targetId, target;
|
||||
|
||||
(function (c1) {
|
||||
while (c1 < answer.draggables.length) {
|
||||
for (draggableId in answer.draggables[c1]) {
|
||||
if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((draggable = getById(state, 'draggables', draggableId)) === null) {
|
||||
logme(
|
||||
'ERROR: In answer there exists a ' +
|
||||
'draggable ID "' + draggableId + '". No ' +
|
||||
'draggable with this ID could be found.'
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
targetId = answer.draggables[c1][draggableId];
|
||||
if ((target = getById(state, 'targets', targetId)) === null) {
|
||||
logme(
|
||||
'ERROR: In answer there exists a target ' +
|
||||
'ID "' + targetId + '". No target with this ' +
|
||||
'ID could be found.'
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
draggable.moveDraggableTo('target', target);
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
function processAnswerPositions(state, answer) {
|
||||
var draggableId, draggable;
|
||||
|
||||
(function (c1) {
|
||||
while (c1 < answer.draggables.length) {
|
||||
for (draggableId in answer.draggables[c1]) {
|
||||
if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((draggable = getById(state, 'draggables', draggableId)) === null) {
|
||||
logme(
|
||||
'ERROR: In answer there exists a ' +
|
||||
'draggable ID "' + draggableId + '". No ' +
|
||||
'draggable with this ID could be found.'
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
draggable.moveDraggableTo('XY', {
|
||||
'x': answer.draggables[c1][draggableId][0],
|
||||
'y': answer.draggables[c1][draggableId][1]
|
||||
});
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
}(0));
|
||||
}
|
||||
|
||||
function repositionDraggables(state, answer) {
|
||||
if (answer.draggables.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.config.individualTargets !== getUseTargets(answer)) {
|
||||
logme('ERROR: JSON config is not consistent with server response.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.config.individualTargets === true) {
|
||||
processAnswerTargets(state, answer);
|
||||
} else if (state.config.individualTargets === false) {
|
||||
processAnswerPositions(state, answer);
|
||||
}
|
||||
}
|
||||
|
||||
function getById(state, type, id) {
|
||||
return (function (c1) {
|
||||
while (c1 < state[type].length) {
|
||||
if (type === 'draggables') {
|
||||
if ((state[type][c1].id === id) && (state[type][c1].isOriginal === true)) {
|
||||
return state[type][c1];
|
||||
}
|
||||
} else { // 'targets'
|
||||
if (state[type][c1].id === id) {
|
||||
return state[type][c1];
|
||||
}
|
||||
}
|
||||
|
||||
c1 += 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}(0));
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
2
common/test/data/graphic_slider_tool/README.md
Normal file
2
common/test/data/graphic_slider_tool/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
This is a very very simple course, useful for debugging graphical slider tool
|
||||
code.
|
||||
1
common/test/data/graphic_slider_tool/course.xml
Symbolic link
1
common/test/data/graphic_slider_tool/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<graphical_slider_tool url_name="sample_gst"/>
|
||||
</chapter>
|
||||
</course>
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
<slider var='a' style="width:400px;float:left;"/><plot style="margin-top:15px;margin-bottom:15px;"/>
|
||||
</render>
|
||||
<configuration>
|
||||
<parameters>
|
||||
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
|
||||
</parameters>
|
||||
<functions>
|
||||
<function color="red">return Math.sqrt(a * a - x * x);</function>
|
||||
<function color="red">return -Math.sqrt(a * a - x * x);</function>
|
||||
</functions>
|
||||
<plot>
|
||||
<xrange>
|
||||
<!-- dynamic range -->
|
||||
<min>
|
||||
return -a;
|
||||
</min>
|
||||
<max>
|
||||
return a;
|
||||
</max>
|
||||
</xrange>
|
||||
<num_points>1000</num_points>
|
||||
<xticks>-30, 6, 30</xticks>
|
||||
<yticks>-30, 6, 30</yticks>
|
||||
</plot>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
|
||||
14
common/test/data/graphic_slider_tool/policies/2012_Fall.json
Normal file
14
common/test/data/graphic_slider_tool/policies/2012_Fall.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2015-07-17T12:00",
|
||||
"display_name": "GST Test",
|
||||
"graded": "false"
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "Overview"
|
||||
},
|
||||
"graphical_slider_tool/sample_gst": {
|
||||
"display_name": "Sample GST",
|
||||
},
|
||||
}
|
||||
1
common/test/data/graphic_slider_tool/roots/2012_Fall.xml
Normal file
1
common/test/data/graphic_slider_tool/roots/2012_Fall.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="gst_test" url_name="2012_Fall"/>
|
||||
2
common/test/data/test_exam_registration/README.md
Normal file
2
common/test/data/test_exam_registration/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Simple course with test center exam information included in policy.json.
|
||||
|
||||
1
common/test/data/test_exam_registration/course.xml
Symbolic link
1
common/test/data/test_exam_registration/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
15
common/test/data/test_exam_registration/course/2012_Fall.xml
Normal file
15
common/test/data/test_exam_registration/course/2012_Fall.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
<chapter url_name="Ch2">
|
||||
<html url_name="test_html">
|
||||
<h2>Welcome</h2>
|
||||
</html>
|
||||
</chapter>
|
||||
|
||||
</course>
|
||||
3
common/test/data/test_exam_registration/html/toylab.html
Normal file
3
common/test/data/test_exam_registration/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
1
common/test/data/test_exam_registration/html/toylab.xml
Normal file
1
common/test/data/test_exam_registration/html/toylab.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="toylab.html"/>
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2011-07-17T12:00",
|
||||
"display_name": "Toy Course",
|
||||
"testcenter_info": {
|
||||
"Midterm_Exam": {
|
||||
"Exam_Series_Code": "Midterm_Exam",
|
||||
"First_Eligible_Appointment_Date": "2012-11-09T00:00",
|
||||
"Last_Eligible_Appointment_Date": "2012-11-09T23:59"
|
||||
},
|
||||
"Final_Exam": {
|
||||
"Exam_Series_Code": "mit6002xfall12a",
|
||||
"Exam_Display_Name": "Final Exam",
|
||||
"First_Eligible_Appointment_Date": "2013-01-25T00:00",
|
||||
"Last_Eligible_Appointment_Date": "2013-01-25T23:59",
|
||||
"Registration_Start_Date": "2013-01-01T00:00",
|
||||
"Registration_End_Date": "2013-01-21T23:59"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "Overview"
|
||||
},
|
||||
"chapter/Ch2": {
|
||||
"display_name": "Chapter 2",
|
||||
"start": "2015-07-17T12:00"
|
||||
},
|
||||
"videosequence/Toy_Videos": {
|
||||
"display_name": "Toy Videos",
|
||||
"format": "Lecture Sequence"
|
||||
},
|
||||
"html/toylab": {
|
||||
"display_name": "Toy lab"
|
||||
},
|
||||
"video/Video_Resources": {
|
||||
"display_name": "Video Resources"
|
||||
},
|
||||
"video/Welcome": {
|
||||
"display_name": "Welcome"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
|
||||
@@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively.
|
||||
Note that the default weight of a problem **is not 1.** The default weight of a
|
||||
problem is the module's max_grade.
|
||||
|
||||
If weighting is set, each problem is worth the number of points assigned, regardless of the number of responses it contains.
|
||||
|
||||
Consider a Homework section that contains two problems.
|
||||
|
||||
<problem display_name=”Problem 1”>
|
||||
<numericalresponse> ... </numericalreponse>
|
||||
</problem>
|
||||
|
||||
and
|
||||
|
||||
<problem display_name=”Problem 2”>
|
||||
<numericalresponse> ... </numericalreponse>
|
||||
<numericalresponse> ... </numericalreponse>
|
||||
<numericalresponse> ... </numericalreponse>
|
||||
</problem>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Without weighting, Problem 1 is worth 25% of the assignment, and Problem 2 is worth 75% of the assignment.
|
||||
|
||||
Weighting for the problems can be set in the policy.json file.
|
||||
|
||||
"problem/problem1": {
|
||||
"weight": 2
|
||||
},
|
||||
"problem/problem2": {
|
||||
"weight": 2
|
||||
},
|
||||
|
||||
With the above weighting, Problems 1 and 2 are each worth 50% of the assignment.
|
||||
|
||||
Please note: When problems have weight, the point value is automatically included in the display name *except* when “weight”: 1.When “weight”: 1, no visual change occurs in the display name, leaving the point value open to interpretation to the student.
|
||||
|
||||
|
||||
|
||||
## Section Weighting
|
||||
|
||||
Once each section has a percentage score, we must total those sections into a
|
||||
|
||||
47
doc/remote_gradebook.md
Normal file
47
doc/remote_gradebook.md
Normal file
@@ -0,0 +1,47 @@
|
||||
Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers".
|
||||
|
||||
1. Definitions
|
||||
|
||||
An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages.
|
||||
|
||||
"Stellar" is the MIT on-campus gradebook system.
|
||||
|
||||
2. Setup
|
||||
|
||||
The remote gradebook xserver should be specified in the lms.envs configuration using
|
||||
|
||||
MITX_FEATURES[REMOTE_GRADEBOOK_URL]
|
||||
|
||||
Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg:
|
||||
|
||||
"remote_gradebook": {
|
||||
"name" : "STELLAR:/project/mitxdemosite",
|
||||
"section" : "r01"
|
||||
},
|
||||
|
||||
3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields:
|
||||
|
||||
- submit: get-assignments, get-membership, post-grades, or get-sections
|
||||
- gradebook: name of gradebook
|
||||
- user: username of staff person initiating the request (for logging)
|
||||
- section: (optional) name of section
|
||||
|
||||
The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard.
|
||||
|
||||
The data is a list of dicts (associative arrays). Each dict should be key:value.
|
||||
|
||||
## For submit=post-grades:
|
||||
|
||||
A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment).
|
||||
|
||||
## For submit=get-assignments
|
||||
|
||||
data keys = "AssignmentName"
|
||||
|
||||
## For submit=get-membership
|
||||
|
||||
data keys = "email", "name", "section"
|
||||
|
||||
## For submit=get-sections
|
||||
|
||||
data keys = "SectionName"
|
||||
@@ -257,6 +257,7 @@ Supported fields at the course level:
|
||||
* "tabs" -- have custom tabs in the courseware. See below for details on config.
|
||||
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
|
||||
* "show_calculator" (value "Yes" if desired)
|
||||
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
|
||||
* TODO: there are others
|
||||
|
||||
### Grading policy file contents
|
||||
|
||||
526
docs/source/drag-n-drop-demo.xml
Normal file
526
docs/source/drag-n-drop-demo.xml
Normal file
@@ -0,0 +1,526 @@
|
||||
<problem display_name="Drag and drop demos: drag and drop icons or labels
|
||||
to proper positions." >
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Anyof rule example]</h4><br/>
|
||||
<h4>Please label hydrogen atoms connected with left carbon atom.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/ethglycol.jpg" target_outline="true"
|
||||
one_per_target="true" no_labels="true" label_bg_color="rgb(222, 139, 238)">
|
||||
<draggable id="1" label="Hydrogen" />
|
||||
<draggable id="2" label="Hydrogen" />
|
||||
|
||||
<target id="t1_o" x="10" y="67" w="100" h="100"/>
|
||||
<target id="t2" x="133" y="3" w="70" h="70"/>
|
||||
<target id="t3" x="2" y="384" w="70" h="70"/>
|
||||
<target id="t4" x="95" y="386" w="70" h="70"/>
|
||||
<target id="t5_c" x="94" y="293" w="91" h="91"/>
|
||||
<target id="t6_c" x="328" y="294" w="91" h="91"/>
|
||||
<target id="t7" x="393" y="463" w="70" h="70"/>
|
||||
<target id="t8" x="344" y="214" w="70" h="70"/>
|
||||
<target id="t9_o" x="445" y="162" w="100" h="100"/>
|
||||
<target id="t10" x="591" y="132" w="70" h="70"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{'draggables': ['1', '2'],
|
||||
'targets': ['t2', 't3', 't4' ],
|
||||
'rule':'anyof'
|
||||
}]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Complex grading example]</h4><br/>
|
||||
<h4>Describe carbon molecule in LCAO-MO.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo.jpg" target_outline="true" >
|
||||
|
||||
<!-- filled bond -->
|
||||
<draggable id="1" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="2" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="3" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="4" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="5" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="6" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
|
||||
<!-- up bond -->
|
||||
<draggable id="7" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="8" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="9" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="10" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
|
||||
<!-- sigma -->
|
||||
<draggable id="11" icon="/static/images/images_list/lcao-mo/sigma.png"/>
|
||||
<draggable id="12" icon="/static/images/images_list/lcao-mo/sigma.png"/>
|
||||
|
||||
<!-- sigma* -->
|
||||
<draggable id="13" icon="/static/images/images_list/lcao-mo/sigma_s.png"/>
|
||||
<draggable id="14" icon="/static/images/images_list/lcao-mo/sigma_s.png"/>
|
||||
|
||||
<!-- pi -->
|
||||
<draggable id="15" icon="/static/images/images_list/lcao-mo/pi.png" />
|
||||
|
||||
<!-- pi* -->
|
||||
<draggable id="16" icon="/static/images/images_list/lcao-mo/pi_s.png" />
|
||||
|
||||
<!-- images that should not be dragged -->
|
||||
<draggable id="17" icon="/static/images/images_list/lcao-mo/d.png" />
|
||||
<draggable id="18" icon="/static/images/images_list/lcao-mo/d.png" />
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s_left" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s_right" x="505" y="360" w="32" h="32"/>
|
||||
<target id="s_sigma" x="320" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star" x="320" y="290" w="32" h="32"/>
|
||||
<target id="p_left_1" x="80" y="100" w="32" h="32"/>
|
||||
<target id="p_left_2" x="125" y="100" w="32" h="32"/>
|
||||
<target id="p_left_3" x="175" y="100" w="32" h="32"/>
|
||||
<target id="p_right_1" x="465" y="100" w="32" h="32"/>
|
||||
<target id="p_right_2" x="515" y="100" w="32" h="32"/>
|
||||
<target id="p_right_3" x="560" y="100" w="32" h="32"/>
|
||||
<target id="p_pi_1" x="290" y="220" w="32" h="32"/>
|
||||
<target id="p_pi_2" x="335" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma" x="315" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_1" x="290" y="40" w="32" h="32"/>
|
||||
<target id="p_pi_star_2" x="340" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star" x="315" y="0" w="32" h="32"/>
|
||||
|
||||
<!-- positions of names of energy levels -->
|
||||
<target id="s_sigma_name" x="400" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star_name" x="400" y="290" w="32" h="32"/>
|
||||
<target id="p_pi_name" x="400" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma_name" x="400" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_name" x="400" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star_name" x="400" y="0" w="32" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}, {
|
||||
'draggables': ['7','8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1','p_right_2'],
|
||||
'rule': 'unordered_equal'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'unordered_equal'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'unordered_equal'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'unordered_equal'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Another complex grading example]</h4><br/>
|
||||
<h4>Describe oxygen molecule in LCAO-MO</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo.jpg" target_outline="true" one_per_target="true">
|
||||
<!-- filled bond -->
|
||||
<draggable id="1" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="2" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="3" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="4" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="5" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="6" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="v_fb_1" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="v_fb_2" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
<draggable id="v_fb_3" icon="/static/images/images_list/lcao-mo/u_d.png" />
|
||||
|
||||
<!-- up bond -->
|
||||
<draggable id="7" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="8" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="9" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="10" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="v_ub_1" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
<draggable id="v_ub_2" icon="/static/images/images_list/lcao-mo/up.png"/>
|
||||
|
||||
<!-- sigma -->
|
||||
<draggable id="11" icon="/static/images/images_list/lcao-mo/sigma.png"/>
|
||||
<draggable id="12" icon="/static/images/images_list/lcao-mo/sigma.png"/>
|
||||
|
||||
<!-- sigma* -->
|
||||
<draggable id="13" icon="/static/images/images_list/lcao-mo/sigma_s.png"/>
|
||||
<draggable id="14" icon="/static/images/images_list/lcao-mo/sigma_s.png"/>
|
||||
|
||||
<!-- pi -->
|
||||
<draggable id="15" icon="/static/images/images_list/lcao-mo/pi.png" />
|
||||
|
||||
<!-- pi* -->
|
||||
<draggable id="16" icon="/static/images/images_list/lcao-mo/pi_s.png" />
|
||||
|
||||
<!-- images that should not be dragged -->
|
||||
<draggable id="17" icon="/static/images/images_list/lcao-mo/d.png" />
|
||||
<draggable id="18" icon="/static/images/images_list/lcao-mo/d.png" />
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s_left" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s_right" x="505" y="360" w="32" h="32"/>
|
||||
<target id="s_sigma" x="320" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star" x="320" y="290" w="32" h="32"/>
|
||||
<target id="p_left_1" x="80" y="100" w="32" h="32"/>
|
||||
<target id="p_left_2" x="125" y="100" w="32" h="32"/>
|
||||
<target id="p_left_3" x="175" y="100" w="32" h="32"/>
|
||||
<target id="p_right_1" x="465" y="100" w="32" h="32"/>
|
||||
<target id="p_right_2" x="515" y="100" w="32" h="32"/>
|
||||
<target id="p_right_3" x="560" y="100" w="32" h="32"/>
|
||||
<target id="p_pi_1" x="290" y="220" w="32" h="32"/>
|
||||
<target id="p_pi_2" x="335" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma" x="315" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_1" x="290" y="40" w="32" h="32"/>
|
||||
<target id="p_pi_star_2" x="340" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star" x="315" y="0" w="32" h="32"/>
|
||||
|
||||
<!-- positions of names of energy levels -->
|
||||
<target id="s_sigma_name" x="400" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star_name" x="400" y="290" w="32" h="32"/>
|
||||
<target id="p_pi_name" x="400" y="220" w="32" h="32"/>
|
||||
<target id="p_pi_star_name" x="400" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_name" x="400" y="170" w="32" h="32"/>
|
||||
<target id="p_sigma_star_name" x="400" y="0" w="32" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6', 'v_fb_1', 'v_fb_2', 'v_fb_3'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2',
|
||||
'p_sigma', 'p_left_1', 'p_right_3'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10', 'v_ub_1', 'v_ub_2'],
|
||||
'targets': [
|
||||
'p_left_2', 'p_left_3', 'p_right_1', 'p_right_2', 'p_pi_star_1',
|
||||
'p_pi_star_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Individual targets with outlines, One draggable per target]</h4><br/>
|
||||
<h4>
|
||||
Drag -Ant- to first position and -Star- to third position </h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" target_outline="true">
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg"/>
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
<target id="t1" x="20" y="20" w="90" h="90"/>
|
||||
<target id="t2" x="300" y="100" w="90" h="90"/>
|
||||
<target id="t3" x="150" y="40" w="50" h="50"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {'name_with_icon': 't1', 'name4': 't2'}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[SMALL IMAGE, Individual targets WITHOUT outlines, One draggable
|
||||
per target]</h4><br/>
|
||||
<h4>
|
||||
Move -Star- to the volcano opening, and -Label3- on to
|
||||
the right ear of the cow.
|
||||
</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow3.png" target_outline="false">
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg"/>
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
<target id="t1" x="111" y="58" w="90" h="90"/>
|
||||
<target id="t2" x="212" y="90" w="90" h="90"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {'name4': 't1',
|
||||
'7': 't2'}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Many draggables per target]</h4><br/>
|
||||
<h4>Move -Star- and -Ant- to most left target
|
||||
and -Label3- and -Label2- to most right target.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" target_outline="true" one_per_target="false">
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg"/>
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
<target id="t1" x="20" y="20" w="90" h="90"/>
|
||||
<target id="t2" x="300" y="100" w="90" h="90"/>
|
||||
<target id="t3" x="150" y="40" w="50" h="50"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {'name4': 't1',
|
||||
'name_with_icon': 't1',
|
||||
'5': 't2',
|
||||
'7':'t2'}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Draggables can be placed anywhere on base image]</h4><br/>
|
||||
<h4>
|
||||
Place -Grass- in the middle of the image and -Ant- in the
|
||||
right upper corner.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" >
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="ant" label="Ant" icon="/static/images/images_list/ant.jpg"/>
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" />
|
||||
<draggable id="grass" label="Grass" icon="/static/images/images_list/grass.jpg" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {'grass': [[300, 200], 200],
|
||||
'ant': [[500, 0], 200]}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Another anyof example]</h4><br/>
|
||||
<h4>Please identify the Carbon and Oxygen atoms in the molecule.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/ethglycol.jpg" target_outline="true" one_per_target="true">
|
||||
<draggable id="l1_c" label="Carbon" />
|
||||
<draggable id="l2" label="Methane"/>
|
||||
<draggable id="l3_o" label="Oxygen" />
|
||||
<draggable id="l4" label="Calcium" />
|
||||
<draggable id="l5" label="Methane"/>
|
||||
<draggable id="l6" label="Calcium" />
|
||||
<draggable id="l7" label="Hydrogen" />
|
||||
<draggable id="l8_c" label="Carbon" />
|
||||
<draggable id="l9" label="Hydrogen" />
|
||||
<draggable id="l10_o" label="Oxygen" />
|
||||
|
||||
<target id="t1_o" x="10" y="67" w="100" h="100"/>
|
||||
<target id="t2" x="133" y="3" w="70" h="70"/>
|
||||
<target id="t3" x="2" y="384" w="70" h="70"/>
|
||||
<target id="t4" x="95" y="386" w="70" h="70"/>
|
||||
<target id="t5_c" x="94" y="293" w="91" h="91"/>
|
||||
<target id="t6_c" x="328" y="294" w="91" h="91"/>
|
||||
<target id="t7" x="393" y="463" w="70" h="70"/>
|
||||
<target id="t8" x="344" y="214" w="70" h="70"/>
|
||||
<target id="t9_o" x="445" y="162" w="100" h="100"/>
|
||||
<target id="t10" x="591" y="132" w="70" h="70"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['l3_o', 'l10_o'],
|
||||
'targets': ['t1_o', 't9_o'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['l1_c','l8_c'],
|
||||
'targets': ['t5_c','t6_c'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Again another anyof example]</h4><br/>
|
||||
<h4>If the element appears in this molecule, drag the label onto it</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/ethglycol.jpg" target_outline="true"
|
||||
one_per_target="true" no_labels="true" label_bg_color="rgb(222, 139, 238)">
|
||||
<draggable id="1" label="Hydrogen" />
|
||||
<draggable id="2" label="Hydrogen" />
|
||||
<draggable id="3" label="Nytrogen" />
|
||||
<draggable id="4" label="Nytrogen" />
|
||||
<draggable id="5" label="Boron" />
|
||||
<draggable id="6" label="Boron" />
|
||||
<draggable id="7" label="Carbon" />
|
||||
<draggable id="8" label="Carbon" />
|
||||
|
||||
<target id="t1_o" x="10" y="67" w="100" h="100"/>
|
||||
<target id="t2_h" x="133" y="3" w="70" h="70"/>
|
||||
<target id="t3_h" x="2" y="384" w="70" h="70"/>
|
||||
<target id="t4_h" x="95" y="386" w="70" h="70"/>
|
||||
<target id="t5_c" x="94" y="293" w="91" h="91"/>
|
||||
<target id="t6_c" x="328" y="294" w="91" h="91"/>
|
||||
<target id="t7_h" x="393" y="463" w="70" h="70"/>
|
||||
<target id="t8_h" x="344" y="214" w="70" h="70"/>
|
||||
<target id="t9_o" x="445" y="162" w="100" h="100"/>
|
||||
<target id="t10_h" x="591" y="132" w="70" h="70"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['t5_c', 't6_c'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['1', '2'],
|
||||
'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Wrong base image url example]
|
||||
</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow3_bad.png" target_outline="false">
|
||||
<draggable id="1" label="Label 1"/>
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg"/>
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" />
|
||||
<draggable id="5" label="Label2" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" />
|
||||
<draggable id="7" label="Label3" />
|
||||
|
||||
<target id="t1" x="111" y="58" w="90" h="90"/>
|
||||
<target id="t2" x="212" y="90" w="90" h="90"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {'name4': 't1',
|
||||
'7': 't2'}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
</problem>
|
||||
373
docs/source/drag-n-drop-demo2.xml
Normal file
373
docs/source/drag-n-drop-demo2.xml
Normal file
@@ -0,0 +1,373 @@
|
||||
<problem display_name="Drag and drop demos: drag and drop icons or labels
|
||||
to proper positions." >
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Draggable is reusable example]</h4>
|
||||
<br/>
|
||||
<h4>Please label all hydrogen atoms.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input
|
||||
img="/static/images/images_list/ethglycol.jpg"
|
||||
target_outline="true"
|
||||
one_per_target="true"
|
||||
no_labels="true"
|
||||
label_bg_color="rgb(222, 139, 238)"
|
||||
>
|
||||
<draggable id="1" label="Hydrogen" can_reuse='true' />
|
||||
|
||||
<target id="t1_o" x="10" y="67" w="100" h="100" />
|
||||
<target id="t2" x="133" y="3" w="70" h="70" />
|
||||
<target id="t3" x="2" y="384" w="70" h="70" />
|
||||
<target id="t4" x="95" y="386" w="70" h="70" />
|
||||
<target id="t5_c" x="94" y="293" w="91" h="91" />
|
||||
<target id="t6_c" x="328" y="294" w="91" h="91" />
|
||||
<target id="t7" x="393" y="463" w="70" h="70" />
|
||||
<target id="t8" x="344" y="214" w="70" h="70" />
|
||||
<target id="t9_o" x="445" y="162" w="100" h="100" />
|
||||
<target id="t10" x="591" y="132" w="70" h="70" />
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python">
|
||||
<![CDATA[
|
||||
correct_answer = [{
|
||||
'draggables': ['1'],
|
||||
'targets': ['t2', 't3', 't4', 't7', 't8', 't10'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]>
|
||||
</answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Complex grading example]</h4><br/>
|
||||
<h4>Describe carbon molecule in LCAO-MO.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo.jpg" target_outline="true" >
|
||||
|
||||
<!-- filled bond -->
|
||||
<draggable id="1" icon="/static/images/images_list/lcao-mo/u_d.png" can_reuse="true" />
|
||||
|
||||
<!-- up bond -->
|
||||
<draggable id="7" icon="/static/images/images_list/lcao-mo/up.png" can_reuse="true" />
|
||||
|
||||
<!-- sigma -->
|
||||
<draggable id="11" icon="/static/images/images_list/lcao-mo/sigma.png" can_reuse="true" />
|
||||
|
||||
<!-- sigma* -->
|
||||
<draggable id="13" icon="/static/images/images_list/lcao-mo/sigma_s.png" can_reuse="true" />
|
||||
|
||||
<!-- pi -->
|
||||
<draggable id="15" icon="/static/images/images_list/lcao-mo/pi.png" can_reuse="true" />
|
||||
|
||||
<!-- pi* -->
|
||||
<draggable id="16" icon="/static/images/images_list/lcao-mo/pi_s.png" can_reuse="true" />
|
||||
|
||||
<!-- images that should not be dragged -->
|
||||
<draggable id="17" icon="/static/images/images_list/lcao-mo/d.png" can_reuse="true" />
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s_left" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s_right" x="505" y="360" w="32" h="32"/>
|
||||
<target id="s_sigma" x="320" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star" x="320" y="290" w="32" h="32"/>
|
||||
<target id="p_left_1" x="80" y="100" w="32" h="32"/>
|
||||
<target id="p_left_2" x="125" y="100" w="32" h="32"/>
|
||||
<target id="p_left_3" x="175" y="100" w="32" h="32"/>
|
||||
<target id="p_right_1" x="465" y="100" w="32" h="32"/>
|
||||
<target id="p_right_2" x="515" y="100" w="32" h="32"/>
|
||||
<target id="p_right_3" x="560" y="100" w="32" h="32"/>
|
||||
<target id="p_pi_1" x="290" y="220" w="32" h="32"/>
|
||||
<target id="p_pi_2" x="335" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma" x="315" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_1" x="290" y="40" w="32" h="32"/>
|
||||
<target id="p_pi_star_2" x="340" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star" x="315" y="0" w="32" h="32"/>
|
||||
|
||||
<!-- positions of names of energy levels -->
|
||||
<target id="s_sigma_name" x="400" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star_name" x="400" y="290" w="32" h="32"/>
|
||||
<target id="p_pi_name" x="400" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma_name" x="400" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_name" x="400" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star_name" x="400" y="0" w="32" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['7'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1','p_right_2'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['11'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['13'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Many draggables per target]</h4><br/>
|
||||
<h4>Move two Stars and three Ants to most left target
|
||||
and one Label3 and four Label2 to most right target.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" target_outline="true" one_per_target="false">
|
||||
<draggable id="1" label="Label 1" can_reuse="true" />
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg" can_reuse="true" />
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" can_reuse="true" />
|
||||
<draggable id="5" label="Label2" can_reuse="true" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" can_reuse="true" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" can_reuse="true" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" can_reuse="true" />
|
||||
<draggable id="7" label="Label3" can_reuse="true" />
|
||||
|
||||
<target id="t1" x="20" y="20" w="90" h="90"/>
|
||||
<target id="t2" x="300" y="100" w="90" h="90"/>
|
||||
<target id="t3" x="150" y="40" w="50" h="50"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['name4'],
|
||||
'targets': [
|
||||
't1', 't1'
|
||||
],
|
||||
'rule': 'exact'
|
||||
},
|
||||
{
|
||||
'draggables': ['name_with_icon'],
|
||||
'targets': [
|
||||
't1', 't1', 't1'
|
||||
],
|
||||
'rule': 'exact'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': [
|
||||
't2', 't2', 't2', 't2'
|
||||
],
|
||||
'rule': 'exact'
|
||||
},
|
||||
{
|
||||
'draggables': ['7'],
|
||||
'targets': [
|
||||
't2'
|
||||
],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Draggables can be placed anywhere on base image]</h4><br/>
|
||||
<h4>
|
||||
Place -Grass- in the middle of the image and -Ant- in the
|
||||
right upper corner.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" >
|
||||
<draggable id="1" label="Label 1" can_reuse="true" />
|
||||
<draggable id="ant" label="Ant" icon="/static/images/images_list/ant.jpg" can_reuse="true" />
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" can_reuse="true" />
|
||||
<draggable id="5" label="Label2" can_reuse="true" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" can_reuse="true" />
|
||||
<draggable id="grass" label="Grass" icon="/static/images/images_list/grass.jpg" can_reuse="true" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" can_reuse="true" />
|
||||
<draggable id="7" label="Label3" can_reuse="true" />
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = {
|
||||
'grass': [[300, 200], 200],
|
||||
'ant': [[500, 0], 200]
|
||||
}
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Another anyof example]</h4><br/>
|
||||
<h4>Please identify the Carbon and Oxygen atoms in the molecule.</h4><br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/ethglycol.jpg" target_outline="true" one_per_target="true">
|
||||
<draggable id="l1_c" label="Carbon" can_reuse="true" />
|
||||
<draggable id="l2" label="Methane" can_reuse="true" />
|
||||
<draggable id="l3_o" label="Oxygen" can_reuse="true" />
|
||||
<draggable id="l4" label="Calcium" can_reuse="true" />
|
||||
<draggable id="l7" label="Hydrogen" can_reuse="true" />
|
||||
|
||||
<target id="t1_o" x="10" y="67" w="100" h="100"/>
|
||||
<target id="t2" x="133" y="3" w="70" h="70"/>
|
||||
<target id="t3" x="2" y="384" w="70" h="70"/>
|
||||
<target id="t4" x="95" y="386" w="70" h="70"/>
|
||||
<target id="t5_c" x="94" y="293" w="91" h="91"/>
|
||||
<target id="t6_c" x="328" y="294" w="91" h="91"/>
|
||||
<target id="t7" x="393" y="463" w="70" h="70"/>
|
||||
<target id="t8" x="344" y="214" w="70" h="70"/>
|
||||
<target id="t9_o" x="445" y="162" w="100" h="100"/>
|
||||
<target id="t10" x="591" y="132" w="70" h="70"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['l3_o'],
|
||||
'targets': ['t1_o', 't9_o'],
|
||||
'rule': 'exact'
|
||||
},
|
||||
{
|
||||
'draggables': ['l1_c'],
|
||||
'targets': ['t5_c', 't6_c'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Exact number of draggables for a set of targets.]</h4><br/>
|
||||
<h4>Drag two Grass and one Star to first or second positions, and three Cloud to any of the three positions.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" target_outline="true" one_per_target="false">
|
||||
<draggable id="1" label="Label 1" can_reuse="true" />
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg" can_reuse="true" />
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" can_reuse="true" />
|
||||
<draggable id="5" label="Label2" can_reuse="true" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" can_reuse="true" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" can_reuse="true" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" can_reuse="true" />
|
||||
<draggable id="7" label="Label3" can_reuse="true" />
|
||||
|
||||
<target id="t1" x="20" y="20" w="90" h="90"/>
|
||||
<target id="t2" x="300" y="100" w="90" h="90"/>
|
||||
<target id="t3" x="150" y="40" w="50" h="50"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['name_label_icon3', 'name_label_icon3'],
|
||||
'targets': ['t1', 't3'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['name4'],
|
||||
'targets': ['t1', 't3'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['with_icon', 'with_icon', 'with_icon'],
|
||||
'targets': ['t1', 't2', 't3'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[As many as you like draggables for a set of targets.]</h4><br/>
|
||||
<h4>Drag some Grass to any of the targets, and some Stars to either first or last target.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/cow.png" target_outline="true" one_per_target="false">
|
||||
<draggable id="1" label="Label 1" can_reuse="true" />
|
||||
<draggable id="name_with_icon" label="Ant" icon="/static/images/images_list/ant.jpg" can_reuse="true" />
|
||||
<draggable id="with_icon" label="Cloud" icon="/static/images/images_list/cloud.jpg" can_reuse="true" />
|
||||
<draggable id="5" label="Label2" can_reuse="true" />
|
||||
<draggable id="2" label="Drop" icon="/static/images/images_list/drop.jpg" can_reuse="true" />
|
||||
<draggable id="name_label_icon3" label="Grass" icon="/static/images/images_list/grass.jpg" can_reuse="true" />
|
||||
<draggable id="name4" label="Star" icon="/static/images/images_list/star.png" can_reuse="true" />
|
||||
<draggable id="7" label="Label3" can_reuse="true" />
|
||||
|
||||
<target id="t1" x="20" y="20" w="90" h="90"/>
|
||||
<target id="t2" x="300" y="100" w="90" h="90"/>
|
||||
<target id="t3" x="150" y="40" w="50" h="50"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['name_label_icon3'],
|
||||
'targets': ['t1', 't2', 't3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['name4'],
|
||||
'targets': ['t1', 't2'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
</problem>
|
||||
323
docs/source/drag_and_drop_input.rst
Normal file
323
docs/source/drag_and_drop_input.rst
Normal file
@@ -0,0 +1,323 @@
|
||||
**********************************************
|
||||
Xml format of drag and drop input [inputtypes]
|
||||
**********************************************
|
||||
|
||||
.. module:: drag_and_drop_input
|
||||
|
||||
Format description
|
||||
==================
|
||||
|
||||
The main tag of Drag and Drop (DnD) input is::
|
||||
|
||||
<drag_and_drop_input> ... </drag_and_drop_input>
|
||||
|
||||
``drag_and_drop_input`` can include any number of the following 2 tags:
|
||||
``draggable`` and ``target``.
|
||||
|
||||
drag_and_drop_input tag
|
||||
-----------------------
|
||||
|
||||
The main container for a single instance of DnD. The following attributes can
|
||||
be specified for this tag::
|
||||
|
||||
img - Relative path to an image that will be the base image. All draggables
|
||||
can be dragged onto it.
|
||||
target_outline - Specify whether an outline (gray dashed line) should be
|
||||
drawn around targets (if they are specified). It can be either
|
||||
'true' or 'false'. If not specified, the default value is
|
||||
'false'.
|
||||
one_per_target - Specify whether to allow more than one draggable to be
|
||||
placed onto a single target. It can be either 'true' or 'false'. If
|
||||
not specified, the default value is 'true'.
|
||||
no_labels - default is false, in default behaviour if label is not set, label
|
||||
is obtained from id. If no_labels is true, labels are not automatically
|
||||
populated from id, and one can not set labels and obtain only icons.
|
||||
|
||||
draggable tag
|
||||
-------------
|
||||
|
||||
Draggable tag specifies a single draggable object which has the following
|
||||
attributes::
|
||||
|
||||
id - Unique identifier of the draggable object.
|
||||
label - Human readable label that will be shown to the user.
|
||||
icon - Relative path to an image that will be shown to the user.
|
||||
can_reuse - true or false, default is false. If true, same draggable can be
|
||||
used multiple times.
|
||||
|
||||
A draggable is what the user must drag out of the slider and place onto the
|
||||
base image. After a drag operation, if the center of the draggable ends up
|
||||
outside the rectangular dimensions of the image, it will be returned back
|
||||
to the slider.
|
||||
|
||||
In order for the grader to work, it is essential that a unique ID
|
||||
is provided. Otherwise, there will be no way to tell which draggable is at what
|
||||
coordinate, or over what target. Label and icon attributes are optional. If
|
||||
they are provided they will be used, otherwise, you can have an empty
|
||||
draggable. The path is relative to 'course_folder' folder, for example,
|
||||
/static/images/img1.png.
|
||||
|
||||
target tag
|
||||
----------
|
||||
|
||||
Target tag specifies a single target object which has the following required
|
||||
attributes::
|
||||
|
||||
id - Unique identifier of the target object.
|
||||
x - X-coordinate on the base image where the top left corner of the target
|
||||
will be positioned.
|
||||
y - Y-coordinate on the base image where the top left corner of the target
|
||||
will be positioned.
|
||||
w - Width of the target.
|
||||
h - Height of the target.
|
||||
|
||||
A target specifies a place on the base image where a draggable can be
|
||||
positioned. By design, if the center of a draggable lies within the target
|
||||
(i.e. in the rectangle defined by [[x, y], [x + w, y + h]], then it is within
|
||||
the target. Otherwise, it is outside.
|
||||
|
||||
If at lest one target is provided, the behavior of the client side logic
|
||||
changes. If a draggable is not dragged on to a target, it is returned back to
|
||||
the slider.
|
||||
|
||||
If no targets are provided, then a draggable can be dragged and placed anywhere
|
||||
on the base image.
|
||||
|
||||
correct answer format
|
||||
---------------------
|
||||
|
||||
There are two correct answer formats: short and long
|
||||
If short from correct answer is mapping of 'draggable_id' to 'target_id'::
|
||||
|
||||
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
|
||||
correct_answer = {'name4': 't1', '7': 't2'}
|
||||
|
||||
In long form correct answer is list of dicts. Every dict has 3 keys:
|
||||
draggables, targets and rule. For example::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['t5_c', 't6_c'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['1', '2'],
|
||||
'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
Draggables is list of draggables id. Target is list of targets id, draggables
|
||||
must be dragged to with considering rule. Rule is string.
|
||||
|
||||
Draggables in dicts inside correct_answer list must not intersect!!!
|
||||
|
||||
Wrong (for draggable id 7)::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['t5_c', 't6_c'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['7', '2'],
|
||||
'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
Rules are: exact, anyof, unordered_equal, anyof+number, unordered_equal+number
|
||||
|
||||
|
||||
.. such long lines are needed for sphinx to display lists correctly
|
||||
|
||||
- Exact rule means that targets for draggable id's in user_answer are the same that targets from correct answer. For example, for draggables 7 and 8 user must drag 7 to target1 and 8 to target2 if correct_answer is::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['tartget1', 'target2'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
|
||||
|
||||
- unordered_equal rule allows draggables be dragged to targets unordered. If one want to allow for student to drag 7 to target1 or target2 and 8 to target2 or target 1 and 7 and 8 must be in different targets, then correct answer must be::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['tartget1', 'target2'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
|
||||
|
||||
- Anyof rule allows draggables to be dragged to any of targets. If one want to allow for student to drag 7 and 8 to target1 or target2, which means that if 7 is on target1 and 8 is on target1 or 7 on target2 and 8 on target2 or 7 on target1 and 8 on target2. Any of theese are correct which anyof rule::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['7', '8'],
|
||||
'targets': ['tartget1', 'target2'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
|
||||
- If you have can_reuse true, then you, for example, have draggables a,b,c and 10 targets. These will allow you to drag 4 'a' draggables to ['target1', 'target4', 'target7', 'target10'] , you do not need to write 'a' four times. Also this will allow you to drag 'b' draggable to target2 or target5 for target5 and target2 etc..::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
|
||||
- And sometimes you want to allow drag only two 'b' draggables, in these case you sould use 'anyof+number' of 'unordered_equal+number' rule::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7'],
|
||||
'rule': 'unordered_equal+numbers'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+numbers'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
|
||||
In case if we have no multiple draggables per targets (one_per_target="true"),
|
||||
for same number of draggables, anyof is equal to unordered_equal
|
||||
|
||||
If we have can_reuse=true, than one must use only long form of correct answer.
|
||||
|
||||
|
||||
Grading logic
|
||||
-------------
|
||||
|
||||
1. User answer (that comes from browser) and correct answer (from xml) are parsed to the same format::
|
||||
|
||||
group_id: group_draggables, group_targets, group_rule
|
||||
|
||||
|
||||
Group_id is ordinal number, for every dict in correct answer incremental
|
||||
group_id is assigned: 0, 1, 2, ...
|
||||
|
||||
Draggables from user answer are added to same group_id where identical draggables
|
||||
from correct answer are, for example::
|
||||
|
||||
If correct_draggables[group_0] = [t1, t2] then
|
||||
user_draggables[group_0] are all draggables t1 and t2 from user answer:
|
||||
[t1] or [t1, t2] or [t1, t2, t2] etc..
|
||||
|
||||
2. For every group from user answer, for that group draggables, if 'number' is in group rule, set() is applied,
|
||||
if 'number' is not in rule, set is not applied::
|
||||
|
||||
set() : [t1, t2, t3, t3] -> [t1, t2, ,t3]
|
||||
|
||||
For every group, at this step, draggables lists are equal.
|
||||
|
||||
|
||||
3. For every group, lists of targets are compared using rule for that group.
|
||||
|
||||
|
||||
Set and '+number' cases
|
||||
.......................
|
||||
|
||||
Set() and '+number' are needed only for case of reusable draggables,
|
||||
for other cases there are no equal draggables in list, so set() does nothing.
|
||||
|
||||
.. such long lines needed for sphinx to display nicely
|
||||
|
||||
* Usage of set() operation allows easily create rule for case of "any number of same draggable can be dragged to some targets"::
|
||||
|
||||
{
|
||||
'draggables': ['draggable_1'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
* 'number' rule is used for the case of reusable draggables, when one want to fix number of draggable to drag. In this example only two instances of draggables_1 are allowed to be dragged::
|
||||
|
||||
{
|
||||
'draggables': ['draggable_1', 'draggable_1'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
|
||||
|
||||
* Note, that in using rule 'exact', one does not need 'number', because you can't recognize from user interface which reusable draggable is on which target. Absurd example::
|
||||
|
||||
{
|
||||
'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
|
||||
|
||||
Correct handling of this example is to create different rules for draggable_1 and
|
||||
draggable_2
|
||||
|
||||
* For 'unordered_equal' (or 'exact' too) we don't need 'number' if you have only same draggable in group, as targets length will provide constraint for the number of draggables::
|
||||
|
||||
{
|
||||
'draggables': ['draggable_1'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
|
||||
|
||||
This means that only three draggaggables 'draggable_1' can be dragged.
|
||||
|
||||
* But if you have more that one different reusable draggable in list, you may use 'number' rule::
|
||||
|
||||
{
|
||||
'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}
|
||||
|
||||
|
||||
If not use number, draggables list will be setted to ['draggable_1', 'draggable_2']
|
||||
|
||||
|
||||
|
||||
|
||||
Logic flow
|
||||
----------
|
||||
|
||||
(Click on image to see full size version.)
|
||||
|
||||
.. image:: draganddrop_logic_flow.png
|
||||
:width: 100%
|
||||
:target: _images/draganddrop_logic_flow.png
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Examples of draggables that can't be reused
|
||||
-------------------------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo.xml
|
||||
|
||||
Draggables can be reused
|
||||
------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo2.xml
|
||||
BIN
docs/source/draganddrop_logic_flow.png
Normal file
BIN
docs/source/draganddrop_logic_flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
563
docs/source/graphical_slider_tool.rst
Normal file
563
docs/source/graphical_slider_tool.rst
Normal file
@@ -0,0 +1,563 @@
|
||||
*********************************************
|
||||
Xml format of graphical slider tool [xmodule]
|
||||
*********************************************
|
||||
|
||||
.. module:: xml_format_gst
|
||||
|
||||
|
||||
Format description
|
||||
==================
|
||||
|
||||
Graphical slider tool (GST) main tag is::
|
||||
|
||||
<graphical_slider_tool> BODY </graphical_slider_tool>
|
||||
|
||||
``graphical_slider_tool`` tag must have two children tags: ``render``
|
||||
and ``configuration``.
|
||||
|
||||
|
||||
Render tag
|
||||
----------
|
||||
|
||||
Render tag can contain usual html tags mixed with some GST specific tags::
|
||||
|
||||
<slider/> - represents jQuery slider for changing a parameter's value
|
||||
<textbox/> - represents a text input field for changing a parameter's value
|
||||
<plot/> - represents Flot JS plot element
|
||||
|
||||
Also GST will track all elements inside ``<render></render>`` where ``id``
|
||||
attribute is set, and a corresponding parameter referencing that ``id`` is present
|
||||
in the configuration section below. These will be referred to as dynamic elements.
|
||||
|
||||
The contents of the <render> section will be shown to the user after
|
||||
all occurrences of::
|
||||
|
||||
<slider var="{parameter name}" [style="{CSS statements}"] />
|
||||
<textbox var="{parameter name}" [style="{CSS statements}"] />
|
||||
<plot [style="{CSS statements}"] />
|
||||
|
||||
have been converted to actual sliders, text inputs, and a plot graph.
|
||||
Everything in square brackets is optional. After initialization, all
|
||||
text input fields, sliders, and dynamic elements will be set to the initial
|
||||
values of the parameters that they are assigned to.
|
||||
|
||||
``{parameter name}`` specifies the parameter to which the slider or text
|
||||
input will be attached to.
|
||||
|
||||
[style="{CSS statements}"] specifies valid CSS styling. It will be passed
|
||||
directly to the browser without any parsing.
|
||||
|
||||
There is a one-to-one relationship between a slider and a parameter.
|
||||
I.e. for one parameter you can put only one ``<slider>`` in the
|
||||
``<render>`` section. However, you don't have to specify a slider - they
|
||||
are optional.
|
||||
|
||||
There is a many-to-one relationship between text inputs and a
|
||||
parameter. I.e. for one parameter you can put many '<textbox>' elements in
|
||||
the ``<render>`` section. However, you don't have to specify a text
|
||||
input - they are optional.
|
||||
|
||||
You can put only one ``<plot>`` in the ``<render>`` section. It is not
|
||||
required.
|
||||
|
||||
|
||||
Slider tag
|
||||
..........
|
||||
|
||||
Slider tag must have ``var`` attribute and optional ``style`` attribute::
|
||||
|
||||
<slider var='a' style="width:400px;float:left;" />
|
||||
|
||||
After processing, slider tags will be replaced by jQuery UI sliders with applied
|
||||
``style`` attribute.
|
||||
|
||||
``var`` attribute must correspond to a parameter. Parameters can be used in any
|
||||
of the ``function`` tags in ``functions`` tag. By moving slider, value of
|
||||
parameter ``a`` will change, and so result of function, that depends on parameter
|
||||
``a``, will also change.
|
||||
|
||||
|
||||
Textbox tag
|
||||
...........
|
||||
|
||||
Texbox tag must have ``var`` attribute and optional ``style`` attribute::
|
||||
|
||||
<textbox var="b" style="width:50px; float:left; margin-left:10px;" />
|
||||
|
||||
After processing, textbox tags will be replaced by html text inputs with applied
|
||||
``style`` attribute. If you want a readonly text input, then you should use a
|
||||
dynamic element instead (see section below "HTML tagsd with ID").
|
||||
|
||||
``var`` attribute must correspond to a parameter. Parameters can be used in any
|
||||
of the ``function`` tags in ``functions`` tag. By changing the value on the text input,
|
||||
value of parameter ``a`` will change, and so result of function, that depends on
|
||||
parameter ``a``, will also change.
|
||||
|
||||
|
||||
Plot tag
|
||||
........
|
||||
|
||||
Plot tag may have optional ``style`` attribute::
|
||||
|
||||
<plot style="width:50px; float:left; margin-left:10px;" />
|
||||
|
||||
After processing plot tags will be replaced by Flot JS plot with applied
|
||||
``style`` attribute.
|
||||
|
||||
|
||||
HTML tags with ID (dynamic elements)
|
||||
....................................
|
||||
|
||||
Any HTML tag with ID, e.g. ``<span id="answer_span_1">`` can be used as a
|
||||
place where result of function can be inserted. To insert function result to
|
||||
an element, element ID must be included in ``function`` tag as ``el_id`` attribute
|
||||
and ``output`` value must be ``"element"``::
|
||||
|
||||
<function output="element" el_id="answer_span_1">
|
||||
function add(a, b, precision) {
|
||||
var x = Math.pow(10, precision || 2);
|
||||
return (Math.round(a * x) + Math.round(b * x)) / x;
|
||||
}
|
||||
|
||||
return add(a, b, 5);
|
||||
</function>
|
||||
|
||||
|
||||
Configuration tag
|
||||
-----------------
|
||||
|
||||
The configuration tag contains parameter settings, graph
|
||||
settings, and function definitions which are to be plotted on the
|
||||
graph and that use specified parameters.
|
||||
|
||||
Configuration tag contains two mandatory tag ``functions`` and ``parameters`` and
|
||||
may contain another ``plot`` tag.
|
||||
|
||||
|
||||
Parameters tag
|
||||
..............
|
||||
|
||||
``Parameters`` tag contains ``parameter`` tags. Each ``parameter`` tag must have
|
||||
``var``, ``max``, ``min``, ``step`` and ``initial`` attributes::
|
||||
|
||||
<parameters>
|
||||
<param var="a" min="-10.0" max="10.0" step="0.1" initial="0" />
|
||||
<param var="b" min="-10.0" max="10.0" step="0.1" initial="0" />
|
||||
</parameters>
|
||||
|
||||
``var`` attribute links min, max, step and initial values to parameter name.
|
||||
|
||||
``min`` attribute is the minimal value that a parameter can take. Slider and input
|
||||
values can not go below it.
|
||||
|
||||
``max`` attribute is the maximal value that a parameter can take. Slider and input
|
||||
values can not go over it.
|
||||
|
||||
``step`` attribute is value of slider step. When a slider increase or decreases
|
||||
the specified parameter, it will do so by the amount specified with 'step'
|
||||
|
||||
``initial`` attribute is the initial value that the specified parameter should be
|
||||
set to. Sliders and inputs will initially show this value.
|
||||
|
||||
The parameter's name is specified by the ``var`` property. All occurrences
|
||||
of sliders and/or text inputs that specify a ``var`` property, will be
|
||||
connected to this parameter - i.e. they will reflect the current
|
||||
value of the parameter, and will be updated when the parameter
|
||||
changes.
|
||||
|
||||
If at lest one of these attributes is not set, then the parameter
|
||||
will not be used, slider's and/or text input elements that specify
|
||||
this parameter will not be activated, and the specified functions
|
||||
which use this parameter will not return a numeric value. This means
|
||||
that neglecting to specify at least one of the attributes for some
|
||||
parameter will have the result of the whole GST instance not working
|
||||
properly.
|
||||
|
||||
|
||||
Functions tag
|
||||
.............
|
||||
|
||||
For the GST to do something, you must defined at least one
|
||||
function, which can use any of the specified parameter values. The
|
||||
function expects to take the ``x`` value, do some calculations, and
|
||||
return the ``y`` value. I.e. this is a 2D plot in Cartesian
|
||||
coordinates. This is how the default function is meant to be used for
|
||||
the graph.
|
||||
|
||||
There are other special cases of functions. They are used mainly for
|
||||
outputting to elements, plot labels, or for custom output. Because
|
||||
the return a single value, and that value is meant for a single element,
|
||||
these function are invoked only with the set of all of the parameters.
|
||||
I.e. no ``x`` value is available inside them. They are useful for
|
||||
showing the current value of a parameter, showing complex static
|
||||
formulas where some parameter's value must change, and other useful
|
||||
things.
|
||||
|
||||
The different style of function is specified by the ``output`` attribute.
|
||||
|
||||
Each function must be defined inside ``function`` tag in ``functions`` tag::
|
||||
|
||||
<functions>
|
||||
<function output="element" el_id="answer_span_1">
|
||||
function add(a, b, precision) {
|
||||
var x = Math.pow(10, precision || 2);
|
||||
return (Math.round(a * x) + Math.round(b * x)) / x;
|
||||
}
|
||||
|
||||
return add(a, b, 5);
|
||||
</function>
|
||||
</functions>
|
||||
|
||||
The parameter names (along with their values, as provided from text
|
||||
inputs and/or sliders), will be available inside all defined
|
||||
functions. A defined function body string will be parsed internally
|
||||
by the browser's JavaScript engine and converted to a true JS
|
||||
function.
|
||||
|
||||
The function's parameter list will automatically be created and
|
||||
populated, and will include the ``x`` (when ``output`` is not specified or
|
||||
is set to ``"graph"``), and all of the specified parameter values (from sliders
|
||||
and text inputs). This means that each of the defined functions will have
|
||||
access to all of the parameter values. You don't have to use them, but
|
||||
they will be there.
|
||||
|
||||
Examples::
|
||||
|
||||
<function>
|
||||
return x;
|
||||
</function>
|
||||
|
||||
<function dot="true" label="\(y_2\)">
|
||||
return (x + a) * Math.sin(x * b);
|
||||
</function>
|
||||
|
||||
<function color="green">
|
||||
function helperFunc(c1) {
|
||||
return c1 * c1 - a;
|
||||
}
|
||||
return helperFunc(x + 10 * a * b) + Math.sin(a - x);
|
||||
</function>
|
||||
|
||||
Required parameters::
|
||||
|
||||
function body:
|
||||
|
||||
A string composing a normal JavaScript function
|
||||
except that there is no function declaration
|
||||
(along with parameters), and no closing bracket.
|
||||
|
||||
So if you normally would have written your
|
||||
JavaScript function like this:
|
||||
|
||||
function myFunc(x, a, b) {
|
||||
return x * a + b;
|
||||
}
|
||||
|
||||
here you must specify just the function body
|
||||
(everything that goes between '{' and '}'). So,
|
||||
you would specify the above function like so (the
|
||||
bare-bone minimum):
|
||||
|
||||
<function>return x * a + b;</function>
|
||||
|
||||
VERY IMPORTANT: Because the function will be passed
|
||||
to the browser as a single string, depending on implementation
|
||||
specifics, the end-of-line characters can be stripped. This
|
||||
means that single line JavaScript comments (starting with "//")
|
||||
can lead to the effect that everything after the first such comment
|
||||
will be treated as a comment. Therefore, it is absolutely
|
||||
necessary that such single line comments are not used when
|
||||
defining functions for GST. You can safely use the alternative
|
||||
multiple line JavaScript comments (such comments start with "/*"
|
||||
and end with "*/).
|
||||
|
||||
VERY IMPORTANT: If you have a large function body, and decide to
|
||||
split it into several lines, than you must wrap it in "CDATA" like
|
||||
so:
|
||||
|
||||
<function>
|
||||
<![CDATA[
|
||||
var dNew;
|
||||
|
||||
dNew = 0.3;
|
||||
|
||||
return x * a + b - dNew;
|
||||
]]>
|
||||
</function>
|
||||
|
||||
Optional parameters::
|
||||
|
||||
|
||||
color: Color name ('red', 'green', etc.) or in the form of
|
||||
'#FFFF00'. If not specified, a default color (different
|
||||
one for each graphed function) will be given by Flot JS.
|
||||
line: A string - 'true' or 'false'. Should the data points be
|
||||
connected by a line on the graph? Default is 'true'.
|
||||
dot: A string - 'true' or 'false'. Should points be shown for
|
||||
each data point on the graph? Default is 'false'.
|
||||
bar: A string - 'true' or 'false'. When set to 'true', points
|
||||
will be plotted as bars.
|
||||
label: A string. If provided, will be shown in the legend, along
|
||||
with the color that was used to plot the function.
|
||||
output: 'element', 'none', 'plot_label', or 'graph'. If not defined,
|
||||
function will be plotted (same as setting 'output' to 'graph').
|
||||
If defined, and other than 'graph', function will not be
|
||||
plotted, but it's output will be inserted into the element
|
||||
with ID specified by 'el_id' attribute.
|
||||
el_id: Id of HTML element, defined in '<render>' section. Value of
|
||||
function will be inserted as content of this element.
|
||||
disable_auto_return: By default, if JavaScript function string is written
|
||||
without a "return" statement, the "return" will be
|
||||
prepended to it. Set to "true" to disable this
|
||||
functionality. This is done so that simple functions
|
||||
can be defined in an easy fashion (for example, "a",
|
||||
which will be translated into "return a").
|
||||
update_on: A string - 'change', or 'slide'. Default (if not set) is
|
||||
'slide'. This defines the event on which a given function is
|
||||
called, and its result is inserted into an element. This
|
||||
setting is relevant only when "output" is other than "graph".
|
||||
|
||||
When specifying ``el_id``, it is essential to set "output" to one of
|
||||
element - GST will invoke the function, and the return of it will be
|
||||
inserted into a HTML element with id specified by ``el_id``.
|
||||
none - GST will simply inoke the function. It is left to the instructor
|
||||
who writes the JavaScript function body to update all necesary
|
||||
HTML elements inside the function, before it exits. This is done
|
||||
so that extra steps can be preformed after an HTML element has
|
||||
been updated with a value. Note, that because the return value
|
||||
from this function is not actually used, it will be tempting to
|
||||
omit the "return" statement. However, in this case, the attribute
|
||||
"disable_auto_return" must be set to "true" in order to prevent
|
||||
GST from inserting a "return" statement automatically.
|
||||
plot_label - GST will process all plot labels (which are strings), and
|
||||
will replace the all instances of substrings specified by
|
||||
``el_id`` with the returned value of the function. This is
|
||||
necessary if you want a label in the graph to have some changing
|
||||
number. Because of the nature of Flot JS, it is impossible to
|
||||
achieve the same effect by setting the "output" attribute
|
||||
to "element", and including a HTML element in the label.
|
||||
|
||||
The above values for "output" will tell GST that the function is meant for an
|
||||
HTML element (not for graph), and that it should not get an 'x' parameter (along
|
||||
with some value).
|
||||
|
||||
|
||||
[Note on MathJax and labels]
|
||||
............................
|
||||
|
||||
Independently of this module, will render all TeX code
|
||||
within the ``<render>`` section into nice mathematical formulas. Just
|
||||
remember to wrap it in one of::
|
||||
|
||||
\( and \) - for inline formulas (formulas surrounded by
|
||||
standard text)
|
||||
\[ and \] - if you want the formula to be a separate line
|
||||
|
||||
It is possible to define a label in standard TeX notation. The JS
|
||||
library MathJax will work on these labels also because they are
|
||||
inserted on top of the plot as standard HTML (text within a DIV).
|
||||
|
||||
If the label is dynamic, i.e. it will contain some text (numeric, or other)
|
||||
that has to be updated on a parameter's change, then one can define
|
||||
a special function to handle this. The "output" of such a function must be
|
||||
set to "none", and the JavaScript code inside this function must update the
|
||||
MathJax element by itself. Before exiting, MathJax typeset function should
|
||||
be called so that the new text will be re-rendered by MathJax. For example,
|
||||
|
||||
<render>
|
||||
...
|
||||
<span id="dynamic_mathjax"></span>
|
||||
</render>
|
||||
...
|
||||
<function output="none" el_id="dynamic_mathjax">
|
||||
<![CDATA[
|
||||
var out_text;
|
||||
|
||||
out_text = "\\[\\mathrm{Percent \\space of \\space treated \\space with \\space YSS=\\frac{"
|
||||
+(treated_men*10)+"\\space men *"
|
||||
+(your_m_tx_yss/100)+"\\space prev. +\\space "
|
||||
+((100-treated_men)*10)+"\\space women *"
|
||||
+(your_f_tx_yss/100)+"\\space prev.}"
|
||||
+"{1000\\space total\\space treated\\space patients}"
|
||||
+"="+drummond_combined[0][1]+"\\%}\\]";
|
||||
mathjax_for_prevalence_calcs+="\\[\\mathrm{Percent \\space of \\space untreated \\space with \\space YSS=\\frac{"
|
||||
+(untreated_men*10)+"\\space men *"
|
||||
+(your_m_utx_yss/100)+"\\space prev. +\\space "
|
||||
+((100-untreated_men)*10)+"\\space women *"
|
||||
+(your_f_utx_yss/100)+"\\space prev.}"
|
||||
+"{1000\\space total\\space untreated\\space patients}"
|
||||
+"="+drummond_combined[1][1]+"\\%}\\]";
|
||||
|
||||
$("#dynamic_mathjax").html(out_text);
|
||||
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub,"dynamic_mathjax"]);
|
||||
]]>
|
||||
</function>
|
||||
...
|
||||
|
||||
|
||||
Plot tag
|
||||
........
|
||||
|
||||
``Plot`` tag inside ``configuration`` tag defines settings for plot output.
|
||||
|
||||
Required parameters::
|
||||
|
||||
xrange: 2 functions that must return value. Value is constant (3.1415)
|
||||
or depend on parameter from parameters section:
|
||||
<xrange>
|
||||
<min>return 0;</min>
|
||||
<max>return 30;</max>
|
||||
</xrange>
|
||||
or
|
||||
<xrange>
|
||||
<min>return -a;</min>
|
||||
<max>return a;</max>
|
||||
</xrange>
|
||||
|
||||
All functions will be calculated over domain between xrange:min
|
||||
and xrange:max. Xrange depending on parameter is extremely
|
||||
useful when domain(s) of your function(s) depends on parameter
|
||||
(like circle, when parameter is radius and you want to allow
|
||||
to change it).
|
||||
|
||||
Optional parameters::
|
||||
|
||||
num_points: Number of data points to generated for the plot. If
|
||||
this is not set, the number of points will be
|
||||
calculated as width / 5.
|
||||
|
||||
bar_width: If functions are present which are to be plotted as bars,
|
||||
then this parameter specifies the width of the bars. A
|
||||
numeric value for this parameter is expected.
|
||||
|
||||
bar_align: If functions are present which are to be plotted as bars,
|
||||
then this parameter specifies how to align the bars relative
|
||||
to the tick. Available values are "left" and "center".
|
||||
|
||||
xticks,
|
||||
yticks: 3 floating point numbers separated by commas. This
|
||||
specifies how many ticks are created, what number they
|
||||
start at, and what number they end at. This is different
|
||||
from the 'xrange' setting in that it has nothing to do
|
||||
with the data points - it control what area of the
|
||||
Cartesian space you will see. The first number is the
|
||||
first tick's value, the second number is the step
|
||||
between each tick, the third number is the value of the
|
||||
last tick. If these configurations are not specified,
|
||||
Flot will chose them for you based on the data points
|
||||
set that he is currently plotting. Usually, this results
|
||||
in a nice graph, however, sometimes you need to fine
|
||||
grain the controls. For example, when you want to show
|
||||
a fixed area of the Cartesian space, even when the data
|
||||
set changes. On it's own, Flot will recalculate the
|
||||
ticks, which will result in a different graph each time.
|
||||
By specifying the xticks, yticks configurations, only
|
||||
the plotted data will change - the axes (ticks) will
|
||||
remain as you have defined them.
|
||||
|
||||
xticks_names, yticks_names:
|
||||
A JSON string which represents a mapping of xticks, yticks
|
||||
values to some defined strings. If specified, the graph will
|
||||
not have any xticks, yticks except those for which a string
|
||||
value has been defined in the JSON string. Note that the
|
||||
matching will be string-based and not numeric. I.e. if a tick
|
||||
value was "3.70" before, then inside the JSON there should be
|
||||
a mapping like {..., "3.70": "Some string", ...}. Example:
|
||||
|
||||
<xticks_names>
|
||||
<![CDATA[
|
||||
{
|
||||
"1": "Treated", "2": "Not Treated",
|
||||
"4": "Treated", "5": "Not Treated",
|
||||
"7": "Treated", "8": "Not Treated"
|
||||
}
|
||||
]]>
|
||||
</xticks_names>
|
||||
|
||||
<yticks_names>
|
||||
<![CDATA[
|
||||
{"0": "0%", "10": "10%", "20": "20%", "30": "30%", "40": "40%", "50": "50%"}
|
||||
]]>
|
||||
</yticks_names>
|
||||
|
||||
xunits,
|
||||
yunits: Units values to be set on axes. Use MathJax. Example:
|
||||
<xunits>\(cm\)</xunits>
|
||||
<yunits>\(m\)</yunits>
|
||||
|
||||
moving_label:
|
||||
A way to specify a label that should be positioned dynamically,
|
||||
based on the values of some parameters, or some other factors.
|
||||
It is similar to a <function>, but it is only valid for a plot
|
||||
because it is drawn relative to the plot coordinate system.
|
||||
|
||||
Multiple "moving_label" configurations can be provided, each one
|
||||
with a unique text and a unique set of functions that determine
|
||||
it's dynamic positioning.
|
||||
|
||||
Each "moving_label" can have a "color" attribute (CSS color notation),
|
||||
and a "weight" attribute. "weight" can be one of "normal" or "bold",
|
||||
and determines the styling of moving label's text.
|
||||
|
||||
Each "moving_label" function should return an object with a 'x'
|
||||
and 'y properties. Within those functions, all of the parameter
|
||||
names along with their value are available.
|
||||
|
||||
Example (note that "return" statement is missing; it will be automatically
|
||||
inserted by GST):
|
||||
|
||||
<moving_label text="Co" weight="bold" color="red>
|
||||
<![CDATA[ {'x': -50, 'y': c0};]]>
|
||||
</moving_label>
|
||||
|
||||
asymptote:
|
||||
Add a vertical or horizontal asymptote to the graph which will
|
||||
be dynamically repositioned based on the specified function.
|
||||
|
||||
It is similar to the function in that it provides a JavaScript body function
|
||||
string. This function will be used to calculate the position of the asymptote
|
||||
relative to the axis specified by the "type" parameter.
|
||||
|
||||
Required parameters:
|
||||
type:
|
||||
Which axis should the asymptote be plotted against. Available values
|
||||
are "x" and "y".
|
||||
|
||||
Optional parameters:
|
||||
color:
|
||||
The color of the line. A valid CSS color string is expected.
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
|
||||
Plotting, sliders and inputs
|
||||
----------------------------
|
||||
|
||||
.. literalinclude:: gst_example_with_documentation.xml
|
||||
|
||||
|
||||
Update of html elements, no plotting
|
||||
------------------------------------
|
||||
|
||||
.. literalinclude:: gst_example_html_element_output.xml
|
||||
|
||||
|
||||
Circle with dynamic radius
|
||||
--------------------------
|
||||
|
||||
.. literalinclude:: gst_example_dynamic_range.xml
|
||||
|
||||
|
||||
Example of a bar graph
|
||||
----------------------
|
||||
|
||||
.. literalinclude:: gst_example_bars.xml
|
||||
|
||||
|
||||
Example of moving labels of graph
|
||||
---------------------------------
|
||||
|
||||
.. literalinclude:: gst_example_dynamic_labels.xml
|
||||
64
docs/source/gst_example_bars.xml
Normal file
64
docs/source/gst_example_bars.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<vertical>
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
<h2>Graphic slider tool: Bar graph example.</h2>
|
||||
|
||||
<p>We can request the API to plot us a bar graph.</p>
|
||||
<div style="clear:both">
|
||||
<p style="width:60px;float:left;">a</p>
|
||||
<slider var='a' style="width:400px;float:left;"/>
|
||||
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
|
||||
<br /><br /><br />
|
||||
<p style="width:60px;float:left;">b</p>
|
||||
<slider var='b' style="width:400px;float:left;"/>
|
||||
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
|
||||
</div>
|
||||
<plot style="clear:left;"/>
|
||||
</render>
|
||||
<configuration>
|
||||
<parameters>
|
||||
<param var="a" min="-100" max="100" step="5" initial="25" />
|
||||
<param var="b" min="-100" max="100" step="5" initial="50" />
|
||||
</parameters>
|
||||
<functions>
|
||||
<function bar="true" color="blue" label="Men">
|
||||
<![CDATA[if (((x>0.9) && (x<1.1)) || ((x>4.9) && (x<5.1))) { return Math.sin(a * 0.01 * Math.PI + 2.952 * x); }
|
||||
else {return undefined;}]]>
|
||||
</function>
|
||||
<function bar="true" color="red" label="Women">
|
||||
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos(b * 0.01 * Math.PI + 3.432 * x); }
|
||||
else {return undefined;}]]>
|
||||
</function>
|
||||
<function bar="true" color="green" label="Other 1">
|
||||
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b - 10 * a) * 0.01 * Math.PI + 3.432 * x); }
|
||||
else {return undefined;}]]>
|
||||
</function>
|
||||
<function bar="true" color="yellow" label="Other 2">
|
||||
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b + 7 * a) * 0.01 * Math.PI + 3.432 * x); }
|
||||
else {return undefined;}]]>
|
||||
</function>
|
||||
</functions>
|
||||
<plot>
|
||||
<xrange><min>1</min><max>5</max></xrange>
|
||||
<num_points>5</num_points>
|
||||
<xticks>0, 0.5, 6</xticks>
|
||||
<yticks>-1.5, 0.1, 1.5</yticks>
|
||||
<xticks_names>
|
||||
<![CDATA[
|
||||
{
|
||||
"1.5": "Single", "4.5": "Married"
|
||||
}
|
||||
]]>
|
||||
</xticks_names>
|
||||
<yticks_names>
|
||||
<![CDATA[
|
||||
{
|
||||
"-1.0": "-100%", "-0.5": "-50%", "0.0": "0%", "0.5": "50%", "1.0": "100%"
|
||||
}
|
||||
]]>
|
||||
</yticks_names>
|
||||
<bar_width>0.4</bar_width>
|
||||
</plot>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
</vertical>
|
||||
40
docs/source/gst_example_dynamic_labels.xml
Normal file
40
docs/source/gst_example_dynamic_labels.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<vertical>
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
<h1>Graphic slider tool: Dynamic labels.</h1>
|
||||
<p>There are two kinds of dynamic lables.
|
||||
1) Dynamic changing values in graph legends.
|
||||
2) Dynamic labels, which coordinates depend on parameters </p>
|
||||
<p>a: <slider var="a"/></p>
|
||||
<br/>
|
||||
<p>b: <slider var="b"/></p>
|
||||
<br/><br/>
|
||||
<plot style="width:400px; height:400px;"/>
|
||||
</render>
|
||||
|
||||
<configuration>
|
||||
<parameters>
|
||||
<param var="a" min="-10" max="10" step="1" initial="0" />
|
||||
<param var="b" min="0" max="10" step="0.5" initial="5" />
|
||||
</parameters>
|
||||
<functions>
|
||||
<function label="Value of a is: dyn_val_1">a * x + b</function>
|
||||
<!-- dynamic values in legend -->
|
||||
<function output="plot_label" el_id="dyn_val_1">a</function>
|
||||
</functions>
|
||||
<plot>
|
||||
<xrange><min>0</min><max>30</max></xrange>
|
||||
<num_points>10</num_points>
|
||||
<xticks>0, 6, 30</xticks>
|
||||
<yticks>-9, 1, 9</yticks>
|
||||
<!-- custom labels with coordinates as any function of parameter -->
|
||||
<moving_label text="Dynam_lbl 1" weight="bold">
|
||||
<![CDATA[ {'x': 10, 'y': a};]]>
|
||||
</moving_label>
|
||||
<moving_label text="Dynam lbl 2" weight="bold">
|
||||
<![CDATA[ {'x': -6, 'y': b};]]>
|
||||
</moving_label>
|
||||
</plot>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
</vertical>
|
||||
37
docs/source/gst_example_dynamic_range.xml
Normal file
37
docs/source/gst_example_dynamic_range.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<vertical>
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
<h2>Graphic slider tool: Dynamic range and implicit functions.</h2>
|
||||
|
||||
<p>You can make x range (not ticks of x axis) of functions to depend on
|
||||
parameter value. This can be useful when function domain depends
|
||||
on parameter.</p>
|
||||
<p>Also implicit functons like circle can be plotted as 2 separate
|
||||
functions of same color.</p>
|
||||
<div style="height:50px;">
|
||||
<slider var='a' style="width:400px;float:left;"/>
|
||||
<textbox var='a' style="float:left;width:60px;margin-left:15px;"/>
|
||||
</div>
|
||||
<plot style="margin-top:15px;margin-bottom:15px;"/>
|
||||
</render>
|
||||
<configuration>
|
||||
<parameters>
|
||||
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
|
||||
</parameters>
|
||||
<functions>
|
||||
<function color="red">Math.sqrt(a * a - x * x)</function>
|
||||
<function color="red">-Math.sqrt(a * a - x * x)</function>
|
||||
</functions>
|
||||
<plot>
|
||||
<xrange>
|
||||
<!-- dynamic range -->
|
||||
<min>-a</min>
|
||||
<max>a</max>
|
||||
</xrange>
|
||||
<num_points>1000</num_points>
|
||||
<xticks>-30, 6, 30</xticks>
|
||||
<yticks>-30, 6, 30</yticks>
|
||||
</plot>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
</vertical>
|
||||
40
docs/source/gst_example_html_element_output.xml
Normal file
40
docs/source/gst_example_html_element_output.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<vertical>
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
<h2>Graphic slider tool: Output to DOM element.</h2>
|
||||
|
||||
<p>a + b = <span id="answer_span_1"></span></p>
|
||||
|
||||
<div style="clear:both">
|
||||
<p style="float:left;margin-right:10px;">a</p>
|
||||
<slider var='a' style="width:400px;float:left;"/>
|
||||
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
|
||||
</div>
|
||||
|
||||
<div style="clear:both">
|
||||
<p style="float:left;margin-right:10px;">b</p>
|
||||
<slider var='b' style="width:400px;float:left;"/>
|
||||
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<plot/>
|
||||
</render>
|
||||
<configuration>
|
||||
<parameters>
|
||||
<param var="a" min="-10.0" max="10.0" step="0.1" initial="0" />
|
||||
<param var="b" min="-10.0" max="10.0" step="0.1" initial="0" />
|
||||
</parameters>
|
||||
|
||||
<functions>
|
||||
<function output="element" el_id="answer_span_1">
|
||||
function add(a, b, precision) {
|
||||
var x = Math.pow(10, precision || 2);
|
||||
return (Math.round(a * x) + Math.round(b * x)) / x;
|
||||
}
|
||||
|
||||
return add(a, b, 5);
|
||||
</function>
|
||||
</functions>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
</vertical>
|
||||
91
docs/source/gst_example_with_documentation.xml
Normal file
91
docs/source/gst_example_with_documentation.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<vertical>
|
||||
<graphical_slider_tool>
|
||||
<render>
|
||||
|
||||
<h2>Graphic slider tool: full example.</h2>
|
||||
<p>
|
||||
A simple equation
|
||||
\(
|
||||
y_1 = 10 \times b \times \frac{sin(a \times x) \times sin(b \times x)}{cos(b \times x) + 10}
|
||||
\)
|
||||
can be plotted.
|
||||
</p>
|
||||
|
||||
<!-- make text and input or slider at the same line -->
|
||||
<div>
|
||||
<p style="float:left;"> Currently \(a\) is</p>
|
||||
<!-- readonly input for a -->
|
||||
<span id="a_readonly" style="width:50px; float:left; margin-left:10px;"/>
|
||||
</div>
|
||||
<!-- clear:left will make next text to begin from new line -->
|
||||
<p style="clear:left"> This one
|
||||
\(
|
||||
y_2 = sin(a \times x)
|
||||
\)
|
||||
will be overlayed on top.
|
||||
</p>
|
||||
<div>
|
||||
<p style="float:left;">Currently \(b\) is </p>
|
||||
<textbox var="b" style="width:50px; float:left; margin-left:10px;"/>
|
||||
</div>
|
||||
<div style="clear:left;">
|
||||
<p style="float:left;">To change \(a\) use:</p>
|
||||
<slider var="a" style="width:400px;float:left;margin-left:10px;"/>
|
||||
</div>
|
||||
<div style="clear:left;">
|
||||
<p style="float:left;">To change \(b\) use:</p>
|
||||
<slider var="b" style="width:400px;float:left;margin-left:10px;"/>
|
||||
</div>
|
||||
<plot style='clear:left;width:600px;padding-top:15px;padding-bottom:20px;'/>
|
||||
<div style="clear:left;height:50px;">
|
||||
<p style="float:left;">Second input for b:</p>
|
||||
<!-- editable input for b -->
|
||||
<textbox var="b" style="color:red;width:60px;float:left;margin-left:10px;"/>
|
||||
</div >
|
||||
</render>
|
||||
|
||||
<configuration>
|
||||
|
||||
<parameters>
|
||||
<param var="a" min="90" max="120" step="10" initial="100" />
|
||||
<param var="b" min="120" max="200" step="2.3" initial="120" />
|
||||
</parameters>
|
||||
|
||||
<functions>
|
||||
|
||||
<function color="#0000FF" line="false" dot="true" label="\(y_1\)">
|
||||
return 10.0 * b * Math.sin(a * x) * Math.sin(b * x) / (Math.cos(b * x) + 10);
|
||||
</function>
|
||||
<function color="red" line="true" dot="false" label="\(y_2\)">
|
||||
<!-- works w/o return, if function is single line -->
|
||||
Math.sin(a * x);
|
||||
</function>
|
||||
<function color="#FFFF00" line="false" dot="false" label="unknown">
|
||||
function helperFunc(c1) {
|
||||
return c1 * c1 - a;
|
||||
}
|
||||
|
||||
return helperFunc(x + 10 * a * b) + Math.sin(a - x);
|
||||
</function>
|
||||
<function output="element" el_id="a_readonly">a</function>
|
||||
</functions>
|
||||
|
||||
<plot>
|
||||
|
||||
<xrange>
|
||||
<min>return 0;</min>
|
||||
<!-- works w/o return -->
|
||||
<max>30</max>
|
||||
</xrange>
|
||||
|
||||
<num_points>120</num_points>
|
||||
|
||||
<xticks>0, 3, 30</xticks>
|
||||
<yticks>-1.5, 1.5, 13.5</yticks>
|
||||
|
||||
<xunits>\(cm\)</xunits>
|
||||
<yunits>\(m\)</yunits>
|
||||
</plot>
|
||||
</configuration>
|
||||
</graphical_slider_tool>
|
||||
</vertical>
|
||||
@@ -14,7 +14,7 @@ Contents:
|
||||
overview.rst
|
||||
common-lib.rst
|
||||
djangoapps.rst
|
||||
|
||||
xml_formats.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user