commit
483400d3cb
@ -0,0 +1,293 @@
|
||||
import os
|
||||
import ast
|
||||
import time
|
||||
import uuid
|
||||
import fcntl
|
||||
import base64
|
||||
import shutil
|
||||
import signal
|
||||
import string
|
||||
import random
|
||||
import tempfile
|
||||
import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
always_rerender = True
|
||||
|
||||
# inherit things from the pythoncode qtype
|
||||
tutor.qtype_inherit('pythoncode')
|
||||
|
||||
time = time.time
|
||||
|
||||
defaults['csq_show_skeleton'] = False
|
||||
defaults['csq_extra_tests'] = []
|
||||
defaults['csq_submission_filename'] = 'lab.py'
|
||||
defaults['csq_npoints'] = 0
|
||||
|
||||
# we don't want the "Run Code" button from pythoncode
|
||||
del checktext
|
||||
del handle_check
|
||||
|
||||
def total_points(**info):
|
||||
return info['csq_npoints']
|
||||
|
||||
# stupid little helper to safely close a file descriptor
|
||||
def safe_close(fd):
|
||||
try:
|
||||
os.close(fd)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def render_html(last_log, **info):
|
||||
name = info['csq_name']
|
||||
init = last_log.get(name, (None, info['csq_initial']))
|
||||
if isinstance(init, str):
|
||||
fname = ''
|
||||
else:
|
||||
fname, init = init
|
||||
params = {
|
||||
'name': name,
|
||||
'init': str(init),
|
||||
'safeinit': (init or '').replace('<', '<'),
|
||||
'b64init': base64.b64encode(make_initial_display(info).encode()).decode(),
|
||||
'dl': (' download="%s"' % info['csq_skeleton_name'])
|
||||
if 'csq_skeleton_name' in info else 'download',
|
||||
'dl2': (' download="%s"' % fname)
|
||||
if 'csq_skeleton_name' in info else 'download',
|
||||
}
|
||||
out = ''
|
||||
if last_log.get(name, None) is not None:
|
||||
try:
|
||||
fname, loc = last_log[name]
|
||||
loc = os.path.basename(loc)
|
||||
qstring = urlencode({'path': json.dumps(info['cs_path_info']),
|
||||
'fname': loc})
|
||||
out += '<br/>'
|
||||
safe_fname = fname.replace('<', '').replace('>', '').replace('"', '').replace("'", '')
|
||||
out += ('<a href="%s/cs_util/get_upload?%s" '
|
||||
'download="%s">Download Your '
|
||||
'Last Submission</a><br/>') % (info['cs_url_root'], qstring, safe_fname)
|
||||
except:
|
||||
pass
|
||||
out += ('''<a href="%s/%s/lab_viewer?lab=%s&name=%s&as=%s" target="_blank"> '''
|
||||
'''Click to View Your Last Submission</a><br />''') % (info['cs_url_root'], info['cs_course'], '/'.join(info['cs_path_info'][1:]),name,info['cs_username'])
|
||||
out += '''<input type="file" style="display: none" id=%(name)s name="%(name)s" />''' % params
|
||||
out += ('''<button class="btn btn-catsoop" id="%s_select_button">Select File</button> '''
|
||||
'''<tt><span id="%s_selected_file">No file selected</span></tt>''') % (name, name)
|
||||
out += ('''<script type="text/javascript">'''
|
||||
'''$('#%s_select_button').click(function (){$("#%s").click();});'''
|
||||
'''$('#%s').change(function (){$('#%s_selected_file').text($('#%s').val());});'''
|
||||
'''</script>''') % (name, name, name, name, name)
|
||||
return out
|
||||
|
||||
|
||||
# helper to correctly write our strings
|
||||
def pluralize(things):
|
||||
l = len(things)
|
||||
leader = 's ' if l > 1 else ' '
|
||||
if l == 1:
|
||||
rest = str(things[0])
|
||||
elif l == 2:
|
||||
rest = '%s and %s' % tuple(things)
|
||||
else:
|
||||
rest = '%s, and %s' % (', '.join(map(str, things[:-1])), things[-1])
|
||||
return leader + rest
|
||||
|
||||
GET_TESTS_CODE = """
|
||||
def get_tests(x):
|
||||
if isinstance(x, unittest.suite.TestSuite):
|
||||
out = []
|
||||
for i in x._tests:
|
||||
out.extend(get_tests(i))
|
||||
return out
|
||||
elif isinstance(x, list):
|
||||
out = []
|
||||
for i in x:
|
||||
out.extend(get_tests(i))
|
||||
return out
|
||||
else:
|
||||
return [x.id().split()[0]]
|
||||
"""
|
||||
|
||||
|
||||
# this is a little bit involved, but so much better with the new supporting
|
||||
# infrastructure!
|
||||
def handle_submission(submissions, **info):
|
||||
try:
|
||||
code = info['csm_loader'].get_file_data(info, submissions, info['csq_name'])
|
||||
code = code.decode().replace('\r\n', '\n')
|
||||
except:
|
||||
return {'score': 0, 'msg': '<div class="bs-callout bs-callout-danger"><span class="text-danger"><b>Error:</b> Unable to decode the specified file. Is this the file you intended to upload?</span></div>'}
|
||||
|
||||
# okay... here we go...
|
||||
# first grab the safe interpreter to use during checking
|
||||
# and the location of the sandbox
|
||||
sandbox_interpreter = info['csq_python_interpreter']
|
||||
id_ = uuid.uuid4().hex
|
||||
this_sandbox_location = os.path.join(info.get('csq_sandbox_dir', '/tmp/sandbox'), '009checks', id_)
|
||||
|
||||
# make sure the sandbox exists
|
||||
shutil.rmtree(this_sandbox_location, True)
|
||||
# now dump the files there.
|
||||
# first, copy the regular files over (test cases, etc)
|
||||
shutil.copytree(os.path.join(info['cs_data_root'], 'courses', *info['cs_path_info'], '_files'), this_sandbox_location)
|
||||
|
||||
# then save the user's code with the appropriate name
|
||||
with open(os.path.join(this_sandbox_location, info['csq_submission_filename']), 'w') as f:
|
||||
f.write(code)
|
||||
|
||||
# and put our modified test.py in place
|
||||
magic = None
|
||||
while magic is None or magic in code:
|
||||
magic = ''.join(random.choice(string.ascii_letters) for _ in range(50))
|
||||
test_filename = os.path.join(this_sandbox_location, 'test.py')
|
||||
with open(test_filename) as f:
|
||||
labtest = f.read()
|
||||
labtest = labtest + '\n\nimport unittest.suite\n%s\nprint(%r, flush=True)\nr = res.result\nres.createTests()' % (GET_TESTS_CODE, magic)
|
||||
labtest += '\n_tests = get_tests(res.test)\nprint(_tests, flush=True)'
|
||||
names = ('errors', 'failures', 'skipped', 'unexpectedSuccesses')
|
||||
labtest += '\n_failed = {i[0].id().split()[0] for i in sum([getattr(r, i, []) for i in %r], [])}' % (names, )
|
||||
labtest += '\nprint([i for i in _tests if i not in _failed], flush=True)'
|
||||
with open(os.path.join(this_sandbox_location, 'test.py'), 'w') as f:
|
||||
f.write(labtest)
|
||||
|
||||
# at this point, everything should be in place. time to actually run the check.
|
||||
if 'csq_tests_to_run' in info:
|
||||
tests_to_run = info['csq_tests_to_run']
|
||||
else:
|
||||
tests_to_run = [{'args': [], 'timeout': info.get('csq_timeout', 2)}] # each test is a 2-tuple: arguments to be given to test.py, and the timeout for this test.
|
||||
|
||||
response = ''
|
||||
overall_passed = []
|
||||
overall_tests = []
|
||||
for count, test in enumerate(tests_to_run):
|
||||
test['args'] = list(map(str, test['args']))
|
||||
|
||||
# set up stdout and stderr
|
||||
_o, outfname = tempfile.mkstemp()
|
||||
_e, errfname = tempfile.mkstemp()
|
||||
|
||||
def _prep():
|
||||
os.setpgrp()
|
||||
info['csm_process'].set_pdeathsig()()
|
||||
|
||||
# run the test, keeping track of time
|
||||
start = time.time()
|
||||
proc = subprocess.Popen([sandbox_interpreter, 'test.py'] + test['args'],
|
||||
cwd=this_sandbox_location,
|
||||
bufsize=0,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
preexec_fn=_prep)
|
||||
out = ''
|
||||
err = ''
|
||||
try:
|
||||
out, err = proc.communicate(timeout=test['timeout'])
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
out, err = proc.communicate()
|
||||
out = out.decode()
|
||||
err = err.decode()
|
||||
|
||||
stop = time.time()
|
||||
|
||||
# hide a little bit of information in stack traces, just in case
|
||||
out = out.replace(this_sandbox_location, 'TESTING_SANDBOX')
|
||||
err = err.replace(this_sandbox_location, 'TESTING_SANDBOX')
|
||||
|
||||
# try to separate our logging output from regular output from the program
|
||||
sout = out.rsplit(magic, 1)
|
||||
failmsg = None
|
||||
if len(sout) == 1:
|
||||
# magic number didn't show up. we didn't reach the print.
|
||||
# infinite loop / timeout?
|
||||
alltests = []
|
||||
passed = []
|
||||
overall_tests = None
|
||||
numtests = None
|
||||
info['cs_debug']('FAILED2', info.get('cs_username', None), repr(out), repr(err))
|
||||
elif len(sout) > 2:
|
||||
# wtf? more than one magic number should not happen.
|
||||
failmsg = 'There was an unidentified error when running this test.'
|
||||
alltests = []
|
||||
passed = []
|
||||
overall_tests = None
|
||||
numtests = None
|
||||
else:
|
||||
# this is where we hope to be.
|
||||
# compute the effect of this test on the overall score
|
||||
out, log = sout
|
||||
res = log.strip().splitlines()
|
||||
try:
|
||||
alltests, passed = [ast.literal_eval(i) for i in res]
|
||||
except:
|
||||
failmsg = 'There was an unidentified error when running this test.'
|
||||
alltests = []
|
||||
passed = []
|
||||
overall_tests = None
|
||||
if overall_tests is not None:
|
||||
overall_tests += alltests
|
||||
numtests = len(alltests)
|
||||
|
||||
overall_passed += passed
|
||||
|
||||
# truncate the stdout if it is really long.
|
||||
outlines = out.strip().splitlines()
|
||||
out = '\n'.join(outlines)
|
||||
|
||||
if len(outlines) > 1000:
|
||||
outlines = outlines[:1000] + ['...OUTPUT TRUNCATED...']
|
||||
out = '\n'.join(outlines)
|
||||
if len(out) > 10000:
|
||||
out = out[:10000] + '\n...OUTPUT TRUNCATED...'
|
||||
|
||||
# how should the response be colored?
|
||||
# green if everything passed; red otherwise
|
||||
color = 'success' if alltests and set(alltests) == set(passed) else 'danger'
|
||||
|
||||
# now construct the HTML for this one test.
|
||||
response += '<div class="bs-callout bs-callout-%s">' % color
|
||||
response += '<p><b>Testing:</b> <code>python3 test.py%s</code> with a timeout of %.1f seconds</p>' % ((' ' if test['args'] else '') + ' '.join(test['args']), test['timeout'])
|
||||
if color == 'danger':
|
||||
response += '<b class="text-danger">Your code did not pass all test cases.</b><br/>'
|
||||
if failmsg is not None:
|
||||
response += failmsg
|
||||
elif numtests is not None:
|
||||
response += 'Your code passed %s of %s tests.<br/>' % (len(passed), len(alltests))
|
||||
else:
|
||||
response += 'Your code passed 0 tests.<br/>'
|
||||
else:
|
||||
if numtests == 1:
|
||||
response += '<b class="text-success">Your code passed this test case.</b>'
|
||||
else:
|
||||
response += '<b class="text-success">Your code passed all %s test cases.</b>' % len(alltests)
|
||||
response += '<p>Your code ran for %f seconds.' % (stop - start)
|
||||
if proc.returncode == -9:
|
||||
response += ' The process running your code did not run to completion. It was killed because it would have taken longer than %s second%s to run.' % (test['timeout'], '' if test['timeout'] == 1 else 's')
|
||||
response += '</p>'
|
||||
|
||||
if out.strip() or err.strip():
|
||||
response += '<p><button onclick="$(\'#%s_%d_results_showhide\').toggle()">Show/Hide Output</button>' % (magic, count)
|
||||
response += '<div id="%s_%d_results_showhide" style="display:none">' % (magic, count)
|
||||
if out.strip():
|
||||
response += '<p>Your code produced the following output:</p>'
|
||||
response += '<p><pre>%s</pre></p>' % out.replace('<','<')
|
||||
|
||||
if err.strip():
|
||||
response += '<p>This test produced the following output:</p>'
|
||||
response += '<p><pre>%s</pre></p>' % err.replace('<','<')
|
||||
response += '</div>'
|
||||
response += '</div>'
|
||||
|
||||
# all tests are done!
|
||||
# clean up (delete all the files associated with this request)
|
||||
shutil.rmtree(this_sandbox_location, True)
|
||||
|
||||
if overall_tests is not None:
|
||||
response = '<h3>Test Results:</h3><p>Your code passed %d of %d tests.</p>%s' % (len(overall_passed), len(overall_tests), response)
|
||||
else:
|
||||
response = '<h3>Test Results:</h3><p>Your code passed %d tests.</p>%s' % (len(overall_passed), response)
|
||||
return {'score': overall_passed, 'msg': response, 'extra_data': {'passed': overall_passed, 'run': overall_tests}}
|
@ -0,0 +1 @@
|
||||
role='Admin'
|
@ -0,0 +1 @@
|
||||
role = "Student"
|
@ -0,0 +1,11 @@
|
||||
Hello! This sample course, which is designed to go over some of the basics of
|
||||
CAT-SOOP, is set up to mirror the structure of a regular CAT-SOOP course.
|
||||
|
||||
This is the main page, which, for a normal class, might contain a calendar, or
|
||||
weekly announcements, or links to assignments. The contents of this page are
|
||||
contained in the `content.catsoop` in the root of the `sample_course`
|
||||
directory. You should also take a look at `preload.py`, which controls much of
|
||||
the behavior of this page.
|
||||
|
||||
Additionally, more detailed information is available from the "Pages" dropdown
|
||||
at the top of this page.
|
After Width: | Height: | Size: 106 KiB |
@ -0,0 +1,60 @@
|
||||
This page briefly describes some pieces of the input syntax for CAT-SOOP pages.
|
||||
|
||||
<section>Markdown</section>
|
||||
|
||||
Markdown works as expected, plus \$...\$ can be used for $\LaTeX$-style math.
|
||||
|
||||
* This is **bold**.
|
||||
* This is _italicized_.
|
||||
* This is `teletype`.
|
||||
|
||||
Some math:
|
||||
|
||||
$$-1 + \frac{1}{3} - \frac{1}{5} + \frac{1}{7} - \frac{1}{9} + \cdots = \sum_{n=1}^\infty \frac{\left(-1\right)^n}{2n - 1} = -\frac{\pi}{4}$$
|
||||
|
||||
Math is rendered with $\KaTeX$ (<https://github.com/Khan/KaTeX>) when possible,
|
||||
and with MathJax (<https://www.mathjax.org/>) as a fallback.
|
||||
|
||||
<section>Python</section>
|
||||
|
||||
Python code can be included within <code><python></code> tags. Anything
|
||||
that is printed within these tags will be rendered to the screen. For example,
|
||||
the following is a random number generated by Python:
|
||||
<python>
|
||||
import random
|
||||
print(random.randint(0, 20))
|
||||
</python>
|
||||
|
||||
There is also a shorter syntax for including pre-computed values. `\@{...}`
|
||||
can be used to print a single value. For example: @{random.randint(30, 70)}
|
||||
|
||||
It is also possible to specify how the value should be formatted. For example:
|
||||
@%.02f{7}
|
||||
|
||||
<section>Images and Hyperlinks</section>
|
||||
|
||||
Images and hyperlinks can be included using regular HTML or Markdown syntax:
|
||||
|
||||
<a href="COURSE/markdown/stinkbug.png">Link</a>
|
||||
|
||||

|
||||
|
||||
In either case, the `src` or `href` can either be a regular URL, or it can be a
|
||||
special form for use within CAT-SOOP. For locations starting with `CURRENT`,
|
||||
CAT-SOOP will start looking in the current directory. For locations starting
|
||||
with `COURSE`, CAT-SOOP will start looking the course's root directory.
|
||||
|
||||
These kinds of lookups first look for a regular CAT-SOOP page with the given
|
||||
name. If one isn't found, CAT-SOOP will look in the corresponding `__STATIC__`
|
||||
directory.
|
||||
|
||||
<section>Question</section>
|
||||
|
||||
One of the main uses of CAT-SOOP is to collect and assess responses to questions.
|
||||
This is handled with the <code><question></code> tag, which is decribed
|
||||
in more detail [on this page](COURSE/questions).
|
||||
|
||||
<question number>
|
||||
csq_prompt='What is 2+2? '
|
||||
csq_soln='4'
|
||||
</question>
|
@ -0,0 +1 @@
|
||||
cs_long_name = 'Markdown'
|
@ -0,0 +1,165 @@
|
||||
# preload.py at each level defines special variables and/or functions to be
|
||||
# inherited by pages farther down the tree.
|
||||
|
||||
# LOOK AND FEEL
|
||||
|
||||
cs_base_color = "#A31F34" # the base color
|
||||
cs_header = '6.SAMP' # the upper-left corner
|
||||
cs_icon_url = 'COURSE/favicon_local.gif' # the favicon, if any
|
||||
# the 'header' text for the page
|
||||
cs_long_name = cs_content_header = "Sample Course"
|
||||
cs_title = 'Sample Course - CAT-SOOP' # the browser's title bar
|
||||
|
||||
# don't try to parse markdown inside of these tags
|
||||
cs_markdown_ignore_tags = ('script', 'svg', 'textarea')
|
||||
|
||||
# defines the menu at the top of the page in the default template.
|
||||
# each dictionary defines one menu item and should contain two keys:
|
||||
# * text: the text to show for the link
|
||||
# * link: the target of the link (either a URL or another list of this same form)
|
||||
cs_top_menu = [
|
||||
{'link': 'COURSE', 'text': 'Homepage'},
|
||||
{'text': 'Pages', 'link': [
|
||||
{'text': 'Structure', 'link': 'COURSE/structure'},
|
||||
{'text': 'Markdown', 'link': 'COURSE/markdown'},
|
||||
{'text': 'Questions', 'link': 'COURSE/questions'},
|
||||
]},
|
||||
# {'text': 'Sample Menu', 'link': [
|
||||
# {'link': 'COURSE/calendar', 'text': 'Calendar and Handouts'},
|
||||
# {'link': 'COURSE/announcements', 'text': 'Archived Announcements'},
|
||||
# 'divider',
|
||||
# {'link': 'COURSE/information', 'text': 'Basic Information'},
|
||||
# {'link': 'COURSE/schedule_staff', 'text': 'Schedule and Staff'},
|
||||
# {'link': 'COURSE/grading', 'text': 'Grading Policies'},
|
||||
# {'link': 'COURSE/collaboration', 'text': 'Collaboration Policy'},
|
||||
# ]},
|
||||
# {'text': 'Piazza', 'link': 'https://piazza.com/mit/spring17/601'},
|
||||
]
|
||||
|
||||
|
||||
# AUTHENTICATION
|
||||
|
||||
cs_auth_type='login' # use the default (username/password based) authentication method
|
||||
# for actually running a course at MIT, I like using OpenID Connect instead (https://oidc.mit.edu/).
|
||||
|
||||
# custom XML tag handling, copied from one i wrote for 6.01 ages ago. can
|
||||
# probably largely be ignored (or can be updated to handle other kinds of
|
||||
# tags).
|
||||
import re
|
||||
import hashlib
|
||||
import subprocess
|
||||
import shutil
|
||||
def environment_matcher(tag):
|
||||
return re.compile("""<%s>(?P<body>.*?)</%s>""" % (tag, tag), re.MULTILINE|re.DOTALL)
|
||||
def cs_course_handle_custom_tags(text):
|
||||
# CHECKOFFS AND CHECK YOURSELFS
|
||||
checkoffs = 0
|
||||
def docheckoff(match):
|
||||
nonlocal checkoffs
|
||||
d = match.groupdict()
|
||||
checkoffs += 1
|
||||
return '<div class="checkoff"><b>Checkoff %d:</b><p>%s</p><p><span id="queue_checkoff_%d"></span></p></div>' % (checkoffs, d['body'], checkoffs)
|
||||
text=re.sub(environment_matcher('checkoff'), docheckoff, text)
|
||||
|
||||
checkyourself = 0
|
||||
def docheckyourself(match):
|
||||
nonlocal checkyourself
|
||||
d = match.groupdict()
|
||||
checkyourself += 1
|
||||
return '<div class="question"><b>Check Yourself %d:</b><p>%s</p><p><span id="queue_checkyourself_%d"></span></p></div>' % (checkyourself, d['body'], checkyourself)
|
||||
text=re.sub(environment_matcher('checkyourself'), docheckyourself, text)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# PYTHON SANDBOX
|
||||
|
||||
csq_python3 = True
|
||||
csq_python_sandbox = "python"
|
||||
# something like the following can be used to use a sandboxed python
|
||||
# interpreter on the production copy (after following the directions at
|
||||
# https://catsoop.mit.edu/website/docs/installing/server_configuration for
|
||||
# setting up the sandbox):
|
||||
#
|
||||
#if 'localhost' in cs_url_root:
|
||||
# # locally, just use the system Python install
|
||||
# csq_python_interpreter = '/usr/bin/python3'
|
||||
#else:
|
||||
# # on the server, use the properly sandboxed python
|
||||
# csq_python_interpreter = '/home/ubuntu/py3_sandbox/bin/python3'
|
||||
|
||||
|
||||
# PERMISSIONS
|
||||
|
||||
# users' roles are determined by the files in the __USERS__ directory. each
|
||||
# has the form username.py other information (such as a section number, if
|
||||
# relevant) can be stored there as well but the system will look for role =
|
||||
# "Student" or similar, and use that to set the user's permissions.
|
||||
#
|
||||
# view: allowed to view the contents of a page
|
||||
# submit: allowed to submit to a page
|
||||
# view_all: always allowed to view every page, regardless of when it releases
|
||||
# submit_all: always allowed to submit to every question, regardless of when it releases or is due
|
||||
# impersonate: allowed to view the page "as" someone else
|
||||
# admin: administrative tasks (such as modifying group assignments)
|
||||
# whdw: allowed to see "WHDW" page (Who Has Done What)
|
||||
# email: allowed to send e-mail through CAT-SOOP
|
||||
# grade: allowed to submit grades
|
||||
cs_default_role = 'Guest'
|
||||
cs_permissions = {'Admin': ['view_all', 'submit_all', 'impersonate', 'admin', 'whdw', 'email', 'grade'],
|
||||
'Instructor': ['view_all', 'submit_all', 'impersonate', 'admin', 'whdw', 'email', 'grade'],
|
||||
'TA': ['view_all', 'submit_all', 'impersonate', 'whdw', 'email', 'grade'],
|
||||
'UTA': ['view_all', 'submit_all', 'impersonate', 'grade'],
|
||||
'LA': ['view_all', 'submit_all','impersonate', 'grade'],
|
||||
'Student': ['view', 'submit'],
|
||||
'Guest': ['view']}
|
||||
|
||||
|
||||
# TIMING
|
||||
|
||||
# release and due dates can always be specified in absolute terms:
|
||||
# "YYYY-MM-DD:HH:MM"
|
||||
# the following allows the use of relative times (and/or per-section times)
|
||||
# section_times maps section names to times (below, the default section has
|
||||
# lecture at 8am on Tuesdays)
|
||||
# this allows setting, e.g., cs_release_date = "lec:2" to mean "release this
|
||||
# at lecture time in week 2"
|
||||
# different sections will get different times if they are specified below.
|
||||
# adding section = 2, for example, to someone's __USERS__ file will cause the
|
||||
# system to look up the key 2 in the dictionary below.
|
||||
cs_first_monday = '2017-02-06:00:00'
|
||||
section_times = {'default': {'lec': 'T:08:00', 'lab': 'T:09:00', 'lab_due':'M+:22:00', 'soln':'S+:08:00', 'tut':'W:08:00'},
|
||||
|
||||
}
|
||||
|
||||
def cs_realize_time(meta, rel):
|
||||
try:
|
||||
start, end = rel.split(':')
|
||||
section = cs_user_info.get('section', 'default')
|
||||
rel = section_times.get(section, {}).get(start, section_times['default'].get(start, 'NEVER'))
|
||||
meta['cs_week_number'] = int(end)
|
||||
except:
|
||||
pass
|
||||
return csm_time.realize_time(meta,rel)
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# cs_post_load is invoked after the page is loaded but before it is rendered.
|
||||
# the example below shows the time at which the current page was last modified
|
||||
# (based on the Git history).
|
||||
def cs_post_load(context):
|
||||
if 'cs_long_name' in context:
|
||||
context['cs_content_header'] = context['cs_long_name']
|
||||
context['cs_title'] = '%s | %s' % (context['cs_long_name'], context['cs_title'])
|
||||
try:
|
||||
loc = os.path.abspath(os.path.join(context['cs_data_root'], 'courses', *context['cs_path_info']))
|
||||
git_info = subprocess.check_output(["git", "log", "--pretty=format:%h %ct",
|
||||
"-n1", "--", "content.catsoop",
|
||||
"content.md", "content.xml",
|
||||
"content.py"], cwd=loc)
|
||||
h, t = git_info.split()
|
||||
t = context['csm_time'].long_timestamp(datetime.fromtimestamp(float(t))).replace(';', ' at')
|
||||
context['cs_footer'] = 'This page was last updated on %s (revision <code>%s</code>).<br/> <br/>' % (t, h.decode())
|
||||
except:
|
||||
pass
|
@ -0,0 +1,305 @@
|
||||
This page describes some of the different kinds of questions that can be asked
|
||||
using things built in to CAT-SOOP. I'd also like, eventually, to expand this
|
||||
page to include some discussion of creating custom question types.
|
||||
|
||||
Questions are always specified using the <code><question></code> tag.
|
||||
Each question has a _type_ (which governs how the question is displayed, how
|
||||
the answers are checked, etc), as well as some options that are specific to
|
||||
that type of question. The options are provided as Python code inside the tag.
|
||||
|
||||
Right now, there aren't a lot of words on this page. But there are examples of
|
||||
all the built-in question types. Look at the source of this page
|
||||
(`sample_course/questions/content.catsoop`) to see how it was generated.
|
||||
|
||||
Note that you will need the `'submit'` permission to submit answers to these
|
||||
questions.
|
||||
|
||||
<section>Options Present in All Questions</section>
|
||||
|
||||
* `csq_prompt`: A piece of text to be displayed before the question's input.
|
||||
* `csq_name`: A unique identifier (alphanumeric only) for the question. If none is provided, a default value is used.
|
||||
* `csq_npoints`: The number of points the question should be worth.
|
||||
* `csq_check_function`: A function of two arguments (submitted value and expected value). If none is specified, a sane default function for the given question type is used. The return value must be one of:
|
||||
* A Boolean representing whether the answer was correct.
|
||||
* A single number between 0 and 1 representing a score.
|
||||
* A tuple/list of two elements. The first is a score, and the second is a string containing a message to be displayed to the user.
|
||||
* A dictionary mapping the keys `'score'` and `'msg'` to a score and a message, respectively.
|
||||
* `csq_explanation`: An optional piece of text describing why the answer is what it is. After a student views the answer, they are given the option to view the explanation as well.
|
||||
* `csq_grading_mode`: How the question should be graded. The default value is `'auto'`. The available modes are:
|
||||
* `'auto'`: Send the response to the asynchronous auto-grader to be checked. Nice in particular for checks that run for a long time, to prevent them eating up the web server's resources.
|
||||
* `'legacy'`: Check the response in the same request that submits it. Nice for short checks.
|
||||
* `'manual'`: No automatic score is assigned. Rather, the submission is logged so it can later be manually graded.
|
||||
|
||||
<section>Base Question Types</section>
|
||||
|
||||
<subsection>smallbox</subsection>
|
||||
|
||||
A small box accepting arbitrary text input.
|
||||
|
||||
<question smallbox>
|
||||
csq_prompt = 'Type the word "cat", with any capitalization: '
|
||||
csq_soln = 'CAT'
|
||||
csq_check_function = lambda sub, sol: sub.lower().strip() == sol.lower()
|
||||
csq_size = 30 # width of text box
|
||||
</question>
|
||||
|
||||
<subsection>bigbox</subsection>
|
||||
|
||||
A bigger box accepting arbitrary text input. Often used for survey responses.
|
||||
|
||||
<question bigbox>
|
||||
csq_prompt = 'Type an essay detailing your experiences. Any answer receives full credit.<br/>'
|
||||
csq_soln = 'CAT' # not necessary
|
||||
csq_rows = 5
|
||||
csq_cols = 50
|
||||
csq_check_function = lambda sub, sol: 1 # check functions two args
|
||||
</question>
|
||||
|
||||
<subsection>multiplechoice</subsection>
|
||||
|
||||
Multiple choice questions come in a few different forms, based on the value of `csq_renderer`. The default mode renders choices as a drop-down menu:
|
||||
|
||||
<question multiplechoice>
|
||||
csq_prompt='What color is an orange?'
|
||||
csq_options=['red', 'orange', 'yellow', 'green', 'blue', 'purple']
|
||||
csq_soln = 'orange'
|
||||
csq_explanation = '''While some oranges may be slightly different colors
|
||||
(particularly if they are not ripe), most oranges are, in fact, orange.'''
|
||||
#
|
||||
</question>
|
||||
|
||||
For complicated options (that use math or other formatting, for example), the options can be rendered as radio buttons:
|
||||
|
||||
<question multiplechoice>
|
||||
csq_renderer = 'radio'
|
||||
csq_prompt='Which of the following is right?'
|
||||
csq_options=['$x^2$', r'$\log\left(x\right)$', r'$\frac{x}{2}$']
|
||||
csq_soln = 0
|
||||
csq_soln_mode = 'index' # (otherwies csq_soln = csq_options[0])
|
||||
</question>
|
||||
|
||||
The forms above are all "multiple choice, single answer." For "multiple choice, multiplce answer" questions, you can use the `'checkbox'` renderer.
|
||||
|
||||
<question multiplechoice>
|
||||
csq_renderer = 'checkbox'
|
||||
csq_prompt='Which of the following are valid ways to greet someone? Check all that apply.'
|
||||
csq_options=['Aloha', 'Guten Tag', 'Auf Wiedersehen', 'Hey', 'Goodbye', 'Tschüß']
|
||||
csq_soln = [True, True, False, True, False, False]
|
||||
</question>
|
||||
|
||||
<section>Python-related Questions</section>
|
||||
|
||||
<subsection>pythonic</subsection>
|
||||
|
||||
`pythonic` questions expect a single Python expression as input, and a single
|
||||
Python expression as output.
|
||||
|
||||
<question pythonic>
|
||||
csq_prompt = 'Enter a list of at least 5 Python numbers, all distinct and all in the range $(0, 0.5]$:<br/>'
|
||||
csq_soln = [0.01, 0.1, 0.2, 0.3, 0.5]
|
||||
def csq_check_function(sub, sol):
|
||||
if not isinstance(sub, list):
|
||||
return 0, 'Please enter a Python list.'
|
||||
if not all(0 < i <= 0.5 for i in sub):
|
||||
return 0, 'All of the numbers must be in the range (0, 0.5].'
|
||||
if len(sub) != len(set(sub)):
|
||||
return 0, 'All the elements must be distinct.'
|
||||
if len(sub) < 5:
|
||||
return 0, 'Your list must contain at least 5 numbers.'
|
||||
return 1
|
||||
</question>
|
||||
|
||||
<subsection>pythonliteral</subsection>
|
||||
|
||||
`pythonliteral` is like `pythoncode`, except that it expects a single literal
|
||||
value instead of an abitrary expression. This can be useful for numbers.
|
||||
|
||||
<question pythonliteral>
|
||||
csq_prompt = 'What is $\sqrt{7}$, accurate to at least 3 decimal places? Entering `7**0.5` won\'t work!<br/>'
|
||||
csq_soln = 7**0.5
|
||||
csq_check_function = lambda sub, sol: abs(sub - sol) <= 1e-3
|
||||
</question>
|
||||
|
||||
<subsection>pythoncode</subsection>
|
||||
|
||||
For larger coding problems (e.g., writing a function or computing a value), the `pythoncode` question
|
||||
should be used. It is a little more complicated than the other question types.
|
||||
|
||||
Its default mode of operation compares the result from submitted code against
|
||||
the result of "exemplar" code with the same input. Test cases are specified via
|
||||
the option `csq_tests`.
|
||||
|
||||
There are multiple options for theinterface students see, controlled by the `csq_interface` option:
|
||||
|
||||
* `'textarea'`: A plain HTML box to paste code into.
|
||||
* `'ace'`: A nicer input box, with syntax highlighting.
|
||||
* `'upload'`: A file upload box.
|
||||
|
||||
Here is a minimal example:
|
||||
|
||||
<python>
|
||||
PRIMES = [127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181,
|
||||
191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263,
|
||||
269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
|
||||
353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433,
|
||||
439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521,
|
||||
523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613,
|
||||
617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701,
|
||||
709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
|
||||
811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887,
|
||||
907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997,
|
||||
1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063,
|
||||
1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151,
|
||||
1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229,
|
||||
1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301,
|
||||
1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409,
|
||||
1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481,
|
||||
1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553,
|
||||
1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619,
|
||||
1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709,
|
||||
1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789,
|
||||
1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879,
|
||||
1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987,
|
||||
1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063,
|
||||
2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137,
|
||||
2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239,
|
||||
2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311,
|
||||
2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389,
|
||||
2393, 2399, 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473,
|
||||
2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591,
|
||||
2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677,
|
||||
2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731,
|
||||
2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819,
|
||||
2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909,
|
||||
2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011,
|
||||
3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109,
|
||||
3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209,
|
||||
3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307,
|
||||
3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373,
|
||||
3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469,
|
||||
3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557,
|
||||
3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637,
|
||||
3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727,
|
||||
3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823,
|
||||
3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917,
|
||||
3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007,
|
||||
4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093,
|
||||
4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201,
|
||||
4211, 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271,
|
||||
4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373,
|
||||
4391, 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481,
|
||||
4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567,
|
||||
4583, 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657,
|
||||
4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759,
|
||||
4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871,
|
||||
4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957,
|
||||
4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023,
|
||||
5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119,
|
||||
5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233,
|
||||
5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347,
|
||||
5351, 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437,
|
||||
5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519,
|
||||
5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639,
|
||||
5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701,
|
||||
5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, 5801, 5807,
|
||||
5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869,
|
||||
5879, 5881, 5897, 5903, 5923, 5927, 5939, 5953, 5981, 5987, 6007,
|
||||
6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091,
|
||||
6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173, 6197, 6199,
|
||||
6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277,
|
||||
6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359,
|
||||
6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469,
|
||||
6473, 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571,
|
||||
6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679,
|
||||
6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779,
|
||||
6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863,
|
||||
6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961,
|
||||
6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039,
|
||||
7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159,
|
||||
7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247,
|
||||
7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369,
|
||||
7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489,
|
||||
7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
|
||||
7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649,
|
||||
7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727, 7741,
|
||||
7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867,
|
||||
7873, 7877, 7879, 7883, 7901, 7907, 7919]
|
||||
</python>
|
||||
|
||||
<question pythoncode>
|
||||
csq_interface = 'ace'
|
||||
|
||||
csq_prompt="""
|
||||
Define a function `prime` that takes a single integer argument
|
||||
and returns `True` if the argument is prime, and `False`
|
||||
otherwise.
|
||||
|
||||
You may assume that the function's argument is a nonnegative integer.
|
||||
0 and 1 should _not_ be considered prime numbers.
|
||||
"""
|
||||
|
||||
csq_initial="""def prime(x):
|
||||
pass # your code here!"""
|
||||
|
||||
csq_soln="""def prime(x):
|
||||
if x == 0 or x == 1:
|
||||
return False
|
||||
for i in range(2,int(x/2+1)):
|
||||
if x%i == 0:
|
||||
return False
|
||||
return True"""
|
||||
|
||||
csq_tests = [{'code':'ans = prime(997)'},
|
||||
{'code':'ans = [(i,prime(i)) for i in range(100)]'},
|
||||
] + [
|
||||
{'code':'ans = prime(%s)' % cs_random.choice(PRIMES)} for i in range(4)
|
||||
] + [
|
||||
{'code':'ans = prime(%s)' % cs_random.randint(5000,10000)} for i in range(4)
|
||||
]
|
||||
</question>
|
||||
|
||||
```py
|
||||
def fib(n):
|
||||
pass # comment
|
||||
```
|
||||
|
||||
For a bigger example from 6.145, see the following page (which requires the
|
||||
`matplotlib` package):
|
||||
|
||||
* [Linear Regression](CURRENT/linregress)
|
||||
|
||||
<section>Symbolic Expressions</section>
|
||||
|
||||
<subsection>expression</subsection>
|
||||
|
||||
This question type is used for checking symolic math expressions.
|
||||
|
||||
<question expression>
|
||||
csq_prompt = 'What is the square root of $\cos\omega + j\sin\omega$?<br/>'
|
||||
csq_syntax='python'
|
||||
csq_soln = ['sqrt(e**(j*omega))', 'e**(j*omega / 2)'] # can either be a single string, or a list of multiple correct answers.
|
||||
</question>
|
||||
|
||||
<section>Per-User Randomized Values</section>
|
||||
|
||||
Invoking the `csm_tutor.init_random()` function will seed the `random.Random`
|
||||
object stored in `cs_random` in such a way that the values it produces will be
|
||||
consistent when the same user revisits a page. This is an easy way to add some
|
||||
small variation to problems. For example, the following code will show
|
||||
different numbers to everyone who visits to page:
|
||||
|
||||
<python>
|
||||
# calling init_random() makes random numbers on the page always be the same for
|
||||
# a given user
|
||||
csm_tutor.init_random()
|
||||
|
||||
number_1 = cs_random.randint(1,9)
|
||||
number_2 = cs_random.randint(21,29)
|
||||
tsum = number_1 + number_2
|
||||
</python>
|
||||
|
||||
<question pythonic>
|
||||
csq_prompt = 'What is the sum of $@{number_1}$ and $@{number_2}$?<br/>'
|
||||
csq_soln = tsum
|
||||
csq_check_function = lambda submission, solution: abs(submission-solution) <= 1e-6
|
||||
</question>
|
@ -0,0 +1,366 @@
|
||||
<python>
|
||||
cs_content_header = cs_long_name
|
||||
|
||||
def sample_mean(xs):
|
||||
return sum(xs) / len(xs)
|
||||
|
||||
def sample_stddev(xs):
|
||||
m = sample_mean(xs)
|
||||
return (sum([(i-m)**2 for i in xs]) / (len(xs)-1))**0.5
|
||||
|
||||
def sample_cov(xs, ys):
|
||||
x_mean = sample_mean(xs)
|
||||
y_mean = sample_mean(ys)
|
||||
o = 0.0
|
||||
for index in range(len(xs)):
|
||||
o += (xs[index] - x_mean)*(ys[index] - y_mean)
|
||||
return o / (len(xs) - 1)
|
||||
|
||||
def pearson_r(xs, ys):
|
||||
return sample_cov(xs, ys) / (sample_stddev(xs)*sample_stddev(ys))
|
||||
|
||||
def lin_regress_2(xs, ys):
|
||||
r = pearson_r(xs, ys)
|
||||
m = r * sample_stddev(ys) / sample_stddev(xs)
|
||||
b = sample_mean(ys) - m * sample_mean(xs)
|
||||
return m, b, r
|
||||
|
||||
#xs and ys are defined in preload.py
|
||||
m, b, r = lin_regress_2(xs, ys)
|
||||
|
||||
import base64
|
||||
|
||||
# see preload.py for definition of the PlotWindow class.
|
||||
def make_regress_plot(data_x, data_y, m, b):
|
||||
fig = PlotWindow()
|
||||
fig.scatter(data_x, data_y)
|
||||
fig.plot([min(data_x), max(data_x)], [m*min(data_x)+b, m*max(data_x)+b], 'r')
|
||||
return fig._show(width='400')
|
||||
|
||||
fig = PlotWindow()
|
||||
fig.scatter(xs, ys)
|
||||
img1 = fig._show(width='400')
|
||||
</python>
|
||||
|
||||
<center><a href="https://www.youtube.com/watch?v=8qjl4lysi_s" target="_blank">Music for this Problem</a></center>
|
||||
|
||||
<section>Introduction</section>
|
||||
|
||||
In this exercise, we will write a program to compute a 2-dimensional _linear
|
||||
regression_ for a set of data. In short, we will try to explain the
|
||||
relationship between an independent variable $X$ and a single dependent
|
||||
variable $Y$ based on a set of $N$ experimental data points $[(x_0, y_0), (x_1,
|
||||
y_1), \ldots, (x_{N-1}, y_{N-1})]$.
|
||||
|
||||
In particular, we will try to find the line $y = mx + b$ that best approximates
|
||||
the relationship represented in the data. In addition, we will calculate
|
||||
[Pearson's r](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient)
|
||||
for the variables, which will tell us how strongly the two variables are
|
||||
correlated.
|
||||
|
||||
Here, we will consider the "best fit" line to be the one that minimizes the sum
|
||||
of squared errors between the sampled $y$ values and the values predicted by
|
||||
the line:
|
||||
|
||||
$$\underset{m, b}{\text{argmin}} \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
|
||||
|
||||
For example, here is a set of data:
|
||||
|
||||
<center>
|
||||
@{img1}
|
||||
</center>
|
||||
|
||||
And here is the line that best approximates the relationship between the two variables:
|
||||
|
||||
<center>
|
||||
@{make_regress_plot(xs, ys, m, b)}
|
||||
</center>
|
||||
|
||||
Next week, we will look at _using_ linear regression to solve some authentic
|
||||
problems, but for now we'll just focus on _implementing_ a linear regression
|
||||
(i.e., writing a program to _find_ the line, given a set of data).
|
||||
|
||||
As you might guess, this will be a fairly complicated program, and so, as we
|
||||
have seen before, we will break it down into smaller pieces first.
|
||||
|
||||
<section>Primitives</section>
|
||||
|
||||
We'll start by defining some primitives:
|
||||
|
||||
<subsection>Sample Mean</subsection>
|
||||
The _sample mean_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by:
|
||||
|
||||
$$\overline{X} = \frac{1}{N}\sum_{i=0}^{N-1} x_i$$
|
||||
|
||||
<subsection>Sample Variance</subsection>
|
||||
The _sample variance_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by the following (note the two separate forms):
|
||||
|
||||
$$\text{Var}(X) = \frac{1}{N-1}\sum_{i=0}^{N-1} (x_i - \overline{X})^2$$
|
||||
$$\text{Var}(X) = \frac{1}{N-1}\left(\sum_{i=0}^{N-1}(x_i^2) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)^2\right)$$
|
||||
|
||||
<subsection>Sample Standard Deviation</subsection>
|
||||
The _sample standard deviation_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by:
|
||||
|
||||
$$\sigma_X = \sqrt{\text{Var}(X)} = \sqrt{\frac{1}{N-1}\sum_{i=0}^{N-1} (x_i - \overline{X})^2}$$
|
||||
|
||||
<subsection>Sample Covariance</subsection>
|
||||
The _sample covariance_ of two variables $X$ and $Y$ (an estimate of the true underlying covariance of the two variables), each with $N$ samples, such that $y_i$ is associated with $x_i$, is given by the following (note the two separate forms):
|
||||
|
||||
$$\text{cov}(X, Y) = \frac{1}{N-1}\sum_{i=0}^{N-1} (x_i-\overline{X})(y_i-\overline{Y})$$
|
||||
$$\text{cov}(X, Y) = \frac{1}{N-1}\left(\sum_{i=0}^{N-1}(x_iy_i) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}y_i\right)\right)$$
|
||||
|
||||
<section>Finding the "Best Fit" Line</section>
|
||||
|
||||
We would like to find the values $m$ and $b$ for which $y = mx+b$ best approximates the data we've gathered.
|
||||
|
||||
Earlier, we said that the line we wanted to find was defined by the values of
|
||||
$m$ and $b$ that minimized the sum of squared errors between our sampled values
|
||||
and the values predicted by the line $y=mx+b$:
|
||||
|
||||
$$\underset{m, b}{\text{argmin}} \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
|
||||
|
||||
It is possible to solve for these values, and we will use them our
|
||||
implementation. The next section shows a derivation of the solutions for $m$
|
||||
and $b$. Understanding the derivation is not crucial for this exercise,
|
||||
however, and so you are welcome to skip the derivation and move on to
|
||||
<ref label="results"><a href="{link}">{type} {number}: {title}</a></ref>.
|
||||
|
||||
<subsection>Derivation</subsection>
|
||||
|
||||
Here, we want to find the values of $m$ and $b$ that minimize the sum of squared error (SSE):
|
||||
|
||||
$$\text{SSE} = \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
|
||||
|
||||
We can use calculus for this: we take the partial derivatives of this
|
||||
expression with respect to $m$ and $b$, respectively; and set each to 0 (the
|
||||
function has a minimum where its derivative is 0).
|
||||
|
||||
$$\frac{\delta}{\delta b}\text{SSE} = \sum_{i=0}^{N-1} -2(y_i - mx_i - b)$$
|
||||
|
||||
$$\frac{\delta}{\delta m}\text{SSE} = \sum_{i=0}^{N-1} -2x_i(y_i - mx_i - b)$$
|
||||
|
||||
<subsubsection>Solving for b</subsubsection>
|
||||
|
||||
Let's start by considering the partial derivative with respect to $b$. We want to find when this expression is 0.
|
||||
|
||||
$$\sum_{i=0}^{N-1} -2(y_i - mx_i - b) = 0$$
|
||||
|
||||
We can start by pulling the constant ($-2$) in front of the summation, and dividing both sides by $-2$:
|
||||
$$-2 \sum_{i=0}^{N-1} (y_i - mx_i - b) = 0$$
|
||||
$$\sum_{i=0}^{N-1} (y_i - mx_i - b) = 0$$
|
||||
|
||||
And distributing the summation operator:
|
||||
$$\sum_{i=0}^{N-1} y_i - \sum_{i=0}^{N-1}mx_i - \sum_{i=0}^{N-1}b = 0$$
|
||||
|
||||
We can pull the "$m$" and "$b$" constants out of their respective sums:
|
||||
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i - b\sum_{i=0}^{N-1}1 = 0$$
|
||||
|
||||
Then we can isolate the "$b$" term by moving it to the right-hand side of the equation:
|
||||
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i = b\sum_{i=0}^{N-1}1$$
|
||||
|
||||
And we can evaluate that sum:
|
||||
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i = Nb$$
|
||||
|
||||
Then, to solve for $b$, we can divide both sides by $N$:
|
||||
$$\frac{1}{N}\sum_{i=0}^{N-1} y_i - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right) = b$$
|
||||
|
||||
Notice that the two expressions on the left have the form of our _sample mean_ equation from above, so we know that this must be:
|
||||
$$\overline{Y} - m\overline{X} = b$$
|
||||
|
||||
<subsubsection>Solving for m</subsubsection>
|
||||
|
||||
Now we'll consider the partial derivative with respect to $m$, and again set it equal to zero.
|
||||
$$\sum_{i=0}^{N-1} -2x_i(y_i - mx_i - b) = 0$$
|
||||
|
||||
Then we can pull the constant $-2$ out in front of the sum and distribute the $x_i$ across the addition:
|
||||
$$-2 \sum_{i=0}^{N-1} (x_iy_i - mx_i^2 - bx_i) = 0$$
|
||||
|
||||
Again, we can divide both sides by $-2$:
|
||||
$$\sum_{i=0}^{N-1} (x_iy_i - mx_i^2 - bx_i) = 0$$
|
||||
|
||||
And we can distribute the summation operator and move the constants $m$ and $b$ outside the summations:
|
||||
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - b\sum_{i=0}^{N-1}x_i = 0$$
|
||||
|
||||
Next, we substitute in our earlier result for $b$:
|
||||
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - \left(\frac{1}{N}\sum_{i=0}^{N-1} y_i - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\right)\left(\sum_{i=0}^{N-1}x_i\right) = 0$$
|
||||
|
||||
And we distribute the multiplication:
|
||||
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) + m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = 0$$
|
||||
|
||||
Then we can move the terms with $m$ in them to the right-hand side of the equation:
|
||||
$$\sum_{i=0}^{N-1}x_iy_i - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = m\sum_{i=0}^{N-1}x_i^2 - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right)$$
|
||||
|
||||
And, to make the next step a _bit_ neater, we can multiply both sides by $N$:
|
||||
$$N\sum_{i=0}^{N-1}x_iy_i - \left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = mN\sum_{i=0}^{N-1}x_i^2 - m\left(\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right)$$
|
||||
|
||||
Dividing through to isolate $m$ gives us the following:
|
||||
$$\frac{\displaystyle{N\sum_{i=0}^{N-1}(x_iy_i) - \left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right)}}{\displaystyle{N\sum_{i=0}^{N-1}(x_i^2) - \left(\sum_{i=0}^{N-1}x_i\right)^2}} = m$$
|
||||
|
||||
Multiplying by ($\frac{1/N}{1/N}$) gets us to a slightly different form:
|
||||
|
||||
$$\frac{\displaystyle{\sum_{i=0}^{N-1}(x_iy_i) - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right)}}{\displaystyle{\sum_{i=0}^{N-1}(x_i^2) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)^2}} = m$$
|
||||
|
||||
Recalling the definitions of _sample variance_ and _sample covariance_, we can see that this is equivalent to:
|
||||
|
||||
$$\frac{\text{cov}(X, Y)}{\text{Var}(X)} = m$$
|
||||
|
||||
<subsection label="results">Results</subsection>
|
||||
|
||||
The analysis above leads us to the following conclusion:
|
||||
|
||||
$$m = \frac{\text{cov}(X, Y)}{\text{Var}(X)}$$
|
||||
|
||||
and
|
||||
|
||||
$$b = \overline{Y} - m\overline{X}$$
|
||||
|
||||
Here, the first equation tells us that the slope of the "best fit" line is the
|
||||
ratio of the covariance of $X$ and $Y$ to the variance of $X$. The second
|
||||
tells us that the line must pass through the point $(\overline{X}, \overline{Y})$.
|
||||
|
||||
<section>Pearson's R</section>
|
||||
|
||||
We will also define the Pearson Correlation Coefficient of two variables $X$ and $Y$ as:
|
||||
|
||||
$$r = \frac{\text{cov}(X,Y)}{\sigma_X\sigma_Y}$$
|
||||
|
||||
This number $r$, which is between $-1$ and $1$, tells us how strongly, and in what direction, the two variables are correlated: a value of 1 implies total positive linear correlation, 0 implies no linear correlation, and -1 implies total negative linear correlation.
|
||||
|
||||
<section>Your Task</section>
|
||||
|
||||
Your task in this exercise is to implement several Python functions related to
|
||||
the discussion above:
|
||||
|
||||
* `sample_mean(values)` should take as its lone argument a list of $x$ values, and it should return the sample mean of those values.
|
||||
|
||||
* `sample_var(values)` should take a list of $x$ values as its argument, and it should return the sample variance of those values.
|
||||
|
||||
* `sample_std(values)` should take a list of $x$ values as its argument, and it should return the sample standard deviation of those values.
|
||||
|
||||
* `sample_cov(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return the sample covariance of $X$ and $Y$.
|
||||
|
||||
* `pearson_r(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return Pearson's r value for $X$ and $Y$.
|
||||
|
||||
* `linear_regression(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return a tuple of three elements, in this order:
|
||||
|
||||
* the slope of the "best-fit" line,
|
||||
* the vertical intercept of the "best-fit" line, and
|
||||
* the Pearson r value for the regression
|
||||
|
||||
**You should implement this code _without the use of `numpy` or `scipy`_.**
|
||||
|
||||
You will want to do some testing on your own machine first! Many explanations
|
||||
of lineare regression have small examples that are good for testing (for
|
||||
example, . You can also try doing a test with values that are exactly on the
|
||||
same line (what should the $m$, $b$, and $r$ values be in that case?), or on
|
||||
other small data sets.
|
||||
|
||||
In order to keep things organized, you should try to implement these functions
|
||||
in terms of the others whenever possible!
|
||||
|
||||
|
||||
<section>Submission</section>
|
||||
|
||||
<question pythoncode>
|
||||
csq_interface="upload"
|
||||
csq_show_skeleton=False
|
||||
|
||||
csq_sandbox_options = {'BADIMPORT': {'numpy', 'scipy'}}
|
||||
|
||||
import ast
|
||||
import random
|
||||
|
||||
def check_num(x, y):
|
||||
try:
|
||||
return abs(x['result']-y['result']) <= 1e-2
|
||||
except:
|
||||
return False
|
||||
|
||||
csq_tests = [
|
||||
{'code': 'values = %r\nans=sample_mean(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
|
||||
]
|
||||
|
||||
csq_tests += [
|
||||
{'code': 'values = %r\nans=sample_var(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
|
||||
]
|
||||
|
||||
csq_tests += [
|
||||
{'code': 'values = %r\nans=sample_std(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
|
||||
]
|
||||
|
||||
|
||||
|
||||
def checkregression(sub, soln):
|
||||
x = sub['result']
|
||||
y = soln['result']
|
||||
return len(x[-1]) == len(y[-1]) and all(abs(i-j) <= 1e-2 for i,j in zip(x[-1], y[-1]))
|
||||
|
||||
def mfunc(x):
|
||||
return '<tt>%r</tt><br/>%s' % (x[-1], make_regress_plot(x[0], x[1], x[2][0], x[2][1]))
|
||||
|
||||
csq_covar_tests = []
|
||||
csq_pearson_tests = []
|
||||
csq_linreg_tests = []
|
||||
for i in range(2):
|
||||
m = random.uniform(-10,10)
|
||||
b = random.uniform(-100,100)
|
||||
x = [round(random.uniform(-30,40), 2) for i in range(random.randint(20,200))]
|
||||
y = [m*i+b for i in x]
|
||||
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
|
||||
if i%2:
|
||||
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
|
||||
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
|
||||
for i in range(5):
|
||||
x = [round(random.uniform(-50,70), 2) for i in range(random.randint(20,200))]
|
||||
y = [round(random.uniform(-80,-90), 2) for i in x]
|
||||
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
|
||||
if i%2:
|
||||
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
|
||||
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
|
||||
for i in range(5):
|
||||
m = random.uniform(-10,10)
|
||||
b = random.uniform(-100,100)
|
||||
x = [round(random.uniform(-1000,1000), 2) for i in range(random.randint(20,200))]
|
||||
y = [random.gauss(m*i+b, 100) for i in x]
|
||||
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
|
||||
if i%2:
|
||||
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
|
||||
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
|
||||
for i in csq_covar_tests + csq_pearson_tests:
|
||||
i['check_function'] = check_num
|
||||
for i in csq_linreg_tests:
|
||||
i['check_function'] = checkregression
|
||||
i['transform_output'] = mfunc
|
||||
|
||||
csq_tests.extend(csq_covar_tests)
|
||||
csq_tests.extend(csq_pearson_tests)
|
||||
csq_tests.extend(csq_linreg_tests)
|
||||
|
||||
|
||||
csq_soln = """def sample_mean(xs):
|
||||
return sum(xs) / len(xs)
|
||||
|
||||
def sample_var(xs):
|
||||
return sample_std(xs)**2
|
||||
|
||||
def sample_std(xs):
|
||||
m = sample_mean(xs)
|
||||
return (sum([(i-m)**2 for i in xs]) / (len(xs)-1))**0.5
|
||||
|
||||
def sample_cov(xs, ys):
|
||||
x_mean = sample_mean(xs)
|
||||
y_mean = sample_mean(ys)
|
||||
o = 0.0
|
||||
for index in range(len(xs)):
|
||||
o += (xs[index] - x_mean)*(ys[index] - y_mean)
|
||||
return o / (len(xs) - 1)
|
||||
|
||||
def pearson_r(xs, ys):
|
||||
return sample_cov(xs, ys) / (sample_std(xs)*sample_std(ys))
|
||||
|
||||
def linear_regression(xs, ys):
|
||||
r = pearson_r(xs, ys)
|
||||
m = r * sample_std(ys) / sample_std(xs)
|
||||
b = sample_mean(ys) - m * sample_mean(xs)
|
||||
return m, b, r"""
|
||||
</question>
|
@ -0,0 +1,146 @@
|
||||
cs_long_name = 'Linear Regression'
|
||||
mode='analysis'
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
|
||||
import matplotlib.pyplot as _p
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk, FigureCanvasAgg
|
||||
|
||||
from io import BytesIO
|
||||
import base64 as _b64
|
||||
|
||||
class PlotWindowBase(_p.Figure):
|
||||
"""
|
||||
Tk window containing a matplotlib plot. In addition to the functions
|
||||
described below, also supports all functions contained in matplotlib's
|
||||
Axes_ and Figure_ objects.
|
||||
|
||||
.. _Axes: http://matplotlib.sourceforge.net/api/axes_api.html
|
||||
.. _Figure: http://matplotlib.sourceforge.net/api/figure_api.html
|
||||
"""
|
||||
def __init__(self, title="Plotting Window", visible=True):
|
||||
"""
|
||||
:param title: The title to be used for the window initially
|
||||
:param visible: Whether to actually display a Tk window (set to
|
||||
``False`` to create and save plots without a window
|
||||
popping up)
|
||||
"""
|
||||
_p.Figure.__init__(self)
|
||||
self.ax = self.add_subplot(111)
|
||||
self.visible = visible
|
||||
if self.visible:
|
||||
self.canvas = FigureCanvasTkAgg(self, tkinter.Tk())
|
||||
self.title(title)
|
||||
self.makeWindow()
|
||||
self.show()
|
||||
else:
|
||||
self.canvas = FigureCanvasAgg(self)
|
||||
|
||||
def makeWindow(self):
|
||||
"""
|
||||
Pack the plot and matplotlib toolbar into the containing Tk window
|
||||
(called by initializer; you will probably never need to use this).
|
||||
"""
|
||||
self.canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
|
||||
self.toolbar = NavigationToolbar2Tk( self.canvas, self.canvas._master )
|
||||
self.toolbar.update()
|
||||
self.canvas._tkcanvas.pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Destroy the Tk window. Note that after calling this method (or
|
||||
manually closing the Tk window), this :py:class:`PlotWindow` cannot be
|
||||
used.
|
||||
"""
|
||||
try:
|
||||
self.canvas._master.destroy()
|
||||
except:
|
||||
pass # probably already destroyed...
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the plot, keeping the Tk window active
|
||||
"""
|
||||
self.clf()
|
||||
self.add_subplot(111)
|
||||
if self.visible:
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Update the canvas image (automatically called for most functions)
|
||||
"""
|
||||
self.canvas.draw()
|
||||
|
||||
def __getattr__(self, name):
|
||||
show = True
|
||||
if name.startswith('_'):
|
||||
name = name[1:]
|
||||
show = False
|
||||
if hasattr(self.axes[0], name):
|
||||
attr = getattr(self.axes[0], name)
|
||||
if hasattr(attr,'__call__'):
|
||||
if show:
|
||||
def tmp(*args,**kwargs):
|
||||
out = attr(*args,**kwargs)
|
||||
if self.visible:
|
||||
self.show()
|
||||
return out
|
||||
return tmp
|
||||
else:
|
||||
return attr
|
||||
else:
|
||||
return attr
|
||||
else:
|
||||
raise AttributeError("PlotWindow object has no attribute %s" % name)
|
||||
|
||||
def title(self,title):
|
||||
"""
|
||||
Change the title of the Tk window
|
||||
"""
|
||||
self.ax.set_title(title)
|
||||
|
||||
def legend(self, *args):
|
||||
"""
|
||||
Create a legend for the figure (requires plots to have been made with
|
||||
labels)
|
||||
"""
|
||||
handles, labels = self.axes[0].get_legend_handles_labels()
|
||||
self.axes[0].legend(handles, labels)
|
||||
if self.visible:
|
||||
self.show()
|
||||
|
||||
def save(self, fname):
|
||||
"""
|
||||
Save this plot as an image. File type determined by extension of filename passed in.
|
||||
See documentation for savefig_.
|
||||
|
||||
:param fname: The name of the file to create.
|
||||
|
||||
.. _savefig: http://matplotlib.sourceforge.net/api/figure_api.html
|
||||
"""
|
||||
self.savefig(fname)
|
||||
|
||||
def stay(self):
|
||||
"""
|
||||
Start the Tkinter window's main loop (e.g., to keep the plot open at
|
||||
the end of the execution of a script)
|
||||
"""
|
||||
self.canvas._master.mainloop()
|
||||
|
||||
|
||||
def PlotWindow(title="Plotting Window"):
|
||||
class _PlotWindow(PlotWindowBase):
|
||||
def __init__(self, title="Plotting Window"):
|
||||
PlotWindowBase.__init__(self, title, visible=False)
|
||||
|
||||
def _show(self, width="75%"):
|
||||
sio = BytesIO()
|
||||
self.savefig(sio, format='png', facecolor='none')
|
||||
return '<img src="data:image/png;base64,%s" width="%s"/>' % (_b64.b64encode(sio.getvalue()).decode(), width)
|
||||
return _PlotWindow(title)
|
||||
|
||||
|
||||
ys = [-26.85901649618508, -4.82309989277328, 358.12325659886665, -236.27303224345013, 78.96311735375048, -122.0718747926675, -91.794917932851, 156.92288603409375, 77.76372162876706, 134.08018802466293, 421.2164012311623, -102.44084767789477, 69.05193026508634, -65.1707634424306, -6.811873517536455, 14.464555720475445, 123.35092804160584, 269.2411300965563, 283.6126314082743, 249.5593597768601, -45.93699758535121, 160.9929838505804, -11.486651027732734, 34.89429129194224, 227.0114874496006, 453.94991714737176, 159.59340132582852, 305.53172734934645, 209.30183669664237, 377.794764790121, 316.6853125571703, 242.97813270158048, 99.55136064457564, 380.8281222370041, 517.9678491133832, 152.82974416562115, 501.3107247844115, 385.00869117981006, 260.76894288405657, 245.62085551544348, 381.93212971867905, 282.37383569434684, 255.6684633511091, 528.9665009201663, 214.89786230001818, 198.47447067627894, 583.0632870160896, 556.0811185746013, 334.2809525610168, 743.3044708948721, 279.7165711058779, 755.3224521456882, 200.5387369220224, 138.47687852393108, 197.63575541409483, 181.71767331525095, 376.89074479245284, 652.3613138217629, 381.4275586382469, 476.0995327254289, 461.46417474466193, 423.8593541888392, 503.3792036069044, 567.7438934709394, 296.9627154414269, 183.36747560528335, 714.4663522847009, 620.296512084993, 315.16553989225656, 644.953867681399, 381.11694172321086, 392.7101550383068, 474.0136908544534, 556.91031778183, 625.6146302263054, 561.5239749404492, 491.3389262641957, 396.09862792366937, 328.9611909946891, 442.51020674282574, 637.3002734137215, 820.1180066579375, 704.0222105099904, 749.7416740127572, 571.3375044210943, 740.3732575510189, 508.23757383850227, 805.9057544835931, 652.1781007402574, 331.2078936574819, 659.394649695742, 779.5249873375656, 417.1728951942524, 727.5381994766699, 846.8237700909708, 372.6078315611338, 735.4394177096567, 359.3157568184622, 742.5223675039202, 645.445434892132]
|
||||
xs = [103+i for i in range(len(ys))]
|
@ -0,0 +1 @@
|
||||
cs_long_name = 'Questions'
|
@ -0,0 +1,98 @@
|
||||
<section>Page Specification and Loading</section>
|
||||
|
||||
CAT-SOOP courses are stored using files on disk. The structure of the web site
|
||||
mirrors the structure of the directories on disk. Having a directory at
|
||||
`course_root/page` means that a page will be available at
|
||||
`http://my.site/page`.
|
||||
|
||||
Names beginning with underscores or dots are hidden (not web accessible), so if
|
||||
you plan on storing other things inside the same repository, you should make
|
||||
sure their names start with an underscore.
|
||||
|
||||
Inside of each page's directory, there should be a file called
|
||||
`content.catsoop`. This file contains the page's source (see the
|
||||
[Markdown](COURSE/markdown) and [Questions](COURSE/questions) pages for more
|
||||
information about how these files are structured).
|
||||
|
||||
<subsection>Magic Variables and Inheritance</subsection>
|
||||
|
||||
A lot of behavior in CAT-SOOP is defined by the values stored in "magic"
|
||||
variables with particular names. These variables control various aspects of
|
||||
the page's appearance, among other things. Typically, all magic variables
|
||||
begin with the prefix `cs_` (or `csq_` if they control aspects of a _question_
|
||||
rather than a page).
|
||||
|
||||
In addition to `content.catsoop`, each directory can also contain a file called
|
||||
`preload.py`, which is typically used to set these magic variables. Each page
|
||||
inherits the values resulting from the `preload.py` "above" it in the directory
|
||||
structure. In particular, on each page load:
|
||||
|
||||
* An empty environment $E_1$ is created
|
||||
* All the `preload.py` files leading up to the requested page are executed in
|
||||
$E_1$, one after the other, starting with the root.
|
||||
* User authentication occurs
|
||||
* Includes are processed
|
||||
* All Python tags on the given page are executed in $E_1$
|
||||
* Each question tag on the page creates a new environment $E_i$ inheriting from
|
||||
$E_1$, and the code from the question tag is evaluated in $E_i$
|
||||
|
||||
So values set in the top-level `preload.py` affect all pages beneath them
|
||||
(unless those values are overridden). Values set in Python tags in a
|
||||
particular page's `content.catsoop` affect only that page, and values set in
|
||||
`question` tags on a page affect only that particular question.
|
||||
|
||||
<section>Static Files and Linking</section>
|
||||
|
||||
Static files (i.e., those not generated by a `content.catsoop`) are treated
|
||||
differently. They should be placed inside a directory called `__STATIC__`
|
||||
within a page's folder.
|
||||
|
||||
Both static files and dynamic content can be linked to using a short-hand
|
||||
syntax. For example, imagine that there is a page `course_root/homeworks/week1`
|
||||
and a static file `course_root/homeworks/__STATIC__/an_image.png`.
|
||||
In the `content.catsoop` for the course, these could be linked to as `CURRENT/week1`
|
||||
and `CURRENT/an_image.png`, respectively. `CURRENT` always refers to the
|
||||
location of the page currently being loaded.
|
||||
|
||||
Similarly, from any page within the course, those files could be linked to as
|
||||
`COURSE/homeworks/week1` and `COURSE/homeworks/an_image.png`, respectively.
|
||||
|
||||
<section>Controlling Access to Pages</section>
|
||||
|
||||
<subsection>Permissions</subsection>
|
||||
|
||||
Users' roles are determined by the files in the `__USERS__` directory. Each
|
||||
user should have a file there, named like `username.py`. One way to handle
|
||||
permissions is to define a variable called `permission` in each of these files,
|
||||
which points to a list containing one or more strings describing what that user
|
||||
is allowed to do:
|
||||
|
||||
* `'view'`: allowed to view the contents of a page
|
||||
* `'submit'`: allowed to submit to a page
|
||||
* `'view_all'`: always allowed to view every page, regardless of when it releases
|
||||
* `'submit_all'`: always allowed to submit to every question, regardless of when it releases or is due
|
||||
* `'impersonate'`: allowed to view the page "as" someone else
|
||||
* `'admin'`: administrative tasks (such as modifying group assignments)
|
||||
* `'whdw'`: allowed to see "WHDW" page (Who Has Done What)
|
||||
* `'email'`: allowed to send e-mail through CAT-SOOP
|
||||
* `'grade'`: allowed to submit grades
|
||||
|
||||
Since many groups of users often share the same permission levels, a more
|
||||
common way to handle permissions is to assign each user a 'role', and to define
|
||||
`cs_permissions` in `preload.py` as a dictionary mapping role names to the
|
||||
permissions associated with that role. Take a look at `preload.py`, where such
|
||||
a mapping is defined. A couple of `__USERS__` files are also included in this
|
||||
sample course.
|
||||
|
||||
<subsection>Release and Due Dates</subsection>
|
||||
|
||||
`cs_release_date` controls the date at which a page is "released" to a student
|
||||
(i.e., that it becomes visible to those with the `view` permission). It can
|
||||
always be specified as a string `"YYYY-MM-DD:HH:MM"`, and it can also be
|
||||
specified based on course-specific meeting times (see the `preload.py` file for
|
||||
more information).
|
||||
|
||||
Similarly, `cs_due_date` controls when the questions on a page come due. In
|
||||
CAT-SOOP, the default behavior is to allow submissions after the due date. To
|
||||
implement a hard deadline and lock students out from submitting after a
|
||||
deadline, set `cs_auto_lock = True`.
|
@ -0,0 +1 @@
|
||||
cs_long_name = 'Course Structure'
|
Loading…
Reference in new issue