Commit c345ca79 by Dmitry Viskov

new API method to return tags distribution info

parent a4b39021
......@@ -6,3 +6,4 @@ Ed Zarecor <ed@edx.org>
Gabe Mulley <gabe@edx.org>
Jason Bau <jbau@stanford.edu>
John Jarvis <jarv@edx.org>
Dmitry Viskov <dmitry.viskov@webenterprise.ru>
......@@ -225,6 +225,34 @@ class Command(BaseCommand):
course_id=course_id, start_date=start_date, end_date=end_date, metric=metric,
range_type='high', low_value=high_floor, high_value=max_value)
def generate_tags_distribution_data(self, course_id):
logger.info("Deleting existed tags distribution data...")
models.ProblemsAndTags.objects.all().delete()
module_id_tpl = 'i4x://test/problem/%d'
difficulty_tag = ['Easy', 'Medium', 'Hard']
learning_outcome_tag = ['Learned nothing', 'Learned a few things', 'Learned everything']
problems_num = 50
chance_difficulty = 5
logger.info("Generating new tags distribution data...")
for i in xrange(problems_num):
module_id = module_id_tpl % i
total_submissions = random.randint(0, 100)
correct_submissions = random.randint(0, total_submissions)
models.ProblemsAndTags.objects.create(
course_id=course_id, module_id=module_id,
tag_name='learning_outcome', tag_value=random.choice(learning_outcome_tag),
total_submissions=total_submissions, correct_submissions=correct_submissions
)
if random.randint(0, chance_difficulty) != chance_difficulty:
models.ProblemsAndTags.objects.create(
course_id=course_id, module_id=module_id,
tag_name='difficulty', tag_value=random.choice(difficulty_tag),
total_submissions=total_submissions, correct_submissions=correct_submissions
)
def handle(self, *args, **options):
course_id = options['course_id']
username = options['username']
......@@ -245,3 +273,4 @@ class Command(BaseCommand):
self.generate_video_timeline_data(video_id)
self.generate_learner_engagement_data(course_id, username, start_date, end_date)
self.generate_learner_engagement_range_data(course_id, start_date, end_date)
self.generate_tags_distribution_data(course_id)
......@@ -131,6 +131,21 @@ class ProblemResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
count = models.IntegerField()
class ProblemsAndTags(models.Model):
""" Model for the tags_distribution table """
class Meta(object):
db_table = 'tags_distribution'
course_id = models.CharField(db_index=True, max_length=255)
module_id = models.CharField(db_index=True, max_length=255)
tag_name = models.CharField(max_length=255)
tag_value = models.CharField(max_length=255)
total_submissions = models.IntegerField(default=0)
correct_submissions = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
class ProblemFirstLastResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
""" Updated model for answer_distribution table with counts of first and last attempts at problems. """
......
......@@ -57,6 +57,18 @@ class ProblemSerializer(serializers.Serializer):
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
class ProblemsAndTagsSerializer(serializers.Serializer):
"""
Serializer for problems and tags.
"""
module_id = serializers.CharField(required=True)
total_submissions = serializers.IntegerField(default=0)
correct_submissions = serializers.IntegerField(default=0)
tags = serializers.CharField()
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
class ProblemResponseAnswerDistributionSerializer(ModelSerializerWithCreatedField):
"""
Representation of the Answer Distribution table, without id.
......
......@@ -650,6 +650,82 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
self.assertEquals(response.status_code, 404)
class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
def _get_data(self, course_id=None):
"""
Retrieve data for the specified course.
"""
course_id = course_id or self.course_id
url = '/api/v0/courses/{}/problems_and_tags/'.format(course_id)
return self.authenticated_get(url)
def test_get(self):
"""
The view should return data when data exists for the course.
"""
# This data should never be returned by the tests below because the course_id doesn't match.
G(models.ProblemsAndTags)
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# natural order and ensure the view properly sorts the objects before grouping.
module_id = 'i4x://test/problem/1'
alt_module_id = 'i4x://test/problem/2'
tags = {
'difficulty': ['Easy', 'Medium', 'Hard'],
'learning_outcome': ['Learned nothing', 'Learned a few things', 'Learned everything']
}
created = datetime.datetime.utcnow()
alt_created = created + datetime.timedelta(seconds=2)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=module_id,
tag_name='difficulty', tag_value=tags['difficulty'][0],
total_submissions=11, correct_submissions=4, created=created)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=module_id,
tag_name='learning_outcome', tag_value=tags['learning_outcome'][1],
total_submissions=11, correct_submissions=4, created=alt_created)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=alt_module_id,
tag_name='learning_outcome', tag_value=tags['learning_outcome'][2],
total_submissions=4, correct_submissions=0, created=created)
expected = [
{
'module_id': module_id,
'total_submissions': 11,
'correct_submissions': 4,
'tags': {
'difficulty': 'Easy',
'learning_outcome': 'Learned a few things',
},
'created': alt_created.strftime(settings.DATETIME_FORMAT)
},
{
'module_id': alt_module_id,
'total_submissions': 4,
'correct_submissions': 0,
'tags': {
'learning_outcome': 'Learned everything',
},
'created': created.strftime(settings.DATETIME_FORMAT)
}
]
response = self._get_data(self.course_id)
self.assertEquals(response.status_code, 200)
self.assertListEqual(sorted(response.data), sorted(expected))
def test_get_404(self):
"""
The view should return 404 if no data exists for the course.
"""
response = self._get_data('foo/bar/course')
self.assertEquals(response.status_code, 404)
class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
def _get_data(self, course_id=None):
"""
......
......@@ -13,6 +13,7 @@ COURSE_URLS = [
('enrollment/gender', views.CourseEnrollmentByGenderView, 'enrollment_by_gender'),
('enrollment/location', views.CourseEnrollmentByLocationView, 'enrollment_by_location'),
('problems', views.ProblemsListView, 'problems'),
('problems_and_tags', views.ProblemsAndTagsListView, 'problems_and_tags'),
('videos', views.VideosListView, 'videos')
]
......
......@@ -628,9 +628,9 @@ class ProblemsListView(BaseCourseView):
Returns a collection of submission counts and part IDs for each problem. Each collection contains:
* module_id: The ID of the problem.
* total_submissions: Total number of submissions
* total_submissions: Total number of submissions.
* correct_submissions: Total number of *correct* submissions.
* part_ids: List of problem part IDs
* part_ids: List of problem part IDs.
"""
serializer_class = serializers.ProblemSerializer
allow_empty = False
......@@ -689,6 +689,53 @@ GROUP BY module_id;
return rows
# pylint: disable=abstract-method
class ProblemsAndTagsListView(BaseCourseView):
"""
Get the problems with the connected tags.
**Example request**
GET /api/v0/courses/{course_id}/problems_and_tags/
**Response Values**
Returns a collection of submission counts and tags for each problem. Each collection contains:
* module_id: The ID of the problem.
* total_submissions: Total number of submissions.
* correct_submissions: Total number of *correct* submissions.
* tags: Dictionary that contains pairs "tag key: tag value".
"""
serializer_class = serializers.ProblemsAndTagsSerializer
allow_empty = False
model = models.ProblemsAndTags
def get_queryset(self):
queryset = self.model.objects.filter(course_id=self.course_id)
items = queryset.all()
result = {}
for v in items:
if v.module_id in result:
result[v.module_id]['tags'][v.tag_name] = v.tag_value
if result[v.module_id]['created'] < v.created:
result[v.module_id]['created'] = v.created
else:
result[v.module_id] = {
'module_id': v.module_id,
'total_submissions': v.total_submissions,
'correct_submissions': v.correct_submissions,
'tags': {
v.tag_name: v.tag_value
},
'created': v.created
}
return result.values()
class VideosListView(BaseCourseView):
"""
Get data for the videos in a course.
......
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