Commit 0797f184 by Sef Kloninger

remove queryable_student_module

Even though not running in Stanford production, was causing tests to
fail.  We can resurrect sometime later if/when it's useful.
parent 7531afdd
"""
queryable app allows 2 commands to be run from rake:
populate_studentgrade
populate_studentmoduleexpand
These commands populate table in the SQL database and allow for the class_dashboard app
to render course metrics under the instructor dashboard.
"""
"""
Commands for queryable_student_module app:
populate_studentgrades
populate_studentmoduleexpand
"""
"""
Commands for queryable app:
populate_studentgrades
populate_studentmoduleexpand
"""
"""
======== Populate StudentModuleExpand ===============================================================================
Populates the StudentModuleExpand table of the queryable_table model.
For the provided course_id, it will find all rows in the StudentModule table of the courseware model that have
module_type 'problem' and the grade is not null. Then for any rows that have changed since the last populate or do not
have a corresponding row, update the attempts value.
"""
import json
from django.core.management.base import BaseCommand
from courseware.models import StudentModule
from queryable_student_module.models import Log, StudentModuleExpand
from queryable_student_module.util import pre_run_command, more_options
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
class Command(BaseCommand):
"""
populate_studentmoduleexpand command
"""
help = "Populates the queryable.StudentModuleExpand table.\n"
help += "Usage: populate_studentmoduleexpand course_id\n"
help += " course_id: course's ID, such as Medicine/HRP258/Statistics_in_Medicine\n"
option_list = BaseCommand.option_list + (more_options(),)
def handle(self, *args, **options):
script_id = "studentmoduleexpand"
print "args = ", args
if len(args) > 0:
course_id = args[0]
else:
print self.help
return
iterative_populate, tstart, last_log_run = pre_run_command(script_id, options, course_id)
# If iterative populate, get all the problems that students have submitted an answer to for this course,
# since the last run
if iterative_populate:
sm_rows = StudentModule.objects.select_related('student', 'student__profile').filter(course_id__exact=course_id, grade__isnull=False,
module_type__exact="problem", modified__gte=last_log_run[0].created)
else:
sm_rows = StudentModule.objects.select_related('student', 'student__profile').filter(course_id__exact=course_id, grade__isnull=False,
module_type__exact="problem")
c_updated_rows = 0
# For each problem, get or create the corresponding StudentModuleExpand row
for sm_row in sm_rows:
# Get the display name for the problem
module_state_key = sm_row.module_state_key
problem = modulestore().get_instance(course_id, module_state_key, 0)
problem_name = own_metadata(problem)['display_name']
sme, created = StudentModuleExpand.objects.get_or_create(student_id=sm_row.student_id, course_id=course_id,
module_state_key=module_state_key,
student_module_id=sm_row.id)
# If the StudentModuleExpand row is new or the StudentModule row was
# more recently updated than the StudentModuleExpand row, fill in/update
# everything and save
if created or (sme.modified < sm_row.modified):
c_updated_rows += 1
sme.grade = sm_row.grade
sme.max_grade = sm_row.max_grade
state = json.loads(sm_row.state)
sme.attempts = state["attempts"]
sme.label = problem_name
sme.username = sm_row.student.username
sme.name = sm_row.student.profile.name
sme.save()
c_all_rows = len(sm_rows)
print "--------------------------------------------------------------------------------"
print "Done! Updated/Created {0} queryable rows out of {1} from courseware_studentmodule".format(
c_updated_rows, c_all_rows)
print "--------------------------------------------------------------------------------"
# Save since everything finished successfully, log latest run.
q_log = Log(script_id=script_id, course_id=course_id, created=tstart)
q_log.save()
"""
model file for queryable app
"""
from django.contrib.auth.models import User
from django.db import models
from courseware.models import StudentModule
class StudentModuleExpand(models.Model):
"""
Expanded version of courseware's model StudentModule. This is only for
instances of module type 'problem'. Adds attribute 'attempts' that is pulled
out of the json in the state attribute.
"""
EXPAND_TYPES = {'problem'}
student_module_id = models.IntegerField(blank=True, null=True, db_index=True)
# The value mapped to 'attempts' in the json in state
attempts = models.IntegerField(null=True, blank=True, db_index=True)
# Values from StudentModule
module_type = models.CharField(max_length=32, default='problem', db_index=True)
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
course_id = models.CharField(max_length=255, db_index=True)
label = models.CharField(max_length=50, null=True, blank=True)
student_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_studentmoduleexpand"
unique_together = (('student_id', 'module_state_key', 'course_id'),)
grade = models.FloatField(null=True, blank=True, db_index=True)
max_grade = models.FloatField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
class CourseGrade(models.Model):
"""
Holds student's overall course grade as a percentage and letter grade (if letter grade present).
"""
course_id = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
grade = models.CharField(max_length=32, db_index=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_coursegrade"
unique_together = (('user_id', 'course_id'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class AssignmentTypeGrade(models.Model):
"""
Holds student's average grade for each assignment type per course.
"""
course_id = models.CharField(max_length=255, db_index=True)
category = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_assignmenttypegrade"
unique_together = (('user_id', 'course_id', 'category'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class AssignmentGrade(models.Model):
"""
Holds student's assignment grades per course.
"""
course_id = models.CharField(max_length=255, db_index=True)
category = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
label = models.CharField(max_length=32, db_index=True)
detail = models.CharField(max_length=255, blank=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_assignmentgrade"
unique_together = (('user_id', 'course_id', 'label'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class Log(models.Model):
"""
Log of when a script in this django app was last run. Use to filter out students or rows that don't need to be
processed in the populate scripts and show instructors how fresh the data is.
"""
script_id = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_log"
ordering = ["-created"]
get_latest_by = "created"
import json
from datetime import datetime
from pytz import UTC
from StringIO import StringIO
from django.test import TestCase
from django.test.utils import override_settings
from django.core import management
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.factories import StudentModuleFactory
from queryable_student_module.models import Log, StudentModuleExpand
from capa.tests.response_xml_factory import StringResponseXMLFactory
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestPopulateStudentModuleExpand(TestCase):
def setUp(self):
self.command = 'populate_studentmoduleexpand'
self.script_id = "studentmoduleexpand"
#self.course_id = 'test/test/test'
self.course = CourseFactory.create()
section = ItemFactory.create(
parent_location=self.course.location,
category="chapter",
display_name="test factory section",
)
sub_section = ItemFactory.create(
parent_location=section.location,
category="sequential",
# metadata={'graded': True, 'format': 'Homework'}
)
unit = ItemFactory.create(
parent_location=sub_section.location,
category="vertical",
metadata={'graded': True, 'format': 'Homework'}
)
category = "problem"
self.item = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
self.item2 = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
def test_missing_input(self):
"""
Fails safely when not given enough input
"""
try:
management.call_command(self.command)
self.assertTrue(True)
except:
self.assertTrue(False)
def test_just_logs_if_empty_course(self):
"""
If the course has nothing in it, just logs the run in the log table
"""
management.call_command(self.command, self.course.id)
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 1)
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 0)
def test_force_update(self):
"""
Even if there is a log entry for incremental update, force a full update
This may be done because something happened in the last update.
"""
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Create the log entry
log = Log(script_id=self.script_id, course_id=self.course.id, created=datetime.now(UTC))
log.save()
# Create a StudentModuleExpand that is after the log entry and has a different attempts value
sme = StudentModuleExpand(
course_id=self.course.id,
module_state_key=sm.module_state_key,
student_module_id=sm.id,
attempts=0,
)
# Call command with the -f flag
management.call_command(self.command, self.course.id, force=True)
# Check to see if new rows have been added
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 2)
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 1)
self.assertEqual(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)[0].attempts, 1)
def test_incremental_update_if_log_exists(self):
"""
Make sure it uses the log entry if it exists and we aren't forcing a full update
"""
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Create the log entry
log = Log(script_id=self.script_id, course_id=self.course.id, created=datetime.now(UTC))
log.save()
# Create a StudentModule that is after the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Call command
management.call_command(self.command, self.course.id)
# Check to see if new row has been added to log
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 2)
# Even though there are two studentmodules only one row should be created
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 1)
def test_update_only_if_row_modified(self):
"""
Test populate does not update a row if it is not necessary
For example the problem may have a more recent modified date but the attempts value has not changed.
"""
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 0)
# Create a StudentModule
sm1 = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
module_state_key=self.item.location
)
# Create a StudentModuleExpand
sme1 = StudentModuleExpand(
course_id=self.course.id,
student_module_id=sm1.id,
module_state_key=sm1.module_state_key,
student_id=sm1.student.id,
attempts=0,
)
sme1.save()
# Touch the StudentModule row so it has a later modified time
sm1.state = json.dumps({'attempts': 1})
sm1.save()
# Create a StudentModule
sm2 = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
module_state_key=self.item2.location,
grade=1,
max_grade=1,
state=json.dumps({'attempts': 2}),
)
# Create a StudentModuleExpand that has the same attempts value
sme2 = StudentModuleExpand(
course_id=self.course.id,
student_module_id=sm2.id,
module_state_key=sm2.module_state_key,
student_id=sm2.student.id,
attempts=2,
)
sme2.save()
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 2)
# Call command
management.call_command(self.command, self.course.id)
self.assertEqual(len(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme1.module_state_key)), 1)
self.assertEqual(len(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme2.module_state_key)), 1)
self.assertEqual(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme1.module_state_key)[0].attempts, 1)
self.assertEqual(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme2.module_state_key)[0].attempts, 2)
from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from queryable_student_module import util
class TestUtilApproxEqual(TestCase):
"""
Check the approx_equal function
"""
def test_default_tolerance(self):
"""
Check that function with default tolerance
"""
self.assertTrue(util.approx_equal(1.00001, 1.0))
self.assertTrue(util.approx_equal(1.0, 1.00001))
self.assertFalse(util.approx_equal(1.0, 2.0))
self.assertFalse(util.approx_equal(1.0, 1.0002))
def test_smaller_default_tolerance(self):
"""
Set tolerance smaller than default and check if still correct
"""
self.assertTrue(util.approx_equal(1.0, 1.0, 1))
self.assertTrue(util.approx_equal(1.0, 1.000001, 0.000001))
def test_bigger_default_tolerance(self):
"""
Set tolerance bigger than default and check if still correct
"""
self.assertFalse(util.approx_equal(1.0, 2.0, 0.75))
self.assertFalse(util.approx_equal(2.0, 1.0, 0.75))
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestUtilGetAssignmentToProblemMap(TestCase):
"""
Tests the get_assignemnt_to_problem_map
"""
def setUp(self):
self.course = CourseFactory.create()
def test_empty_course(self):
"""
Test for course with nothing in it
"""
problems_map = util.get_assignment_to_problem_map(self.course.id)
self.assertEqual(problems_map, {})
def test_single_assignment(self):
"""
Test returns the problems for a course with a single assignment
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection = ItemFactory.create(
parent_location=section.location.url(),
category="sequential",
)
subsection_metadata = own_metadata(subsection)
subsection_metadata['graded'] = True
subsection_metadata['format'] = "Homework"
modulestore().update_metadata(subsection.location, subsection_metadata)
unit = ItemFactory.create(
parent_location=subsection.location.url(),
category="vertical",
)
problem1 = ItemFactory.create(
parent_location=unit.location.url(),
category="problem",
)
problem2 = ItemFactory.create(
parent_location=unit.location.url(),
category="problem",
)
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url(), problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_two_assignments_same_type(self):
"""
Test if has two assignments
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['graded'] = True
subsection_metadata2['format'] = "Homework"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], [problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_two_assignments_different_types(self):
"""
Creates two assignments of different types
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['graded'] = True
subsection_metadata2['format'] = "Quiz"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], ],
'Quiz': [[problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_return_only_graded_subsections(self):
"""
Make sure only returns problems and assignments that are graded
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['format'] = "Quiz"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], ],
}
self.assertEqual(problems_map, answer)
"""
Utility functions to help with population
"""
from datetime import datetime
from pytz import UTC
import logging
from optparse import make_option
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from queryable_student_module.models import Log
log = logging.getLogger("mitx.queryable")
def get_assignment_to_problem_map(course_id):
"""
Returns a dictionary with assignment types/categories as keys and the value is an array of arrays. Each inner array
holds problem ids for an assignment. The arrays are ordered in the outer array as they are seen in the course, which
is how they are numbered in a student's progress page.
"""
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
assignment_problems_map = {}
for section in course.get_children():
for subsection in section.get_children():
subsection_metadata = own_metadata(subsection)
if ('graded' in subsection_metadata) and subsection_metadata['graded']:
category = subsection_metadata['format']
if category not in assignment_problems_map:
assignment_problems_map[category] = []
problems = []
for unit in subsection.get_children():
for child in unit.get_children():
if child.location.category == 'problem':
problems.append(child.location.url())
assignment_problems_map[category].append(problems)
return assignment_problems_map
def approx_equal(first, second, tolerance=0.0001):
"""
Checks if first and second are at most the specified tolerance away from each other.
"""
return abs(first - second) <= tolerance
def pre_run_command(script_id, options, course_id):
"""
Common pre-run method for both populate_studentgrades and populate_studentmoduleexpand commands.
"""
log.info("--------------------------------------------------------------------------------")
log.info("Populating queryable.{0} table for course {1}".format(script_id, course_id))
log.info("--------------------------------------------------------------------------------")
# Grab when we start, to log later
tstart = datetime.now(UTC)
iterative_populate = True
last_log_run = {}
if options['force']:
log.info("--------------------------------------------------------------------------------")
log.info("Full populate: Forced full populate")
log.info("--------------------------------------------------------------------------------")
iterative_populate = False
if iterative_populate:
# Get when this script was last run for this course
last_log_run = Log.objects.filter(script_id__exact=script_id, course_id__exact=course_id)
length = len(last_log_run)
log.info("--------------------------------------------------------------------------------")
if length > 0:
log.info("Iterative populate: Last log run %s", str(last_log_run[0].created))
else:
log.info("Full populate: Can't find log of last run")
iterative_populate = False
log.info("--------------------------------------------------------------------------------")
return iterative_populate, tstart, last_log_run
def more_options():
"""
Appends common options to options list
"""
option_list = make_option('-f', '--force',
action='store_true',
dest='force',
default=False,
help='Forces a full populate for all students and rows, rather than iterative.')
return option_list
\ No newline at end of file
......@@ -298,9 +298,6 @@ FEATURES['CLASS_DASHBOARD'] = True
import openid.oidutil
openid.oidutil.log = lambda message, level = 0: None
### QUERYABLE APP ###
INSTALLED_APPS += ('queryable_student_module',)
# set up some testing for microsites
MICROSITE_CONFIGURATION = {
"test_microsite": {
......
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