Commit 10aae0d4 by Kyle McCormick

MA-1063 Add versioning and timestamping to CourseOverview

parent 848e72c7
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'CourseOverview.created'
db.add_column('course_overviews_courseoverview', 'created',
self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now),
keep_default=False)
# Adding field 'CourseOverview.modified'
db.add_column('course_overviews_courseoverview', 'modified',
self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now),
keep_default=False)
# Adding field 'CourseOverview.version'
db.add_column('course_overviews_courseoverview', 'version',
self.gf('django.db.models.fields.IntegerField')(default=0),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CourseOverview.created'
db.delete_column('course_overviews_courseoverview', 'created')
# Deleting field 'CourseOverview.modified'
db.delete_column('course_overviews_courseoverview', 'modified')
# Deleting field 'CourseOverview.version'
db.delete_column('course_overviews_courseoverview', 'version')
models = {
'course_overviews.courseoverview': {
'Meta': {'object_name': 'CourseOverview'},
'_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}),
'_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}),
'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'cert_html_view_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'cert_name_long': ('django.db.models.fields.TextField', [], {}),
'cert_name_short': ('django.db.models.fields.TextField', [], {}),
'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_image_url': ('django.db.models.fields.TextField', [], {}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'display_number_with_default': ('django.db.models.fields.TextField', [], {}),
'display_org_with_default': ('django.db.models.fields.TextField', [], {}),
'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'enrollment_domain': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'enrollment_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'enrollment_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}),
'invitation_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2'}),
'max_student_enrollments_allowed': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'version': ('django.db.models.fields.IntegerField', [], {}),
'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
}
}
complete_apps = ['course_overviews']
\ No newline at end of file
...@@ -4,9 +4,9 @@ Declaration of CourseOverview model ...@@ -4,9 +4,9 @@ Declaration of CourseOverview model
import json import json
import django.db.models
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField
from django.utils.translation import ugettext from django.utils.translation import ugettext
from model_utils.models import TimeStampedModel
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from xmodule import course_metadata_utils from xmodule import course_metadata_utils
...@@ -16,7 +16,7 @@ from xmodule.modulestore.django import modulestore ...@@ -16,7 +16,7 @@ from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField, UsageKeyField from xmodule_django.models import CourseKeyField, UsageKeyField
class CourseOverview(django.db.models.Model): class CourseOverview(TimeStampedModel):
""" """
Model for storing and caching basic information about a course. Model for storing and caching basic information about a course.
...@@ -25,6 +25,12 @@ class CourseOverview(django.db.models.Model): ...@@ -25,6 +25,12 @@ class CourseOverview(django.db.models.Model):
a course as part of a user dashboard or enrollment API. a course as part of a user dashboard or enrollment API.
""" """
# IMPORTANT: Bump this whenever you modify this model and/or add a migration.
VERSION = 1
# Cache entry versioning.
version = IntegerField()
# Course identification # Course identification
id = CourseKeyField(db_index=True, primary_key=True, max_length=255) # pylint: disable=invalid-name id = CourseKeyField(db_index=True, primary_key=True, max_length=255) # pylint: disable=invalid-name
_location = UsageKeyField(max_length=255) _location = UsageKeyField(max_length=255)
...@@ -67,8 +73,8 @@ class CourseOverview(django.db.models.Model): ...@@ -67,8 +73,8 @@ class CourseOverview(django.db.models.Model):
invitation_only = BooleanField(default=False) invitation_only = BooleanField(default=False)
max_student_enrollments_allowed = IntegerField(null=True) max_student_enrollments_allowed = IntegerField(null=True)
@staticmethod @classmethod
def _create_from_course(course): def _create_from_course(cls, course):
""" """
Creates a CourseOverview object from a CourseDescriptor. Creates a CourseOverview object from a CourseDescriptor.
...@@ -94,7 +100,9 @@ class CourseOverview(django.db.models.Model): ...@@ -94,7 +100,9 @@ class CourseOverview(django.db.models.Model):
except ValueError: except ValueError:
lowest_passing_grade = None lowest_passing_grade = None
return CourseOverview( return cls(
version=cls.VERSION,
id=course.id, id=course.id,
_location=course.location, _location=course.location,
display_name=course.display_name, display_name=course.display_name,
...@@ -130,8 +138,42 @@ class CourseOverview(django.db.models.Model): ...@@ -130,8 +138,42 @@ class CourseOverview(django.db.models.Model):
max_student_enrollments_allowed=course.max_student_enrollments_allowed, max_student_enrollments_allowed=course.max_student_enrollments_allowed,
) )
@staticmethod @classmethod
def get_from_id(course_id): def _load_from_module_store(cls, course_id):
"""
Load a CourseDescriptor, create a new CourseOverview from it, cache the
overview, and return it.
Arguments:
course_id (CourseKey): the ID of the course overview to be loaded.
Returns:
CourseOverview: overview of the requested course.
Raises:
- CourseOverview.DoesNotExist if the course specified by course_id
was not found.
- IOError if some other error occurs while trying to load the
course from the module store.
"""
store = modulestore()
with store.bulk_operations(course_id):
course = store.get_course(course_id)
if isinstance(course, CourseDescriptor):
course_overview = cls._create_from_course(course)
course_overview.save()
return course_overview
elif course is not None:
raise IOError(
"Error while loading course {} from the module store: {}",
unicode(course_id),
course.error_msg if isinstance(course, ErrorDescriptor) else unicode(course)
)
else:
raise cls.DoesNotExist()
@classmethod
def get_from_id(cls, course_id):
""" """
Load a CourseOverview object for a given course ID. Load a CourseOverview object for a given course ID.
...@@ -144,8 +186,7 @@ class CourseOverview(django.db.models.Model): ...@@ -144,8 +186,7 @@ class CourseOverview(django.db.models.Model):
course_id (CourseKey): the ID of the course overview to be loaded. course_id (CourseKey): the ID of the course overview to be loaded.
Returns: Returns:
CourseOverview: overview of the requested course. If loading course CourseOverview: overview of the requested course.
from the module store failed, returns None.
Raises: Raises:
- CourseOverview.DoesNotExist if the course specified by course_id - CourseOverview.DoesNotExist if the course specified by course_id
...@@ -153,25 +194,15 @@ class CourseOverview(django.db.models.Model): ...@@ -153,25 +194,15 @@ class CourseOverview(django.db.models.Model):
- IOError if some other error occurs while trying to load the - IOError if some other error occurs while trying to load the
course from the module store. course from the module store.
""" """
course_overview = None
try: try:
course_overview = CourseOverview.objects.get(id=course_id) course_overview = cls.objects.get(id=course_id)
except CourseOverview.DoesNotExist: if course_overview.version != cls.VERSION:
store = modulestore() # Throw away old versions of CourseOverview, as they might contain stale data.
with store.bulk_operations(course_id): course_overview.delete()
course = store.get_course(course_id) course_overview = None
if isinstance(course, CourseDescriptor): except cls.DoesNotExist:
course_overview = CourseOverview._create_from_course(course) course_overview = None
course_overview.save() return course_overview or cls._load_from_module_store(course_id)
elif course is not None:
raise IOError(
"Error while loading course {} from the module store: {}",
unicode(course_id),
course.error_msg if isinstance(course, ErrorDescriptor) else unicode(course)
)
else:
raise CourseOverview.DoesNotExist()
return course_overview
def clean_id(self, padding_char='='): def clean_id(self, padding_char='='):
""" """
......
...@@ -331,3 +331,21 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -331,3 +331,21 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
__ = course.lowest_passing_grade __ = course.lowest_passing_grade
course_overview = CourseOverview._create_from_course(course) # pylint: disable=protected-access course_overview = CourseOverview._create_from_course(course) # pylint: disable=protected-access
self.assertEqual(course_overview.lowest_passing_grade, None) self.assertEqual(course_overview.lowest_passing_grade, None)
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 1), (ModuleStoreEnum.Type.split, 3, 4))
@ddt.unpack
def test_versioning(self, modulestore_type, min_mongo_calls, max_mongo_calls):
"""
Test that CourseOverviews with old version numbers are thrown out.
"""
with self.store.default_store(modulestore_type):
course = CourseFactory.create()
course_overview = CourseOverview.get_from_id(course.id)
course_overview.version = CourseOverview.VERSION - 1
course_overview.save()
# Because the course overview now has an old version number, it should
# be thrown out after being loaded from the cache, which results in
# a call to get_course.
with check_mongo_calls_range(max_finds=max_mongo_calls, min_finds=min_mongo_calls):
_course_overview_2 = CourseOverview.get_from_id(course.id)
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