Commit 6a7d036f by Brian Talbot Committed by Will Daly

adding transition, error and complete states for assessment feedback form:

* provides static states for submission cases
* adds in abstracted system-feedback sass partial
* adds in starter animation sass partial
* syncs up loading style for steps with standards
* moves message UI archetypes/variables to the xblock level of Sass architecture

Move JS grade step into a separate file
Feedback on submission UI wiring
parent 9896d39f
...@@ -40,7 +40,7 @@ htmlcov ...@@ -40,7 +40,7 @@ htmlcov
.pydevproject .pydevproject
# Sass/Codekit # Sass/Codekit
apps/openassessment/xblock/static/sass/.sass-cache/ .sass-cache/
config.codekit config.codekit
# some mac thing # some mac thing
......
...@@ -138,15 +138,32 @@ ...@@ -138,15 +138,32 @@
</ol> </ol>
</article> </article>
<form id="submission__feeedback" class="submission__feeedback" method="post"> <form id="submission__feedback" class="submission__feedback" method="post">
<h3 class="submission__feeedback__title">Give Feedback On Peer Evaluations</h3> <h3 class="submission__feedback__title">Give Feedback On Peer Evaluations</h3>
<div class="submission__feedback__content {{ has_submitted_feedback|yesno:"is--submitted," }}">
<span class="transition__status is--hidden" aria-hidden="true">
<span class="wrapper--anim">
<i class="ico icon-refresh icon-spin"></i>
<span class="copy">Submitting Feedback</span>
</span>
</span>
<div class="message message--complete {{ has_submitted_feedback|yesno:",is--hidden" }}"
{{ has_submitted_feedback|yesno:'aria-hidden=false,aria-hidden=true' }}>
<h3 class="message__title">Your Feedback Has Been Submitted</h3>
<div class="message__content">
<p>Your feedback will be sent to this course's staff for use when they review course records.</p>
</div>
</div>
<div class="submission__feeedback__content"> <div class="submission__feedback__instructions {{ has_submitted_feedback|yesno:"is--hidden," }}"
<div class="submission__feeedback__instructions"> {{ has_submitted_feedback|yesno:'aria-hidden=true,aria-hidden=false' }}>
<p>Course staff will be able to see any feedback that you provide here when they review course records.</p> <p>Course staff will be able to see any feedback that you provide here when they review course records.</p>
</div> </div>
<ol class="list list--fields submission__feedback__fields"> <ol class="list list--fields submission__feedback__fields {{ has_submitted_feedback|yesno:"is--hidden," }}"
{{ has_submitted_feedback|yesno:'aria-hidden=true,aria-hidden=false' }}>
<li class="field field-group field--radio feedback__overall" id="feedback__overall"> <li class="field field-group field--radio feedback__overall" id="feedback__overall">
<h4 class="field-group__label">Please select the statements below that reflect what you think of this peer grading experience:</h4> <h4 class="field-group__label">Please select the statements below that reflect what you think of this peer grading experience:</h4>
<ol class="list--options"> <ol class="list--options">
...@@ -189,19 +206,19 @@ ...@@ -189,19 +206,19 @@
<textarea id="feedback__remarks__value" placeholder="I feel the feedback I received was...">{{ feedback_text }}</textarea> <textarea id="feedback__remarks__value" placeholder="I feel the feedback I received was...">{{ feedback_text }}</textarea>
</li> </li>
</ol> </ol>
</div> <div class="submission__feedback__actions {{ has_submitted_feedback|yesno:"is--hidden," }}"
{{ has_submitted_feedback|yesno:'aria-hidden=true,aria-hidden=false' }}>
<div class="submission__feedback__actions"> <div class="message message--inline message--error message--error-server">
<div class="message message--inline message--error message--error-server"> <h3 class="message__title">We could not submit your feedback</h3>
<h3 class="message__title">We could not submit your feedback</h3> <div class="message__content"></div>
<div class="message__content"></div> </div>
<ul class="list list--actions submission__feedback__actions">
<li class="list--actions__item">
<button type="submit" id="feedback__submit" class="action action--submit feedback__submit">Submit Feedback On Peer Evaluations</button>
</li>
</ul>
</div> </div>
<ul class="list list--actions submission__feeedback__actions">
<li class="list--actions__item">
<button type="submit" id="feedback__submit" class="action action--submit feedback__submit">Submit Feedback On Peer Evaluations</button>
</li>
</ul>
</div> </div>
</form> </form>
</div> </div>
......
...@@ -54,8 +54,10 @@ ...@@ -54,8 +54,10 @@
<span class="step__status"> <span class="step__status">
<span class="step__status__label">This step's status:</span> <span class="step__status__label">This step's status:</span>
<span class="step__status__value"> <span class="step__status__value">
<span class="copy">Loading</span> <span class="wrapper--anim">
<i class="ico icon-refresh icon-spin"></i> <span class="copy">Loading</span>
<i class="ico icon-refresh icon-spin"></i>
</span>
</span> </span>
</span> </span>
</header> </header>
......
"""
Grade step in the OpenAssessment XBlock.
"""
import copy import copy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -5,7 +8,7 @@ from xblock.core import XBlock ...@@ -5,7 +8,7 @@ from xblock.core import XBlock
from openassessment.assessment import peer_api from openassessment.assessment import peer_api
from openassessment.assessment import self_api from openassessment.assessment import self_api
from submissions import api as submission_api from submissions import api as sub_api
class GradeMixin(object): class GradeMixin(object):
...@@ -21,54 +24,99 @@ class GradeMixin(object): ...@@ -21,54 +24,99 @@ class GradeMixin(object):
@XBlock.handler @XBlock.handler
def render_grade(self, data, suffix=''): def render_grade(self, data, suffix=''):
"""
Render the grade step.
Args:
data: Not used.
Kwargs:
suffix: Not used.
Returns:
unicode: HTML content of the grade step.
"""
# Retrieve the status of the workflow. If no workflows have been
# started this will be an empty dict, so status will be None.
workflow = self.get_workflow_info() workflow = self.get_workflow_info()
status = workflow.get('status') status = workflow.get('status')
# Default context is empty
context = {} context = {}
if status == "done":
try: # Render the grading section based on the status of the workflow
feedback = peer_api.get_assessment_feedback(self.submission_uuid) try:
feedback_text = feedback.get('feedback', '') if feedback else '' if status == "done":
max_scores = peer_api.get_rubric_max_scores(self.submission_uuid) path, context = self.render_grade_complete(workflow)
path = 'openassessmentblock/grade/oa_grade_complete.html' elif status == "waiting":
student_submission = submission_api.get_submission(workflow["submission_uuid"]) path = 'openassessmentblock/grade/oa_grade_waiting.html'
student_score = workflow["score"] elif status is None:
peer_assessments = peer_api.get_assessments(student_submission["uuid"]) path = 'openassessmentblock/grade/oa_grade_not_started.html'
self_assessment = self_api.get_assessment(student_submission["uuid"]) else: # status is 'self' or 'peer', which implies that the workflow is incomplete
median_scores = peer_api.get_assessment_median_scores( path, context = self.render_grade_incomplete(workflow)
student_submission["uuid"] except (sub_api.SubmissionError, peer_api.PeerAssessmentError, self_api.SelfAssessmentRequestError):
) return self.render_error(_(u"An unexpected error occurred."))
except (
submission_api.SubmissionError,
peer_api.PeerAssessmentError,
self_api.SelfAssessmentRequestError
):
return self.render_error(_(u"An unexpected error occurred."))
context["feedback_text"] = feedback_text
context["student_submission"] = student_submission
context["peer_assessments"] = peer_assessments
context["self_assessment"] = self_assessment
context["rubric_criteria"] = copy.deepcopy(self.rubric_criteria)
context["score"] = student_score
if median_scores is not None and max_scores is not None:
for criterion in context["rubric_criteria"]:
criterion["median_score"] = median_scores[criterion["name"]]
criterion["total_value"] = max_scores[criterion["name"]]
elif workflow.get('status') == "waiting":
path = 'openassessmentblock/grade/oa_grade_waiting.html'
elif not status:
path = 'openassessmentblock/grade/oa_grade_not_started.html'
else: else:
incomplete_steps = [] return self.render_assessment(path, context)
if not workflow["status_details"]["peer"]["complete"]:
incomplete_steps.append("Peer Assessment") def render_grade_complete(self, workflow):
if not workflow["status_details"]["self"]["complete"]: """
incomplete_steps.append("Self Assessment") Render the grade complete state.
context = {"incomplete_steps": incomplete_steps}
path = 'openassessmentblock/grade/oa_grade_incomplete.html' Args:
workflow (dict): The serialized Workflow model.
return self.render_assessment(path, context)
Returns:
tuple of context (dict), template_path (string)
"""
feedback = peer_api.get_assessment_feedback(self.submission_uuid)
feedback_text = feedback.get('feedback', '') if feedback else ''
student_submission = sub_api.get_submission(workflow['submission_uuid'])
peer_assessments = peer_api.get_assessments(student_submission['uuid'])
self_assessment = self_api.get_assessment(student_submission['uuid'])
has_submitted_feedback = peer_api.get_assessment_feedback(workflow['submission_uuid']) is not None
context = {
'score': workflow['score'],
'feedback_text': feedback_text,
'student_submission': student_submission,
'peer_assessments': peer_assessments,
'self_assessment': self_assessment,
'rubric_criteria': copy.deepcopy(self.rubric_criteria),
'has_submitted_feedback': has_submitted_feedback,
}
# Update the scores we will display to the user
# Note that we are updating a *copy* of the rubric criteria stored in the XBlock field
max_scores = peer_api.get_rubric_max_scores(self.submission_uuid)
median_scores = peer_api.get_assessment_median_scores(student_submission["uuid"])
if median_scores is not None and max_scores is not None:
for criterion in context["rubric_criteria"]:
criterion["median_score"] = median_scores[criterion["name"]]
criterion["total_value"] = max_scores[criterion["name"]]
return ('openassessmentblock/grade/oa_grade_complete.html', context)
def render_grade_incomplete(self, workflow):
"""
Render the grade incomplete state.
Args:
workflow (dict): The serialized Workflow model.
Returns:
tuple of context (dict), template_path (string)
"""
incomplete_steps = []
if not workflow["status_details"]["peer"]["complete"]:
incomplete_steps.append("Peer Assessment")
if not workflow["status_details"]["self"]["complete"]:
incomplete_steps.append("Self Assessment")
return (
'openassessmentblock/grade/oa_grade_incomplete.html',
{'incomplete_steps': incomplete_steps}
)
@XBlock.json_handler @XBlock.json_handler
def submit_feedback(self, data, suffix=''): def submit_feedback(self, data, suffix=''):
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<div id='openassessment-base'> <div id='openassessment-base'>
<form id="submission__feeedback" class="submission__feeedback" method="post"> <form id="submission__feedback" class="submission__feedback" method="post">
<h3 class="submission__feeedback__title">Give Feedback On Peer Evaluations</h3> <h3 class="submission__feedback__title">Give Feedback On Peer Evaluations</h3>
<div class="submission__feeedback__content"> <div class="submission__feedback__content">
<div class="submission__feeedback__instructions"> <span class="transition__status">
<span class="wrapper--anim">
<i class="ico icon-refresh icon-spin"></i>
<span class="copy">Submitting Feedback</span>
</span>
</span>
<div class="message message--complete">
<h3 class="message__title">Your Feedback Has Been Submitted</h3>
<div class="message__content">
<p>Your feedback will be sent to this course's staff for use when they review course records.</p>
</div>
</div>
<div class="submission__feedback__instructions">
<p>Course staff will be able to see any feedback that you provide here when they review course records.</p> <p>Course staff will be able to see any feedback that you provide here when they review course records.</p>
</div> </div>
...@@ -50,6 +62,17 @@ ...@@ -50,6 +62,17 @@
<textarea id="feedback__remarks__value" placeholder="I feel the feedback I received was...">{{ feedback_text }}</textarea> <textarea id="feedback__remarks__value" placeholder="I feel the feedback I received was...">{{ feedback_text }}</textarea>
</li> </li>
</ol> </ol>
<div class="submission__feedback__actions">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">We could not submit your feedback</h3>
</div>
<ul class="list list--actions submission__feedback__actions">
<li class="list--actions__item">
<button type="submit" id="feedback__submit" class="action action--submit feedback__submit">Submit Feedback On Peer Evaluations</button>
</li>
</ul>
</div>
</div> </div>
</form> </form>
</div> </div>
...@@ -29,15 +29,6 @@ describe("OpenAssessment.BaseView", function() { ...@@ -29,15 +29,6 @@ describe("OpenAssessment.BaseView", function() {
defer.resolveWith(this, [server.fragments[component]]); defer.resolveWith(this, [server.fragments[component]]);
}).promise(); }).promise();
}; };
this.submitFeedbackOnAssessment = function(text, options) {
// Store the args we receive so we can check them later
this.feedbackText = text;
this.feedbackOptions = options;
// Return a promise that always resolves successfully
return $.Deferred(function(defer) { defer.resolve(); }).promise();
};
}; };
// Stub runtime // Stub runtime
...@@ -98,30 +89,4 @@ describe("OpenAssessment.BaseView", function() { ...@@ -98,30 +89,4 @@ describe("OpenAssessment.BaseView", function() {
}); });
}); });
it("Sends feedback on a submission to the server", function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('grade_complete.html');
// Simulate user feedback
$('#feedback__remarks__value').val('I disliked the feedback I received.');
$('#feedback__overall__value--notuseful').attr('checked','checked');
$('#feedback__overall__value--disagree').attr('checked','checked');
// Create a new stub server
server = new StubServer();
// Create the object under test
var el = $("#openassessment-base").get(0);
view = new OpenAssessment.BaseView(runtime, el, server);
// Submit feedback on an assessment
view.submitFeedbackOnAssessment();
// Expect that the feedback was retrieved from the DOM and sent to the server
expect(server.feedbackText).toEqual('I disliked the feedback I received.');
expect(server.feedbackOptions).toEqual([
'These assessments were not useful.',
'I disagree with the ways that my peers assessed me.'
]);
});
}); });
/**
Tests for OpenAssessment grade view.
**/
describe("OpenAssessment.GradeView", function() {
// Stub server
var StubServer = function() {
var successPromise = $.Deferred(
function(defer) {
defer.resolve();
}
).promise();
this.submitFeedbackOnAssessment = function(text, options) {
// Store the args we receive so we can check them later
this.feedbackText = text;
this.feedbackOptions = options;
// Return a promise that always resolves successfully
return successPromise;
};
this.render = function(step) {
return successPromise;
};
};
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
};
// Stubs
var baseView = null;
var server = null;
// View under test
var view = null;
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_grade_complete.html');
// Create the stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.GradeView(el, server, baseView);
view.installHandlers();
});
it("sends feedback on a submission to the server", function() {
// Simulate user feedback
view.feedbackText('I disliked the feedback I received');
view.feedbackOptions(['notuseful', 'disagree']);
// Submit feedback on an assessment
view.submitFeedbackOnAssessment();
// Expect that the feedback was retrieved from the DOM and sent to the server
expect(server.feedbackText).toEqual('I disliked the feedback I received');
expect(server.feedbackOptions).toEqual([
'These assessments were not useful.',
'I disagree with the ways that my peers assessed me.'
]);
});
it("updates the feedback state when the user submits feedback", function() {
// Set the initial feedback state to open
view.feedbackState('open');
expect(view.feedbackState()).toEqual('open');
// Submit feedback on an assessment
view.feedbackText('I liked the feedback I received');
view.feedbackOptions(['useful']);
view.submitFeedbackOnAssessment();
// Expect that the feedback state to be submitted
expect(view.feedbackState()).toEqual('submitted');
});
});
/** /**
Tests for OpenAssessment response (submission) step. Tests for OpenAssessment response (submission) view.
**/ **/
describe("OpenAssessment.ResponseView", function() { describe("OpenAssessment.ResponseView", function() {
......
...@@ -21,6 +21,9 @@ OpenAssessment.BaseView = function(runtime, element, server) { ...@@ -21,6 +21,9 @@ OpenAssessment.BaseView = function(runtime, element, server) {
this.runtime = runtime; this.runtime = runtime;
this.element = element; this.element = element;
this.server = server; this.server = server;
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.gradeView = new OpenAssessment.GradeView(this.element, this.server, this);
}; };
...@@ -65,12 +68,10 @@ OpenAssessment.BaseView.prototype = { ...@@ -65,12 +68,10 @@ OpenAssessment.BaseView.prototype = {
* Asynchronously load each sub-view into the DOM. * Asynchronously load each sub-view into the DOM.
*/ */
load: function() { load: function() {
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.responseView.load(); this.responseView.load();
this.renderPeerAssessmentStep(); this.renderPeerAssessmentStep();
this.renderSelfAssessmentStep(); this.renderSelfAssessmentStep();
this.renderGradeStep(); this.gradeView.load();
}, },
/** /**
...@@ -203,53 +204,6 @@ OpenAssessment.BaseView.prototype = { ...@@ -203,53 +204,6 @@ OpenAssessment.BaseView.prototype = {
}, },
/** /**
Render the grade step.
**/
renderGradeStep: function() {
var view = this;
this.server.render('grade').done(
function(html) {
// Load the HTML
$('#openassessment__grade', view.element).replaceWith(html);
// Install a click handler for collapse/expand
var sel = $('#openassessment__grade', view.element);
view.setUpCollapseExpand(sel);
// Install a click handler for assessment feedback
sel.find('#feedback__submit').click(function(eventObject) {
eventObject.preventDefault();
view.submitFeedbackOnAssessment();
});
}
).fail(function(errMsg) {
view.showLoadError('grade', errMsg);
});
},
/**
Send assessment feedback to the server and update the view.
**/
submitFeedbackOnAssessment: function() {
// Send the submission to the server
var view = this;
var text = $('#feedback__remarks__value', view.element).val();
var options = $.map(
$('.feedback__overall__value:checked', view.element),
function(element, index) { return $(element).val(); }
);
view.server.submitFeedbackOnAssessment(text, options).done(function() {
// When we have successfully sent the submission, textarea no longer editable
// TODO
// When we have successfully sent the submission, textarea no longer editable
// console.log("Feedback to the assessments submitted, thanks!");
}).fail(function(errMsg) {
view.toggleActionError('feedback_assess', errMsg);
});
},
/**
Send an assessment to the server and update the view. Send an assessment to the server and update the view.
**/ **/
peerAssess: function() { peerAssess: function() {
...@@ -257,7 +211,7 @@ OpenAssessment.BaseView.prototype = { ...@@ -257,7 +211,7 @@ OpenAssessment.BaseView.prototype = {
this.peerAssessRequest(function() { this.peerAssessRequest(function() {
view.renderPeerAssessmentStep(); view.renderPeerAssessmentStep();
view.renderSelfAssessmentStep(); view.renderSelfAssessmentStep();
view.renderGradeStep(); view.gradeView.load();
view.scrollToTop(); view.scrollToTop();
}); });
}, },
...@@ -270,7 +224,7 @@ OpenAssessment.BaseView.prototype = { ...@@ -270,7 +224,7 @@ OpenAssessment.BaseView.prototype = {
var view = this; var view = this;
view.peerAssessRequest(function() { view.peerAssessRequest(function() {
view.renderContinuedPeerAssessmentStep(); view.renderContinuedPeerAssessmentStep();
view.renderGradeStep(); view.gradeView.load();
}); });
}, },
...@@ -324,7 +278,7 @@ OpenAssessment.BaseView.prototype = { ...@@ -324,7 +278,7 @@ OpenAssessment.BaseView.prototype = {
function() { function() {
view.renderPeerAssessmentStep(); view.renderPeerAssessmentStep();
view.renderSelfAssessmentStep(); view.renderSelfAssessmentStep();
view.renderGradeStep(); view.gradeView.load();
view.scrollToTop(); view.scrollToTop();
} }
).fail(function(errMsg) { ).fail(function(errMsg) {
......
/* JavaScript for grade view */
/* Namespace for open assessment */
if (typeof OpenAssessment == "undefined" || !OpenAssessment) {
OpenAssessment = {};
}
/**
Interface for grade view.
Args:
element (DOM element): The DOM element representing the XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
baseView (OpenAssessment.BaseView): Container view.
Returns:
OpenAssessment.ResponseView
**/
OpenAssessment.GradeView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
};
OpenAssessment.GradeView.prototype = {
/**
Load the grade view.
**/
load: function() {
var view = this;
var baseView = this.baseView;
this.server.render('grade').done(
function(html) {
// Load the HTML and install event handlers
$('#openassessment__grade', view.element).replaceWith(html);
view.installHandlers();
}
).fail(function(errMsg) {
baseView.showLoadError('grade', errMsg);
});
},
/**
Install event handlers for the view.
**/
installHandlers: function() {
// Install a click handler for collapse/expand
var sel = $('#openassessment__grade', this.element);
this.baseView.setUpCollapseExpand(sel);
// Install a click handler for assessment feedback
var view = this;
sel.find('#feedback__submit').click(function(eventObject) {
eventObject.preventDefault();
view.submitFeedbackOnAssessment();
});
},
/**
Get or set the text for feedback on assessments.
Args:
text (string or undefined): The text of the assessment to set (optional).
Returns:
string or undefined: The text of the feedback.
Example usage:
>>> view.feedbackText('I liked my assessment'); // Set the feedback text
>>> view.feedbackText(); // Retrieve the feedback text
'I liked my assessment'
**/
feedbackText: function(text) {
if (typeof text === 'undefined') {
return $('#feedback__remarks__value', this.element).val();
} else {
$('#feedback__remarks__value', this.element).val(text);
}
},
/**
Get or set the options for feedback on assessments.
Args:
options (array of strings or undefined): List of options to check (optional).
Returns:
list of strings or undefined: The values of the options the user selected.
Example usage:
// Set the feedback options; all others will be unchecked
>>> view.feedbackOptions('notuseful', 'disagree');
// Retrieve the feedback options that are checked
>>> view.feedbackOptions();
[
'These assessments were not useful.',
'I disagree with the ways that my peers assessed me'
]
**/
feedbackOptions: function(options) {
var view = this;
if (typeof options === 'undefined') {
return $.map(
$('.feedback__overall__value:checked', view.element),
function(element, index) { return $(element).val(); }
);
} else {
// Uncheck all the options
$('.feedback__overall__value', this.element).prop('checked', false);
// Check the selected options
$.each(options, function(index, opt) {
$('#feedback__overall__value--' + opt, view.element).prop('checked', true);
});
}
},
/**
Hide elements, including setting the aria-hidden attribute for screen readers.
Args:
sel (JQuery selector): The selector matching elements to hide.
hidden (boolean): Whether to hide or show the elements.
Returns:
undefined
**/
setHidden: function(sel, hidden) {
sel.toggleClass('is--hidden', hidden);
sel.attr('aria-hidden', hidden ? 'true' : 'false');
},
/**
Check whether elements are hidden.
Args:
sel (JQuery selector): The selector matching elements to hide.
Returns:
boolean
**/
isHidden: function(sel) {
return sel.hasClass('is--hidden') && sel.attr('aria-hidden') == 'true';
},
/**
Get or set the state of the feedback on assessment.
Each state corresponds to a particular configuration of attributes
in the DOM, which control what the user sees in the UI.
Valid states are:
'open': The user has not yet submitted feedback on assessments.
'submitting': The user has submitted feedback, but the server has not yet responded.
'submitted': The feedback was successfully submitted
Args:
newState (string or undefined): One of above states.
Returns:
string or undefined: The current state.
Throws:
'Invalid feedback state' if the DOM is not in one of the valid states.
Example usage:
>>> view.feedbackState();
'open'
>>> view.feedbackState('submitted');
>>> view.feedbackState();
'submitted'
**/
feedbackState: function(newState) {
var containerSel = $('.submission__feedback__content', this.element);
var instructionsSel = containerSel.find('.submission__feedback__instructions');
var fieldsSel = containerSel.find('.submission__feedback__fields');
var actionsSel = containerSel.find('.submission__feedback__actions');
var transitionSel = containerSel.find('.transition__status');
var messageSel = containerSel.find('.message--complete');
if (typeof newState === 'undefined') {
var isSubmitting = (
containerSel.hasClass('is--transitioning') && containerSel.hasClass('is--submitting') &&
!this.isHidden(transitionSel) && this.isHidden(messageSel) &&
this.isHidden(instructionsSel) && this.isHidden(fieldsSel) && this.isHidden(actionsSel)
);
var hasSubmitted = (
containerSel.hasClass('is--submitted') &&
this.isHidden(transitionSel) && !this.isHidden(messageSel) &&
this.isHidden(instructionsSel) && this.isHidden(fieldsSel) && this.isHidden(actionsSel)
);
var isOpen = (
!containerSel.hasClass('is--submitted') &&
!containerSel.hasClass('is--transitioning') && !containerSel.hasClass('is--submitting') &&
this.isHidden(transitionSel) && this.isHidden(messageSel) &&
!this.isHidden(instructionsSel) && !this.isHidden(fieldsSel) && !this.isHidden(actionsSel)
);
if (isOpen) { return 'open'; }
else if (isSubmitting) { return 'submitting'; }
else if (hasSubmitted) { return 'submitted'; }
else { throw 'Invalid feedback state'; }
}
else {
if (newState == 'open') {
containerSel.toggleClass('is--transitioning', false);
containerSel.toggleClass('is--submitting', false);
containerSel.toggleClass('is--submitted', false);
this.setHidden(instructionsSel, false);
this.setHidden(fieldsSel, false);
this.setHidden(actionsSel, false);
this.setHidden(transitionSel, true);
this.setHidden(messageSel, true);
}
else if (newState == 'submitting') {
containerSel.toggleClass('is--transitioning', true);
containerSel.toggleClass('is--submitting', true);
containerSel.toggleClass('is--submitted', false);
this.setHidden(instructionsSel, true);
this.setHidden(fieldsSel, true);
this.setHidden(actionsSel, true);
this.setHidden(transitionSel, false);
this.setHidden(messageSel, true);
}
else if (newState == 'submitted') {
containerSel.toggleClass('is--transitioning', false);
containerSel.toggleClass('is--submitting', false);
containerSel.toggleClass('is--submitted', true);
this.setHidden(instructionsSel, true);
this.setHidden(fieldsSel, true);
this.setHidden(actionsSel, true);
this.setHidden(transitionSel, true);
this.setHidden(messageSel, false);
}
}
},
/**
Send assessment feedback to the server and update the view.
**/
submitFeedbackOnAssessment: function() {
// Send the submission to the server
var view = this;
var baseView = this.baseView;
// Disable the submission button to prevent duplicate submissions
$("#feedback__submit", this.element).toggleClass('is--disabled', true);
// Indicate to the user that we're starting to submit
view.feedbackState('submitting');
// Submit the feedback to the server
// When the server reports success, update the UI to indicate that we'v submitted.
this.server.submitFeedbackOnAssessment(
this.feedbackText(), this.feedbackOptions()
).done(
function() { view.feedbackState('submitted'); }
).fail(function(errMsg) {
baseView.toggleActionError('feedback_assess', errMsg);
});
}
};
// openassessment: contexts - responsive sizing
// ====================
// NOTES:
// * this creates different sized layouts/grid based to use in lieu of native media queries (since XBlocks don't control the chrome they are rendered in)
.xblock {
// --------------------
// CASE: extra small digital screen
// --------------------
&.ui-size-xs .wrapper--grid {
}
// --------------------
// CASE: small digital screen
// --------------------
&.ui-size-s .wrapper--grid {
}
// --------------------
// CASE: medium digital screen
// --------------------
&.ui-size-m .wrapper--grid {
}
// --------------------
// CASE: large digital screen
// --------------------
&.ui-size-l .wrapper--grid {
}
// --------------------
// CASE: extra large large digital screen
// --------------------
&.ui-size-xl .wrapper--grid {
}
}
...@@ -21,14 +21,3 @@ $bp-ds: new-breakpoint(min-width $grid-size-s max-width ($grid-size-m - 1) 6); ...@@ -21,14 +21,3 @@ $bp-ds: new-breakpoint(min-width $grid-size-s max-width ($grid-size-m - 1) 6);
$bp-dm: new-breakpoint(min-width $grid-size-m max-width ($grid-size-l - 1) 12); // medium displays - make grid 12 columns $bp-dm: new-breakpoint(min-width $grid-size-m max-width ($grid-size-l - 1) 12); // medium displays - make grid 12 columns
$bp-dl: new-breakpoint(min-width $grid-size-l max-width ($grid-size-x - 1) 12); // large displays - make grid 12 columns $bp-dl: new-breakpoint(min-width $grid-size-l max-width ($grid-size-x - 1) 12); // large displays - make grid 12 columns
$bp-dx: new-breakpoint(min-width $grid-size-x 12); // large displays - make grid 12 columns $bp-dx: new-breakpoint(min-width $grid-size-x 12); // large displays - make grid 12 columns
// --------------------
// // application - colors: states
// --------------------
$color-error: rgb(188, 85, 71);
$color-warning: rgb(229, 166, 53);
$color-complete: rgb(98, 194, 74);
$color-incomplete: $color-warning;
$color-confirm: $heading-primary-color;
$color-unavailable: tint($copy-color, 85%);
...@@ -255,7 +255,7 @@ ...@@ -255,7 +255,7 @@
} }
.ico { .ico {
@extend %icon-2; @extend %icon-3;
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
margin-left: ($baseline-v/4); margin-left: ($baseline-v/4);
...@@ -362,7 +362,7 @@ ...@@ -362,7 +362,7 @@
} }
.ico { .ico {
@extend %icon-1; @extend %icon-2;
} }
} }
...@@ -392,13 +392,17 @@ ...@@ -392,13 +392,17 @@
.step__status__value { .step__status__value {
background: $color-unavailable; background: $color-unavailable;
.wrapper--anim {
@include animation(pulse $tmg-s3 ease-in-out infinite);
}
.ico { .ico {
display: inline-block; display: inline-block;
color: $copy-color; color: $copy-secondary-color;
} }
.copy { .copy {
color: $copy-color; color: $copy-secondary-color;
} }
} }
} }
...@@ -1073,22 +1077,23 @@ ...@@ -1073,22 +1077,23 @@
} }
// feedback form // feedback form
.submission__feeedback { .submission__feedback {
@extend %ui-subsection; @extend %ui-subsection;
} }
.submission__feeedback__title { .submission__feedback__title {
@extend %ui-subsection-title; @extend %ui-subsection-title;
@extend %t-heading; @extend %t-heading;
margin-bottom: ($baseline-v/2); margin-bottom: ($baseline-v/2);
color: $heading-secondary-color; color: $heading-secondary-color;
} }
.submission__feeedback__content { .submission__feedback__content {
@extend %ui-subsection-content; @extend %ui-subsection-content;
margin-bottom: $baseline-v;
} }
.submission__feeedback__instructions { .submission__feedback__instructions {
@extend %copy-2; @extend %copy-2;
margin-bottom: $baseline-v; margin-bottom: $baseline-v;
color: $copy-secondary-color; color: $copy-secondary-color;
...@@ -1121,7 +1126,7 @@ ...@@ -1121,7 +1126,7 @@
} }
} }
.submission__feeedback__actions { .submission__feedback__actions {
@extend %ui-subsection-content; @extend %ui-subsection-content;
padding-top: 0; padding-top: 0;
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
@import 'xb/utilities/variables'; // default settings and values @import 'xb/utilities/variables'; // default settings and values
@import 'xb/utilities/mixins'; // mixins and functions @import 'xb/utilities/mixins'; // mixins and functions
@import 'xb/utilities/extends'; // re-usable extends, placeholders, archetypes @import 'xb/utilities/extends'; // re-usable extends, placeholders, archetypes
@import 'xb/utilities/animations'; // re-usable CSS-based animations
// general xblocks: assets // general xblocks: assets
@import 'xb/assets/fonts'; // imported/used fonts @import 'xb/assets/fonts'; // imported/used fonts
...@@ -52,6 +53,7 @@ ...@@ -52,6 +53,7 @@
@import 'xb/elements/typography'; // font sizes/scale and applied/canned definitions @import 'xb/elements/typography'; // font sizes/scale and applied/canned definitions
@import 'xb/elements/controls'; // buttons, link styles, sliders, etc. @import 'xb/elements/controls'; // buttons, link styles, sliders, etc.
@import 'xb/elements/forms'; // form elements @import 'xb/elements/forms'; // form elements
@import 'xb/elements/system-feedback'; // system messages, feedback, transitions
@import 'xb/elements/layout'; // applied layouts and deliberate class-based breakpoints @import 'xb/elements/layout'; // applied layouts and deliberate class-based breakpoints
// xblock: contextual // xblock: contextual
......
...@@ -38,6 +38,14 @@ hr.divider, ...@@ -38,6 +38,14 @@ hr.divider,
// -------------------- // --------------------
// toggling visibility
// --------------------
.is--hidden {
@extend %state-hidden;
}
// --------------------
// semantic lists used for UI // semantic lists used for UI
// -------------------- // --------------------
.list--actions, .list--actions,
......
// xblock: elements - system feedback
// ====================
// NOTES:
// * General system feedback UI archetypes - messages, transitions, etc.
.wrapper--xblock {
// --------------------
// messages
// --------------------
.message {
margin-bottom: $baseline-v;
border-radius: ($baseline-v/10);
padding: $baseline-v ($baseline-h/2);
background: $color-decorative-quaternary;
.message__title {
@extend %t-heading;
margin-bottom: ($baseline-v/4);
border-bottom: ($baseline-v/10) solid $color-decorative-tertiary;
padding-bottom: ($baseline-v/4);
}
.message__content {
@extend %copy-3;
color: $copy-secondary-color;
p {
margin-bottom: ($baseline-v/2);
&:last-child {
@extend %wipe-last-child;
}
}
a {
@extend %link-copy;
}
}
}
// TYPE: error
.message--error {
background: tint($color-error, 95%);
.message__title {
color: $color-error;
border-bottom-color: $color-error;
}
}
// TYPE: warning
.message--warning {
background: tint($color-warning, 95%);
.message__title {
color: $color-warning;
border-bottom-color: $color-warning;
}
}
// TYPE: confirmation
.message--confirmation {
background: tint($color-confirm, 95%);
.message__title {
color: $color-confirm;
border-bottom-color: $color-confirm;
}
}
// TYPE: complete
.message--complete {
background: tint($color-complete, 95%);
.message__title {
color: $color-complete;
border-bottom-color: $color-complete;
}
}
// TYPE: incomplete
.message--incomplete {
background: tint($color-incomplete, 95%);
.message__title {
color: $color-incomplete;
border-bottom-color: $color-incomplete;
}
}
// CASE: showing errors is shown
.message--error {
@extend %trans-opacity;
display: none;
opacity: 0.0;
}
.has--error {
.message--error {
display: block;
opacity: 1.0;
}
}
// TYPE: inline message
.message--inline {
padding: ($baseline-v/2) ($baseline-h/2);
background: tint($color-confirm, 15%);
.message__title {
margin-bottom: 0;
border: none;
padding-bottom: 0;
color: $white-t;
text-align: center;
}
&.message--error {
background: tint($color-error, 15%);
}
&.message--warning {
background: tint($color-warning, 15%);
}
&.message--confirm {
background: tint($color-warning, 15%);
}
}
// --------------------
// transitions
// --------------------
.is--transitioning {
@extend %state-disabled;
padding: ($baseline-v*2) ($baseline-h);
background: $color-decorative-quaternary;
.transition__status {
@include alignVertically();
text-align: center;
.wrapper--anim {
display: block;
@include animation(pulse $tmg-s3 ease-in-out infinite);
}
.ico, .copy {
display: block;
color: $copy-secondary-color;
}
.ico {
@extend %icon-0;
margin-bottom: ($baseline-v/2);
}
.copy {
@extend %hd-2;
@extend %t-strong;
@extend %t-titlecase;
}
}
}
}
...@@ -145,20 +145,24 @@ ...@@ -145,20 +145,24 @@
// -------------------- // --------------------
// canned icons // canned icons
// -------------------- // --------------------
%icon-0 {
@extend %t-xlarge;
}
%icon-1 { %icon-1 {
@extend %t-medium; @extend %t-large;
} }
%icon-2 { %icon-2 {
@extend %t-base; @extend %t-medium;
} }
%icon-3 { %icon-3 {
@extend %t-small; @extend %t-base;
} }
%icon-4 { %icon-4 {
@extend %t-xsmall; @extend %t-small;
} }
......
...@@ -36,4 +36,4 @@ ...@@ -36,4 +36,4 @@
// float: left; // float: left;
// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%;
// } // }
// } // }
\ No newline at end of file
...@@ -19,4 +19,4 @@ ...@@ -19,4 +19,4 @@
@else { @else {
@return $prop; @return $prop;
} }
} }
\ No newline at end of file
// xblock: utilities - CSS animations
// ====================
// --------------------
// pulse
// --------------------
@include keyframes(pulse) {
0% {
opacity: 0.50;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.50;
}
}
// canned animation - use if you want out of the box/non-customized anim
%anim-pulse {
@include animation(pulse $tmg-f1 ease-in-out 1);
}
...@@ -117,6 +117,15 @@ ...@@ -117,6 +117,15 @@
outline: invert none medium; outline: invert none medium;
} }
// --------------------
// UI: element rendering/visibility
// --------------------
%state-hidden {
@extend %state-disabled;
display: none;
visibility: hidden;
}
// -------------------- // --------------------
// UI: element depth // UI: element depth
......
...@@ -16,6 +16,15 @@ ...@@ -16,6 +16,15 @@
left: $left; left: $left;
} }
// --------------------
// shorthand - vertically align something
// --------------------
@mixin alignVertically {
position: relative;
top: 50%;
@include transform(translateY(-50%));
}
// -------------------- // --------------------
// fontSize (rems) // fontSize (rems)
...@@ -68,4 +77,3 @@ ...@@ -68,4 +77,3 @@
@include lh($f-size-xlarge); @include lh($f-size-xlarge);
} }
} }
...@@ -183,3 +183,13 @@ $selected-color: $blue-s1; ...@@ -183,3 +183,13 @@ $selected-color: $blue-s1;
$bg-view: $gray-l7; $bg-view: $gray-l7;
$bg-content: $white; $bg-content: $white;
$bg-message: $black; $bg-message: $black;
// --------------------
// // application - colors: states
// --------------------
$color-error: rgb(188, 85, 71);
$color-warning: rgb(229, 166, 53);
$color-complete: rgb(98, 194, 74);
$color-incomplete: $color-warning;
$color-confirm: $heading-primary-color;
$color-unavailable: tint($copy-color, 85%);
#!/usr/bin/env bash
cd `dirname $BASH_SOURCE` && cd ../apps/openassessment/xblock/static
sass --update sass:css --force
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment