Commit 3f5924a3 by Will Daly

Course staff can see counts

parent c2509762
......@@ -64,6 +64,25 @@
</li>
{% endfor %}
</ol>
{% if is_course_staff %}
<div>
<h2>Course Staff Debug Information</h2>
<p><b>Total number of submissions</b>: {{ num_submissions }}</p>
<table border="1">
<tr>
<th>Step</th>
<th>Number of students</th>
</tr>
{% for item in status_counts %}
<tr>
<td>{{ item.status }}</td>
<td>{{ item.count }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
</div>
</div>
......
......@@ -292,6 +292,36 @@ def update_from_assessments(submission_uuid, assessment_requirements):
return _serialized_with_details(workflow, assessment_requirements)
def get_status_counts(**kwargs):
"""
Count how many workflows have each status, for a given item in a course.
Kwargs:
course_id (unicode): The ID of the course.
item_id (unicode): The ID of the item in the course.
item_type (unicode): The type of the item.
Returns:
list of dictionaries with keys "status" (str) and "count" (int)
Example usage:
>>> get_status_counts("ora2/1/1", "peer-assessment-problem")
[
{"status": "peer", "count": 5},
{"status": "self", "count": 10},
{"status": "waiting", "count": 43},
{"status": "done", "count": 12},
]
"""
submission_uuids = sub_api.get_submission_uuids(**kwargs)
workflows = AssessmentWorkflow.objects.filter(submission_uuid__in=submission_uuids)
return [
{"status": status, "count": workflows.filter(status=status).count()}
for status in AssessmentWorkflow.STATUS_VALUES
]
def _get_workflow_model(submission_uuid):
"""Return the `AssessmentWorkflow` model for a given `submission_uuid`.
......
......@@ -30,14 +30,16 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
an after the fact recording of the last known state of that information so
we can search easily.
"""
STATUS = Choices( # implicit "status" field
STATUS_VALUES = [
"peer", # User needs to assess peer submissions
"self", # User needs to assess themselves
"waiting", # User has done all necessary assessment but hasn't been
# graded yet -- we're waiting for assessments of their
# submission by others.
"done", # Complete
)
]
STATUS = Choices(*STATUS_VALUES) # implicit "status" field
submission_uuid = models.CharField(max_length=36, db_index=True, unique=True)
uuid = UUIDField(version=1, db_index=True, unique=True)
......
......@@ -81,3 +81,68 @@ class TestAssessmentWorkflowApi(TestCase):
submission = sub_api.create_submission(ITEM_1, "We talk TV!")
workflow = workflow_api.create_workflow(submission["uuid"])
workflow_api.get_workflow_for_submission(workflow["uuid"], REQUIREMENTS)
def test_get_status_counts(self):
# Initially, the counts should all be zero
counts = workflow_api.get_status_counts()
self.assertEqual(counts, [
{"status": "peer", "count": 0},
{"status": "self", "count": 0},
{"status": "waiting", "count": 0},
{"status": "done", "count": 0},
])
# Create assessments with each status
# We're going to cheat a little bit by using the model objects
# directly, since the API does not provide access to the status directly.
self._create_workflow_with_status("user 1", "test/1/1", "peer-problem", "peer")
self._create_workflow_with_status("user 2", "test/1/1", "peer-problem", "self")
self._create_workflow_with_status("user 3", "test/1/1", "peer-problem", "self")
self._create_workflow_with_status("user 4", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 5", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 6", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 7", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 8", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 9", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 10", "test/1/1", "peer-problem", "done")
# Now the counts should be updated
counts = workflow_api.get_status_counts()
self.assertEqual(counts, [
{"status": "peer", "count": 1},
{"status": "self", "count": 2},
{"status": "waiting", "count": 3},
{"status": "done", "count": 4},
])
def _create_workflow_with_status(
self, student_id, course_id, item_id, status,
item_type="openassessment", answer="answer"
):
"""
Create a submission and workflow with a given status.
Args:
student_id (unicode): Student ID for the submission.
course_id (unicode): Course ID for the submission.
item_id (unicode): Item ID for the submission
status (unicode): One of acceptable status values (e.g. "peer", "self", "waiting", "done")
Kwargs:
item_type (unicode): Type of item for the submission.
answer (unicode): Submission answer.
Returns:
None
"""
submission = sub_api.create_submission({
"student_id": student_id,
"course_id": course_id,
"item_id": item_id,
"item_type": item_type
}, answer)
AssessmentWorkflow.objects.create(
submission_uuid=submission['uuid'],
status=status
)
......@@ -290,8 +290,15 @@ class OpenAssessmentBlock(
"question": self.prompt,
"rubric_criteria": self.rubric_criteria,
"rubric_assessments": ui_models,
"is_course_staff": False,
}
if self.is_course_staff:
status_counts, num_submissions = self.get_workflow_status_counts()
context_dict['is_course_staff'] = True
context_dict['status_counts'] = status_counts
context_dict['num_submissions'] = num_submissions
template = get_template("openassessmentblock/oa_base.html")
context = Context(context_dict)
frag = Fragment(template.render(context))
......@@ -300,6 +307,19 @@ class OpenAssessmentBlock(
frag.initialize_js('OpenAssessmentBlock')
return frag
@property
def is_course_staff(self):
"""
Check whether the user has course staff permissions for this XBlock.
Returns:
bool
"""
if hasattr(self, 'xmodule_runtime'):
return getattr(self.xmodule_runtime, 'user_is_staff', False)
else:
return False
def _create_ui_models(self):
"""Combine UI attributes and XBlock configuration into a UI model.
......
......@@ -333,6 +333,37 @@ class TestDates(XBlockHandlerTestCase):
# If the runtime doesn't provide a published_date field, assume we've been published
self.assertTrue(xblock.is_released())
@scenario('data/basic_scenario.xml')
def test_is_course_staff(self, xblock):
# By default, we shouldn't be course staff
self.assertFalse(xblock.is_course_staff)
# If the LMS runtime tells us we're not course staff,
# we shouldn't be course staff.
xblock.xmodule_runtime = Mock(user_is_staff=False)
self.assertFalse(xblock.is_course_staff)
# If the LMS runtime tells us that we ARE course staff,
# then we're course staff.
xblock.xmodule_runtime.user_is_staff = True
self.assertTrue(xblock.is_course_staff)
@scenario('data/basic_scenario.xml')
def test_course_staff_debug_info(self, xblock):
# If we're not course staff, we shouldn't see the debug info
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
user_is_staff=False
)
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertNotIn("course staff debug", xblock_fragment.body_html().lower())
# If we ARE course staff, then we should see the debug info
xblock.xmodule_runtime.user_is_staff = True
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertIn("course staff debug", xblock_fragment.body_html().lower())
def assert_is_open(self, xblock, now, step, expected_is_open, expected_reason, released=None):
"""
Assert whether the XBlock step is open/closed.
......
......@@ -67,3 +67,33 @@ class WorkflowMixin(object):
return workflow_api.get_workflow_for_submission(
self.submission_uuid, self.workflow_requirements()
)
def get_workflow_status_counts(self):
"""
Retrieve the counts of students in each step of the workflow.
Returns:
tuple of (list, int), where the list contains dicts with keys
"status" (unicode value) and "count" (int value), and the
integer represents the total number of submissions.
Example Usage:
>>> status_counts, num_submissions = xblock.get_workflow_status_counts()
>>> num_submissions
12
>>> status_counts
[
{"status": "peer", "count": 2},
{"status": "self", "count": 1},
{"status": "waiting": "count": 4},
{"status": "done", "count": 5}
]
"""
student_item = self.get_student_item_dict()
status_counts = workflow_api.get_status_counts(
course_id=student_item['course_id'],
item_id=student_item['item_id'],
item_type=student_item['item_type'],
)
num_submissions = sum(item['count'] for item in status_counts)
return status_counts, num_submissions
......@@ -4,6 +4,7 @@ Public interface for the submissions app.
"""
import copy
import logging
from collections import defaultdict
from django.core.cache import cache
from django.db import IntegrityError, DatabaseError
......@@ -465,6 +466,60 @@ def set_score(submission_uuid, score, points_possible):
pass
def get_submission_uuids(course_id=None, item_id=None, item_type=None):
"""
Return a list of submission UUIDs that have a particular course_id, item_id, and/or item_type.
Kwargs:
course_id (unicode): The ID of the course.
item_id (unicode): The ID of the item in the course.
item_type (unicode): The type of the item.
Returns:
list of submission UUIDs
Example Usage:
>>> get_submissions_for_course_item(course_id="ora2/1/1", item_id="peer-assessment-problem")
[
'de97b11169ab4761b80b56cc42f6cb4a',
'bcb9fcae309a4628859c72f1d33eb89c',
...
]
"""
kwargs = {}
if course_id is not None:
kwargs['student_item__course_id'] = course_id
if item_id is not None:
kwargs['student_item__item_id'] = item_id
if item_type is not None:
kwargs['student_item__item_type'] = item_type
return Submission.objects.filter(**kwargs).values_list('uuid', flat=True)
def get_course_items():
"""
Retrieve the IDs of all items with workflows, grouped by course.
Returns:
dict
Example Usage:
>>> get_course_items()
{
"test/course/1": ["item-1", "item-2", "item-3"],
"test/course/2": ["item-4", "item-5", "item-6"],
...
}
"""
courses_dict = defaultdict(list)
items = StudentItem.objects.values_list('course_id', 'item_id').distinct()
for course_id, item_id in items:
courses_dict[course_id].append(item_id)
return courses_dict
def _get_or_create_student_item(student_item_dict):
"""Gets or creates a Student Item that matches the values specified.
......
......@@ -177,6 +177,61 @@ class TestSubmissionsApi(TestCase):
submissions = api.get_submissions(STUDENT_ITEM, 1)
self.assertEqual(u"Testing unicode answers.", submissions[0]["answer"])
def test_get_course_items(self):
# Initially, no course items available
self.assertEqual(api.get_course_items(), dict())
# Same course, same item
api.create_submission({
'student_id': 'Tim',
'course_id': 'Foo',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 1")
api.create_submission({
'student_id': 'Alice',
'course_id': 'Foo',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 2")
self.assertItemsEqual(api.get_course_items(), {"Foo": ["Bar"]})
# Same course, different items
api.create_submission({
'student_id': 'Alice',
'course_id': 'Foo',
'item_id': 'Different',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
# Different course, same item
api.create_submission({
'student_id': 'Alice',
'course_id': 'Lorem',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo", "Lorem"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
self.assertItemsEqual(api.get_course_items()["Lorem"], ["Bar"])
# Different course, different item
api.create_submission({
'student_id': 'Alice',
'course_id': 'Ipsum',
'item_id': 'Dolar',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo", "Lorem", "Ipsum"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
self.assertItemsEqual(api.get_course_items()["Lorem"], ["Bar"])
self.assertItemsEqual(api.get_course_items()["Ipsum"], ["Dolar"])
def _assert_submission(self, submission, expected_answer, expected_item,
expected_attempt):
self.assertIsNotNone(submission)
......
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