forked from Stanford-Online/xblock-submit-and-compare
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
da5a09e
commit 5a2f53a
Showing
20 changed files
with
1,425 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
""" | ||
This is an XBlock for submit and compare | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
""" | ||
Mixin behavior to XBlocks | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
""" | ||
Extend XBlocks with datetime helpers | ||
""" | ||
import datetime | ||
|
||
|
||
# pylint: disable=too-few-public-methods | ||
class EnforceDueDates: | ||
""" | ||
xBlock Mixin to allow xblocks to check the due date | ||
(taking the graceperiod into account) of the | ||
subsection in which they are placed | ||
""" | ||
|
||
def is_past_due(self): | ||
""" | ||
Determine if component is past-due | ||
""" | ||
# These values are pulled from platform. | ||
# They are defaulted to None for tests. | ||
due = getattr(self, 'due', None) | ||
graceperiod = getattr(self, 'graceperiod', None) | ||
# Calculate the current DateTime so we can compare the due date to it. | ||
# datetime.utcnow() returns timezone naive date object. | ||
now = datetime.datetime.utcnow() | ||
if due is not None: | ||
# Remove timezone information from platform provided due date. | ||
# Dates are stored as UTC timezone aware objects on platform. | ||
due = due.replace(tzinfo=None) | ||
if graceperiod is not None: | ||
# Compare the datetime objects (both have to be timezone naive) | ||
due = due + graceperiod | ||
return now > due | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
""" | ||
Provide event-related mixin functionality | ||
""" | ||
from xblock.core import XBlock | ||
|
||
|
||
class EventableMixin: | ||
""" | ||
Mix in standard event logic | ||
""" | ||
|
||
@XBlock.json_handler | ||
def publish_event(self, data, *args, **kwargs): | ||
""" | ||
Publish events | ||
""" | ||
try: | ||
event_type = data.pop('event_type') | ||
except KeyError: | ||
return { | ||
'result': 'error', | ||
'message': 'Missing event_type in JSON data', | ||
} | ||
data['user_id'] = self.scope_ids.user_id | ||
data['component_id'] = self._get_unique_id() | ||
self.runtime.publish(self, event_type, data) | ||
result = { | ||
'result': 'success', | ||
} | ||
return result | ||
|
||
def _get_unique_id(self): | ||
""" | ||
Get a unique component identifier | ||
""" | ||
try: | ||
unique_id = self.location.name | ||
except AttributeError: | ||
# workaround for xblock workbench | ||
unique_id = 'workbench-workaround-id' | ||
return unique_id | ||
|
||
def _publish_grade(self): | ||
""" | ||
Publish a grade event | ||
""" | ||
self.runtime.publish( | ||
self, | ||
'grade', | ||
{ | ||
'value': self.score, | ||
'max_value': 1.0, | ||
} | ||
) | ||
|
||
def _publish_problem_check(self): | ||
""" | ||
Publish a problem_check event | ||
""" | ||
self.runtime.publish( | ||
self, | ||
'problem_check', | ||
{ | ||
'grade': self.score, | ||
'max_grade': 1.0, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
""" | ||
Mixin fragment/html behavior into XBlocks | ||
Note: We should resume test coverage for all lines in this file once | ||
split into its own library. | ||
""" | ||
from django.template.context import Context | ||
from xblock.core import XBlock | ||
from web_fragments.fragment import Fragment | ||
|
||
|
||
class XBlockFragmentBuilderMixin: | ||
""" | ||
Create a default XBlock fragment builder | ||
""" | ||
static_css = [ | ||
'view.css', | ||
] | ||
static_js = [ | ||
'view.js', | ||
] | ||
static_js_init = None | ||
template = 'view.html' | ||
|
||
def get_i18n_service(self): | ||
""" | ||
Get the i18n service from the runtime | ||
""" | ||
return self.runtime.service(self, 'i18n') | ||
|
||
def provide_context(self, context): # pragma: no cover | ||
""" | ||
Build a context dictionary to render the student view | ||
This should generally be overriden by child classes. | ||
""" | ||
context = context or {} | ||
context = dict(context) | ||
return context | ||
|
||
@XBlock.supports('multi_device') | ||
def student_view(self, context=None): | ||
""" | ||
Build the fragment for the default student view | ||
""" | ||
template = self.template | ||
context = self.provide_context(context) | ||
static_css = self.static_css or [] | ||
static_js = self.static_js or [] | ||
js_init = self.static_js_init | ||
fragment = self.build_fragment( | ||
template=template, | ||
context=context, | ||
css=static_css, | ||
js=static_js, | ||
js_init=js_init, | ||
) | ||
return fragment | ||
|
||
def build_fragment( | ||
self, | ||
template='', | ||
context=None, | ||
css=None, | ||
js=None, | ||
js_init=None, | ||
): | ||
""" | ||
Creates a fragment for display. | ||
""" | ||
context = context or {} | ||
css = css or [] | ||
js = js or [] | ||
rendered_template = '' | ||
if template: # pragma: no cover | ||
template = 'templates/' + template | ||
rendered_template = self.loader.render_django_template( | ||
template, | ||
context=Context(context), | ||
i18n_service=self.get_i18n_service(), | ||
) | ||
fragment = Fragment(rendered_template) | ||
for item in css: | ||
if item.startswith('/'): | ||
url = item | ||
else: | ||
item = 'public/' + item | ||
url = self.runtime.local_resource_url(self, item) | ||
fragment.add_css_url(url) | ||
for item in js: | ||
item = 'public/' + item | ||
url = self.runtime.local_resource_url(self, item) | ||
fragment.add_javascript_url(url) | ||
if js_init: # pragma: no cover | ||
fragment.initialize_js(js_init) | ||
return fragment |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
""" | ||
Mixin workbench behavior into XBlocks | ||
""" | ||
from glob import glob | ||
import pkg_resources | ||
|
||
|
||
def _read_file(file_path): | ||
""" | ||
Read in a file's contents | ||
""" | ||
with open(file_path, encoding="utf-8") as file_input: | ||
file_contents = file_input.read() | ||
return file_contents | ||
|
||
|
||
def _parse_title(file_path): | ||
""" | ||
Parse a title from a file name | ||
""" | ||
title = file_path | ||
title = title.split('/')[-1] | ||
title = '.'.join(title.split('.')[:-1]) | ||
title = ' '.join(title.split('-')) | ||
title = ' '.join([ | ||
word.capitalize() | ||
for word in title.split(' ') | ||
]) | ||
return title | ||
|
||
|
||
def _read_files(files): | ||
""" | ||
Read the contents of a list of files | ||
""" | ||
file_contents = [ | ||
( | ||
_parse_title(file_path), | ||
_read_file(file_path), | ||
) | ||
for file_path in files | ||
] | ||
return file_contents | ||
|
||
|
||
def _find_files(directory): | ||
""" | ||
Find XML files in the directory | ||
""" | ||
pattern = "{directory}/*.xml".format( | ||
directory=directory, | ||
) | ||
files = glob(pattern) | ||
return files | ||
|
||
|
||
class XBlockWorkbenchMixin: | ||
""" | ||
Provide a default test workbench for the XBlock | ||
""" | ||
|
||
@classmethod | ||
def workbench_scenarios(cls): | ||
""" | ||
Gather scenarios to be displayed in the workbench | ||
""" | ||
module = cls.__module__ | ||
module = module.split('.', maxsplit=1)[0] | ||
directory = pkg_resources.resource_filename(module, 'scenarios') | ||
files = _find_files(directory) | ||
scenarios = _read_files(files) | ||
return scenarios |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
""" | ||
Handle data access logic for the XBlock | ||
""" | ||
import textwrap | ||
|
||
from xblock.fields import Float | ||
from xblock.fields import Integer | ||
from xblock.fields import List | ||
from xblock.fields import Scope | ||
from xblock.fields import String | ||
|
||
|
||
class SubmitAndCompareModelMixin: | ||
""" | ||
Handle data access logic for the XBlock | ||
""" | ||
|
||
has_score = True | ||
display_name = String( | ||
display_name='Display Name', | ||
default='Submit and Compare', | ||
scope=Scope.settings, | ||
help=( | ||
'This name appears in the horizontal' | ||
' navigation at the top of the page' | ||
), | ||
) | ||
student_answer = String( | ||
default='', | ||
scope=Scope.user_state, | ||
help='This is the student\'s answer to the question', | ||
) | ||
max_attempts = Integer( | ||
default=0, | ||
scope=Scope.settings, | ||
) | ||
count_attempts = Integer( | ||
default=0, | ||
scope=Scope.user_state, | ||
) | ||
your_answer_label = String( | ||
default='Your Answer:', | ||
scope=Scope.settings, | ||
help='Label for the text area containing the student\'s answer', | ||
) | ||
our_answer_label = String( | ||
default='Our Answer:', | ||
scope=Scope.settings, | ||
help='Label for the \'expert\' answer', | ||
) | ||
submit_button_label = String( | ||
default='Submit and Compare', | ||
scope=Scope.settings, | ||
help='Label for the submit button', | ||
) | ||
hints = List( | ||
default=[], | ||
scope=Scope.content, | ||
help='Hints for the question', | ||
) | ||
question_string = String( | ||
help='Default question content ', | ||
scope=Scope.content, | ||
multiline_editor=True, | ||
default=textwrap.dedent(""" | ||
<submit_and_compare schema_version='1'> | ||
<body> | ||
<p> | ||
Before you begin the simulation, | ||
think for a minute about your hypothesis. | ||
What do you expect the outcome of the simulation | ||
will be? What data do you need to gather in order | ||
to prove or disprove your hypothesis? | ||
</p> | ||
</body> | ||
<explanation> | ||
<p> | ||
We would expect the simulation to show that | ||
there is no difference between the two scenarios. | ||
Relevant data to gather would include time and | ||
temperature. | ||
</p> | ||
</explanation> | ||
<demandhint> | ||
<hint> | ||
A hypothesis is a proposed explanation for a | ||
phenomenon. In this case, the hypothesis is what | ||
we think the simulation will show. | ||
</hint> | ||
<hint> | ||
Once you've decided on your hypothesis, which data | ||
would help you determine if that hypothesis is | ||
correct or incorrect? | ||
</hint> | ||
</demandhint> | ||
</submit_and_compare> | ||
""")) | ||
score = Float( | ||
default=0.0, | ||
scope=Scope.user_state, | ||
) | ||
weight = Integer( | ||
display_name='Weight', | ||
help='This assigns an integer value representing ' | ||
'the weight of this problem', | ||
default=0, | ||
scope=Scope.settings, | ||
) | ||
|
||
def max_score(self): | ||
""" | ||
Returns the configured number of possible points for this component. | ||
Arguments: | ||
None | ||
Returns: | ||
float: The number of possible points for this component | ||
""" | ||
return self.weight |
Oops, something went wrong.