diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 61313376d1df8d26de1924091a65d0ddf8cdc323..235f4b414f1027abb096ba72734a2c75ea0e4ce4 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -39,12 +39,15 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud
 from xmodule.course_module import CourseDescriptor
 from xmodule.modulestore.exceptions import ItemNotFoundError
 from xmodule.modulestore.django import modulestore
+from xmodule.modulestore import Location
 
 from collections import namedtuple
 
 from courseware.courses import get_courses, sort_by_announcement
 from courseware.access import has_access
-from courseware.models import TimedModule
+from courseware.models import StudentModuleCache
+from courseware.views import get_module_for_descriptor
+from courseware.module_render import get_instance_module
 
 from statsd import statsd
 
@@ -1082,13 +1085,14 @@ def test_center_login(request):
     # errors are returned by navigating to the error_url, adding a query parameter named "code" 
     # which contains the error code describing the exceptional condition.
     def makeErrorURL(error_url, error_code):
-        return "{}&code={}".format(error_url, error_code);
+        log.error("generating error URL with error code {}".format(error_code))
+        return "{}?code={}".format(error_url, error_code);
      
     # get provided error URL, which will be used as a known prefix for returning error messages to the
-    # Pearson shell.  It does not have a trailing slash, so we need to add one when creating output URLs.
+    # Pearson shell.  
     error_url = request.POST.get("errorURL")
 
-    # check that the parameters have not been tampered with, by comparing the code provided by Pearson
+    # TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
     # with the code we calculate for the same parameters.
     if 'code' not in request.POST:
         return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"));
@@ -1112,65 +1116,81 @@ def test_center_login(request):
     try:
         testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
     except TestCenterUser.DoesNotExist:
+        log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
         return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
 
 
     # find testcenter_registration that matches the provided exam code:
-    # Note that we could rely on either the registrationId or the exam code, 
-    # or possibly both.
+    # Note that we could rely in future on either the registrationId or the exam code, 
+    # or possibly both.  But for now we know what to do with an ExamSeriesCode, 
+    # while we currently have no record of RegistrationID values at all.
     if 'vueExamSeriesCode' not in request.POST:
         # TODO: confirm this error code (made up, not in documentation)
+        log.error("missing exam series code for cand ID {}".format(client_candidate_id))
         return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
     exam_series_code = request.POST.get('vueExamSeriesCode')
     
     registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
-    
     if not registrations:
+        log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
         return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
     
     # TODO: figure out what to do if there are more than one registrations....
     # for now, just take the first...
     registration = registrations[0]
-    course_id = registration.course_id
     
-    # if we want to look up whether the test has already been taken, or to 
-    # communicate that a time accommodation needs to be applied, we need to 
-    # know the module_id to use that corresponds to the particular exam_series_code.
-    # For now, we can hardcode that...
-    if exam_series_code == '6002x001':
-        # This should not be hardcoded here, but should be added to the exam definition.
-        # TODO: look the location up in the course, by finding the exam_info with the matching code,
-        # and get the location from that.
-        location = 'i4x://MITx/6.002x/sequential/Final_Exam_Fall_2012'
-        redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
-    else:
-        # TODO: clarify if this is the right error code for this condition.
+    course_id = registration.course_id
+    course = course_from_id(course_id)  # assume it will be found....
+    if not course:
+        log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
         return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
+    exam = course.get_test_center_exam(exam_series_code)
+    if not exam:
+        log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
+        return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
+    location = exam.exam_url
+    redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
+
+    log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
+
+    # check if the test has already been taken
+    timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
+    if not timelimit_descriptor:
+        log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
+        return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
+        
+    timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, 
+                                                                                 timelimit_descriptor, depth=None)
+    timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, 
+                                                 timelimit_module_cache, course_id, position=None)
+    if not timelimit_module.category == 'timelimit':
+        log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
+        return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
+        
+    if timelimit_module and timelimit_module.has_ended:
+        log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
+        return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
     
-    
+    # check if we need to provide an accommodation:
     time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
                                   'ET30MN' : 'ADD30MIN',
                                   'ETDBTM' : 'ADDDOUBLE', }
-    
-    # check if the test has already been taken
-    timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, location=location)
-    if timed_modules:
-        timed_module = timed_modules[0]
-        if timed_module.has_ended:
-            return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
-    elif registration.get_accommodation_codes():
-        # we don't have a timed module created yet, so if we have time accommodations
-        # to implement, create an entry now:
-        time_accommodation_code = None
+
+    time_accommodation_code = None
+    if registration.get_accommodation_codes():
         for code in registration.get_accommodation_codes():
             if code in time_accommodation_mapping:
                 time_accommodation_code = time_accommodation_mapping[code]
-        if client_candidate_id == "edX003671291147":
-            time_accommodation_code = 'TESTING'
-        if time_accommodation_code:
-            timed_module = TimedModule(student=request.user, course_id=course_id, location=location)
-            timed_module.accommodation_code = time_accommodation_code
-            timed_module.save()
+    # special, hard-coded client ID used by Pearson shell for testing:
+    if client_candidate_id == "edX003671291147":
+        time_accommodation_code = 'TESTING'
+        
+    if time_accommodation_code:
+        timelimit_module.accommodation_code = time_accommodation_code
+        instance_module = get_instance_module(course_id, testcenteruser.user, timelimit_module, timelimit_module_cache)
+        instance_module.state = timelimit_module.get_instance_state()
+        instance_module.save()
+        log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
         
     # UGLY HACK!!!
     # Login assumes that authentication has occurred, and that there is a 
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index f61e6f6f36546976e2fd4996b2ddf459e3e35262..d3a0562b414e72f902e7eedd24b998fa91dcef88 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -23,7 +23,6 @@ setup(
             "course = xmodule.course_module:CourseDescriptor",
             "customtag = xmodule.template_module:CustomTagDescriptor",
             "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
-            "fixedtime = xmodule.fixed_time_module:FixedTimeDescriptor",
             "html = xmodule.html_module:HtmlDescriptor",
             "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
             "error = xmodule.error_module:ErrorDescriptor",
@@ -32,6 +31,7 @@ setup(
             "section = xmodule.backcompat_module:SemanticSectionDescriptor",
             "sequential = xmodule.seq_module:SequenceDescriptor",
             "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
+            "timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
             "vertical = xmodule.vertical_module:VerticalDescriptor",
             "video = xmodule.video_module:VideoDescriptor",
             "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 6e3e2cfa39ab8118e413ad4bbe7e0cf100bc36b4..4c5c3a0a90e5fdfa60fb4bc42327b70e5bb494c2 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
                 raise ValueError("First appointment date must be before last appointment date")
             if self.registration_end_date > self.last_eligible_appointment_date:
                 raise ValueError("Registration end date must be before last appointment date")
-
+            self.exam_url = exam_info.get('Exam_URL')
 
         def _try_parse_time(self, key):
             """
@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
         else:
             return None
 
+    def get_test_center_exam(self, exam_series_code):
+        exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
+        return exams[0] if len(exams) == 1 else None
+        
     @property
     def title(self):
         return self.display_name
diff --git a/common/lib/xmodule/xmodule/fixed_time_module.py b/common/lib/xmodule/xmodule/timelimit_module.py
similarity index 77%
rename from common/lib/xmodule/xmodule/fixed_time_module.py
rename to common/lib/xmodule/xmodule/timelimit_module.py
index f1fec26dc3af11502284a19867a2aaf425adf0a2..23ed06eb5964d4a1d9857ed0ad705f29fa9dba45 100644
--- a/common/lib/xmodule/xmodule/fixed_time_module.py
+++ b/common/lib/xmodule/xmodule/timelimit_module.py
@@ -4,22 +4,16 @@ import logging
 from lxml import etree
 from time import time
 
-from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.editing_module import XMLEditingDescriptor
 from xmodule.xml_module import XmlDescriptor
 from xmodule.x_module import XModule
 from xmodule.progress import Progress
 from xmodule.exceptions import NotFoundError
-from pkg_resources import resource_string
 
 
 log = logging.getLogger(__name__)
 
-# HACK: This shouldn't be hard-coded to two types
-# OBSOLETE: This obsoletes 'type'
-# class_priority = ['video', 'problem']
-
-
-class FixedTimeModule(XModule):
+class TimeLimitModule(XModule):
     ''' 
     Wrapper module which imposes a time constraint for the completion of its child.
     '''
@@ -29,9 +23,7 @@ class FixedTimeModule(XModule):
         XModule.__init__(self, system, location, definition, descriptor,
                          instance_state, shared_state, **kwargs)
 
-        # NOTE: Position is 1-indexed.  This is silly, but there are now student
-        # positions saved on prod, so it's not easy to fix.
-#        self.position = 1
+        self.rendered = False
         self.beginning_at = None
         self.ending_at = None
         self.accommodation_code = None
@@ -46,13 +38,6 @@ class FixedTimeModule(XModule):
             if 'accommodation_code' in state:
                 self.accommodation_code = state['accommodation_code']
                 
-
-        # if position is specified in system, then use that instead
-#        if system.get('position'):
-#            self.position = int(system.get('position'))
-
-        self.rendered = False
-
     # For a timed activity, we are only interested here
     # in time-related accommodations, and these should be disjoint.
     # (For proctored exams, it is possible to have multiple accommodations
@@ -81,8 +66,6 @@ class FixedTimeModule(XModule):
         elif self.accommodation_code == 'TESTING':
             # when testing, set timer to run for a week at a time.
             return 3600 * 24 * 7
-       
-    # store state:
 
     @property
     def has_begun(self):
@@ -101,8 +84,6 @@ class FixedTimeModule(XModule):
         '''
         self.beginning_at = time()
         modified_duration = self._get_accommodated_duration(duration)
-        # datetime_duration = timedelta(seconds=modified_duration)
-        # self.ending_at = self.beginning_at + datetime_duration
         self.ending_at = self.beginning_at + modified_duration
         
     def get_end_time_in_ms(self):
@@ -132,31 +113,32 @@ class FixedTimeModule(XModule):
         progress = reduce(Progress.add_counts, progresses)
         return progress
 
-    def handle_ajax(self, dispatch, get):        # TODO: bounds checking
-#        ''' get = request.POST instance '''
-#        if dispatch == 'goto_position':
-#            self.position = int(get['position'])
-#            return json.dumps({'success': True})
+    def handle_ajax(self, dispatch, get):
         raise NotFoundError('Unexpected dispatch type')
 
     def render(self):
         if self.rendered:
             return
         # assumes there is one and only one child, so it only renders the first child
-        child = self.get_display_items()[0]
-        self.content = child.get_html()
+        children = self.get_display_items()
+        if children:
+            child = children[0]
+            self.content = child.get_html()
         self.rendered = True
 
     def get_icon_class(self):
-        return self.get_children()[0].get_icon_class()
+        children = self.get_children()
+        if children:
+            return children[0].get_icon_class()
+        else:
+            return "other"
 
+class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
 
-class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
-    # TODO: fix this template?!
-    mako_template = 'widgets/sequence-edit.html'
-    module_class = FixedTimeModule
+    module_class = TimeLimitModule
 
-    stores_state = True # For remembering when a student started, and when they should end
+    # For remembering when a student started, and when they should end
+    stores_state = True 
 
     @classmethod
     def definition_from_xml(cls, xml_object, system):
@@ -165,14 +147,14 @@ class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
             try:
                 children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
             except Exception as e:
-                log.exception("Unable to load child when parsing FixedTime wrapper. Continuing...")
+                log.exception("Unable to load child when parsing TimeLimit wrapper. Continuing...")
                 if system.error_tracker is not None:
                     system.error_tracker("ERROR: " + str(e))
                 continue
         return {'children': children}
 
     def definition_to_xml(self, resource_fs):
-        xml_object = etree.Element('fixedtime')
+        xml_object = etree.Element('timelimit')
         for child in self.get_children():
             xml_object.append(
                 etree.fromstring(child.export_to_xml(resource_fs)))
diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
deleted file mode 100644
index 6e8791a97520e8baf04c19aa3b15bfd923e64fa1..0000000000000000000000000000000000000000
--- a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-
-class Migration(SchemaMigration):
-
-    def forwards(self, orm):
-        # Adding model 'TimedModule'
-        db.create_table('courseware_timedmodule', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('location', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='location', db_index=True)),
-            ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
-            ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
-            ('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)),
-            ('beginning_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
-            ('ending_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
-            ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
-            ('modified_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
-        ))
-        db.send_create_signal('courseware', ['TimedModule'])
-
-        # Adding unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
-        db.create_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
-
-
-    def backwards(self, orm):
-        # Removing unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
-        db.delete_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
-
-        # Deleting model 'TimedModule'
-        db.delete_table('courseware_timedmodule')
-
-
-    models = {
-        'auth.group': {
-            'Meta': {'object_name': 'Group'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
-        },
-        'auth.permission': {
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
-        },
-        'auth.user': {
-            'Meta': {'object_name': 'User'},
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
-        },
-        'contenttypes.contenttype': {
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
-        },
-        'courseware.offlinecomputedgrade': {
-            'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
-            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        },
-        'courseware.offlinecomputedgradelog': {
-            'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
-            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
-            'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
-        },
-        'courseware.studentmodule': {
-            'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
-            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
-            'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
-            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
-            'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
-            'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
-            'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        },
-        'courseware.timedmodule': {
-            'Meta': {'unique_together': "(('student', 'location', 'course_id'),)", 'object_name': 'TimedModule'},
-            'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}),
-            'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
-            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
-            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'location'", 'db_index': 'True'}),
-            'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
-            'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        }
-    }
-
-    complete_apps = ['courseware']
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index d9cc560215c84c62a7a92fc88f2e0bff454f3808..87b9edaac2330335abe679362f595a773c89637b 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -212,87 +212,3 @@ class OfflineComputedGradeLog(models.Model):
     def __unicode__(self):
         return "[OCGLog] %s: %s" % (self.course_id, self.created)
 
-class TimedModule(models.Model):
-    """
-    Keeps student state for a timed activity in a particular course.
-    Includes information about time accommodations granted,
-    time started, and ending time.
-    """
-    ## These three are the key for the object
-
-    # 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')
-    location = models.CharField(max_length=255, db_index=True, db_column='location')
-    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'),)
-        unique_together = (('student', 'location', 'course_id'),)
-
-    # For a timed activity, we are only interested here
-    # in time-related accommodations, and these should be disjoint.
-    # (For proctored exams, it is possible to have multiple accommodations
-    # apply to an exam, so they require accommodating a multi-choice.)
-    TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
-                      ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
-                      ('ADD30MIN', 'Extra Time - 30 Minutes'),
-                      ('DOUBLE', 'Extra Time - Double Time'),
-                      ('TESTING', 'Extra Time -- Large amount for testing purposes')
-                    )
-    accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True)
-
-    def _get_accommodated_duration(self, duration):
-        ''' 
-        Get duration for activity, as adjusted for accommodations.
-        Input and output are expressed in seconds.
-        '''
-        if self.accommodation_code == 'NONE':
-            return duration
-        elif self.accommodation_code == 'ADDHALFTIME':
-            # TODO:  determine what type to return
-            return int(duration * 1.5)
-        elif self.accommodation_code == 'ADD30MIN':
-            return (duration + (30 * 60))
-        elif self.accommodation_code == 'DOUBLE':
-            return (duration * 2)
-        elif self.accommodation_code == 'TESTING':
-            # when testing, set timer to run for a week at a time.
-            return 3600 * 24 * 7
-       
-    # store state:
-    
-    beginning_at = models.DateTimeField(null=True, db_index=True)
-    ending_at = models.DateTimeField(null=True, db_index=True)
-    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
-    modified_at = models.DateTimeField(auto_now=True, db_index=True)
-
-    @property
-    def has_begun(self):
-        return self.beginning_at is not None
-    
-    @property    
-    def has_ended(self):
-        if not self.ending_at:
-            return False
-        return self.ending_at < datetime.utcnow()
-        
-    def begin(self, duration):
-        ''' 
-        Sets the starting time and ending time for the activity,
-        based on the duration provided (in seconds).
-        '''
-        self.beginning_at = datetime.utcnow()
-        modified_duration = self._get_accommodated_duration(duration)
-        datetime_duration = timedelta(seconds=modified_duration)
-        self.ending_at = self.beginning_at + datetime_duration
-        
-    def get_end_time_in_ms(self):
-        return (timegm(self.ending_at.timetuple()) * 1000)
-
-    def __unicode__(self):
-        return '/'.join([self.course_id, self.student.username, self.module_state_key])
-
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 0acf435f0bc31339aea2a7d71fb748a3f95e7e89..07b177979a9cbe59b16fd53a83728a8aea8ed4ca 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -20,7 +20,7 @@ from courseware.access import has_access
 from courseware.courses import (get_courses, get_course_with_access,
                                 get_courses_by_university, sort_by_announcement)
 import courseware.tabs as tabs
-from courseware.models import StudentModuleCache, TimedModule
+from courseware.models import StudentModule, StudentModuleCache
 from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
 
 from django_comment_client.utils import get_discussion_title
@@ -31,6 +31,7 @@ from xmodule.modulestore import Location
 from xmodule.modulestore.django import modulestore
 from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
 from xmodule.modulestore.search import path_to_location
+#from xmodule.fixed_time_module import FixedTimeModule
 
 import comment_client
 
@@ -152,6 +153,80 @@ def save_child_position(seq_module, child_name, instance_module):
                 instance_module.state = seq_module.get_instance_state()
                 instance_module.save()
 
+def check_for_active_timelimit_module(request, course_id, course):
+    '''
+    Looks for a timing module for the given user and course that is currently active.
+    If found, returns a context dict with timer-related values to enable display of time remaining.
+    ''' 
+    context = {}
+    timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
+    if timelimit_student_modules:
+        for timelimit_student_module in timelimit_student_modules: 
+            # get the corresponding section_descriptor for the given StudentModel entry:
+            module_state_key = timelimit_student_module.module_state_key
+            timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
+            timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user, 
+                                                                                     timelimit_descriptor, depth=None)
+            timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, 
+                                                     timelimit_module_cache, course.id, position=None)
+            if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
+                    timelimit_module.has_begun and not timelimit_module.has_ended:
+                location = timelimit_module.location
+                # determine where to go when the timer expires:
+                if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
+                    # TODO: provide a better error
+                    raise Http404
+                time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
+                context['time_expired_redirect_url'] = time_expired_redirect_url
+                # Fetch the end time (in GMT) as stored in the module when it was started.
+                # This value should be UTC time as number of milliseconds since epoch.
+                end_date = timelimit_module.get_end_time_in_ms()
+                context['timer_expiration_datetime'] = end_date
+                if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
+                    context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
+                return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location})
+                context['timer_navigation_return_url'] = return_url
+    return context
+
+def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module):
+    '''
+    Updates the state of the provided timing module, starting it if it hasn't begun.
+    Returns dict with timer-related values to enable display of time remaining.
+    Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired.
+    ''' 
+    context = {}
+    # determine where to go when the exam ends:
+    if 'time_expired_redirect_url' not in timelimit_descriptor.metadata: 
+        # TODO: provide a better error
+        raise Http404
+    time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
+    context['time_expired_redirect_url'] = time_expired_redirect_url
+
+    if not timelimit_module.has_ended:
+        if not timelimit_module.has_begun:
+            # user has not started the exam, so start it now.
+            if 'duration' not in timelimit_descriptor.metadata:
+                # TODO: provide a better error
+                raise Http404
+            # The user may have an accommodation that has been granted to them.
+            # This accommodation information should already be stored in the module's state.
+            duration = int(timelimit_descriptor.metadata.get('duration'))
+            timelimit_module.begin(duration)
+            # we have changed state, so we need to persist the change:
+            instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache)
+            instance_module.state = timelimit_module.get_instance_state()
+            instance_module.save()
+            
+        # the exam has been started, either because the student is returning to the
+        # exam page, or because they have just visited it.  Fetch the end time (in GMT) as stored
+        # in the module when it was started.
+        # This value should be UTC time as number of milliseconds since epoch.
+        context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms()
+        # also use the timed module to determine whether top-level navigation is visible:
+        if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
+            context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
+    return context
+
 @login_required
 @ensure_csrf_cookie
 @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@@ -215,43 +290,6 @@ def index(request, course_id, chapter=None, section=None,
             'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
             }
 
-        # check here if this page is within a course that has an active timed module running.  If so, then 
-        # display the appropriate timer information:
-        timed_modules = TimedModule.objects.filter(student=request.user, course_id=course_id)
-        if timed_modules:
-            for timed_module in timed_modules:
-                if timed_module.has_begun and not timed_module.has_ended:
-                    # a timed module has been found that is active, so display 
-                    # the relevant time:
-                    # module_state_key = timed_module.module_state_key
-                    location = timed_module.location
-                    
-                    # when we actually make the state be stored in the StudentModule, then 
-                    # we can fetch what we need from that.
-                    # student_module = student_module_cache.lookup(course_id, 'sequential', module_state_key)
-                    # But the module doesn't give us anything helpful to find the corresponding descriptor
-                    
-                    # get the corresponding section_descriptor for this timed_module entry:
-                    section_descriptor = modulestore().get_instance(course_id, Location(location))
-                    
-                    # determine where to go when the timer expires:
-                    # Note that if we could get this from the timed_module, we wouldn't have to 
-                    # fetch the section_descriptor in the first place.
-                    if 'time_expired_redirect_url' not in section_descriptor.metadata:
-                        raise Http404
-                    time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
-                    context['time_expired_redirect_url'] = time_expired_redirect_url
-                
-                    # Fetch the end time (in GMT) as stored in the module when it was started.
-                    # This value should be UTC time as number of milliseconds since epoch.
-                    end_date = timed_module.get_end_time_in_ms()
-                    context['timer_expiration_datetime'] =  end_date
-                    if 'suppress_toplevel_navigation' in section_descriptor.metadata:
-                        context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
-                    return_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
-                    context['timer_navigation_return_url'] = return_url
-      
-
         chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
         if chapter_descriptor is not None:
             instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
@@ -286,7 +324,20 @@ def index(request, course_id, chapter=None, section=None,
             instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
             save_child_position(chapter_module, section, instance_module)
 
-
+            # check here if this section *is* a timed module.  
+            if section_module.category == 'timelimit':
+                timer_context = update_timelimit_module(request.user, course_id, student_module_cache, 
+                                                        section_descriptor, section_module)
+                if 'timer_expiration_datetime' in timer_context:
+                    context.update(timer_context)
+                else:
+                    # if there is no expiration defined, then we know the timer has expired:
+                    return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
+            else:
+                # check here if this page is within a course that has an active timed module running.  If so, then 
+                # add in the appropriate timer information to the rendering context:
+                context.update(check_for_active_timelimit_module(request, course_id, course))
+                
             context['content'] = section_module.get_html()
         else:
             # section is none, so display a message
@@ -334,201 +385,6 @@ def index(request, course_id, chapter=None, section=None,
 
     return result
 
-@login_required
-@ensure_csrf_cookie
-@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-def timed_exam(request, course_id, chapter, section):
-    """
-    Displays only associated content.  If course, chapter,
-    and section are all specified, renders the page, or returns an error if they
-    are invalid.
-
-    Returns an error if these are not all specified and correct.
-    
-    Arguments:
-
-     - request    : HTTP request
-     - course_id  : course id (str: ORG/course/URL_NAME)
-     - chapter    : chapter url_name (str)
-     - section    : section url_name (str)
-
-    Returns:
-
-     - HTTPresponse
-    """
-    course = get_course_with_access(request.user, course_id, 'load', depth=2)
-    staff_access = has_access(request.user, course, 'staff')
-    registered = registered_for_course(course, request.user)
-    if not registered:
-        log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
-        raise # error
-    try:
-        student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
-            course.id, request.user, course, depth=2)
-
-        # Has this student been in this course before?
-        # first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
-
-        # Load the module for the course
-        course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
-        if course_module is None:
-            log.warning('If you see this, something went wrong: if we got this'
-                        ' far, should have gotten a course module for this user')
-            # return redirect(reverse('about_course', args=[course.id]))
-            raise # error
-        
-        if chapter is None:
-            # return redirect_to_course_position(course_module, first_time)
-            raise # error
-
-        # BW: add this test earlier, and remove later clause
-        if section is None:
-            # return redirect_to_course_position(course_module, first_time)
-            raise # error
-        
-        context = {
-            'csrf': csrf(request)['csrf_token'],
-            'COURSE_TITLE': course.title,
-            'course': course,
-            'init': '',
-            'content': '',
-            'staff_access': staff_access,
-            'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
-            }
-
-        # in general, we may want to disable accordion display on timed exams.  
-        provide_accordion = True
-        if provide_accordion:
-            context['accordion'] = render_accordion(request, course, chapter, section)
-
-        chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
-        if chapter_descriptor is not None:
-            instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
-            save_child_position(course_module, chapter, instance_module)
-        else:
-            raise Http404
-
-        chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
-        if chapter_module is None:
-            # User may be trying to access a chapter that isn't live yet
-            raise Http404
-
-        section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
-        if section_descriptor is None:
-            # Specifically asked-for section doesn't exist
-            raise Http404
-
-        # Load all descendents of the section, because we're going to display its
-        # html, which in general will need all of its children
-        section_module = get_module(request.user, request, section_descriptor.location,
-                                    student_module_cache, course.id, position=None, depth=None)
-        if section_module is None:
-            # User may be trying to be clever and access something
-            # they don't have access to.
-            raise Http404
-
-        # Save where we are in the chapter:
-        instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
-        save_child_position(chapter_module, section, instance_module)
-
-
-        context['content'] = section_module.get_html()
-        
-        # determine where to go when the exam ends:
-        if 'time_expired_redirect_url' not in section_descriptor.metadata:
-            raise Http404
-        time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
-        context['time_expired_redirect_url'] = time_expired_redirect_url
-        
-        # figure out when the timed exam should end.  Going forward, this is determined by getting a "normal"
-        # duration from the test, then doing some math to modify the duration based on accommodations,
-        # and then use that value as the end.  Once we have calculated this, it should be sticky -- we
-        # use the same value for future requests, unless it's a tester.
-
-        # get value for duration from the section's metadata:
-        # for now, assume that the duration is set as an integer value, indicating the number of seconds:
-        if 'duration' not in section_descriptor.metadata:
-            raise Http404
-        duration = int(section_descriptor.metadata.get('duration'))
-        
-        # get corresponding time module, if one is present:
-        try: 
-            timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, location=section_module.location)
-            
-            # if a module exists, check to see if it has already been started,
-            # and if it has already ended.
-            if timed_module.has_ended:
-                # the exam has already ended, and the student has tried to
-                # revisit the exam.  
-                # TODO: determine what do we do here.  
-                # For a Pearson exam, we want to go to the exit page.  
-                # (Not so sure what to do in general.)
-                # Proposal:  store URL in the section descriptor,
-                # along with the duration.  If no such URL is set, 
-                # just put up the error page,
-                if time_expired_redirect_url is None:
-                    raise Exception("Time expired on {}".format(timed_module))
-                else:
-                    return HttpResponseRedirect(time_expired_redirect_url)
-                
-            elif not timed_module.has_begun:
-                # user has not started the exam, but may have an accommodation
-                # that has been granted to them.
-                # modified_duration = timed_module.get_accommodated_duration(duration)
-                # timed_module.started_at = datetime.utcnow() #  time() * 1000
-                # timed_module.end_date = timed_module. 
-                timed_module.begin(duration)
-                timed_module.save()
-                
-        except TimedModule.DoesNotExist:
-            # no entry found.  So we're starting this test
-            # without any accommodations being preset.
-            timed_module = TimedModule(student=request.user, course_id=course_id, location=section_module.location)
-            timed_module.begin(duration)
-            timed_module.save()
-        
-                
-        # the exam has already been started, and the student is returning to the 
-        # exam page.  Fetch the end time (in GMT) as stored
-        # in the module when it was started.
-        end_date = timed_module.get_end_time_in_ms()
-        
-        # This value should be UTC time as number of milliseconds since epoch.
-        # context['end_date'] =  end_date
-        context['timer_expiration_datetime'] =  end_date
-        if 'suppress_toplevel_navigation' in section_descriptor.metadata:
-            context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
-        
-        result = render_to_response('courseware/courseware.html', context)
-    except Exception as e:
-        if isinstance(e, Http404):
-            # let it propagate
-            raise
-
-        # In production, don't want to let a 500 out for any reason
-        if settings.DEBUG:
-            raise
-        else:
-            log.exception("Error in exam view: user={user}, course={course},"
-                          " chapter={chapter} section={section}"
-                          "position={position}".format(
-                              user=request.user,
-                              course=course,
-                              chapter=chapter,
-                              section=section
-                              ))
-            try:
-                result = render_to_response('courseware/courseware-error.html',
-                                            {'staff_access': staff_access,
-                                            'course' : course})
-            except:
-                # Let the exception propagate, relying on global config to at
-                # at least return a nice error message
-                log.exception("Error while rendering courseware-error page")
-                raise
-
-    return result
-
 @ensure_csrf_cookie
 def jump_to(request, course_id, location):
     '''
diff --git a/lms/urls.py b/lms/urls.py
index f6819d05a2da8b3216577998f607e5873327ed85..f92b63aac235ae88d205b60f40994891226f447a 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -217,16 +217,6 @@ if settings.COURSEWARE_ENABLED:
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
             'courseware.views.course_about', name="about_course"),
 
-        # timed exam:
-        url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/timed_exam/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
-            'courseware.views.timed_exam', name="timed_exam"),
-        # (handle hard-coded 6.002x exam explicitly as a timed exam, but without changing the URL.
-        # not only because Pearson doesn't want us to change its location, but because we also include it
-        # in the navigation accordion we display with this exam (so students can see what work they have already
-        # done).  Those are generated automatically using reverse(courseware_section).
-        url(r'^courses/(?P<course_id>MITx/6.002x/2012_Fall)/courseware/(?P<chapter>Final_Exam)/(?P<section>Final_Exam_Fall_2012)/$',
-            'courseware.views.timed_exam'),
-
         #Inside the course
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
             'courseware.views.course_info', name="course_root"),