Commit de4d39d2 by Dennis Jen

Check for assignment type/format when retrieving assignments.

parent 423a7154
...@@ -34,8 +34,8 @@ clean: ...@@ -34,8 +34,8 @@ clean:
test_python: clean test_python: clean
python manage.py compress --settings=analytics_dashboard.settings.test python manage.py compress --settings=analytics_dashboard.settings.test
python manage.py test analytics_dashboard --settings=analytics_dashboard.settings.test --with-coverage \ python manage.py test analytics_dashboard common --settings=analytics_dashboard.settings.test --with-coverage \
--cover-package=analytics_dashboard --cover-branches --cover-html --cover-html-dir=$(COVERAGE)/html/ \ --cover-package=analytics_dashboard --cover-package=common --cover-branches --cover-html --cover-html-dir=$(COVERAGE)/html/ \
--with-ignore-docstrings --cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml --with-ignore-docstrings --cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml
accept: accept:
......
...@@ -7,7 +7,7 @@ from acceptance_tests import ENABLE_COURSE_API ...@@ -7,7 +7,7 @@ from acceptance_tests import ENABLE_COURSE_API
from acceptance_tests.mixins import CoursePageTestsMixin from acceptance_tests.mixins import CoursePageTestsMixin
from acceptance_tests.pages import CoursePerformanceGradedContentPage, CoursePerformanceAnswerDistributionPage, \ from acceptance_tests.pages import CoursePerformanceGradedContentPage, CoursePerformanceAnswerDistributionPage, \
CoursePerformanceGradedContentByTypePage, CoursePerformanceAssignmentPage CoursePerformanceGradedContentByTypePage, CoursePerformanceAssignmentPage
from common import CourseStructure from common.course_structure import CourseStructure
_multiprocess_can_split_ = True _multiprocess_can_split_ = True
...@@ -70,6 +70,12 @@ class CoursePerformancePageTestsMixin(CoursePageTestsMixin): ...@@ -70,6 +70,12 @@ class CoursePerformancePageTestsMixin(CoursePageTestsMixin):
for problem in assignment['problems']: for problem in assignment['problems']:
submission_entry = problems.get(problem['id'], None) submission_entry = problems.get(problem['id'], None)
problem.update({
'total_submissions': 0,
'correct_submissions': 0
})
if submission_entry: if submission_entry:
total += submission_entry['total_submissions'] total += submission_entry['total_submissions']
correct += submission_entry['correct_submissions'] correct += submission_entry['correct_submissions']
...@@ -304,7 +310,7 @@ class CoursePerformanceAnswerDistributionTests(CoursePerformancePageTestsMixin, ...@@ -304,7 +310,7 @@ class CoursePerformanceAnswerDistributionTests(CoursePerformancePageTestsMixin,
for col in columns: for col in columns:
actual.append(col.text) actual.append(col.text)
expected = [answer[value_field]] expected = [answer[value_field] if answer[value_field] else u'(empty)']
correct = '-' correct = '-'
if answer['correct']: if answer['correct']:
correct = u'Correct' correct = u'Correct'
......
...@@ -8,8 +8,8 @@ from django.core.cache import cache ...@@ -8,8 +8,8 @@ from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import common
from common.clients import CourseStructureApiClient from common.clients import CourseStructureApiClient
from common.course_structure import CourseStructure
from courses import utils from courses import utils
from courses.exceptions import NoAnswerSubmissionsError from courses.exceptions import NoAnswerSubmissionsError
from courses.presenters import BasePresenter from courses.presenters import BasePresenter
...@@ -317,7 +317,7 @@ class CoursePerformancePresenter(BasePresenter): ...@@ -317,7 +317,7 @@ class CoursePerformancePresenter(BasePresenter):
if not assignments: if not assignments:
structure = self._structure() structure = self._structure()
assignments = common.CourseStructure.course_structure_to_assignments( assignments = CourseStructure.course_structure_to_assignments(
structure, graded=True, assignment_type=None) structure, graded=True, assignment_type=None)
cache.set(all_assignments_key, assignments) cache.set(all_assignments_key, assignments)
......
import copy
import urllib import urllib
import uuid
from common.tests.factories import CourseStructureFactory
from courses.tests.utils import CREATED_DATETIME_STRING from courses.tests.utils import CREATED_DATETIME_STRING
class CoursePerformanceDataFactory(object): class CoursePerformanceDataFactory(CourseStructureFactory):
""" Factory that can be used to generate data for course performance-related presenters and APIs. """ """ Factory that can be used to generate data for course performance-related presenters and APIs. """
course_id = "edX/DemoX/Demo_Course" def present_assignments(self):
assignment_types = ['Homework', 'Exam']
grading_policy = [
{
"assignment_type": "Homework",
"count": 5,
"dropped": 1,
"weight": 0.2
},
{
"assignment_type": "Exam",
"count": 4,
"dropped": 0,
"weight": 0.8
}
]
def __init__(self):
self._structure = {}
self._assignments = []
self._generate_structure()
def _generate_block(self, block_type, block_format=None, display_name=None, graded=True, children=None):
return {
'id': 'i4x://edX/DemoX/{}/{}'.format(block_type, uuid.uuid4().hex),
'display_name': display_name,
'graded': graded,
'format': block_format,
'type': block_type,
'children': children or []
}
def _generate_structure(self):
root = 'i4x://edX/DemoX/course/Demo_Course'
self._structure = {
'root': root,
'blocks': {
root: {
'id': root,
'display_name': 'Demo Course',
'graded': False,
'format': None,
'type': 'course',
'children': []
}
}
}
self._assignments = []
for gp in self.grading_policy:
count = gp['count']
assignment_type = gp['assignment_type']
for assignment_index in range(1, count + 1):
display_name = '{} {}'.format(assignment_type, assignment_index)
children = []
# Generate the children
for problem_index in range(1, 4):
problem = self._generate_block('problem',
assignment_type,
'{} Problem {}'.format(display_name, problem_index))
problem_id = problem['id']
children.append(problem_id)
self._structure['blocks'][problem_id] = problem
assignment = self._generate_block('sequential', assignment_type, display_name, children=children)
assignment_id = assignment['id']
self._structure['blocks'][assignment_id] = assignment
self._structure['blocks'][root]['children'].append(assignment_id)
self._assignments.append(assignment)
def present_assignments(self, include_submissions=True):
presented = [] presented = []
for assignment_index, assignment in enumerate(self._assignments): # Exclude assignments with no assignment type
assignments = [assignment for assignment in self._assignments if assignment['format']]
for assignment_index, assignment in enumerate(assignments):
problems = [] problems = []
for problem_index, child in enumerate(assignment['children']): for problem_index, child in enumerate(assignment['children']):
block = self._structure['blocks'][child] block = self._structure['blocks'][child]
...@@ -95,36 +25,22 @@ class CoursePerformanceDataFactory(object): ...@@ -95,36 +25,22 @@ class CoursePerformanceDataFactory(object):
correct_percent = 0 correct_percent = 0
url_template = '/courses/{}/performance/graded_content/assignments/{}/problems/' \ url_template = '/courses/{}/performance/graded_content/assignments/{}/problems/' \
'{}/parts/{}/answer_distribution/' '{}/parts/{}/answer_distribution/'
problems.append({
problem = {
'index': problem_index + 1, 'index': problem_index + 1,
'total_submissions': problem_index,
'correct_submissions': problem_index,
'correct_percent': correct_percent,
'incorrect_submissions': 0.0,
'incorrect_percent': 0,
'id': _id, 'id': _id,
'name': block['display_name'], 'name': block['display_name'],
'total_submissions': 0, 'part_ids': [part_id],
'correct_submissions': 0, 'url': urllib.quote(url_template.format(
'correct_percent': 0, CoursePerformanceDataFactory.course_id, assignment['id'], _id, part_id))
'incorrect_submissions': 0, })
'incorrect_percent': 0,
'part_ids': []
}
if include_submissions:
problem.update({
'part_ids': [part_id],
'total_submissions': problem_index,
'correct_submissions': problem_index,
'correct_percent': correct_percent,
'incorrect_submissions': 0,
'incorrect_percent': 0,
'url': urllib.quote(url_template.format(self.course_id, assignment['id'], _id, part_id)),
})
problems.append(problem)
num_problems = len(problems) num_problems = len(problems)
url_template = '/courses/{}/performance/graded_content/assignments/{}/' url_template = '/courses/{}/performance/graded_content/assignments/{}/'
total_submissions = sum([problem['total_submissions'] for problem in problems])
correct_submissions = sum([problem['correct_submissions'] for problem in problems])
presented_assignment = { presented_assignment = {
'index': assignment_index + 1, 'index': assignment_index + 1,
'id': assignment['id'], 'id': assignment['id'],
...@@ -132,16 +48,15 @@ class CoursePerformanceDataFactory(object): ...@@ -132,16 +48,15 @@ class CoursePerformanceDataFactory(object):
'assignment_type': assignment['format'], 'assignment_type': assignment['format'],
'problems': problems, 'problems': problems,
'num_problems': num_problems, 'num_problems': num_problems,
'total_submissions': total_submissions, 'total_submissions': num_problems,
'correct_submissions': correct_submissions, 'correct_submissions': num_problems,
'correct_percent': 0.0 if not total_submissions else correct_submissions / total_submissions, 'correct_percent': 1.0,
'incorrect_submissions': 0, 'incorrect_submissions': 0,
'incorrect_percent': 0.0 'incorrect_percent': 0.0,
'url': urllib.quote(url_template.format(
CoursePerformanceDataFactory.course_id, assignment['id']))
} }
if total_submissions > 0:
presented_assignment['url'] = urllib.quote(url_template.format(self.course_id, assignment['id']))
presented.append(presented_assignment) presented.append(presented_assignment)
return presented return presented
...@@ -161,11 +76,3 @@ class CoursePerformanceDataFactory(object): ...@@ -161,11 +76,3 @@ class CoursePerformanceDataFactory(object):
}) })
return problems return problems
@property
def structure(self):
return copy.deepcopy(self._structure)
@property
def assignments(self):
return copy.deepcopy(self._assignments)
from requests.auth import AuthBase
class CourseStructure(object):
@staticmethod
def _filter_children(blocks, key, **kwargs):
"""
Given the blocks locates the nested graded or ungraded problems.
"""
block = blocks[key]
block_type = kwargs.pop(u'block_type', None)
if block_type:
kwargs[u'type'] = block_type
kwargs.setdefault(u'graded', False)
matched = True
for name, value in kwargs.iteritems():
matched &= (block.get(name, None) == value)
if not matched:
break
if matched:
return [block]
children = []
for child in block[u'children']:
children += CourseStructure._filter_children(blocks, child, **kwargs)
return children
@staticmethod
def course_structure_to_assignments(structure, graded=None, assignment_type=None):
"""
Returns the assignments and nested problems from the given course structure.
"""
blocks = structure[u'blocks']
root = blocks[structure[u'root']]
# Break down the course structure into assignments and nested problems, returning only the data
# we absolutely need.
assignments = []
kwargs = {
'graded': graded
}
if assignment_type:
kwargs[u'format'] = assignment_type
filtered = CourseStructure._filter_children(blocks, root[u'id'], **kwargs)
for assignment in filtered:
filtered_children = CourseStructure._filter_children(blocks, assignment[u'id'], graded=graded,
block_type=u'problem')
problems = []
for problem in filtered_children:
problems.append({
'id': problem['id'],
'name': problem['display_name'],
'total_submissions': 0,
'correct_submissions': 0,
'incorrect_submissions': 0,
})
assignments.append({
'id': assignment['id'],
'name': assignment['display_name'],
'assignment_type': assignment['format'],
'problems': problems,
})
return assignments
class BearerAuth(AuthBase):
""" Attaches Bearer Authentication to the given Request object. """
def __init__(self, token):
""" Instantiate the auth class. """
self.token = token
def __call__(self, r):
""" Update the request headers. """
r.headers['Authorization'] = 'Bearer {0}'.format(self.token)
return r
from requests.auth import AuthBase
class BearerAuth(AuthBase):
""" Attaches Bearer Authentication to the given Request object. """
def __init__(self, token):
""" Instantiate the auth class. """
self.token = token
def __call__(self, r):
""" Update the request headers. """
r.headers['Authorization'] = 'Bearer {0}'.format(self.token)
return r
import slumber import slumber
from common import BearerAuth from common.auth import BearerAuth
class CourseStructureApiClient(slumber.API): class CourseStructureApiClient(slumber.API):
......
class CourseStructure(object):
@staticmethod
def _filter_children(blocks, key, require_format=False, **kwargs):
"""
Given the blocks, locates the nested blocks matching the criteria set in kwargs.
Arguments
blocks -- Dictionary mapping ID strings to block dicts
key -- ID of the root node where tree traversal should begin
require_format -- Boolean indicating if the format field should be required to have a
non-empty (truthy) value if a match is made
kwargs -- Dictionary mapping field names to required values for matches
"""
block = blocks[key]
block_type = kwargs.pop(u'block_type', None)
if block_type:
kwargs[u'type'] = block_type
kwargs.setdefault(u'graded', False)
matched = True
for name, value in kwargs.iteritems():
matched &= (block.get(name, None) == value)
if not matched:
break
if matched:
if require_format:
if block[u'format']:
return [block]
else:
return [block]
children = []
for child in block[u'children']:
children += CourseStructure._filter_children(blocks, child, require_format=require_format, **kwargs)
return children
@staticmethod
def course_structure_to_assignments(structure, graded=None, assignment_type=None):
"""
Returns the assignments and nested problems from the given course structure.
"""
blocks = structure[u'blocks']
root = blocks[structure[u'root']]
# Break down the course structure into assignments and nested problems, returning only the data
# we absolutely need.
assignments = []
kwargs = {
'graded': graded
}
if assignment_type:
kwargs[u'format'] = assignment_type
filtered = CourseStructure._filter_children(blocks, root[u'id'], require_format=True, **kwargs)
for assignment in filtered:
filtered_children = CourseStructure._filter_children(blocks, assignment[u'id'], graded=graded,
block_type=u'problem', require_format=False)
problems = []
for problem in filtered_children:
problems.append({
'id': problem['id'],
'name': problem['display_name']
})
assignments.append({
'id': assignment['id'],
'name': assignment['display_name'],
'assignment_type': assignment['format'],
'problems': problems,
})
return assignments
import copy
import uuid
class CourseStructureFactory(object):
""" Factory that can be used to generate course structure. """
course_id = "edX/DemoX/Demo_Course"
assignment_types = ['Homework', 'Exam']
grading_policy = [
{
"assignment_type": "Homework",
"count": 5,
"dropped": 1,
"weight": 0.2
},
{
"assignment_type": "Exam",
"count": 4,
"dropped": 0,
"weight": 0.8
}
]
def __init__(self):
self._structure = {}
self._assignments = []
self._generate_structure()
def _generate_block(self, block_type, block_format=None, display_name=None, graded=True, children=None):
return {
'id': 'i4x://edX/DemoX/{}/{}'.format(block_type, uuid.uuid4().hex),
'display_name': display_name,
'graded': graded,
'format': block_format,
'type': block_type,
'children': children or []
}
def _generate_structure(self):
root = 'i4x://edX/DemoX/course/Demo_Course'
self._structure = {
'root': root,
'blocks': {
root: {
'id': root,
'display_name': 'Demo Course',
'graded': False,
'format': None,
'type': 'course',
'children': []
}
}
}
self._assignments = []
for gp in self.grading_policy:
count = gp['count']
assignment_type = gp['assignment_type']
for assignment_index in range(1, count + 1):
display_name = '{} {}'.format(assignment_type, assignment_index)
children = []
# Generate the children
for problem_index in range(1, 4):
problem = self._generate_block('problem',
assignment_type,
'{} Problem {}'.format(display_name, problem_index))
problem_id = problem['id']
children.append(problem_id)
self._structure['blocks'][problem_id] = problem
assignment = self._generate_block('sequential', assignment_type, display_name, children=children)
assignment_id = assignment['id']
self._structure['blocks'][assignment_id] = assignment
self._structure['blocks'][root]['children'].append(assignment_id)
self._assignments.append(assignment)
# Add invalid data that should be filtered out by consuming code
self._assignments.append(self._generate_block('sequential', None, 'Block with no format'))
self._assignments.append(self._generate_block('sequential', '', 'Block with format set to empty string'))
@property
def structure(self):
return copy.deepcopy(self._structure)
@property
def assignments(self):
return copy.deepcopy(self._assignments)
from unittest import TestCase
import httpretty
import requests
from common.auth import BearerAuth
class BearerAuthTests(TestCase):
@httpretty.activate
def test_headers(self):
"""
Verify the class adds an Authorization header that includes the token.
:return:
"""
token = 'this-is-a-test'
url = 'http://example.com/'
# Mock the HTTP response and issue the request
httpretty.register_uri(httpretty.GET, url)
requests.get(url, auth=BearerAuth(token))
# Verify the header was set on the request
self.assertEqual(httpretty.last_request().headers['Authorization'], 'Bearer {0}'.format(token))
from unittest import TestCase
from common.course_structure import CourseStructure
from common.tests.factories import CourseStructureFactory
class CourseStructureTests(TestCase):
def _prepare_assignments(self, structure, assignments):
prepared = []
for assignment in assignments:
# Exclude assignments with invalid assignment types
if not assignment['format']:
continue
problems = []
for child in assignment.pop('children'):
block = structure['blocks'][child]
problems.append({
'id': block['id'],
'name': block['display_name']
})
prepared.append({
'id': assignment['id'],
'name': assignment['display_name'],
'assignment_type': assignment['format'],
'problems': problems,
})
return prepared
def test_course_structure_to_assignments(self):
factory = CourseStructureFactory()
structure = factory.structure
actual = CourseStructure.course_structure_to_assignments(structure, graded=True)
expected = self._prepare_assignments(structure, factory.assignments)
self.assertListEqual(actual, expected)
# Test for assignment type filtering
assignment_type = factory.grading_policy[0]['assignment_type']
actual = CourseStructure.course_structure_to_assignments(structure, graded=True,
assignment_type=assignment_type)
assignments = [assignment for assignment in factory.assignments if assignment['format'] == assignment_type]
expected = self._prepare_assignments(structure, assignments)
self.assertListEqual(actual, expected)
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