models.py 6.87 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
"""
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/

11 12 13

ASSUMPTIONS: modules have unique IDs, even across different module_types

14
"""
Piotr Mitros committed
15
from django.db import models
Piotr Mitros committed
16
#from django.core.cache import cache
Piotr Mitros committed
17 18
from django.contrib.auth.models import User

Piotr Mitros committed
19
#from cache_toolbox import cache_model, cache_relation
20

Piotr Mitros committed
21
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
22

23

Piotr Mitros committed
24
class StudentModule(models.Model):
25 26 27
    """
    Keeps student state for a particular module in a particular course.
    """
Piotr Mitros committed
28 29
    # For a homework problem, contains a JSON
    # object consisting of state
30 31 32
    MODULE_TYPES = (('problem', 'problem'),
                    ('video', 'video'),
                    ('html', 'html'),
33
                    )
34
    ## These three are the key for the object
35
    module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
36 37 38 39 40 41

    # 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')
42
    student = models.ForeignKey(User, db_index=True)
43
    course_id = models.CharField(max_length=255, db_index=True)
44

Ernie Park committed
45
    class Meta:
46
        unique_together = (('student', 'module_state_key', 'course_id'),)
Ernie Park committed
47

48
    ## Internal state of the object
Piotr Mitros committed
49
    state = models.TextField(null=True, blank=True)
50

51
    ## Grade, and are we done?
52
    grade = models.FloatField(null=True, blank=True, db_index=True)
53
    max_grade = models.FloatField(null=True, blank=True)
54 55 56
    DONE_TYPES = (('na', 'NOT_APPLICABLE'),
                    ('f', 'FINISHED'),
                    ('i', 'INCOMPLETE'),
57 58 59
                    )
    done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True)

60 61
    created = models.DateTimeField(auto_now_add=True, db_index=True)
    modified = models.DateTimeField(auto_now=True, db_index=True)
62 63

    def __unicode__(self):
64 65
        return '/'.join([self.course_id, self.module_type,
                         self.student.username, self.module_state_key, str(self.state)[:20]])
66 67 68 69 70 71 72 73 74


# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors


class StudentModuleCache(object):
    """
    A cache of StudentModules for a specific student
    """
75
    def __init__(self, course_id, user, descriptors, select_for_update=False):
76
        '''
77 78 79 80
        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.
81

82 83 84
        Arguments
        user: The user for which to fetch maching StudentModules
        descriptors: An array of XModuleDescriptors.
kimth committed
85
        select_for_update: Flag indicating whether the rows should be locked until end of transaction
86 87
        '''
        if user.is_authenticated():
88
            module_ids = self._get_module_state_keys(descriptors)
89 90 91 92 93

            # 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
94
            for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
95
                if select_for_update:
96
                    self.cache.extend(StudentModule.objects.select_for_update().filter(
97
                        course_id=course_id,
98 99 100 101 102
                        student=user,
                        module_state_key__in=id_chunk)
                    )
                else:
                    self.cache.extend(StudentModule.objects.filter(
103
                        course_id=course_id,
104 105 106
                        student=user,
                        module_state_key__in=id_chunk)
                    )
107

108 109
        else:
            self.cache = []
110 111


112
    @classmethod
113 114 115
    def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
                                         descriptor_filter=lambda descriptor: True,
                                         select_for_update=False):
116
        """
117 118
        course_id: the course in the context of which we want StudentModules.
        user: the django user for whom to load modules.
119 120 121
        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
122
        descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
123
            should be cached
kimth committed
124
        select_for_update: Flag indicating whether the rows should be locked until end of transaction
125
        """
126

127 128 129 130 131
        def get_child_descriptors(descriptor, depth, descriptor_filter):
            if descriptor_filter(descriptor):
                descriptors = [descriptor]
            else:
                descriptors = []
132

133 134
            if depth is None or depth > 0:
                new_depth = depth - 1 if depth is not None else depth
135

136 137
                for child in descriptor.get_children():
                    descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
138

139
            return descriptors
140 141


142
        descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
143 144 145

        return StudentModuleCache(course_id, user, descriptors, select_for_update)

146
    def _get_module_state_keys(self, descriptors):
147 148 149
        '''
        Get a list of the state_keys needed for StudentModules
        required for this module descriptor
150 151

        descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
152 153 154 155
            should be cached
        '''
        keys = []
        for descriptor in descriptors:
156 157
            if descriptor.stores_state:
                keys.append(descriptor.location.url())
158

159 160 161
            shared_state_key = getattr(descriptor, 'shared_state_key', None)
            if shared_state_key is not None:
                keys.append(shared_state_key)
162

163
        return keys
164

165
    def lookup(self, course_id, module_type, module_state_key):
166
        '''
167
        Look for a student module with the given course_id, type, and id in the cache.
168

169
        cache -- list of student modules
170

171 172 173
        returns first found object, or None
        '''
        for o in self.cache:
174 175 176
            if (o.course_id == course_id and
                o.module_type == module_type and
                o.module_state_key == module_state_key):
177 178
                return o
        return None
179

180 181
    def append(self, student_module):
        self.cache.append(student_module)