182 lines
6.9 KiB
Python
182 lines
6.9 KiB
Python
"""
|
|
WE'RE USING MIGRATIONS!
|
|
|
|
If you make changes to this model, be sure to create an appropriate migration
|
|
file and check it in at the same time as your model changes. To do that,
|
|
|
|
1. Go to the mitx dir
|
|
2. ./manage.py schemamigration courseware --auto description_of_your_change
|
|
3. Add the migration file created in mitx/courseware/migrations/
|
|
|
|
|
|
ASSUMPTIONS: modules have unique IDs, even across different module_types
|
|
|
|
"""
|
|
from django.db import models
|
|
#from django.core.cache import cache
|
|
from django.contrib.auth.models import User
|
|
|
|
#from cache_toolbox import cache_model, cache_relation
|
|
|
|
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
|
|
|
|
|
|
class StudentModule(models.Model):
|
|
"""
|
|
Keeps student state for a particular module in a particular course.
|
|
"""
|
|
# For a homework problem, contains a JSON
|
|
# object consisting of state
|
|
MODULE_TYPES = (('problem', 'problem'),
|
|
('video', 'video'),
|
|
('html', 'html'),
|
|
)
|
|
## These three are the key for the object
|
|
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
|
|
|
|
# Key used to share state. By default, this is the module_id,
|
|
# but for abtests and the like, this can be set to a shared value
|
|
# for many instances of the module.
|
|
# Filename for homeworks, etc.
|
|
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
|
|
student = models.ForeignKey(User, db_index=True)
|
|
course_id = models.CharField(max_length=255, db_index=True)
|
|
|
|
class Meta:
|
|
unique_together = (('student', 'module_state_key', 'course_id'),)
|
|
|
|
## Internal state of the object
|
|
state = models.TextField(null=True, blank=True)
|
|
|
|
## Grade, and are we done?
|
|
grade = models.FloatField(null=True, blank=True, db_index=True)
|
|
max_grade = models.FloatField(null=True, blank=True)
|
|
DONE_TYPES = (('na', 'NOT_APPLICABLE'),
|
|
('f', 'FINISHED'),
|
|
('i', 'INCOMPLETE'),
|
|
)
|
|
done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True)
|
|
|
|
created = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
modified = models.DateTimeField(auto_now=True, db_index=True)
|
|
|
|
def __unicode__(self):
|
|
return '/'.join([self.course_id, self.module_type,
|
|
self.student.username, self.module_state_key, str(self.state)[:20]])
|
|
|
|
|
|
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
|
|
|
|
|
|
class StudentModuleCache(object):
|
|
"""
|
|
A cache of StudentModules for a specific student
|
|
"""
|
|
def __init__(self, course_id, user, descriptors, select_for_update=False):
|
|
'''
|
|
Find any StudentModule objects that are needed by any descriptor
|
|
in descriptors. Avoids making multiple queries to the database.
|
|
Note: Only modules that have store_state = True or have shared
|
|
state will have a StudentModule.
|
|
|
|
Arguments
|
|
user: The user for which to fetch maching StudentModules
|
|
descriptors: An array of XModuleDescriptors.
|
|
select_for_update: Flag indicating whether the rows should be locked until end of transaction
|
|
'''
|
|
if user.is_authenticated():
|
|
module_ids = self._get_module_state_keys(descriptors)
|
|
|
|
# This works around a limitation in sqlite3 on the number of parameters
|
|
# that can be put into a single query
|
|
self.cache = []
|
|
chunk_size = 500
|
|
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
|
if select_for_update:
|
|
self.cache.extend(StudentModule.objects.select_for_update().filter(
|
|
course_id=course_id,
|
|
student=user,
|
|
module_state_key__in=id_chunk)
|
|
)
|
|
else:
|
|
self.cache.extend(StudentModule.objects.filter(
|
|
course_id=course_id,
|
|
student=user,
|
|
module_state_key__in=id_chunk)
|
|
)
|
|
|
|
else:
|
|
self.cache = []
|
|
|
|
|
|
@classmethod
|
|
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
|
|
descriptor_filter=lambda descriptor: True,
|
|
select_for_update=False):
|
|
"""
|
|
course_id: the course in the context of which we want StudentModules.
|
|
user: the django user for whom to load modules.
|
|
descriptor: An XModuleDescriptor
|
|
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
|
the supplied descriptor. If depth is None, load all descendent StudentModules
|
|
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
|
should be cached
|
|
select_for_update: Flag indicating whether the rows should be locked until end of transaction
|
|
"""
|
|
|
|
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
|
if descriptor_filter(descriptor):
|
|
descriptors = [descriptor]
|
|
else:
|
|
descriptors = []
|
|
|
|
if depth is None or depth > 0:
|
|
new_depth = depth - 1 if depth is not None else depth
|
|
|
|
for child in descriptor.get_children():
|
|
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
|
|
|
|
return descriptors
|
|
|
|
|
|
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
|
|
|
return StudentModuleCache(course_id, user, descriptors, select_for_update)
|
|
|
|
def _get_module_state_keys(self, descriptors):
|
|
'''
|
|
Get a list of the state_keys needed for StudentModules
|
|
required for this module descriptor
|
|
|
|
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
|
should be cached
|
|
'''
|
|
keys = []
|
|
for descriptor in descriptors:
|
|
if descriptor.stores_state:
|
|
keys.append(descriptor.location.url())
|
|
|
|
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
|
if shared_state_key is not None:
|
|
keys.append(shared_state_key)
|
|
|
|
return keys
|
|
|
|
def lookup(self, course_id, module_type, module_state_key):
|
|
'''
|
|
Look for a student module with the given course_id, type, and id in the cache.
|
|
|
|
cache -- list of student modules
|
|
|
|
returns first found object, or None
|
|
'''
|
|
for o in self.cache:
|
|
if (o.course_id == course_id and
|
|
o.module_type == module_type and
|
|
o.module_state_key == module_state_key):
|
|
return o
|
|
return None
|
|
|
|
def append(self, student_module):
|
|
self.cache.append(student_module)
|