From 39d48b1966fca0d3b5629907f5060875be573478 Mon Sep 17 00:00:00 2001
From: JM Van Thong
Date: Tue, 4 Dec 2012 12:00:27 -0500
Subject: [PATCH 006/285] Change daily activity to be course specific.
---
lms/djangoapps/instructor/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 666a5d2025..ffada9a482 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -281,7 +281,7 @@ def instructor_dashboard(request, course_id):
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id)
students_enrolled_json = req.json
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-11-27")
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-12-04&course_id=%s" % course_id)
daily_activity_json = req.json
#----------------------------------------
From b10b6e2b3f1ce98d74eb24f51519e2f121334526 Mon Sep 17 00:00:00 2001
From: JM Van Thong
Date: Wed, 5 Dec 2012 23:21:00 -0500
Subject: [PATCH 007/285] Added more analytics to instructor dashboard.
---
lms/djangoapps/instructor/views.py | 24 ++++++++++++++++-
.../courseware/instructor_dashboard.html | 26 ++++++++++++++++++-
2 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index ffada9a482..f638d55f61 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -5,6 +5,8 @@ import csv
import logging
import os
import urllib
+import datetime
+from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import User, Group
@@ -273,15 +275,33 @@ def instructor_dashboard(request, course_id):
analytics_json = None
students_enrolled_json = None
daily_activity_json = None
+ students_daily_activity_json = None
+ students_per_problem_correct_json = None
if idash_mode == 'Analytics':
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id)
analytics_json = req.json
+ # get the day
+ to_day = datetime.today().date()
+ from_day = to_day - timedelta(days=7)
+
+ # number of students active in the past 7 days (including current day)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day))
+ students_daily_activity_json = req.json
+
+ # number of students per problem who have problem graded correct
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
+ students_per_problem_correct_json = req.json
+
+ # number of students enrolled in this course
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id)
students_enrolled_json = req.json
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-12-04&course_id=%s" % course_id)
+ # number of students active in the past 7 days (including current day) --- online version!
+ to_day = datetime.today().date()
+ from_day = to_day - timedelta(days=7)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day))
daily_activity_json = req.json
#----------------------------------------
@@ -301,6 +321,8 @@ def instructor_dashboard(request, course_id):
'analytics_json' : analytics_json,
'students_enrolled_json' : students_enrolled_json,
'daily_activity_json' : daily_activity_json,
+ 'students_daily_activity_json' : students_daily_activity_json,
+ 'students_per_problem_correct_json' : students_per_problem_correct_json,
}
return render_to_response('courseware/instructor_dashboard.html', context)
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 6d696a3558..f15647ccd2 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -179,6 +179,30 @@ function goto( mode)
Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']}
+
+
Daily activity for the past 7 days:
+
+
Day
Number of students
+ % for k,v in students_daily_activity_json['data'].items():
+
+
${k}
${v}
+
+ % endfor
+
+
+
+
+
Number of students with correct problems for the past 7 days:
+
+
Problem
Number of students
+ % for k,v in students_per_problem_correct_json['data'].items():
+
+
${k}
${v}
+
+ % endfor
+
+
+
Students who attempted at least one exercise:
@@ -196,7 +220,7 @@ function goto( mode)
-
Daily activity:
+
Daily activity (online version):
Day
Number of students
% for k,v in daily_activity_json['data'].items():
From 27b8f01ffac2e8aa47e499e8ac0a5846e5b68227 Mon Sep 17 00:00:00 2001
From: JM Van Thong
Date: Fri, 7 Dec 2012 17:26:33 -0500
Subject: [PATCH 008/285] Added the following analyzers: StudentsActive,
OverallGradeDistribution, StudentsDropoffPerDay.
---
lms/djangoapps/instructor/views.py | 43 +++++++++----
.../courseware/instructor_dashboard.html | 63 ++++++++++++++-----
2 files changed, 80 insertions(+), 26 deletions(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index f638d55f61..5c743561a9 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -274,36 +274,52 @@ def instructor_dashboard(request, course_id):
analytics_json = None
students_enrolled_json = None
+ students_active_json = None
daily_activity_json = None
students_daily_activity_json = None
students_per_problem_correct_json = None
+ overall_grade_distribution = None
+ dropoff_per_day = None
if idash_mode == 'Analytics':
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id)
- analytics_json = req.json
- # get the day
+ # get current day
to_day = datetime.today().date()
from_day = to_day - timedelta(days=7)
- # number of students active in the past 7 days (including current day)
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day))
- students_daily_activity_json = req.json
-
- # number of students per problem who have problem graded correct
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
- students_per_problem_correct_json = req.json
-
# number of students enrolled in this course
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id)
students_enrolled_json = req.json
- # number of students active in the past 7 days (including current day) --- online version!
+ # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day))
+ students_active_json = req.json
+
+ # number of students per problem who have problem graded correct
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
+ students_per_problem_correct_json = req.json
+
+ # grade distribution for the course
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,))
+ overall_grade_distribution = req.json
+
+ # number of students distribution drop off per day
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day))
+ dropoff_per_day = req.json
+
+ # the following is ++incorrect++ use of studentmodule table
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day))
+ students_daily_activity_json = req.json
+
+ # number of students active in the past 7 days (including current day) --- online version! experimental
to_day = datetime.today().date()
from_day = to_day - timedelta(days=7)
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day))
daily_activity_json = req.json
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id)
+ analytics_json = req.json
+
#----------------------------------------
# context for rendering
context = {'course': course,
@@ -320,9 +336,12 @@ def instructor_dashboard(request, course_id):
'djangopid' : os.getpid(),
'analytics_json' : analytics_json,
'students_enrolled_json' : students_enrolled_json,
+ 'students_active_json' : students_active_json,
'daily_activity_json' : daily_activity_json,
'students_daily_activity_json' : students_daily_activity_json,
'students_per_problem_correct_json' : students_per_problem_correct_json,
+ 'overall_grade_distribution' : overall_grade_distribution,
+ 'dropoff_per_day' : dropoff_per_day,
}
return render_to_response('courseware/instructor_dashboard.html', context)
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index f15647ccd2..b06eb43944 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -176,23 +176,16 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Analytics'):
-
Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']}
+
+ Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']}
+
+
+ Number of active students for the past 7 days: ${students_active_json['data']['nb_students_active']}
-
Daily activity for the past 7 days:
-
-
Day
Number of students
- % for k,v in students_daily_activity_json['data'].items():
-
-
${k}
${v}
-
- % endfor
-
-
+
Number of active students per problems who have this problem graded as correct:
-
-
Number of students with correct problems for the past 7 days:
Problem
Number of students
% for k,v in students_per_problem_correct_json['data'].items():
@@ -204,7 +197,38 @@ function goto( mode)
-
Students who attempted at least one exercise:
+
Grade distribution:
+
+
+
+
Grade
Number of students
+ % for k,v in overall_grade_distribution['data'].items():
+
+
${k}
${v}
+
+ % endfor
+
+
+
+
+
+
+
Number of students who dropped off per day before becoming inactive:
+
+
+
+
Day
Number of students
+ % for k,v in dropoff_per_day['data'].items():
+
+
${k}
${v}
+
+ % endfor
+
+
+
+
+
+
Students who attempted at least one exercise:
@@ -231,6 +255,17 @@ function goto( mode)
+
+
Daily activity for the past 7 days:
+
+
Day
Number of students
+ % for k,v in students_daily_activity_json['data'].items():
+
+
${k}
${v}
+
+ % endfor
+
+
%endif
From e40b0d0d081e0a1a7e40c29349e5f224aef6ffdd Mon Sep 17 00:00:00 2001
From: JM Van Thong
Date: Fri, 14 Dec 2012 17:16:57 -0500
Subject: [PATCH 009/285] Fixed data order when loading json record from
request.
---
lms/djangoapps/instructor/views.py | 32 +++++++++----------
.../courseware/instructor_dashboard.html | 18 ++---------
2 files changed, 19 insertions(+), 31 deletions(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 5c743561a9..5da0209ace 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -7,6 +7,8 @@ import os
import urllib
import datetime
from datetime import datetime, timedelta
+import json
+from collections import OrderedDict
from django.conf import settings
from django.contrib.auth.models import User, Group
@@ -272,11 +274,10 @@ def instructor_dashboard(request, course_id):
#----------------------------------------
# analytics
- analytics_json = None
+ attempted_problems = None
students_enrolled_json = None
students_active_json = None
daily_activity_json = None
- students_daily_activity_json = None
students_per_problem_correct_json = None
overall_grade_distribution = None
dropoff_per_day = None
@@ -287,29 +288,32 @@ def instructor_dashboard(request, course_id):
to_day = datetime.today().date()
from_day = to_day - timedelta(days=7)
+ # WARNING: do not use req.json because the preloaded json doesn't preserve the order of the original record
+
# number of students enrolled in this course
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id)
- students_enrolled_json = req.json
+ students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students active in the past 7 days (including current day), i.e. with at least one activity for the period
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day))
- students_active_json = req.json
+ students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students per problem who have problem graded correct
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
- students_per_problem_correct_json = req.json
+ students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict)
- # grade distribution for the course
+ # grade distribution for the course +++ this is not the desired distribution +++
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,))
- overall_grade_distribution = req.json
+ overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students distribution drop off per day
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day))
- dropoff_per_day = req.json
+ dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict)
+
+ # number of students per problem who attempted this problem at least once
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsAttemptedProblems&course_id=%s" % course_id)
+ attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict)
- # the following is ++incorrect++ use of studentmodule table
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day))
- students_daily_activity_json = req.json
# number of students active in the past 7 days (including current day) --- online version! experimental
to_day = datetime.today().date()
@@ -317,9 +321,6 @@ def instructor_dashboard(request, course_id):
req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day))
daily_activity_json = req.json
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id)
- analytics_json = req.json
-
#----------------------------------------
# context for rendering
context = {'course': course,
@@ -334,14 +335,13 @@ def instructor_dashboard(request, course_id):
'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location),
'djangopid' : os.getpid(),
- 'analytics_json' : analytics_json,
'students_enrolled_json' : students_enrolled_json,
'students_active_json' : students_active_json,
'daily_activity_json' : daily_activity_json,
- 'students_daily_activity_json' : students_daily_activity_json,
'students_per_problem_correct_json' : students_per_problem_correct_json,
'overall_grade_distribution' : overall_grade_distribution,
'dropoff_per_day' : dropoff_per_day,
+ 'attempted_problems' : attempted_problems,
}
return render_to_response('courseware/instructor_dashboard.html', context)
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index b06eb43944..e347ab2c60 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -177,10 +177,10 @@ function goto( mode)
%if modeflag.get('Analytics'):
- Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']}
+ Number of students enrolled: ${students_enrolled_json['data']['value']}
- Number of active students for the past 7 days: ${students_active_json['data']['nb_students_active']}
+ Number of active students for the past 7 days: ${students_active_json['data']['value']}
@@ -233,7 +233,7 @@ function goto( mode)
Module
Number of students
- % for k,v in analytics_json['data'].items():
+ % for k,v in attempted_problems['data'].items():
${k}
${v}
@@ -255,18 +255,6 @@ function goto( mode)
-
-
Daily activity for the past 7 days:
-
-
Day
Number of students
- % for k,v in students_daily_activity_json['data'].items():
-
-
${k}
${v}
-
- % endfor
-
-
-
%endif
##-----------------------------------------------------------------------------
From 8ba41635572002b45a5725b42d53e1bdda3096c2 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 7 Dec 2012 12:48:57 -0500
Subject: [PATCH 010/285] WIP. Data loads, but not all of it
---
cms/djangoapps/contentstore/views.py | 2 +-
common/lib/capa/capa/capa_problem.py | 18 +-
common/lib/xmodule/xmodule/abtest_module.py | 33 +--
common/lib/xmodule/xmodule/capa_module.py | 154 +++++-------
common/lib/xmodule/xmodule/course_module.py | 82 ++++---
common/lib/xmodule/xmodule/error_module.py | 7 +-
common/lib/xmodule/xmodule/mako_module.py | 9 +-
common/lib/xmodule/xmodule/model.py | 79 +++++++
common/lib/xmodule/xmodule/modulestore/xml.py | 25 +-
common/lib/xmodule/xmodule/template_module.py | 4 -
common/lib/xmodule/xmodule/x_module.py | 222 +++++-------------
common/lib/xmodule/xmodule/xml_module.py | 17 +-
jenkins/base.sh | 12 +
lms/djangoapps/courseware/courses.py | 8 +-
lms/djangoapps/courseware/module_render.py | 4 +-
rakefile | 2 +-
16 files changed, 296 insertions(+), 382 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/model.py
create mode 100644 jenkins/base.sh
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 8f10eadc4b..833662b218 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -499,7 +499,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
"""
system = preview_module_system(request, preview_id, descriptor)
try:
- module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
+ module = descriptor.xmodule(system)
except:
module = ErrorDescriptor.from_descriptor(
descriptor,
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 451891d067..eb39d8a2c6 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -83,7 +83,7 @@ class LoncapaProblem(object):
Main class for capa Problems.
'''
- def __init__(self, problem_text, id, state=None, seed=None, system=None):
+ def __init__(self, problem_text, id, correct_map=None, done=None, seed=None, system=None):
'''
Initializes capa Problem.
@@ -91,7 +91,8 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- - state (dict): student state
+ - correct_map (dict): data specifying whether the student has completed the problem
+ - done (bool): Whether the student has answered the problem
- seed (int): random number generator seed (int)
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
@@ -103,16 +104,11 @@ class LoncapaProblem(object):
self.problem_id = id
self.system = system
self.seed = seed
+ self.done = done
+ self.correct_map = CorrectMap()
- if state:
- if 'seed' in state:
- self.seed = state['seed']
- if 'student_answers' in state:
- self.student_answers = state['student_answers']
- if 'correct_map' in state:
- self.correct_map.set_dict(state['correct_map'])
- if 'done' in state:
- self.done = state['done']
+ if correct_map is not None:
+ self.correct_map.set_dict(correct_map)
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 3091b6ec02..0f655ded6c 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -7,6 +7,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
+from .model import String, Scope
DEFAULT = "_DEFAULT_GROUP"
@@ -68,37 +69,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
template_dir_name = "abtest"
- def __init__(self, system, definition=None, **kwargs):
- """
- definition is a dictionary with the following layout:
- {'data': {
- 'experiment': 'the name of the experiment',
- 'group_portions': {
- 'group_a': 0.1,
- 'group_b': 0.2
- },
- 'group_contents': {
- 'group_a': [
- 'url://for/content/module/1',
- 'url://for/content/module/2',
- ],
- 'group_b': [
- 'url://for/content/module/3',
- ],
- DEFAULT: [
- 'url://for/default/content/1'
- ]
- }
- },
- 'children': [
- 'url://for/content/module/1',
- 'url://for/content/module/2',
- 'url://for/content/module/3',
- 'url://for/default/content/1',
- ]}
- """
- kwargs['shared_state_key'] = definition['data']['experiment']
- RawDescriptor.__init__(self, system, definition, **kwargs)
+ experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
@classmethod
def definition_from_xml(cls, xml_object, system):
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 9922b1b8a0..6ad6de2be6 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -19,6 +19,9 @@ from progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
+from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
+
+Date = Timedelta = ModelType
log = logging.getLogger("mitx.courseware")
@@ -77,6 +80,17 @@ class CapaModule(XModule):
'''
icon_class = 'problem'
+ attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
+ max_attempts = Int(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
+ due = Date(help="Date that this problem is due by", scope=Scope.settings)
+ graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
+ show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
+ force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings)
+ rerandomize = String(help="When to rerandomize the problem", default="always")
+ data = String(help="XML data for the problem", scope=Scope.content)
+ correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state)
+ done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
+
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
@@ -87,51 +101,15 @@ class CapaModule(XModule):
js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
- def __init__(self, system, location, definition, descriptor, instance_state=None,
- shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor, instance_state,
- shared_state, **kwargs)
+ def __init__(self, system, location, descriptor, model_data):
+ XModule.__init__(self, system, location, descriptor, model_data)
- self.attempts = 0
- self.max_attempts = None
-
- dom2 = etree.fromstring(definition['data'])
-
- display_due_date_string = self.metadata.get('due', None)
- if display_due_date_string is not None:
- self.display_due_date = dateutil.parser.parse(display_due_date_string)
- #log.debug("Parsed " + display_due_date_string +
- # " to " + str(self.display_due_date))
- else:
- self.display_due_date = None
-
- grace_period_string = self.metadata.get('graceperiod', None)
- if grace_period_string is not None and self.display_due_date:
- self.grace_period = parse_timedelta(grace_period_string)
- self.close_date = self.display_due_date + self.grace_period
+ if self.graceperiod is not None and self.due:
+ self.close_date = self.due + self.graceperiod
#log.debug("Then parsed " + grace_period_string +
# " to closing date" + str(self.close_date))
else:
- self.grace_period = None
- self.close_date = self.display_due_date
-
- self.max_attempts = self.metadata.get('attempts', None)
- if self.max_attempts is not None:
- self.max_attempts = int(self.max_attempts)
-
- self.show_answer = self.metadata.get('showanswer', 'closed')
-
- self.force_save_button = self.metadata.get('force_save_button', 'false')
-
- if self.show_answer == "":
- self.show_answer = "closed"
-
- if instance_state is not None:
- instance_state = json.loads(instance_state)
- if instance_state is not None and 'attempts' in instance_state:
- self.attempts = instance_state['attempts']
-
- self.name = only_one(dom2.xpath('/problem/@name'))
+ self.close_date = self.due
if self.rerandomize == 'never':
self.seed = 1
@@ -148,8 +126,8 @@ class CapaModule(XModule):
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
- self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
- instance_state, seed=self.seed, system=self.system)
+ self.lcp = LoncapaProblem(self.data, self.location.html_id(),
+ self.correct_map, self.done, self.seed, self.system)
except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
@@ -168,33 +146,21 @@ class CapaModule(XModule):
(self.location.url(), msg))
self.lcp = LoncapaProblem(
problem_text, self.location.html_id(),
- instance_state, seed=self.seed, system=self.system)
+ self.correct_map, self.done, self.seed, self.system)
else:
# add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
- @property
- def rerandomize(self):
- """
- Property accessor that returns self.metadata['rerandomize'] in a
- canonical form
- """
- rerandomize = self.metadata.get('rerandomize', 'always')
- if rerandomize in ("", "always", "true"):
- return "always"
- elif rerandomize in ("false", "per_student"):
- return "per_student"
- elif rerandomize == "never":
- return "never"
- elif rerandomize == "onreset":
- return "onreset"
- else:
- raise Exception("Invalid rerandomize attribute " + rerandomize)
+ if self.rerandomize in ("", "true"):
+ self.rerandomize = "always"
+ elif self.rerandomize == "false":
+ self.rerandomize = "per_student"
- def get_instance_state(self):
- state = self.lcp.get_state()
- state['attempts'] = self.attempts
- return json.dumps(state)
+ def sync_lcp_state(self):
+ lcp_state = self.lcp.get_state()
+ self.done = lcp_state['done']
+ self.correct_map = lcp_state['correct_map']
+ self.seed = lcp_state['seed']
def get_score(self):
return self.lcp.get_score()
@@ -211,7 +177,7 @@ class CapaModule(XModule):
if total > 0:
try:
return Progress(score, total)
- except Exception as err:
+ except Exception:
log.exception("Got bad progress")
return None
return None
@@ -261,8 +227,8 @@ class CapaModule(XModule):
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
- state=None, # Tabula rasa
seed=self.seed, system=self.system)
+ self.sync_lcp_state()
# Prepend a scary warning to the student
warning = '
'\
@@ -280,8 +246,8 @@ class CapaModule(XModule):
html = warning
try:
html += self.lcp.get_html()
- except Exception, err: # Couldn't do it. Give up
- log.exception(err)
+ except Exception: # Couldn't do it. Give up
+ log.exception("Unable to generate html from LoncapaProblem")
raise
content = {'name': self.display_name,
@@ -311,7 +277,7 @@ class CapaModule(XModule):
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
- if self.lcp.done and self.rerandomize == "always":
+ if self.done and self.rerandomize == "always":
check_button = False
save_button = False
@@ -320,7 +286,7 @@ class CapaModule(XModule):
reset_button = False
# User hasn't submitted an answer yet -- we don't want resets
- if not self.lcp.done:
+ if not self.done:
reset_button = False
# We may not need a "save" button if infinite number of attempts and
@@ -406,7 +372,7 @@ class CapaModule(XModule):
return self.attempts > 0
if self.show_answer == 'answered':
- return self.lcp.done
+ return self.done
if self.show_answer == 'closed':
return self.closed()
@@ -429,6 +395,7 @@ class CapaModule(XModule):
queuekey = get['queuekey']
score_msg = get['xqueue_body']
self.lcp.update_score(score_msg, queuekey)
+ self.sync_lcp_state()
return dict() # No AJAX return is needed
@@ -445,8 +412,9 @@ class CapaModule(XModule):
raise NotFoundError('Answer is not available')
else:
answers = self.lcp.get_question_answers()
+ self.sync_lcp_state()
- # answers (eg ) may have embedded images
+ # answers (eg ) may have embedded images
# but be careful, some problems are using non-string answer dicts
new_answers = dict()
for answer_id in answers:
@@ -512,7 +480,7 @@ class CapaModule(XModule):
raise NotFoundError('Problem is closed')
# Problem submitted. Student should reset before checking again
- if self.lcp.done and self.rerandomize == "always":
+ if self.done and self.rerandomize == "always":
event_info['failure'] = 'unreset'
self.system.track_function('save_problem_check_fail', event_info)
raise NotFoundError('Problem must be reset before it can be checked again')
@@ -522,14 +490,13 @@ class CapaModule(XModule):
current_time = datetime.datetime.now()
prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime']
- if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
+ if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
- return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
+ return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try:
- old_state = self.lcp.get_state()
- lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
+ self.sync_lcp_state()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
@@ -554,11 +521,11 @@ class CapaModule(XModule):
# 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
- event_info['attempts'] = self.attempts
+ event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info)
- if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
- self.system.psychometrics_handler(self.get_instance_state())
+ if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
+ self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
@@ -589,7 +556,7 @@ class CapaModule(XModule):
# Problem submitted. Student should reset before saving
# again.
- if self.lcp.done and self.rerandomize == "always":
+ if self.done and self.rerandomize == "always":
event_info['failure'] = 'done'
self.system.track_function('save_problem_fail', event_info)
return {'success': False,
@@ -617,7 +584,7 @@ class CapaModule(XModule):
return {'success': False,
'error': "Problem is closed"}
- if not self.lcp.done:
+ if not self.done:
event_info['failure'] = 'not_done'
self.system.track_function('reset_problem_fail', event_info)
return {'success': False,
@@ -629,9 +596,13 @@ class CapaModule(XModule):
# in next line)
self.lcp.seed = None
- self.lcp = LoncapaProblem(self.definition['data'],
- self.location.html_id(), self.lcp.get_state(),
- system=self.system)
+ self.lcp = LoncapaProblem(self.data,
+ self.location.html_id(),
+ self.lcp.correct_map,
+ self.lcp.done,
+ self.lcp.seed,
+ self.system)
+ self.sync_lcp_state()
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
@@ -647,6 +618,8 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule
+ weight = Float(help="How much to weight this problem by", scope=Scope.settings)
+
stores_state = True
has_score = True
template_dir_name = 'problem'
@@ -665,12 +638,3 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:],
path[8:],
]
-
- def __init__(self, *args, **kwargs):
- super(CapaDescriptor, self).__init__(*args, **kwargs)
-
- weight_string = self.metadata.get('weight', None)
- if weight_string:
- self.weight = float(weight_string)
- else:
- self.weight = None
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 474cec0a45..019a79e7ab 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -12,6 +12,9 @@ import requests
import time
import copy
+from .model import Scope, ModelType, List, String, Object, Boolean
+
+Date = ModelType
log = logging.getLogger(__name__)
@@ -21,6 +24,39 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
+ textbooks = List(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content)
+ wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
+ enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
+ enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
+ end = Date(help="Date that this class ends", scope=Scope.settings)
+ advertised_start = Date(help="Date that this course is advertised to start", scope=Scope.settings)
+ grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
+
+ info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
+
+ # An extra property is used rather than the wiki_slug/number because
+ # there are courses that change the number for different runs. This allows
+ # courses to share the same css_class across runs even if they have
+ # different numbers.
+ #
+ # TODO get rid of this as soon as possible or potentially build in a robust
+ # way to add in course-specific styling. There needs to be a discussion
+ # about the right way to do this, but arjun will address this ASAP. Also
+ # note that the courseware template needs to change when this is removed.
+ css_class = String(help="DO NOT USE THIS", scope=Scope.settings)
+
+ # TODO: This is a quick kludge to allow CS50 (and other courses) to
+ # specify their own discussion forums as external links by specifying a
+ # "discussion_link" in their policy JSON file. This should later get
+ # folded in with Syllabus, Course Info, and additional Custom tabs in a
+ # more sensible framework later.
+ discussion_link = String(help="DO NOT USE THIS", scope=Scope.settings)
+
+ # TODO: same as above, intended to let internal CS50 hide the progress tab
+ # until we get grade integration set up.
+ # Explicit comparison to True because we always want to return a bool.
+ hide_progress_tab = Boolean(help="DO NOT USE THIS", scope=Scope.settings)
+
template_dir_name = 'course'
class Textbook:
@@ -69,10 +105,11 @@ class CourseDescriptor(SequenceDescriptor):
return table_of_contents
- def __init__(self, system, definition=None, **kwargs):
- super(CourseDescriptor, self).__init__(system, definition, **kwargs)
+ def __init__(self, *args, **kwargs):
+ super(CourseDescriptor, self).__init__(*args, **kwargs)
+
self.textbooks = []
- for title, book_url in self.definition['data']['textbooks']:
+ for title, book_url in self.textbooks:
try:
self.textbooks.append(self.Textbook(title, book_url))
except:
@@ -81,7 +118,8 @@ class CourseDescriptor(SequenceDescriptor):
log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url))
continue
- self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course
+ if self.wiki_slug is None:
+ self.wiki_slug = self.location.course
msg = None
if self.start is None:
@@ -98,7 +136,7 @@ class CourseDescriptor(SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
- self.set_grading_policy(self.definition['data'].get('grading_policy', None))
+ self.set_grading_policy(self.grading_policy)
def defaut_grading_policy(self):
"""
@@ -203,7 +241,7 @@ class CourseDescriptor(SequenceDescriptor):
# cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
- instance.definition['data']['grading_policy'] = policy
+ instance.grading_policy = policy
# now set the current instance. set_grading_policy() will apply some inheritance rules
instance.set_grading_policy(policy)
@@ -395,38 +433,14 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
- displayed_start = self._try_parse_time('advertised_start') or self.start
- return time.strftime("%b %d, %Y", displayed_start)
+ return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property
def end_date_text(self):
return time.strftime("%b %d, %Y", self.end)
- # An extra property is used rather than the wiki_slug/number because
- # there are courses that change the number for different runs. This allows
- # courses to share the same css_class across runs even if they have
- # different numbers.
- #
- # TODO get rid of this as soon as possible or potentially build in a robust
- # way to add in course-specific styling. There needs to be a discussion
- # about the right way to do this, but arjun will address this ASAP. Also
- # note that the courseware template needs to change when this is removed.
- @property
- def css_class(self):
- return self.metadata.get('css_class', '')
- @property
- def info_sidebar_name(self):
- return self.metadata.get('info_sidebar_name', 'Course Handouts')
- @property
- def discussion_link(self):
- """TODO: This is a quick kludge to allow CS50 (and other courses) to
- specify their own discussion forums as external links by specifying a
- "discussion_link" in their policy JSON file. This should later get
- folded in with Syllabus, Course Info, and additional Custom tabs in a
- more sensible framework later."""
- return self.metadata.get('discussion_link', None)
@property
def forum_posts_allowed(self):
@@ -443,12 +457,6 @@ class CourseDescriptor(SequenceDescriptor):
return True
- @property
- def hide_progress_tab(self):
- """TODO: same as above, intended to let internal CS50 hide the progress tab
- until we get grade integration set up."""
- # Explicit comparison to True because we always want to return a bool.
- return self.metadata.get('hide_progress_tab') == True
@property
def end_of_course_survey_url(self):
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index 65fceb77c7..0f95bcd256 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -74,12 +74,11 @@ class ErrorDescriptor(JSONEditingDescriptor):
}
# real metadata stays in the content, but add a display name
- metadata = {'display_name': 'Error: ' + location.name}
+ model_data = {'display_name': 'Error: ' + location.name}
return ErrorDescriptor(
system,
- definition,
- location=location,
- metadata=metadata
+ location,
+ model_data,
)
def get_context(self):
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index f5f2fae23b..bdf3cb4749 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -21,20 +21,19 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template
"""
- def __init__(self, system, definition=None, **kwargs):
+ def __init__(self, system, location, model_data):
if getattr(system, 'render_template', None) is None:
raise TypeError('{system} must have a render_template function'
' in order to use a MakoDescriptor'.format(
system=system))
- super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
+ super(MakoModuleDescriptor, self).__init__(system, location, model_data)
def get_context(self):
"""
Return the context to render the mako template with
"""
return {'module': self,
- 'metadata': self.metadata,
- 'editable_metadata_fields' : self.editable_metadata_fields
+ 'editable_metadata_fields': self.editable_fields
}
def get_html(self):
@@ -44,6 +43,6 @@ class MakoModuleDescriptor(XModuleDescriptor):
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
- subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
+ subset = [field.name for field in self.fields if field.name not in self.system_metadata_fields]
return subset
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
new file mode 100644
index 0000000000..b58ffa267a
--- /dev/null
+++ b/common/lib/xmodule/xmodule/model.py
@@ -0,0 +1,79 @@
+from collections import namedtuple
+
+class ModuleScope(object):
+ USAGE, DEFINITION, TYPE, ALL = xrange(4)
+
+
+class Scope(namedtuple('ScopeBase', 'student module')):
+ pass
+
+Scope.content = Scope(student=False, module=ModuleScope.DEFINITION)
+Scope.student_state = Scope(student=True, module=ModuleScope.USAGE)
+Scope.settings = Scope(student=True, module=ModuleScope.USAGE)
+Scope.student_preferences = Scope(student=True, module=ModuleScope.TYPE)
+Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
+
+
+class ModelType(object):
+ sequence = 0
+
+ def __init__(self, help=None, default=None, scope=Scope.content):
+ self._seq = self.sequence
+ self._name = "unknown"
+ self.help = help
+ self.default = default
+ self.scope = scope
+ ModelType.sequence += 1
+
+ @property
+ def name(self):
+ return self._name
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+
+ return instance._model_data.get(self.name, self.default)
+
+ def __set__(self, instance, value):
+ instance._model_data[self.name] = value
+
+ def __delete__(self, instance):
+ del instance._model_data[self.name]
+
+ def __repr__(self):
+ return "<{0.__class__.__name} {0.__name__}>".format(self)
+
+ def __lt__(self, other):
+ return self._seq < other._seq
+
+Int = Float = Boolean = Object = List = String = Any = ModelType
+
+
+class ModelMetaclass(type):
+ def __new__(cls, name, bases, attrs):
+ # Find registered methods
+ reg_methods = {}
+ for value in attrs.itervalues():
+ for reg_type, names in getattr(value, "_method_registrations", {}).iteritems():
+ for n in names:
+ reg_methods[reg_type + n] = value
+ attrs['registered_methods'] = reg_methods
+
+ if attrs.get('has_children', False):
+ attrs['children'] = ModelType(help='The children of this XModule', default=[], scope=None)
+
+ @property
+ def child_map(self):
+ return dict((child.name, child) for child in self.children)
+ attrs['child_map'] = child_map
+
+ fields = []
+ for n, v in attrs.items():
+ if isinstance(v, ModelType):
+ v._name = n
+ fields.append(v)
+ fields.sort()
+ attrs['fields'] = fields
+
+ return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index e3ad1fb0dd..7c6887696e 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -187,12 +187,13 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
err_msg
)
- descriptor.metadata['data_dir'] = course_dir
+ setattr(descriptor, 'data_dir', course_dir)
xmlstore.modules[course_id][descriptor.location] = descriptor
- for child in descriptor.get_children():
- parent_tracker.add_parent(child.location, descriptor.location)
+ if hasattr(descriptor, 'children'):
+ for child in descriptor.children:
+ parent_tracker.add_parent(child.location, descriptor.location)
return descriptor
render_template = lambda: ''
@@ -425,14 +426,14 @@ class XMLModuleStore(ModuleStoreBase):
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
- XModuleDescriptor.compute_inherited_metadata(course_descriptor)
+ #XModuleDescriptor.compute_inherited_metadata(course_descriptor)
# now import all pieces of course_info which is expected to be stored
# in /info or /info/
self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name)
# now import all static tabs which are expected to be stored in
- # in /tabs or /tabs/
+ # in /tabs or /tabs/
self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name)
self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name)
@@ -444,30 +445,30 @@ class XMLModuleStore(ModuleStoreBase):
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
if url_name:
- path = base_dir / url_name
+ path = base_dir / url_name
if not os.path.exists(path):
path = base_dir
- for filepath in glob.glob(path/ '*'):
+ for filepath in glob.glob(path / '*'):
with open(filepath) as f:
try:
html = f.read().decode('utf-8')
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
- module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
+ module = HtmlDescriptor(system, loc, {'data': html})
# VS[compat]:
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
if category == "static_tab":
for tab in course_descriptor.tabs or []:
if tab.get('url_slug') == slug:
- module.metadata['display_name'] = tab['name']
- module.metadata['data_dir'] = course_dir
- self.modules[course_descriptor.id][module.location] = module
+ module.display_name = tab['name']
+ module.data_dir = course_dir
+ self.modules[course_descriptor.id][module.location] = module
except Exception, e:
- logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
+ logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
system.error_tracker("ERROR: " + str(e))
def get_instance(self, course_id, location, depth=0):
diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py
index 07ef2c6511..f14254c011 100644
--- a/common/lib/xmodule/xmodule/template_module.py
+++ b/common/lib/xmodule/xmodule/template_module.py
@@ -67,10 +67,6 @@ class CustomTagDescriptor(RawDescriptor):
return template.render(**params)
- def __init__(self, system, definition, **kwargs):
- '''Render and save the template for this descriptor instance'''
- super(CustomTagDescriptor, self).__init__(system, definition, **kwargs)
-
@property
def rendered_html(self):
return self.render_template(self.system, self.definition['data'])
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 690f78fd53..9cf45652ca 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -2,19 +2,41 @@ import logging
import pkg_resources
import yaml
import os
+import time
-from functools import partial
from lxml import etree
from pprint import pprint
from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
-from xmodule.timeparse import parse_time, stringify_time
+from .model import ModelMetaclass, String, Scope, ModuleScope, ModelType
-from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
-from xmodule.modulestore.exceptions import ItemNotFoundError
-import time
+Date = ModelType
+
+
+class Date(ModelType):
+ time_format = "%Y-%m-%dT%H:%M"
+
+ def from_json(self, value):
+ """
+ Parse an optional metadata key containing a time: if present, complain
+ if it doesn't parse.
+ Return None if not present or invalid.
+ """
+ try:
+ return time.strptime(value, self.time_format)
+ except ValueError as e:
+ msg = "Field {0} has bad value '{1}': '{2}'".format(
+ self._name, value, e)
+ log.warning(msg)
+ return None
+
+ def to_json(self, value):
+ """
+ Convert a time struct to a string
+ """
+ return time.strftime(self.time_format, value)
log = logging.getLogger('mitx.' + __name__)
@@ -157,6 +179,10 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example.
'''
+ __metaclass__ = ModelMetaclass
+
+ display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
+
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
#
@@ -165,8 +191,7 @@ class XModule(HTMLSnippet):
# in the module
icon_class = 'other'
- def __init__(self, system, location, definition, descriptor,
- instance_state=None, shared_state=None, **kwargs):
+ def __init__(self, system, location, descriptor, model_data):
'''
Construct a new xmodule
@@ -214,63 +239,25 @@ class XModule(HTMLSnippet):
'''
self.system = system
self.location = Location(location)
- self.definition = definition
self.descriptor = descriptor
- self.instance_state = instance_state
- self.shared_state = shared_state
self.id = self.location.url()
self.url_name = self.location.name
self.category = self.location.category
- self.metadata = kwargs.get('metadata', {})
- self._loaded_children = None
+ self._model_data = model_data
- @property
- def display_name(self):
- '''
- Return a display name for the module: use display_name if defined in
- metadata, otherwise convert the url name.
- '''
- return self.metadata.get('display_name',
- self.url_name.replace('_', ' '))
+ if self.display_name is None:
+ self.display_name = self.url_name.replace('_', ' ')
def __unicode__(self):
return ''.format(self.id)
- def get_children(self):
- '''
- Return module instances for all the children of this module.
- '''
- if self._loaded_children is None:
- child_locations = self.get_children_locations()
- children = [self.system.get_module(loc) for loc in child_locations]
- # get_module returns None if the current user doesn't have access
- # to the location.
- self._loaded_children = [c for c in children if c is not None]
-
- return self._loaded_children
-
- def get_children_locations(self):
- '''
- Returns the locations of each of child modules.
-
- Overriding this changes the behavior of get_children and
- anything that uses get_children, such as get_display_items.
-
- This method will not instantiate the modules of the children
- unless absolutely necessary, so it is cheaper to call than get_children
-
- These children will be the same children returned by the
- descriptor unless descriptor.has_dynamic_children() is true.
- '''
- return self.definition.get('children', [])
-
def get_display_items(self):
'''
Returns a list of descendent module instances that will display
immediately inside this module.
'''
items = []
- for child in self.get_children():
+ for child in self.children():
items.extend(child.displayable_items())
return items
@@ -290,18 +277,6 @@ class XModule(HTMLSnippet):
### Functions used in the LMS
- def get_instance_state(self):
- ''' State of the object, as stored in the database
- '''
- return '{}'
-
- def get_shared_state(self):
- '''
- Get state that should be shared with other instances
- using the same 'shared_state_key' attribute.
- '''
- return '{}'
-
def get_score(self):
''' Score the student received on the problem.
'''
@@ -391,7 +366,10 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
entry_point = "xmodule.v1"
module_class = XModule
+ __metaclass__ = ModelMetaclass
+ display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
+ start = Date(help="Start time when this module is visible", scope=Scope(student=False, module=ModuleScope.USAGE))
# Attributes for inspection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
@@ -424,8 +402,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# ============================= STRUCTURAL MANIPULATION ===================
def __init__(self,
system,
- definition=None,
- **kwargs):
+ location,
+ model_data):
"""
Construct a new XModuleDescriptor. The only required arguments are the
system, used for interaction with external resources, and the
@@ -467,116 +445,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
instance of the module data
"""
self.system = system
- self.metadata = kwargs.get('metadata', {})
- self.definition = definition if definition is not None else {}
- self.location = Location(kwargs.get('location'))
+ self.location = Location(location)
self.url_name = self.location.name
self.category = self.location.category
- self.shared_state_key = kwargs.get('shared_state_key')
+ self._model_data = model_data
self._child_instances = None
self._inherited_metadata = set()
- @property
- def display_name(self):
- '''
- Return a display name for the module: use display_name if defined in
- metadata, otherwise convert the url name.
- '''
- return self.metadata.get('display_name',
- self.url_name.replace('_', ' '))
-
- @property
- def start(self):
- """
- If self.metadata contains start, return it. Else return None.
- """
- if 'start' not in self.metadata:
- return None
- return self._try_parse_time('start')
-
- @start.setter
- def start(self, value):
- if isinstance(value, time.struct_time):
- self.metadata['start'] = stringify_time(value)
-
- @property
- def own_metadata(self):
- """
- Return the metadata that is not inherited, but was defined on this module.
- """
- return dict((k, v) for k, v in self.metadata.items()
- if k not in self._inherited_metadata)
-
- @staticmethod
- def compute_inherited_metadata(node):
- """Given a descriptor, traverse all of its descendants and do metadata
- inheritance. Should be called on a CourseDescriptor after importing a
- course.
-
- NOTE: This means that there is no such thing as lazy loading at the
- moment--this accesses all the children."""
- for c in node.get_children():
- c.inherit_metadata(node.metadata)
- XModuleDescriptor.compute_inherited_metadata(c)
-
- def inherit_metadata(self, metadata):
- """
- Updates this module with metadata inherited from a containing module.
- Only metadata specified in self.inheritable_metadata will
- be inherited
- """
- # Set all inheritable metadata from kwargs that are
- # in self.inheritable_metadata and aren't already set in metadata
- for attr in self.inheritable_metadata:
- if attr not in self.metadata and attr in metadata:
- self._inherited_metadata.add(attr)
- self.metadata[attr] = metadata[attr]
-
- def get_children(self):
- """Returns a list of XModuleDescriptor instances for the children of
- this module"""
- if self._child_instances is None:
- self._child_instances = []
- for child_loc in self.definition.get('children', []):
- try:
- child = self.system.load_item(child_loc)
- except ItemNotFoundError:
- log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
- continue
- # TODO (vshnayder): this should go away once we have
- # proper inheritance support in mongo. The xml
- # datastore does all inheritance on course load.
- child.inherit_metadata(self.metadata)
- self._child_instances.append(child)
-
- return self._child_instances
-
def get_child_by_url_name(self, url_name):
"""
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
"""
- for c in self.get_children():
+ for c in self.children:
if c.url_name == url_name:
return c
return None
- def xmodule_constructor(self, system):
+ def xmodule(self, system):
"""
Returns a constructor for an XModule. This constructor takes two
arguments: instance_state and shared_state, and returns a fully
instantiated XModule
"""
- return partial(
- self.module_class,
+ return self.module_class(
system,
self.location,
- self.definition,
self,
- metadata=self.metadata
+ system.xmodule_model_data(self.model_data),
)
-
def has_dynamic_children(self):
"""
Returns True if this descriptor has dynamic children for a given
@@ -701,31 +599,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
return eq
def __repr__(self):
- return ("{class_}({system!r}, {definition!r}, location={location!r},"
- " metadata={metadata!r})".format(
+ return ("{class_}({system!r}, location={location!r},"
+ " model_data={model_data!r})".format(
class_=self.__class__.__name__,
system=self.system,
- definition=self.definition,
location=self.location,
- metadata=self.metadata
+ model_data=self._model_data,
))
- # ================================ Internal helpers =======================
-
- def _try_parse_time(self, key):
- """
- Parse an optional metadata key containing a time: if present, complain
- if it doesn't parse.
- Return None if not present or invalid.
- """
- if key in self.metadata:
- try:
- return parse_time(self.metadata[key])
- except ValueError as e:
- msg = "Descriptor {0} loaded with a bad metadata key '{1}': '{2}'".format(
- self.location.url(), self.metadata[key], e)
- log.warning(msg)
- return None
class DescriptorSystem(object):
@@ -867,6 +748,9 @@ class ModuleSystem(object):
'''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val
+ def xmodule_module_data(self, module_data):
+ return module_data
+
def __repr__(self):
return repr(self.__dict__)
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 38fcaddd20..36bea6edb2 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -287,9 +287,9 @@ class XmlDescriptor(XModuleDescriptor):
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
definition_xml = cls.load_file(filepath, system.resources_fs, location)
else:
- definition_xml = xml_object # this is just a pointer, not the real definition content
+ definition_xml = xml_object # this is just a pointer, not the real definition content
- definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
+ definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
# VS[compat] -- make Ike's github preview links work in both old and
# new file layouts
if is_pointer_tag(xml_object):
@@ -299,13 +299,13 @@ class XmlDescriptor(XModuleDescriptor):
metadata = cls.load_metadata(definition_xml)
# move definition metadata into dict
- dmdata = definition.get('definition_metadata','')
+ dmdata = definition.get('definition_metadata', '')
if dmdata:
metadata['definition_metadata_raw'] = dmdata
try:
metadata.update(json.loads(dmdata))
except Exception as err:
- log.debug('Error %s in loading metadata %s' % (err,dmdata))
+ log.debug('Error %s in loading metadata %s' % (err, dmdata))
metadata['definition_metadata_err'] = str(err)
# Set/override any metadata specified by policy
@@ -313,11 +313,14 @@ class XmlDescriptor(XModuleDescriptor):
if k in system.policy:
cls.apply_policy(metadata, system.policy[k])
+ model_data = {}
+ model_data.update(metadata)
+ model_data.update(definition)
+
return cls(
system,
- definition,
- location=location,
- metadata=metadata,
+ location,
+ model_data,
)
@classmethod
diff --git a/jenkins/base.sh b/jenkins/base.sh
new file mode 100644
index 0000000000..c7175e6e52
--- /dev/null
+++ b/jenkins/base.sh
@@ -0,0 +1,12 @@
+
+function github_status {
+ gcli status create mitx mitx $GIT_COMMIT \
+ --params=$1 \
+ target_url:$BUILD_URL \
+ description:"Build #$BUILD_NUMBER is running" \
+ -f csv
+}
+
+function github_mark_failed_on_exit {
+ trap '[ $? == "0" ] || github_status state:failed' EXIT
+}
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 643382b485..5312a228a4 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -85,7 +85,7 @@ def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if isinstance(modulestore(), XMLModuleStore):
- path = course.metadata['data_dir'] + "/images/course_image.jpg"
+ path = course.data_dir + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
else:
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
@@ -162,7 +162,9 @@ def get_course_about_section(course, section_key):
key=section_key, url=course.location.url()))
return None
elif section_key == "title":
- return course.metadata.get('display_name', course.url_name)
+ if course.display_name is None:
+ return course.url_name
+ return course.display_name
elif section_key == "university":
return course.location.org
elif section_key == "number":
@@ -220,7 +222,7 @@ def get_course_syllabus_section(course, section_key):
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
- course.metadata['data_dir'], course_namespace=course.location)
+ course.data_dir, course_namespace=course.location)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url()))
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 9343301fb7..9c40767e99 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -245,7 +245,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
make_psychometrics_data_update_handler(instance_module))
try:
- module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
+ module = descriptor.xmodule(system)
except:
log.exception("Error creating module from descriptor {0}".format(descriptor))
@@ -259,7 +259,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
error_msg=exc_info_to_str(sys.exc_info()))
# Make an error module
- return err_descriptor.xmodule_constructor(system)(None, None)
+ return err_descriptor.xmodule(system)
_get_html = module.get_html
diff --git a/rakefile b/rakefile
index ca20de9a39..adf16cc462 100644
--- a/rakefile
+++ b/rakefile
@@ -40,7 +40,7 @@ end
def django_admin(system, env, command, *args)
django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
- return "#{django_admin} #{command} --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
+ return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
end
def django_for_jasmine(system, django_reload)
From cbfc7b201af3742c52b5527eae3132d9f6b9aaa7 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 10 Dec 2012 15:40:23 -0500
Subject: [PATCH 011/285] WIP more changes to model definitions. Next Up:
actually wiring model data into the rdbms
---
cms/djangoapps/contentstore/views.py | 4 +-
cms/templates/edit_subsection.html | 8 +-
cms/templates/new_item.html | 2 +-
cms/templates/overview.html | 6 +-
cms/templates/unit.html | 8 +-
cms/templates/widgets/header.html | 2 +-
cms/templates/widgets/navigation.html | 2 +-
cms/templates/widgets/sequence-edit.html | 2 +-
cms/templates/widgets/units.html | 2 +-
common/djangoapps/student/views.py | 2 +-
common/djangoapps/xmodule_modifiers.py | 2 +-
common/lib/xmodule/setup.py | 2 +-
common/lib/xmodule/xmodule/capa_module.py | 59 +++++---
common/lib/xmodule/xmodule/course_module.py | 12 +-
.../lib/xmodule/xmodule/discussion_module.py | 14 +-
common/lib/xmodule/xmodule/html_module.py | 11 +-
.../xmodule/js/src/video/display.coffee | 1 -
common/lib/xmodule/xmodule/model.py | 106 +++++++++++---
.../lib/xmodule/xmodule/modulestore/mongo.py | 3 +-
common/lib/xmodule/xmodule/modulestore/xml.py | 8 +-
common/lib/xmodule/xmodule/plugin.py | 64 +++++++++
common/lib/xmodule/xmodule/seq_module.py | 38 ++---
common/lib/xmodule/xmodule/vertical_module.py | 8 +-
common/lib/xmodule/xmodule/video_module.py | 23 ++-
common/lib/xmodule/xmodule/x_module.py | 132 +++++++-----------
lms/djangoapps/courseware/grades.py | 6 +-
lms/djangoapps/courseware/module_render.py | 21 ++-
lms/setup.py | 18 +++
lms/templates/courseware/welcome-back.html | 4 +-
lms/templates/video.html | 2 +-
lms/xmodule_namespace.py | 23 +++
local-requirements.txt | 1 +
32 files changed, 376 insertions(+), 220 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/plugin.py
create mode 100644 lms/setup.py
create mode 100644 lms/xmodule_namespace.py
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 833662b218..bf706f2996 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -115,7 +115,7 @@ def index(request):
return render_to_response('index.html', {
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.metadata.get('display_name'),
+ 'courses': [(course.title,
reverse('course_index', args=[
course.location.org,
course.location.course,
@@ -269,7 +269,7 @@ def edit_unit(request, location):
for template in templates:
if template.location.category in COMPONENT_TYPES:
component_templates[template.location.category].append((
- template.display_name,
+ template.lms.display_name,
template.location.url(),
))
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index d3b6f73f13..53567c73e1 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -21,7 +21,7 @@
-
+
@@ -62,11 +62,11 @@
% if subsection.start != parent_item.start and subsection.start:
% if parent_start_date is None:
-
The date above differs from the release date of ${parent_item.display_name}, which is unset.
+
The date above differs from the release date of ${parent_item.lms.display_name}, which is unset.
% else:
-
The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
+
The date above differs from the release date of ${parent_item.lms.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
% endif
- Sync to ${parent_item.display_name}.
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index e7562f83d0..bfde1b1419 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -231,7 +231,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
- .format(course.display_name)}
+ .format(course.lms.display_name)}
org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment",
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 5c19a2f1d7..81f0205c3e 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -32,7 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html():
context.update({
'content': get_html(),
- 'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
+ 'display_name': module.lms.display_name,
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 24cddd2047..b3a8dabe1b 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -40,6 +40,6 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor"
- ]
+ ],
}
)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 6ad6de2be6..acb4b63e1c 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -21,8 +21,6 @@ from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
-Date = Timedelta = ModelType
-
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
@@ -45,25 +43,34 @@ def only_one(lst, default="", process=lambda x: x):
raise Exception('Malformed XML: expected at most one element in list.')
-def parse_timedelta(time_str):
- """
- time_str: A string with the following components:
- day[s] (optional)
- hour[s] (optional)
- minute[s] (optional)
- second[s] (optional)
+class Timedelta(ModelType):
+ def from_json(self, time_str):
+ """
+ time_str: A string with the following components:
+ day[s] (optional)
+ hour[s] (optional)
+ minute[s] (optional)
+ second[s] (optional)
- Returns a datetime.timedelta parsed from the string
- """
- parts = TIMEDELTA_REGEX.match(time_str)
- if not parts:
- return
- parts = parts.groupdict()
- time_params = {}
- for (name, param) in parts.iteritems():
- if param:
- time_params[name] = int(param)
- return timedelta(**time_params)
+ Returns a datetime.timedelta parsed from the string
+ """
+ parts = TIMEDELTA_REGEX.match(time_str)
+ if not parts:
+ return
+ parts = parts.groupdict()
+ time_params = {}
+ for (name, param) in parts.iteritems():
+ if param:
+ time_params[name] = int(param)
+ return timedelta(**time_params)
+
+ def to_json(self, value):
+ values = []
+ for attr in ('days', 'hours', 'minutes', 'seconds'):
+ cur_value = getattr(value, attr, 0)
+ if cur_value > 0:
+ values.append("%d %s" % (cur_value, attr))
+ return ' '.join(values)
class ComplexEncoder(json.JSONEncoder):
@@ -82,7 +89,7 @@ class CapaModule(XModule):
attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
max_attempts = Int(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
- due = Date(help="Date that this problem is due by", scope=Scope.settings)
+ due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings)
@@ -90,6 +97,7 @@ class CapaModule(XModule):
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
+ display_name = String(help="Display name for this module", scope=Scope.settings)
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
@@ -104,8 +112,13 @@ class CapaModule(XModule):
def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data)
- if self.graceperiod is not None and self.due:
- self.close_date = self.due + self.graceperiod
+ if self.due:
+ due_date = dateutil.parser.parse(self.due)
+ else:
+ due_date = None
+
+ if self.graceperiod is not None and due_date:
+ self.close_date = due_date + self.graceperiod
#log.debug("Then parsed " + grace_period_string +
# " to closing date" + str(self.close_date))
else:
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 019a79e7ab..c4dc63c1b4 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -13,8 +13,7 @@ import time
import copy
from .model import Scope, ModelType, List, String, Object, Boolean
-
-Date = ModelType
+from .x_module import Date
log = logging.getLogger(__name__)
@@ -31,6 +30,10 @@ class CourseDescriptor(SequenceDescriptor):
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = Date(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
+ show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
+ start = Date(help="Start time when this module is visible", scope=Scope.settings)
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+ has_children = True
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
@@ -342,10 +345,6 @@ class CourseDescriptor(SequenceDescriptor):
def tabs(self, value):
self.metadata['tabs'] = value
- @property
- def show_calculator(self):
- return self.metadata.get("show_calculator", None) == "Yes"
-
@lazyproperty
def grading_context(self):
"""
@@ -433,6 +432,7 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
+ print self.advertised_start, self.start
return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index 1deceac5d0..a193604278 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -3,8 +3,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
-
-import json
+from .model import String, Scope
class DiscussionModule(XModule):
js = {'coffee':
@@ -12,18 +11,19 @@ class DiscussionModule(XModule):
resource_string(__name__, 'js/src/discussion/display.coffee')]
}
js_module_name = "InlineDiscussion"
+
+ data = String(help="XML definition of inline discussion", scope=Scope.content)
+
def get_html(self):
context = {
'discussion_id': self.discussion_id,
}
return self.system.render_template('discussion/_discussion_module.html', context)
- def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
- if isinstance(instance_state, str):
- instance_state = json.loads(instance_state)
- xml_data = etree.fromstring(definition['data'])
+ xml_data = etree.fromstring(self.data)
self.discussion_id = xml_data.attrib['id']
self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category']
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 709f86bf45..6d86fb90a8 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -15,6 +15,7 @@ from .html_checker import check_html
from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
+from .model import Scope, String
log = logging.getLogger("mitx.courseware")
@@ -26,15 +27,11 @@ class HtmlModule(XModule):
]
}
js_module_name = "HTMLModule"
+
+ data = String(help="Html contents to display for this module", scope=Scope.content)
def get_html(self):
- return self.html
-
- def __init__(self, system, location, definition, descriptor,
- instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor,
- instance_state, shared_state, **kwargs)
- self.html = self.definition['data']
+ return self.data
diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee
index bb87679d37..dbcab4a670 100644
--- a/common/lib/xmodule/xmodule/js/src/video/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee
@@ -2,7 +2,6 @@ class @Video
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
- @caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true"
window.player = null
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
index b58ffa267a..edd94f14a9 100644
--- a/common/lib/xmodule/xmodule/model.py
+++ b/common/lib/xmodule/xmodule/model.py
@@ -1,4 +1,6 @@
from collections import namedtuple
+from .plugin import Plugin
+
class ModuleScope(object):
USAGE, DEFINITION, TYPE, ALL = xrange(4)
@@ -15,6 +17,13 @@ Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
class ModelType(object):
+ """
+ A field class that can be used as a class attribute to define what data the class will want
+ to refer to.
+
+ When the class is instantiated, it will be available as an instance attribute of the same
+ name, by proxying through to self._model_data on the containing object.
+ """
sequence = 0
def __init__(self, help=None, default=None, scope=Scope.content):
@@ -33,10 +42,13 @@ class ModelType(object):
if instance is None:
return self
- return instance._model_data.get(self.name, self.default)
+ if self.name not in instance._model_data:
+ return self.default
+
+ return self.from_json(instance._model_data[self.name])
def __set__(self, instance, value):
- instance._model_data[self.name] = value
+ instance._model_data[self.name] = self.to_json(value)
def __delete__(self, instance):
del instance._model_data[self.name]
@@ -47,27 +59,27 @@ class ModelType(object):
def __lt__(self, other):
return self._seq < other._seq
+ def to_json(self, value):
+ return value
+
+ def from_json(self, value):
+ return value
+
Int = Float = Boolean = Object = List = String = Any = ModelType
class ModelMetaclass(type):
+ """
+ A metaclass to be used for classes that want to use ModelTypes as class attributes
+ to define data access.
+
+ All class attributes that are ModelTypes will be added to the 'fields' attribute on
+ the instance.
+
+ Additionally, any namespaces registered in the `xmodule.namespace` will be added to
+ the instance
+ """
def __new__(cls, name, bases, attrs):
- # Find registered methods
- reg_methods = {}
- for value in attrs.itervalues():
- for reg_type, names in getattr(value, "_method_registrations", {}).iteritems():
- for n in names:
- reg_methods[reg_type + n] = value
- attrs['registered_methods'] = reg_methods
-
- if attrs.get('has_children', False):
- attrs['children'] = ModelType(help='The children of this XModule', default=[], scope=None)
-
- @property
- def child_map(self):
- return dict((child.name, child) for child in self.children)
- attrs['child_map'] = child_map
-
fields = []
for n, v in attrs.items():
if isinstance(v, ModelType):
@@ -77,3 +89,61 @@ class ModelMetaclass(type):
attrs['fields'] = fields
return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs)
+
+
+class NamespacesMetaclass(type):
+ """
+ A metaclass to be used for classes that want to include namespaced fields in their
+ instances.
+
+ Any namespaces registered in the `xmodule.namespace` will be added to
+ the instance
+ """
+ def __new__(cls, name, bases, attrs):
+ for ns_name, namespace in Namespace.load_classes():
+ if issubclass(namespace, Namespace):
+ attrs[ns_name] = NamespaceDescriptor(namespace)
+
+ return super(NamespacesMetaclass, cls).__new__(cls, name, bases, attrs)
+
+
+class ParentModelMetaclass(type):
+ """
+ A ModelMetaclass that transforms the attribute `has_children = True`
+ into a List field with an empty scope.
+ """
+ def __new__(cls, name, bases, attrs):
+ if attrs.get('has_children', False):
+ attrs['children'] = List(help='The children of this XModule', default=[], scope=None)
+ else:
+ attrs['has_children'] = False
+
+ return super(ParentModelMetaclass, cls).__new__(cls, name, bases, attrs)
+
+
+class NamespaceDescriptor(object):
+ def __init__(self, namespace):
+ self._namespace = namespace
+
+ def __get__(self, instance, owner):
+ if owner is None:
+ return self
+ return self._namespace(instance)
+
+
+class Namespace(Plugin):
+ """
+ A baseclass that sets up machinery for ModelType fields that proxies the contained fields
+ requests for _model_data to self._container._model_data.
+ """
+ __metaclass__ = ModelMetaclass
+ __slots__ = ['container']
+
+ entry_point = 'xmodule.namespace'
+
+ def __init__(self, container):
+ self._container = container
+
+ @property
+ def _model_data(self):
+ return self._container._model_data
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index c65c031c9a..c0ba73423c 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -1,6 +1,5 @@
import pymongo
import sys
-import logging
from bson.son import SON
from fs.osfs import OSFS
@@ -9,8 +8,8 @@ from path import path
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
-from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
+from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor
from . import ModuleStoreBase, Location
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 7c6887696e..8463f945a8 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -192,7 +192,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor
if hasattr(descriptor, 'children'):
- for child in descriptor.children:
+ for child in descriptor.get_children():
parent_tracker.add_parent(child.location, descriptor.location)
return descriptor
@@ -318,8 +318,6 @@ class XMLModuleStore(ModuleStoreBase):
# Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog
-
-
def __unicode__(self):
'''
String representation - for debugging
@@ -345,8 +343,6 @@ class XMLModuleStore(ModuleStoreBase):
log.warning(msg + " " + str(err))
return {}
-
-
def load_course(self, course_dir, tracker):
"""
Load a course into this module store
@@ -363,7 +359,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read()))
- course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
+ course_data = etree.parse(course_file, parser=edx_xml_parser).getroot()
org = course_data.get('org')
diff --git a/common/lib/xmodule/xmodule/plugin.py b/common/lib/xmodule/xmodule/plugin.py
new file mode 100644
index 0000000000..5cf9c647aa
--- /dev/null
+++ b/common/lib/xmodule/xmodule/plugin.py
@@ -0,0 +1,64 @@
+import pkg_resources
+import logging
+
+log = logging.getLogger(__name__)
+
+class PluginNotFoundError(Exception):
+ pass
+
+
+class Plugin(object):
+ """
+ Base class for a system that uses entry_points to load plugins.
+
+ Implementing classes are expected to have the following attributes:
+
+ entry_point: The name of the entry point to load plugins from
+ """
+
+ _plugin_cache = None
+
+ @classmethod
+ def load_class(cls, identifier, default=None):
+ """
+ Loads a single class instance specified by identifier. If identifier
+ specifies more than a single class, then logs a warning and returns the
+ first class identified.
+
+ If default is not None, will return default if no entry_point matching
+ identifier is found. Otherwise, will raise a ModuleMissingError
+ """
+ if cls._plugin_cache is None:
+ cls._plugin_cache = {}
+
+ if identifier not in cls._plugin_cache:
+ identifier = identifier.lower()
+ classes = list(pkg_resources.iter_entry_points(
+ cls.entry_point, name=identifier))
+
+ if len(classes) > 1:
+ log.warning("Found multiple classes for {entry_point} with "
+ "identifier {id}: {classes}. "
+ "Returning the first one.".format(
+ entry_point=cls.entry_point,
+ id=identifier,
+ classes=", ".join(
+ class_.module_name for class_ in classes)))
+
+ if len(classes) == 0:
+ if default is not None:
+ return default
+ raise PluginNotFoundError(identifier)
+
+ cls._plugin_cache[identifier] = classes[0].load()
+ return cls._plugin_cache[identifier]
+
+ @classmethod
+ def load_classes(cls):
+ """
+ Returns a list of containing the identifiers and their corresponding classes for all
+ of the available instances of this plugin
+ """
+ return [(class_.name, class_.load())
+ for class_
+ in pkg_resources.iter_entry_points(cls.entry_point)]
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 155ad99480..6e80d1cf61 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -8,6 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
+from .model import Int, Scope
from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module")
@@ -16,6 +17,12 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
+def display_name(module):
+ if hasattr(module, 'display_name'):
+ return module.display_name
+
+ if hasattr(module, 'lms'):
+ return module.lms.display_name
class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence
@@ -26,22 +33,18 @@ class SequenceModule(XModule):
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence"
- def __init__(self, system, location, definition, descriptor, instance_state=None,
- shared_state=None, **kwargs):
- 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
+ has_children = True
- if instance_state is not None:
- state = json.loads(instance_state)
- if 'position' in state:
- self.position = int(state['position'])
+ # NOTE: Position is 1-indexed. This is silly, but there are now student
+ # positions saved on prod, so it's not easy to fix.
+ position = Int(help="Last tab viewed in this sequence", default=1, scope=Scope.student_state)
+
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
# if position is specified in system, then use that instead
- if system.get('position'):
- self.position = int(system.get('position'))
+ if self.system.get('position'):
+ self.position = int(self.system.get('position'))
self.rendered = False
@@ -79,9 +82,9 @@ class SequenceModule(XModule):
childinfo = {
'content': child.get_html(),
'title': "\n".join(
- grand_child.display_name.strip()
+ display_name(grand_child)
for grand_child in child.get_children()
- if 'display_name' in grand_child.metadata
+ if display_name(grand_child) is not None
),
'progress_status': Progress.to_js_status_str(progress),
'progress_detail': Progress.to_js_detail_str(progress),
@@ -89,7 +92,7 @@ class SequenceModule(XModule):
'id': child.id,
}
if childinfo['title']=='':
- childinfo['title'] = child.metadata.get('display_name','')
+ childinfo['title'] = display_name(child)
contents.append(childinfo)
params = {'items': contents,
@@ -116,7 +119,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
- stores_state = True # For remembering where in the sequence the student is
+ has_children = True
+ stores_state = True # For remembering where in the sequence the student is
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor"
diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py
index 397bd3e136..a4cd779285 100644
--- a/common/lib/xmodule/xmodule/vertical_module.py
+++ b/common/lib/xmodule/xmodule/vertical_module.py
@@ -11,8 +11,10 @@ class_priority = ['video', 'problem']
class VerticalModule(XModule):
''' Layout module for laying out submodules vertically.'''
- def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
+ has_children = True
+
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
self.contents = None
def get_html(self):
@@ -45,6 +47,8 @@ class VerticalModule(XModule):
class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule
+ has_children = True
+
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index 8a9e0aadd7..34ce353afd 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -9,6 +9,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
+from .model import Int, Scope, String
log = logging.getLogger(__name__)
@@ -27,22 +28,20 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
- def __init__(self, system, location, definition, descriptor,
- instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor,
- instance_state, shared_state, **kwargs)
- xmltree = etree.fromstring(self.definition['data'])
+ data = String(help="XML data for the problem", scope=Scope.content)
+ position = Int(help="Current position in the video", scope=Scope.student_state)
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
+
+ xmltree = etree.fromstring(self.data)
self.youtube = xmltree.get('youtube')
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
- if instance_state is not None:
- state = json.loads(instance_state)
- if 'position' in state:
- self.position = int(float(state['position']))
-
def _get_source(self, xmltree):
# find the first valid source
return self._get_first_external(xmltree, 'source')
@@ -102,7 +101,7 @@ class VideoModule(XModule):
else:
# VS[compat]
# cdodge: filesystem static content support.
- caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
+ caption_asset_path = "/static/{0}/subs/".format(self.descriptor.data_dir)
return self.system.render_template('video.html', {
'streams': self.video_list(),
@@ -111,8 +110,6 @@ class VideoModule(XModule):
'source': self.source,
'track' : self.track,
'display_name': self.display_name,
- # TODO (cpennington): This won't work when we move to data that isn't on the filesystem
- 'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions
})
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 9cf45652ca..9cad93ad5b 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -1,5 +1,4 @@
import logging
-import pkg_resources
import yaml
import os
import time
@@ -10,9 +9,9 @@ from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
-from .model import ModelMetaclass, String, Scope, ModuleScope, ModelType
-Date = ModelType
+from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass, ModelType
+from .plugin import Plugin
class Date(ModelType):
@@ -38,6 +37,10 @@ class Date(ModelType):
"""
return time.strftime(self.time_format, value)
+
+class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
+ pass
+
log = logging.getLogger('mitx.' + __name__)
@@ -45,67 +48,6 @@ def dummy_track(event_type, event):
pass
-class ModuleMissingError(Exception):
- pass
-
-
-class Plugin(object):
- """
- Base class for a system that uses entry_points to load plugins.
-
- Implementing classes are expected to have the following attributes:
-
- entry_point: The name of the entry point to load plugins from
- """
-
- _plugin_cache = None
-
- @classmethod
- def load_class(cls, identifier, default=None):
- """
- Loads a single class instance specified by identifier. If identifier
- specifies more than a single class, then logs a warning and returns the
- first class identified.
-
- If default is not None, will return default if no entry_point matching
- identifier is found. Otherwise, will raise a ModuleMissingError
- """
- if cls._plugin_cache is None:
- cls._plugin_cache = {}
-
- if identifier not in cls._plugin_cache:
- identifier = identifier.lower()
- classes = list(pkg_resources.iter_entry_points(
- cls.entry_point, name=identifier))
-
- if len(classes) > 1:
- log.warning("Found multiple classes for {entry_point} with "
- "identifier {id}: {classes}. "
- "Returning the first one.".format(
- entry_point=cls.entry_point,
- id=identifier,
- classes=", ".join(
- class_.module_name for class_ in classes)))
-
- if len(classes) == 0:
- if default is not None:
- return default
- raise ModuleMissingError(identifier)
-
- cls._plugin_cache[identifier] = classes[0].load()
- return cls._plugin_cache[identifier]
-
- @classmethod
- def load_classes(cls):
- """
- Returns a list of containing the identifiers and their corresponding classes for all
- of the available instances of this plugin
- """
- return [(class_.name, class_.load())
- for class_
- in pkg_resources.iter_entry_points(cls.entry_point)]
-
-
class HTMLSnippet(object):
"""
A base class defining an interface for an object that is able to present an
@@ -179,9 +121,7 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example.
'''
- __metaclass__ = ModelMetaclass
-
- display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
+ __metaclass__ = XModuleMetaclass
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
@@ -244,9 +184,22 @@ class XModule(HTMLSnippet):
self.url_name = self.location.name
self.category = self.location.category
self._model_data = model_data
+ self._loaded_children = None
- if self.display_name is None:
- self.display_name = self.url_name.replace('_', ' ')
+ def get_children(self):
+ '''
+ Return module instances for all the children of this module.
+ '''
+ if not self.has_children:
+ return []
+
+ if self._loaded_children is None:
+ children = [self.system.get_module(loc) for loc in self.children]
+ # get_module returns None if the current user doesn't have access
+ # to the location.
+ self._loaded_children = [c for c in children if c is not None]
+
+ return self._loaded_children
def __unicode__(self):
return ''.format(self.id)
@@ -257,7 +210,7 @@ class XModule(HTMLSnippet):
immediately inside this module.
'''
items = []
- for child in self.children():
+ for child in self.get_children():
items.extend(child.displayable_items())
return items
@@ -366,10 +319,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
entry_point = "xmodule.v1"
module_class = XModule
- __metaclass__ = ModelMetaclass
+ __metaclass__ = XModuleMetaclass
- display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
- start = Date(help="Start time when this module is visible", scope=Scope(student=False, module=ModuleScope.USAGE))
# Attributes for inspection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
@@ -430,8 +381,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
metadata: A dictionary containing the following optional keys:
goals: A list of strings of learning goals associated with this
module
- display_name: The name to use for displaying this module to the
- user
url_name: The name to use for this module in urls and other places
where a unique name is needed.
format: The format of this module ('Homework', 'Lab', etc)
@@ -452,12 +401,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._child_instances = None
self._inherited_metadata = set()
+ self._child_instances = None
+
+ def get_children(self):
+ """Returns a list of XModuleDescriptor instances for the children of
+ this module"""
+ if not self.has_children:
+ return []
+
+ if self._child_instances is None:
+ self._child_instances = []
+ for child_loc in self.children:
+ try:
+ child = self.system.load_item(child_loc)
+ except ItemNotFoundError:
+ log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
+ continue
+ # TODO (vshnayder): this should go away once we have
+ # proper inheritance support in mongo. The xml
+ # datastore does all inheritance on course load.
+ #child.inherit_metadata(self.metadata)
+ self._child_instances.append(child)
+
+ return self._child_instances
+
def get_child_by_url_name(self, url_name):
"""
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
"""
- for c in self.children:
+ for c in self.get_children():
if c.url_name == url_name:
return c
return None
@@ -472,7 +445,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system,
self.location,
self,
- system.xmodule_model_data(self.model_data),
+ system.xmodule_model_data(self._model_data),
)
def has_dynamic_children(self):
@@ -686,6 +659,7 @@ class ModuleSystem(object):
get_module,
render_template,
replace_urls,
+ xmodule_model_data,
user=None,
filestore=None,
debug=False,
@@ -739,6 +713,7 @@ class ModuleSystem(object):
self.node_path = node_path
self.anonymous_student_id = anonymous_student_id
self.user_is_staff = user is not None and user.is_staff
+ self.xmodule_model_data = xmodule_model_data
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
@@ -748,9 +723,6 @@ class ModuleSystem(object):
'''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val
- def xmodule_module_data(self, module_data):
- return module_data
-
def __repr__(self):
return repr(self.__dict__)
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 36932f9e42..1283844ade 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
format_scores = []
for section in sections:
section_descriptor = section['section_descriptor']
- section_name = section_descriptor.metadata.get('display_name')
+ section_name = section_descriptor.display_name
should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
@@ -276,14 +276,14 @@ def progress_summary(student, request, course, student_module_cache):
# Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden
- hidden = chapter_module.metadata.get('hide_from_toc','false')
+ hidden = chapter_module._model_data.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
sections = []
for section_module in chapter_module.get_display_items():
# Skip if the section is hidden
- hidden = section_module.metadata.get('hide_from_toc','false')
+ hidden = section_module._model_data.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 9c40767e99..57a125adba 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -88,8 +88,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list()
for chapter in course_module.get_display_items():
- hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
- if hide_from_toc:
+ if chapter.lms.hide_from_toc:
continue
sections = list()
@@ -97,18 +96,17 @@ def toc_for_course(user, request, course, active_chapter, active_section):
active = (chapter.url_name == active_chapter and
section.url_name == active_section)
- hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
- if not hide_from_toc:
- sections.append({'display_name': section.display_name,
+ if not section.lms.hide_from_toc:
+ sections.append({'display_name': section.lms.display_name,
'url_name': section.url_name,
- 'format': section.metadata.get('format', ''),
- 'due': section.metadata.get('due', ''),
+ 'format': section.lms.format,
+ 'due': section.lms.due,
'active': active,
- 'graded': section.metadata.get('graded', False),
+ 'graded': section.lms.graded,
})
- chapters.append({'display_name': chapter.display_name,
+ chapters.append({'display_name': chapter.lms.display_name,
'url_name': chapter.url_name,
'sections': sections,
'active': chapter.url_name == active_chapter})
@@ -146,7 +144,8 @@ def get_module(user, request, location, student_module_cache, course_id, positio
log.exception("Error in get_module")
return None
-def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display = True):
+
+def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display=True):
"""
Actually implement get_module. See docstring there for details.
"""
@@ -268,7 +267,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls(
_get_html,
- module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
+ getattr(module, 'data_dir', ''),
course_namespace = module.location._replace(category=None, name=None))
# Allow URLs of the form '/course/' refer to the root of multicourse directory
diff --git a/lms/setup.py b/lms/setup.py
new file mode 100644
index 0000000000..1755b7d37e
--- /dev/null
+++ b/lms/setup.py
@@ -0,0 +1,18 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="edX LMS",
+ version="0.1",
+ install_requires=['distribute'],
+ requires=[
+ 'xmodule',
+ ],
+
+ # See http://guide.python-distribute.org/creation.html#entry-points
+ # for a description of entry_points
+ entry_points={
+ 'xmodule.namespace': [
+ 'lms = lms.xmodule_namespace:LmsNamespace'
+ ],
+ }
+)
\ No newline at end of file
diff --git a/lms/templates/courseware/welcome-back.html b/lms/templates/courseware/welcome-back.html
index 5d4e0fe1e3..389a54aa53 100644
--- a/lms/templates/courseware/welcome-back.html
+++ b/lms/templates/courseware/welcome-back.html
@@ -1,3 +1,3 @@
-
${chapter_module.display_name}
+
${chapter_module.lms.display_name}
-
You were most recently in ${prev_section.display_name}. If you're done with that, choose another section on the left.
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
new file mode 100644
index 0000000000..3673de73df
--- /dev/null
+++ b/lms/xmodule_namespace.py
@@ -0,0 +1,23 @@
+from xmodule.model import Namespace, Boolean, Scope, String
+from xmodule.x_module import Date
+
+class LmsNamespace(Namespace):
+ hide_from_toc = Boolean(
+ help="Whether to display this module in the table of contents",
+ default=False,
+ scope=Scope.settings
+ )
+ graded = Boolean(
+ help="Whether this module contributes to the final course grade",
+ default=False,
+ scope=Scope.settings
+ )
+ format = String(
+ help="What format this module is in (used for deciding which "
+ "grader to apply, and what to show in the TOC)",
+ scope=Scope.settings
+ )
+
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+ start = Date(help="Start time when this module is visible", scope=Scope.settings)
+ due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
diff --git a/local-requirements.txt b/local-requirements.txt
index a4d153dd36..2e951a180c 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -1,3 +1,4 @@
# Python libraries to install that are local to the mitx repo
-e common/lib/capa
-e common/lib/xmodule
+-e lms
From c5e3380b711201f8e389d6b4653d37925a97d39d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 11 Dec 2012 13:36:23 -0500
Subject: [PATCH 012/285] WIP: Save student state via StudentModule.
Inheritance doesn't work
---
common/djangoapps/xmodule_modifiers.py | 17 +--
common/lib/capa/capa/capa_problem.py | 20 ++-
common/lib/xmodule/xmodule/capa_module.py | 86 ++++++-----
common/lib/xmodule/xmodule/model.py | 9 +-
common/lib/xmodule/xmodule/modulestore/xml.py | 2 +-
lms/djangoapps/courseware/models.py | 3 +
lms/djangoapps/courseware/module_render.py | 135 ++++++++++--------
lms/templates/staff_problem_info.html | 5 +-
lms/xmodule_namespace.py | 6 +-
9 files changed, 167 insertions(+), 116 deletions(-)
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 81f0205c3e..5a13582a2d 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -108,36 +108,33 @@ def add_histogram(get_html, module, user):
# TODO (ichuang): Remove after fall 2012 LMS migration done
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
- [filepath, filename] = module.definition.get('filename', ['', None])
+ [filepath, filename] = module.lms.filename
osfs = module.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
# doesn't like symlinks)
filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1]
- giturl = module.metadata.get('giturl','https://github.com/MITx')
- edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
+ edit_link = "%s/%s/tree/master/%s" % (module.lms.giturl, data_dir, filepath)
else:
edit_link = False
# Need to define all the variables that are about to be used
- giturl = ""
data_dir = ""
- source_file = module.metadata.get('source_file','') # source used to generate the problem XML, eg latex or word
+ source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime()
is_released = "unknown"
- mstart = getattr(module.descriptor,'start')
+ mstart = getattr(module.descriptor.lms,'start')
if mstart is not None:
is_released = "Yes!" if (now > mstart) else "Not yet"
- staff_context = {'definition': module.definition.get('data'),
- 'metadata': json.dumps(module.metadata, indent=4),
+ staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
'location': module.location,
- 'xqa_key': module.metadata.get('xqa_key',''),
+ 'xqa_key': module.lms.xqa_key,
'source_file' : source_file,
- 'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file),
+ 'source_url': '%s/%s/tree/master/%s' % (module.lms.giturl, data_dir, source_file),
'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-','_'),
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index eb39d8a2c6..85670063c5 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -83,7 +83,7 @@ class LoncapaProblem(object):
Main class for capa Problems.
'''
- def __init__(self, problem_text, id, correct_map=None, done=None, seed=None, system=None):
+ def __init__(self, problem_text, id, state=None, seed=None, system=None):
'''
Initializes capa Problem.
@@ -91,8 +91,7 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- - correct_map (dict): data specifying whether the student has completed the problem
- - done (bool): Whether the student has answered the problem
+ - state (dict): student state
- seed (int): random number generator seed (int)
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
@@ -103,12 +102,19 @@ class LoncapaProblem(object):
self.do_reset()
self.problem_id = id
self.system = system
+ if self.system is None:
+ raise Exception()
self.seed = seed
- self.done = done
- self.correct_map = CorrectMap()
- if correct_map is not None:
- self.correct_map.set_dict(correct_map)
+ if state:
+ if 'seed' in state:
+ self.seed = state['seed']
+ if 'student_answers' in state:
+ self.student_answers = state['student_answers']
+ if 'correct_map' in state:
+ self.correct_map.set_dict(state['correct_map'])
+ if 'done' in state:
+ self.done = state['done']
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index acb4b63e1c..7e8ad9210f 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -92,12 +92,14 @@ class CapaModule(XModule):
due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
- force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings)
+ force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = String(help="When to rerandomize the problem", default="always")
data = String(help="XML data for the problem", scope=Scope.content)
- correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state)
+ correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
+ student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
+ seed = Int(help="Random seed for this student", scope=Scope.student_state)
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
@@ -124,23 +126,23 @@ class CapaModule(XModule):
else:
self.close_date = self.due
- if self.rerandomize == 'never':
- self.seed = 1
- elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
- # TODO: This line is badly broken:
- # (1) We're passing student ID to xmodule.
- # (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
- # to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
- # - analytics really needs small number of bins.
- self.seed = system.id
- else:
- self.seed = None
+ if self.seed is None:
+ if self.rerandomize == 'never':
+ self.seed = 1
+ elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
+ # TODO: This line is badly broken:
+ # (1) We're passing student ID to xmodule.
+ # (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
+ # to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
+ # - analytics really needs small number of bins.
+ self.seed = system.id
+ else:
+ self.seed = None
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
- self.lcp = LoncapaProblem(self.data, self.location.html_id(),
- self.correct_map, self.done, self.seed, self.system)
+ self.lcp = self.new_lcp(self.get_state_for_lcp())
except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
@@ -157,9 +159,7 @@ class CapaModule(XModule):
problem_text = (''
'Problem %s has an error:%s' %
(self.location.url(), msg))
- self.lcp = LoncapaProblem(
- problem_text, self.location.html_id(),
- self.correct_map, self.done, self.seed, self.system)
+ self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text)
else:
# add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
@@ -169,10 +169,30 @@ class CapaModule(XModule):
elif self.rerandomize == "false":
self.rerandomize = "per_student"
- def sync_lcp_state(self):
+ def new_lcp(self, state, text=None):
+ if text is None:
+ text = self.data
+
+ return LoncapaProblem(
+ problem_text=text,
+ id=self.location.html_id(),
+ state=state,
+ system=self.system,
+ )
+
+ def get_state_for_lcp(self):
+ return {
+ 'done': self.done,
+ 'correct_map': self.correct_map,
+ 'student_answers': self.student_answers,
+ 'seed': self.seed,
+ }
+
+ def set_state_from_lcp(self):
lcp_state = self.lcp.get_state()
self.done = lcp_state['done']
self.correct_map = lcp_state['correct_map']
+ self.student_answers = lcp_state['student_answers']
self.seed = lcp_state['seed']
def get_score(self):
@@ -239,9 +259,8 @@ class CapaModule(XModule):
student_answers.pop(answer_id)
# Next, generate a fresh LoncapaProblem
- self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
- seed=self.seed, system=self.system)
- self.sync_lcp_state()
+ self.lcp = self.new_lcp(None)
+ self.set_state_from_lcp()
# Prepend a scary warning to the student
warning = '
'\
@@ -305,7 +324,7 @@ class CapaModule(XModule):
# We may not need a "save" button if infinite number of attempts and
# non-randomized. The problem author can force it. It's a bit weird for
# randomization to control this; should perhaps be cleaned up.
- if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
+ if (not self.force_save_button) and (self.max_attempts is None and self.rerandomize != "always"):
save_button = False
context = {'problem': content,
@@ -326,7 +345,7 @@ class CapaModule(XModule):
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
"
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
- return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
+ return self.system.replace_urls(html, self.descriptor.data_dir, course_namespace=self.location)
def handle_ajax(self, dispatch, get):
'''
@@ -408,7 +427,7 @@ class CapaModule(XModule):
queuekey = get['queuekey']
score_msg = get['xqueue_body']
self.lcp.update_score(score_msg, queuekey)
- self.sync_lcp_state()
+ self.set_state_from_lcp()
return dict() # No AJAX return is needed
@@ -425,14 +444,18 @@ class CapaModule(XModule):
raise NotFoundError('Answer is not available')
else:
answers = self.lcp.get_question_answers()
- self.sync_lcp_state()
+ self.set_state_from_lcp()
# answers (eg ) may have embedded images
# but be careful, some problems are using non-string answer dicts
new_answers = dict()
for answer_id in answers:
try:
+<<<<<<< HEAD
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
+=======
+ new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.descriptor.data_dir)}
+>>>>>>> WIP: Save student state via StudentModule. Inheritance doesn't work
except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]}
@@ -509,7 +532,7 @@ class CapaModule(XModule):
try:
correct_map = self.lcp.grade_answers(answers)
- self.sync_lcp_state()
+ self.set_state_from_lcp()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
@@ -609,13 +632,8 @@ class CapaModule(XModule):
# in next line)
self.lcp.seed = None
- self.lcp = LoncapaProblem(self.data,
- self.location.html_id(),
- self.lcp.correct_map,
- self.lcp.done,
- self.lcp.seed,
- self.system)
- self.sync_lcp_state()
+ self.set_state_from_lcp()
+ self.lcp = self.new_lcp()
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
index edd94f14a9..9286b0337e 100644
--- a/common/lib/xmodule/xmodule/model.py
+++ b/common/lib/xmodule/xmodule/model.py
@@ -10,8 +10,8 @@ class Scope(namedtuple('ScopeBase', 'student module')):
pass
Scope.content = Scope(student=False, module=ModuleScope.DEFINITION)
+Scope.settings = Scope(student=False, module=ModuleScope.USAGE)
Scope.student_state = Scope(student=True, module=ModuleScope.USAGE)
-Scope.settings = Scope(student=True, module=ModuleScope.USAGE)
Scope.student_preferences = Scope(student=True, module=ModuleScope.TYPE)
Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
@@ -54,7 +54,7 @@ class ModelType(object):
del instance._model_data[self.name]
def __repr__(self):
- return "<{0.__class__.__name} {0.__name__}>".format(self)
+ return "<{0.__class__.__name__} {0._name}>".format(self)
def __lt__(self, other):
return self._seq < other._seq
@@ -100,9 +100,12 @@ class NamespacesMetaclass(type):
the instance
"""
def __new__(cls, name, bases, attrs):
+ namespaces = []
for ns_name, namespace in Namespace.load_classes():
if issubclass(namespace, Namespace):
attrs[ns_name] = NamespaceDescriptor(namespace)
+ namespaces.append(ns_name)
+ attrs['namespaces'] = namespaces
return super(NamespacesMetaclass, cls).__new__(cls, name, bases, attrs)
@@ -114,7 +117,7 @@ class ParentModelMetaclass(type):
"""
def __new__(cls, name, bases, attrs):
if attrs.get('has_children', False):
- attrs['children'] = List(help='The children of this XModule', default=[], scope=None)
+ attrs['children'] = List(help='The children of this XModule', default=[], scope=Scope.settings)
else:
attrs['has_children'] = False
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 8463f945a8..349975ea77 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -175,7 +175,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# Normally, we don't want lots of exception traces in our logs from common
# content problems. But if you're debugging the xml loading code itself,
# uncomment the next line.
- # log.exception(msg)
+ log.exception(msg)
self.error_tracker(msg)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index ffc7c929de..2b7b12ac45 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -64,6 +64,9 @@ class StudentModule(models.Model):
return '/'.join([self.course_id, self.module_type,
self.student.username, self.module_state_key, str(self.state)[:20]])
+ def __repr__(self):
+ return 'StudentModule%r' % ((self.course_id, self.module_type, self.student, self.module_state_key, str(self.state)[:20]),)
+
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 57a125adba..f83ff35f1f 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -11,6 +11,8 @@ from django.http import Http404
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
+from collections import namedtuple
+
from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface
@@ -26,6 +28,8 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
+from xmodule.runtime import DbModel, KeyValueStore
+from xmodule.model import Scope
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -145,6 +149,70 @@ def get_module(user, request, location, student_module_cache, course_id, positio
return None
+class LmsKeyValueStore(KeyValueStore):
+ def __init__(self, course_id, user, descriptor_model_data, student_module_cache):
+ self._course_id = course_id
+ self._user = user
+ self._descriptor_model_data = descriptor_model_data
+ self._student_module_cache = student_module_cache
+
+ def _student_module(self, key):
+ student_module = self._student_module_cache.lookup(
+ self._course_id, key.module_scope_id.category, key.module_scope_id.url()
+ )
+ return student_module
+
+ def get(self, key):
+ if not key.scope.student:
+ return self._descriptor_model_data[key.field_name]
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+
+ if student_module is None:
+ raise KeyError(key.field_name)
+
+ return json.loads(student_module.state)[key.field_name]
+
+ def set(self, key, value):
+ if not key.scope.student:
+ self._descriptor_model_data[key.field_name] = value
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+ if student_module is None:
+ student_module = StudentModule(
+ course_id=self._course_id,
+ student=self._user,
+ module_type=key.module_scope_id.category,
+ module_state_key=key.module_scope_id,
+ state=json.dumps({})
+ )
+ self._student_module_cache.append(student_module)
+ state = json.loads(student_module.state)
+ state[key.field_name] = value
+ student_module.state = json.dumps(state)
+ student_module.save()
+
+ def delete(self, key):
+ if not key.scope.student:
+ del self._descriptor_model_data[key.field_name]
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+
+ if student_module is None:
+ raise KeyError(key.field_name)
+
+ state = json.loads(student_module.state)
+ del state[key.field_name]
+ student_module.state = json.dumps(state)
+ student_module.save()
+
+
+LmsUsage = namedtuple('LmsUsage', 'id, def_id')
+
+
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display=True):
"""
Actually implement get_module. See docstring there for details.
@@ -162,23 +230,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
h.update(str(user.id))
anonymous_student_id = h.hexdigest()
- # Only check the cache if this module can possibly have state
- instance_module = None
- shared_module = None
- if user.is_authenticated():
- if descriptor.stores_state:
- instance_module = student_module_cache.lookup(
- course_id, descriptor.category, descriptor.location.url())
-
- shared_state_key = getattr(descriptor, 'shared_state_key', None)
- if shared_state_key is not None:
- shared_module = student_module_cache.lookup(course_id,
- descriptor.category,
- shared_state_key)
-
- instance_state = instance_module.state if instance_module is not None else None
- shared_state = shared_module.state if shared_module is not None else None
-
# Setup system context for module instance
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=course_id,
@@ -218,6 +269,14 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
return get_module(user, request, location,
student_module_cache, course_id, position)
+ def xmodule_model_data(descriptor_model_data):
+ return DbModel(
+ LmsKeyValueStore(course_id, user, descriptor_model_data, student_module_cache),
+ descriptor.module_class,
+ user.id,
+ LmsUsage(location, location)
+ )
+
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from
@@ -235,6 +294,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
replace_urls=replace_urls,
node_path=settings.NODE_PATH,
anonymous_student_id=anonymous_student_id,
+ xmodule_model_data=xmodule_model_data
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)
@@ -453,19 +513,6 @@ def modx_dispatch(request, dispatch, location, course_id):
log.debug("No module {0} for user {1}--access denied?".format(location, user))
raise Http404
- instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
- shared_module = get_shared_instance_module(course_id, request.user, instance, student_module_cache)
-
- # Don't track state for anonymous users (who don't have student modules)
- if instance_module is not None:
- oldgrade = instance_module.grade
- # The max grade shouldn't change under normal circumstances, but
- # sometimes the problem changes with the same name but a new max grade.
- # This updates the module if that happens.
- old_instance_max_grade = instance_module.max_grade
- old_instance_state = instance_module.state
- old_shared_state = shared_module.state if shared_module is not None else None
-
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, p)
@@ -476,34 +523,6 @@ def modx_dispatch(request, dispatch, location, course_id):
log.exception("error processing ajax call")
raise
- # Save the state back to the database
- # Don't track state for anonymous users (who don't have student modules)
- if instance_module is not None:
- instance_module.state = instance.get_instance_state()
- instance_module.max_grade=instance.max_score()
- if instance.get_score():
- instance_module.grade = instance.get_score()['score']
- if (instance_module.grade != oldgrade or
- instance_module.state != old_instance_state or
- instance_module.max_grade != old_instance_max_grade):
- instance_module.save()
-
- #Bin score into range and increment stats
- score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
- org, course_num, run=course_id.split("/")
- statsd.increment("lms.courseware.question_answered",
- tags=["org:{0}".format(org),
- "course:{0}".format(course_num),
- "run:{0}".format(run),
- "score_bucket:{0}".format(score_bucket),
- "type:ajax"])
-
-
- if shared_module is not None:
- shared_module.state = instance.get_shared_state()
- if shared_module.state != old_shared_state:
- shared_module.save()
-
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html
index 21044c1f80..ad4852335f 100644
--- a/lms/templates/staff_problem_info.html
+++ b/lms/templates/staff_problem_info.html
@@ -46,8 +46,9 @@ github = ${edit_link | h}
%if source_file:
source_url = ${source_file | h}
%endif
-definition =
${definition | h}
-metadata = ${metadata | h}
+%for name, field in fields:
+${name} =
${field | h}
+%endfor
category = ${category | h}
%if render_histogram:
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 3673de73df..80b5fc6e61 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,4 +1,4 @@
-from xmodule.model import Namespace, Boolean, Scope, String
+from xmodule.model import Namespace, Boolean, Scope, String, List
from xmodule.x_module import Date
class LmsNamespace(Namespace):
@@ -21,3 +21,7 @@ class LmsNamespace(Namespace):
display_name = String(help="Display name for this module", scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
+ filename = List(help="DO NOT USE", scope=Scope.content, default=['', None])
+ source_file = String(help="DO NOT USE", scope=Scope.settings)
+ giturl = String(help="DO NOT USE", scope=Scope.settings, default='https://github.com/MITx')
+ xqa_key = String(help="DO NOT USE", scope=Scope.settings)
From 25754b50ff0c8d737e39b776822f89869867b9ab Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 12 Dec 2012 09:22:26 -0500
Subject: [PATCH 013/285] WIP. Trying to fix inheritance
---
common/lib/xmodule/xmodule/modulestore/xml.py | 27 +++++-
common/lib/xmodule/xmodule/runtime.py | 92 +++++++++++++++++++
common/lib/xmodule/xmodule/x_module.py | 5 -
3 files changed, 118 insertions(+), 6 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/runtime.py
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 349975ea77..677b2e938e 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -422,7 +422,32 @@ class XMLModuleStore(ModuleStoreBase):
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
- #XModuleDescriptor.compute_inherited_metadata(course_descriptor)
+ def compute_inherited_metadata(descriptor):
+ """Given a descriptor, traverse all of its descendants and do metadata
+ inheritance. Should be called on a CourseDescriptor after importing a
+ course.
+
+ NOTE: This means that there is no such thing as lazy loading at the
+ moment--this accesses all the children."""
+ for child in descriptor.get_children():
+ inherit_metadata(child, descriptor.metadata)
+ compute_inherited_metadata(child)
+
+ def inherit_metadata(descriptor, metadata):
+ """
+ Updates this module with metadata inherited from a containing module.
+ Only metadata specified in self.inheritable_metadata will
+ be inherited
+ """
+ # Set all inheritable metadata from kwargs that are
+ # in self.inheritable_metadata and aren't already set in metadata
+ for attr in self.inheritable_metadata:
+ if attr not in self.metadata and attr in metadata:
+ self._inherited_metadata.add(attr)
+ self.metadata[attr] = metadata[attr]
+
+
+ compute_inherited_metadata(course_descriptor)
# now import all pieces of course_info which is expected to be stored
# in /info or /info/
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
new file mode 100644
index 0000000000..8be4bfe346
--- /dev/null
+++ b/common/lib/xmodule/xmodule/runtime.py
@@ -0,0 +1,92 @@
+from collections import MutableMapping, namedtuple
+
+from .model import ModuleScope, ModelType
+
+
+class KeyValueStore(object):
+ """The abstract interface for Key Value Stores."""
+
+ # Keys are structured to retain information about the scope of the data.
+ # Stores can use this information however they like to store and retrieve
+ # data.
+ Key = namedtuple("Key", "scope, student_id, module_scope_id, field_name")
+
+ def get(key):
+ pass
+
+ def set(key, value):
+ pass
+
+ def delete(key):
+ pass
+
+
+class DbModel(MutableMapping):
+ """A dictionary-like interface to the fields on a module."""
+
+ def __init__(self, kvs, module_cls, student_id, usage):
+ self._kvs = kvs
+ self._student_id = student_id
+ self._module_cls = module_cls
+ self._usage = usage
+
+ def __repr__(self):
+ return "<{0.__class__.__name__} {0._module_cls!r}>".format(self)
+
+ def __str__(self):
+ return str(dict(self.iteritems()))
+
+ def _getfield(self, name):
+ if (not hasattr(self._module_cls, name) or
+ not isinstance(getattr(self._module_cls, name), ModelType)):
+
+ raise KeyError(name)
+
+ return getattr(self._module_cls, name)
+
+ def _key(self, name):
+ field = self._getfield(name)
+ module = field.scope.module
+
+ if module == ModuleScope.ALL:
+ module_id = None
+ elif module == ModuleScope.USAGE:
+ module_id = self._usage.id
+ elif module == ModuleScope.DEFINITION:
+ module_id = self._usage.def_id
+ elif module == ModuleScope.TYPE:
+ module_id = self.module_type.__name__
+
+ if field.scope.student:
+ student_id = self._student_id
+ else:
+ student_id = None
+
+ key = KeyValueStore.Key(
+ scope=field.scope,
+ student_id=student_id,
+ module_scope_id=module_id,
+ field_name=name
+ )
+ return key
+
+ def __getitem__(self, name):
+ return self._kvs.get(self._key(name))
+
+ def __setitem__(self, name, value):
+ self._kvs.set(self._key(name), value)
+
+ def __delitem__(self, name):
+ self._kvs.delete(self._key(name))
+
+ def __iter__(self):
+ return iter(self.keys())
+
+ def __len__(self):
+ return len(self.keys())
+
+ def keys(self):
+ fields = [field.name for field in self._module_cls.fields]
+ for namespace_name in self._module_cls.namespaces:
+ fields.extend(field.name for field in getattr(self._module_cls, namespace_name))
+ return fields
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 9cad93ad5b..a2393aa235 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -417,15 +417,10 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
except ItemNotFoundError:
log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
continue
- # TODO (vshnayder): this should go away once we have
- # proper inheritance support in mongo. The xml
- # datastore does all inheritance on course load.
- #child.inherit_metadata(self.metadata)
self._child_instances.append(child)
return self._child_instances
-
def get_child_by_url_name(self, url_name):
"""
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
From 45544396a89ebb235bd2c70582ea74caa28ef59d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 12 Dec 2012 12:47:31 -0500
Subject: [PATCH 014/285] Make computed defaults work, even in namespaces
---
common/lib/xmodule/xmodule/model.py | 45 ++++++++++++++++++++++++-----
lms/xmodule_namespace.py | 6 +++-
2 files changed, 43 insertions(+), 8 deletions(-)
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
index 9286b0337e..89fc9d56d1 100644
--- a/common/lib/xmodule/xmodule/model.py
+++ b/common/lib/xmodule/xmodule/model.py
@@ -26,11 +26,12 @@ class ModelType(object):
"""
sequence = 0
- def __init__(self, help=None, default=None, scope=Scope.content):
+ def __init__(self, help=None, default=None, scope=Scope.content, computed_default=None):
self._seq = self.sequence
self._name = "unknown"
self.help = help
self.default = default
+ self.computed_default = computed_default
self.scope = scope
ModelType.sequence += 1
@@ -43,6 +44,9 @@ class ModelType(object):
return self
if self.name not in instance._model_data:
+ if self.default is None and self.computed_default is not None:
+ return self.computed_default(instance)
+
return self.default
return self.from_json(instance._model_data[self.name])
@@ -136,17 +140,44 @@ class NamespaceDescriptor(object):
class Namespace(Plugin):
"""
- A baseclass that sets up machinery for ModelType fields that proxies the contained fields
- requests for _model_data to self._container._model_data.
+ A baseclass that sets up machinery for ModelType fields that makes those fields be called
+ with the container as the field instance
"""
__metaclass__ = ModelMetaclass
- __slots__ = ['container']
entry_point = 'xmodule.namespace'
def __init__(self, container):
self._container = container
- @property
- def _model_data(self):
- return self._container._model_data
+ def __getattribute__(self, name):
+ container = super(Namespace, self).__getattribute__('_container')
+ namespace_attr = getattr(type(self), name, None)
+
+ if namespace_attr is None or not isinstance(namespace_attr, ModelType):
+ return super(Namespace, self).__getattribute__(name)
+
+ return namespace_attr.__get__(container, type(container))
+
+ def __setattr__(self, name, value):
+ try:
+ container = super(Namespace, self).__getattribute__('_container')
+ except AttributeError:
+ super(Namespace, self).__setattr__(name, value)
+ return
+
+ container_class_attr = getattr(type(container), name, None)
+
+ if container_class_attr is None or not isinstance(container_class_attr, ModelType):
+ return super(Namespace, self).__setattr__(name, value)
+
+ return container_class_attr.__set__(container)
+
+ def __delattr__(self, name):
+ container = super(Namespace, self).__getattribute__('_container')
+ container_class_attr = getattr(type(container), name, None)
+
+ if container_class_attr is None or not isinstance(container_class_attr, ModelType):
+ return super(Namespace, self).__detattr__(name)
+
+ return container_class_attr.__delete__(container)
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 80b5fc6e61..7f30108b73 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -18,7 +18,11 @@ class LmsNamespace(Namespace):
scope=Scope.settings
)
- display_name = String(help="Display name for this module", scope=Scope.settings)
+ display_name = String(
+ help="Display name for this module",
+ scope=Scope.settings,
+ computed_default=lambda module: module.url_name.replace('_', ' ')
+ )
start = Date(help="Start time when this module is visible", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
filename = List(help="DO NOT USE", scope=Scope.content, default=['', None])
From 57b3ceba2709537bc2669460af5c00a25fab05d8 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 12 Dec 2012 12:47:50 -0500
Subject: [PATCH 015/285] Add a field type that treats a string as an int
---
common/lib/xmodule/xmodule/capa_module.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 7e8ad9210f..1f49435ca2 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -21,6 +21,16 @@ from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
+
+class StringyInt(Int):
+ """
+ A model type that converts from strings to integers when reading from json
+ """
+ def from_json(self, value):
+ if isinstance(value, basestring):
+ return int(value)
+ return value
+
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
@@ -88,7 +98,7 @@ class CapaModule(XModule):
icon_class = 'problem'
attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
- max_attempts = Int(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
+ max_attempts = StringyInt(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
From 7e224f58479d4fdb17edd76b20b3ec2a0d798571 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 12 Dec 2012 12:48:49 -0500
Subject: [PATCH 016/285] Convert a bunch more references from metadata to
fields
---
common/lib/xmodule/xmodule/capa_module.py | 5 +++
common/lib/xmodule/xmodule/course_module.py | 41 ++++---------------
.../lib/xmodule/xmodule/discussion_module.py | 11 +++++
common/lib/xmodule/xmodule/modulestore/xml.py | 15 ++++---
lms/djangoapps/courseware/access.py | 2 +-
lms/djangoapps/courseware/grades.py | 14 +++----
lms/djangoapps/courseware/tests/tests.py | 4 +-
lms/djangoapps/django_comment_client/utils.py | 6 +--
lms/xmodule_namespace.py | 3 +-
9 files changed, 47 insertions(+), 54 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 1f49435ca2..b8a85595dc 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -670,6 +670,11 @@ class CapaDescriptor(RawDescriptor):
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
+ # The capa format specifies that what we call max_attempts in the code
+ # is the attribute `attempts`. This will do that conversion
+ metadata_translations = dict(RawDescriptor.metadata_translations)
+ metadata_translations['attempts'] = 'max_attempts'
+
# VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index c4dc63c1b4..b2a1917912 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -33,6 +33,9 @@ class CourseDescriptor(SequenceDescriptor):
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
+ tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
+ end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
+ discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings, default=[])
has_children = True
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
@@ -128,7 +131,7 @@ class CourseDescriptor(SequenceDescriptor):
if self.start is None:
msg = "Course loaded without a valid start date. id = %s" % self.id
# hack it -- start in 1970
- self.metadata['start'] = stringify_time(time.gmtime(0))
+ self.lms.start = time.gmtime(0)
log.critical(msg)
system.error_tracker(msg)
@@ -198,8 +201,6 @@ class CourseDescriptor(SequenceDescriptor):
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
self._grading_policy = grading_policy
-
-
@classmethod
def read_grading_policy(cls, paths, system):
"""Load a grading policy from the specified paths, in order, if it exists."""
@@ -221,14 +222,13 @@ class CourseDescriptor(SequenceDescriptor):
return policy_str
-
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
instance = super(CourseDescriptor, cls).from_xml(xml_data, system, org, course)
# bleh, have to parse the XML here to just pull out the url_name attribute
course_file = StringIO(xml_data)
- xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot()
+ xml_obj = etree.parse(course_file, parser=edx_xml_parser).getroot()
policy_dir = None
url_name = xml_obj.get('url_name', xml_obj.get('slug'))
@@ -241,7 +241,7 @@ class CourseDescriptor(SequenceDescriptor):
paths = [policy_dir + '/grading_policy.json'] + paths
policy = json.loads(cls.read_grading_policy(paths, system))
-
+
# cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
instance.grading_policy = policy
@@ -250,7 +250,6 @@ class CourseDescriptor(SequenceDescriptor):
instance.set_grading_policy(policy)
return instance
-
@classmethod
def definition_from_xml(cls, xml_object, system):
@@ -334,17 +333,6 @@ class CourseDescriptor(SequenceDescriptor):
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
- @property
- def tabs(self):
- """
- Return the tabs config, as a python object, or None if not specified.
- """
- return self.metadata.get('tabs')
-
- @tabs.setter
- def tabs(self, value):
- self.metadata['tabs'] = value
-
@lazyproperty
def grading_context(self):
"""
@@ -383,14 +371,14 @@ class CourseDescriptor(SequenceDescriptor):
for c in self.get_children():
sections = []
for s in c.get_children():
- if s.metadata.get('graded', False):
+ if s.lms.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s))
xmoduledescriptors.append(s)
# The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
- section_format = s.metadata.get('format', "")
+ section_format = s.lms.format
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
all_descriptors.extend(xmoduledescriptors)
@@ -447,7 +435,7 @@ class CourseDescriptor(SequenceDescriptor):
try:
blackout_periods = [(parse_time(start), parse_time(end))
for start, end
- in self.metadata.get('discussion_blackouts', [])]
+ in self.discussion_blackouts]
now = time.gmtime()
for start, end in blackout_periods:
if start <= now <= end:
@@ -457,17 +445,6 @@ class CourseDescriptor(SequenceDescriptor):
return True
-
- @property
- def end_of_course_survey_url(self):
- """
- Pull from policy. Once we have our own survey module set up, can change this to point to an automatically
- created survey for each class.
-
- Returns None if no url specified.
- """
- return self.metadata.get('end_of_course_survey_url')
-
@property
def title(self):
return self.display_name
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index a193604278..19a2b4300f 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -12,6 +12,10 @@ class DiscussionModule(XModule):
}
js_module_name = "InlineDiscussion"
+ discussion_id = String(scope=Scope.settings)
+ discussion_category = String(scope=Scope.settings)
+ discussion_target = String(scope=Scope.settings)
+
data = String(help="XML definition of inline discussion", scope=Scope.content)
def get_html(self):
@@ -31,3 +35,10 @@ class DiscussionModule(XModule):
class DiscussionDescriptor(RawDescriptor):
module_class = DiscussionModule
template_dir_name = "discussion"
+
+ # The discussion XML format uses `id` and `for` attributes,
+ # but these would overload other module attributes, so we prefix them
+ # for actual use in the code
+ metadata_translations = dict(RawDescriptor.metadata_translations)
+ metadata_translations['id'] = 'discussion_id'
+ metadata_translations['for'] = 'discussion_target'
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 677b2e938e..ce6d9df093 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -430,10 +430,10 @@ class XMLModuleStore(ModuleStoreBase):
NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children."""
for child in descriptor.get_children():
- inherit_metadata(child, descriptor.metadata)
+ inherit_metadata(child, descriptor._model_data)
compute_inherited_metadata(child)
- def inherit_metadata(descriptor, metadata):
+ def inherit_metadata(descriptor, model_data):
"""
Updates this module with metadata inherited from a containing module.
Only metadata specified in self.inheritable_metadata will
@@ -441,11 +441,10 @@ class XMLModuleStore(ModuleStoreBase):
"""
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
- for attr in self.inheritable_metadata:
- if attr not in self.metadata and attr in metadata:
- self._inherited_metadata.add(attr)
- self.metadata[attr] = metadata[attr]
-
+ for attr in descriptor.inheritable_metadata:
+ if attr not in descriptor._model_data and attr in model_data:
+ descriptor._inherited_metadata.add(attr)
+ descriptor._model_data[attr] = model_data[attr]
compute_inherited_metadata(course_descriptor)
@@ -485,7 +484,7 @@ class XMLModuleStore(ModuleStoreBase):
if category == "static_tab":
for tab in course_descriptor.tabs or []:
if tab.get('url_slug') == slug:
- module.display_name = tab['name']
+ module.lms.display_name = tab['name']
module.data_dir = course_dir
self.modules[course_descriptor.id][module.location] = module
except Exception, e:
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index 0bd2311021..c6342e9d13 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -155,7 +155,7 @@ def _has_access_course_desc(user, course, action):
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
# if this feature is on, only allow courses that have ispublic set to be
# seen by non-staff
- if course.metadata.get('ispublic'):
+ if course.lms.ispublic:
debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic")
return True
return _has_staff_access_to_descriptor(user, course)
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 1283844ade..f6fd7bda88 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -179,12 +179,12 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
else:
correct = total
- graded = module_descriptor.metadata.get("graded", False)
+ graded = module_descriptor.lms.graded
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False
- scores.append(Score(correct, total, graded, module_descriptor.metadata.get('display_name')))
+ scores.append(Score(correct, total, graded, module_descriptor.lms.display_name))
section_total, graded_total = graders.aggregate_scores(scores, section_name)
if keep_raw_scores:
@@ -288,7 +288,7 @@ def progress_summary(student, request, course, student_module_cache):
continue
# Same for sections
- graded = section_module.metadata.get('graded', False)
+ graded = section_module.lms.graded
scores = []
module_creator = lambda descriptor : section_module.system.get_module(descriptor.location)
@@ -301,20 +301,20 @@ def progress_summary(student, request, course, student_module_cache):
continue
scores.append(Score(correct, total, graded,
- module_descriptor.metadata.get('display_name')))
+ module_descriptor.lms.display_name))
scores.reverse()
section_total, graded_total = graders.aggregate_scores(
- scores, section_module.metadata.get('display_name'))
+ scores, section_module.lms.display_name)
- format = section_module.metadata.get('format', "")
+ format = section_module.lms.format
sections.append({
'display_name': section_module.display_name,
'url_name': section_module.url_name,
'scores': scores,
'section_total': section_total,
'format': format,
- 'due': section_module.metadata.get("due", ""),
+ 'due': section_module.due,
'graded': graded,
})
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index 5af79d4983..7a6d498660 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -488,8 +488,8 @@ class TestViewAuth(PageLoader):
# Make courses start in the future
tomorrow = time.time() + 24*3600
- self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
- self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
+ self.toy.lms.start = time.gmtime(tomorrow)
+ self.full.lms.start = time.gmtime(tomorrow)
self.assertFalse(self.toy.has_started())
self.assertFalse(self.full.has_started())
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index b3a1626d22..174805a999 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -142,9 +142,9 @@ def initialize_discussion_info(course):
for location, module in all_modules.items():
if location.category == 'discussion':
- id = module.metadata['id']
- category = module.metadata['discussion_category']
- title = module.metadata['for']
+ id = module.discussion_id
+ category = module.discussion_category
+ title = module.discussion_target
sort_key = module.metadata.get('sort_key', title)
category = " / ".join([x.strip() for x in category.split("/")])
last_category = category.split("/")[-1]
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 7f30108b73..c78052ed1b 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -15,7 +15,7 @@ class LmsNamespace(Namespace):
format = String(
help="What format this module is in (used for deciding which "
"grader to apply, and what to show in the TOC)",
- scope=Scope.settings
+ scope=Scope.settings,
)
display_name = String(
@@ -29,3 +29,4 @@ class LmsNamespace(Namespace):
source_file = String(help="DO NOT USE", scope=Scope.settings)
giturl = String(help="DO NOT USE", scope=Scope.settings, default='https://github.com/MITx')
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
+ ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
From 01411ae66e9a596d19e43595a64d4192708de36d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 13 Dec 2012 11:06:10 -0500
Subject: [PATCH 017/285] WIP: Trying to get tests working
---
common/djangoapps/mitxmako/shortcuts.py | 2 +-
common/djangoapps/xmodule_modifiers.py | 1 +
common/lib/capa/capa/capa_problem.py | 2 +-
common/lib/capa/capa/customrender.py | 2 +-
common/lib/capa/capa/inputtypes.py | 2 +-
common/lib/capa/capa/responsetypes.py | 2 +-
common/lib/capa/capa/xqueue_interface.py | 2 +-
common/lib/xmodule/xmodule/capa_module.py | 3 +-
common/lib/xmodule/xmodule/course_module.py | 4 +-
common/lib/xmodule/xmodule/error_module.py | 31 ++++---
common/lib/xmodule/xmodule/fields.py | 30 +++++++
common/lib/xmodule/xmodule/model.py | 18 ++--
common/lib/xmodule/xmodule/modulestore/xml.py | 3 +-
.../xmodule/modulestore/xml_importer.py | 52 ++++++-----
common/lib/xmodule/xmodule/runtime.py | 30 +++++--
.../xmodule/xmodule/self_assessment_module.py | 88 ++++++-------------
common/lib/xmodule/xmodule/tests/__init__.py | 3 +-
.../lib/xmodule/xmodule/tests/test_import.py | 42 +++++----
.../lib/xmodule/xmodule/tests/test_model.py | 8 ++
.../lib/xmodule/xmodule/tests/test_runtime.py | 0
common/lib/xmodule/xmodule/x_module.py | 29 +-----
common/lib/xmodule/xmodule/xml_module.py | 2 +
lms/djangoapps/courseware/grades.py | 16 ++--
lms/djangoapps/courseware/module_render.py | 1 +
lms/djangoapps/instructor/views.py | 2 +-
lms/lib/comment_client/utils.py | 2 +-
lms/templates/staff_problem_info.html | 19 +++-
lms/xmodule_namespace.py | 14 ++-
local-requirements.txt | 2 +-
lms/setup.py => setup.py | 4 +-
30 files changed, 219 insertions(+), 197 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/fields.py
create mode 100644 common/lib/xmodule/xmodule/tests/test_model.py
create mode 100644 common/lib/xmodule/xmodule/tests/test_runtime.py
rename lms/setup.py => setup.py (85%)
diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py
index 181d3befd5..8f39efdc02 100644
--- a/common/djangoapps/mitxmako/shortcuts.py
+++ b/common/djangoapps/mitxmako/shortcuts.py
@@ -14,7 +14,7 @@
import logging
-log = logging.getLogger("mitx." + __name__)
+log = logging.getLogger(__name__)
from django.template import Context
from django.http import HttpResponse
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 5a13582a2d..30098c94fc 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -131,6 +131,7 @@ def add_histogram(get_html, module, user):
is_released = "Yes!" if (now > mstart) else "Not yet"
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
+ 'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'location': module.location,
'xqa_key': module.lms.xqa_key,
'source_file' : source_file,
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 85670063c5..d5ec6a1439 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -72,7 +72,7 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# main class for this module
diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py
index ef1044e8b1..0268d7b266 100644
--- a/common/lib/capa/capa/customrender.py
+++ b/common/lib/capa/capa/customrender.py
@@ -17,7 +17,7 @@ from lxml import etree
import xml.sax.saxutils as saxutils
from registry import TagRegistry
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
registry = TagRegistry()
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 0b2250f98d..8a15434d1d 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -44,7 +44,7 @@ import sys
from registry import TagRegistry
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#########################################################################
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 418ee9d8ae..2074e8f648 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -33,7 +33,7 @@ from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py
index 0214488cce..a7d31db7e1 100644
--- a/common/lib/capa/capa/xqueue_interface.py
+++ b/common/lib/capa/capa/xqueue_interface.py
@@ -7,7 +7,7 @@ import logging
import requests
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
dateformat = '%Y%m%d%H%M%S'
def make_hashkey(seed):
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index b8a85595dc..36da929926 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -551,8 +551,7 @@ class CapaModule(XModule):
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg}
- log.exception("Error in capa_module problem checking")
- raise Exception("error in capa_module")
+ raise
self.attempts = self.attempts + 1
self.lcp.done = True
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index b2a1917912..b8d4032a18 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -131,9 +131,9 @@ class CourseDescriptor(SequenceDescriptor):
if self.start is None:
msg = "Course loaded without a valid start date. id = %s" % self.id
# hack it -- start in 1970
- self.lms.start = time.gmtime(0)
+ self.start = time.gmtime(0)
log.critical(msg)
- system.error_tracker(msg)
+ self.system.error_tracker(msg)
# NOTE: relies on the modulestore to call set_grading_policy() right after
# init. (Modulestore is in charge of figuring out where to load the policy from)
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index 0f95bcd256..67b52d383c 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -8,6 +8,7 @@ from xmodule.x_module import XModule
from xmodule.editing_module import JSONEditingDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
+from .model import String, Scope
log = logging.getLogger(__name__)
@@ -52,6 +53,10 @@ class ErrorDescriptor(JSONEditingDescriptor):
"""
module_class = ErrorModule
+ contents = String(scope=Scope.content)
+ error_msg = String(scope=Scope.content)
+ display_name = String(scope=Scope.settings)
+
@classmethod
def _construct(self, system, contents, error_msg, location):
@@ -66,15 +71,12 @@ class ErrorDescriptor(JSONEditingDescriptor):
name=hashlib.sha1(contents).hexdigest()
)
- definition = {
- 'data': {
- 'error_msg': str(error_msg),
- 'contents': contents,
- }
- }
-
# real metadata stays in the content, but add a display name
- model_data = {'display_name': 'Error: ' + location.name}
+ model_data = {
+ 'error_msg': str(error_msg),
+ 'contents': contents,
+ 'display_name': 'Error: ' + location.name
+ }
return ErrorDescriptor(
system,
location,
@@ -84,7 +86,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def get_context(self):
return {
'module': self,
- 'data': self.definition['data']['contents'],
+ 'data': self.contents,
}
@classmethod
@@ -100,10 +102,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
- json.dumps({
- 'definition': descriptor.definition,
- 'metadata': descriptor.metadata,
- }, indent=4),
+ json.dumps(descriptor._model_data, indent=4),
error_msg,
location=descriptor.location,
)
@@ -147,14 +146,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
files, etc. That would just get re-wrapped on import.
'''
try:
- xml = etree.fromstring(self.definition['data']['contents'])
+ xml = etree.fromstring(self.contents)
return etree.tostring(xml)
except etree.XMLSyntaxError:
# still not valid.
root = etree.Element('error')
- root.text = self.definition['data']['contents']
+ root.text = self.contents
err_node = etree.SubElement(root, 'error_msg')
- err_node.text = self.definition['data']['error_msg']
+ err_node.text = self.error_msg
return etree.tostring(root)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
new file mode 100644
index 0000000000..715aea0c7c
--- /dev/null
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -0,0 +1,30 @@
+import time
+import logging
+
+from .model import ModelType
+
+log = logging.getLogger(__name__)
+
+
+class Date(ModelType):
+ time_format = "%Y-%m-%dT%H:%M"
+
+ def from_json(self, value):
+ """
+ Parse an optional metadata key containing a time: if present, complain
+ if it doesn't parse.
+ Return None if not present or invalid.
+ """
+ try:
+ return time.strptime(value, self.time_format)
+ except ValueError as e:
+ msg = "Field {0} has bad value '{1}': '{2}'".format(
+ self._name, value, e)
+ log.warning(msg)
+ return None
+
+ def to_json(self, value):
+ """
+ Convert a time struct to a string
+ """
+ return time.strftime(self.time_format, value)
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
index 89fc9d56d1..93ed12f69d 100644
--- a/common/lib/xmodule/xmodule/model.py
+++ b/common/lib/xmodule/xmodule/model.py
@@ -43,14 +43,14 @@ class ModelType(object):
if instance is None:
return self
- if self.name not in instance._model_data:
+ try:
+ return self.from_json(instance._model_data[self.name])
+ except KeyError:
if self.default is None and self.computed_default is not None:
return self.computed_default(instance)
return self.default
- return self.from_json(instance._model_data[self.name])
-
def __set__(self, instance, value):
instance._model_data[self.name] = self.to_json(value)
@@ -166,18 +166,18 @@ class Namespace(Plugin):
super(Namespace, self).__setattr__(name, value)
return
- container_class_attr = getattr(type(container), name, None)
+ namespace_attr = getattr(type(self), name, None)
- if container_class_attr is None or not isinstance(container_class_attr, ModelType):
+ if namespace_attr is None or not isinstance(namespace_attr, ModelType):
return super(Namespace, self).__setattr__(name, value)
- return container_class_attr.__set__(container)
+ return namespace_attr.__set__(container, value)
def __delattr__(self, name):
container = super(Namespace, self).__getattribute__('_container')
- container_class_attr = getattr(type(container), name, None)
+ namespace_attr = getattr(type(self), name, None)
- if container_class_attr is None or not isinstance(container_class_attr, ModelType):
+ if namespace_attr is None or not isinstance(namespace_attr, ModelType):
return super(Namespace, self).__detattr__(name)
- return container_class_attr.__delete__(container)
+ return namespace_attr.__delete__(container)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index ce6d9df093..72c9093bbf 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -28,7 +28,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
etree.set_default_parser(edx_xml_parser)
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
# VS[compat]
@@ -160,7 +160,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
etree.tostring(xml_data), self, self.org,
self.course, xmlstore.default_class)
except Exception as err:
- print err, self.load_error_modules
if not self.load_error_modules:
raise
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index 35375d7c51..f90291aaa2 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -8,6 +8,7 @@ from .xml import XMLModuleStore
from .exceptions import DuplicateItemError
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
+from xmodule.model import Scope
log = logging.getLogger(__name__)
@@ -123,7 +124,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# Quick scan to get course Location as well as the course_data_path
for module in module_store.modules[course_id].itervalues():
if module.category == 'course':
- course_data_path = path(data_dir) / module.metadata['data_dir']
+ course_data_path = path(data_dir) / module.data_dir
course_location = module.location
if static_content_store is not None:
@@ -159,18 +160,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
module.definition['children'] = new_locs
-
if module.category == 'course':
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
- module.metadata['hide_progress_tab'] = True
+ module.hide_progress_tab = True
- # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
- # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
- # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
+ # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
+ # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
+ # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
# if there is *any* tabs - then there at least needs to be some predefined ones
if module.tabs is None or len(module.tabs) == 0:
- module.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
+ module.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
@@ -180,39 +180,43 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_items.append(module)
- if 'data' in module.definition:
- module_data = module.definition['data']
-
+ if hasattr(module, 'data'):
# cdodge: now go through any link references to '/static/' and make sure we've imported
# it as a StaticContent asset
- try:
+ try:
remap_dict = {}
# use the rewrite_links as a utility means to enumerate through all links
# in the module data. We use that to load that reference into our asset store
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
# do the rewrites natively in that code.
- # For example, what I'm seeing is ->
+ # For example, what I'm seeing is ->
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
# no good, so we have to do this kludge
- if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
- lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
- static_content_store, link, remap_dict))
+ if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
+ lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path,
+ static_content_store, link, remap_dict))
for key in remap_dict.keys():
- module_data = module_data.replace(key, remap_dict[key])
+ module.data = module.data.replace(key, remap_dict[key])
- except Exception, e:
+ except Exception:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
- store.update_item(module.location, module_data)
+ store.update_item(module.location, module.data)
- if 'children' in module.definition:
- store.update_children(module.location, module.definition['children'])
+ if module.has_children:
+ store.update_children(module.location, module.children)
- # NOTE: It's important to use own_metadata here to avoid writing
- # inherited metadata everywhere.
- store.update_metadata(module.location, dict(module.own_metadata))
+ metadata = {}
+ for field in module.fields + module.lms.fields:
+ # Only save metadata that wasn't inherited
+ if (field.scope == Scope.settings and
+ field.name in module._inherited_metadata and
+ field.name in module._model_data):
+
+ metadata[field.name] = module._model_data[field.name]
+ store.update_metadata(module.location, metadata)
return module_store, course_items
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
index 8be4bfe346..7030f04f9f 100644
--- a/common/lib/xmodule/xmodule/runtime.py
+++ b/common/lib/xmodule/xmodule/runtime.py
@@ -33,19 +33,33 @@ class DbModel(MutableMapping):
def __repr__(self):
return "<{0.__class__.__name__} {0._module_cls!r}>".format(self)
- def __str__(self):
- return str(dict(self.iteritems()))
-
def _getfield(self, name):
- if (not hasattr(self._module_cls, name) or
- not isinstance(getattr(self._module_cls, name), ModelType)):
+ # First, get the field from the class, if defined
+ module_field = getattr(self._module_cls, name, None)
+ if module_field is not None and isinstance(module_field, ModelType):
+ return module_field
- raise KeyError(name)
+ # If the class doesn't have the field, and it also
+ # doesn't have any namespaces, then the the name isn't a field
+ # so KeyError
+ if not hasattr(self._module_cls, 'namespaces'):
+ return KeyError(name)
- return getattr(self._module_cls, name)
+ # Resolve the field name in the first namespace where it's
+ # available
+ for namespace_name in self._module_cls.namespaces:
+ namespace = getattr(self._module_cls, namespace_name)
+ namespace_field = getattr(type(namespace), name, None)
+ if namespace_field is not None and isinstance(module_field, ModelType):
+ return namespace_field
+
+ # Not in the class or in any of the namespaces, so name
+ # really doesn't name a field
+ raise KeyError(name)
def _key(self, name):
field = self._getfield(name)
+ print name, field
module = field.scope.module
if module == ModuleScope.ALL:
@@ -88,5 +102,5 @@ class DbModel(MutableMapping):
def keys(self):
fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces:
- fields.extend(field.name for field in getattr(self._module_cls, namespace_name))
+ fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
return fields
diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py
index 2edf5467b2..2f4674cf7c 100644
--- a/common/lib/xmodule/xmodule/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/self_assessment_module.py
@@ -25,6 +25,7 @@ from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
+from .model import List, String, Scope, Int
log = logging.getLogger("mitx.courseware")
@@ -61,67 +62,21 @@ class SelfAssessmentModule(XModule):
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
js_module_name = "SelfAssessment"
- def __init__(self, system, location, definition, descriptor,
- instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor,
- instance_state, shared_state, **kwargs)
+ student_answers = List(scope=Scope.student_state, default=[])
+ scores = List(scope=Scope.student_state, default=[])
+ hints = List(scope=Scope.student_state, default=[])
+ state = String(scope=Scope.student_state, default=INITIAL)
- """
- Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt,
- and two optional attributes:
- attempts, which should be an integer that defaults to 1.
- If it's > 1, the student will be able to re-submit after they see
- the rubric.
- max_score, which should be an integer that defaults to 1.
- It defines the maximum number of points a student can get. Assumed to be integer scale
- from 0 to max_score, with an interval of 1.
+ # Used for progress / grading. Currently get credit just for
+ # completion (doesn't matter if you self-assessed correct/incorrect).
+ max_score = Int(scope=Scope.settings, default=MAX_SCORE)
- Note: all the submissions are stored.
-
- Sample file:
-
-
-
- Insert prompt text here. (arbitrary html)
-
-
- Insert grading rubric here. (arbitrary html)
-
-
- Please enter a hint below: (arbitrary html)
-
-
- Thanks for submitting! (arbitrary html)
-
-
- """
-
- # Load instance state
- if instance_state is not None:
- instance_state = json.loads(instance_state)
- else:
- instance_state = {}
-
- # Note: score responses are on scale from 0 to max_score
- self.student_answers = instance_state.get('student_answers', [])
- self.scores = instance_state.get('scores', [])
- self.hints = instance_state.get('hints', [])
-
- self.state = instance_state.get('state', 'initial')
-
- # Used for progress / grading. Currently get credit just for
- # completion (doesn't matter if you self-assessed correct/incorrect).
-
- self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
-
- self.attempts = instance_state.get('attempts', 0)
-
- self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
-
- self.rubric = definition['rubric']
- self.prompt = definition['prompt']
- self.submit_message = definition['submitmessage']
- self.hint_prompt = definition['hintprompt']
+ attempts = Int(scope=Scope.student_state, default=0), Int
+ max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
+ rubric = String(scope=Scope.content)
+ prompt = String(scope=Scope.content)
+ submit_message = String(scope=Scope.content)
+ hint_prompt = String(scope=Scope.content)
def _allow_reset(self):
"""Can the module be reset?"""
@@ -432,6 +387,21 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
+ # The capa format specifies that what we call max_attempts in the code
+ # is the attribute `attempts`. This will do that conversion
+ metadata_translations = dict(XmlDescriptor.metadata_translations)
+ metadata_translations['attempts'] = 'max_attempts'
+
+ # Used for progress / grading. Currently get credit just for
+ # completion (doesn't matter if you self-assessed correct/incorrect).
+ max_score = Int(scope=Scope.settings, default=MAX_SCORE)
+
+ max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
+ rubric = String(scope=Scope.content)
+ prompt = String(scope=Scope.content)
+ submit_message = String(scope=Scope.content)
+ hint_prompt = String(scope=Scope.content)
+
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index ed64c45118..c16c6d7596 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -30,7 +30,8 @@ i4xs = ModuleSystem(
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
- anonymous_student_id = 'student'
+ anonymous_student_id = 'student',
+ xmodule_model_data = lambda x: x,
)
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 77532959d7..d243ca1609 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -91,8 +91,10 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(re_import_descriptor.__class__.__name__,
'ErrorDescriptor')
- self.assertEqual(descriptor.definition['data'],
- re_import_descriptor.definition['data'])
+ self.assertEqual(descriptor.contents,
+ re_import_descriptor.contents)
+ self.assertEqual(descriptor.error_msg,
+ re_import_descriptor.error_msg)
def test_fixed_xml_tag(self):
"""Make sure a tag that's been fixed exports as the original tag type"""
@@ -126,23 +128,19 @@ class ImportTestCase(unittest.TestCase):
url_name = 'test1'
start_xml = '''
+ due="{due}" url_name="{url_name}" unicorn="purple">
Two houses, ...
- '''.format(grace=v, org=ORG, course=COURSE, url_name=url_name)
+ '''.format(due=v, org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml)
- print descriptor, descriptor.metadata
- self.assertEqual(descriptor.metadata['graceperiod'], v)
- self.assertEqual(descriptor.metadata['unicorn'], 'purple')
+ print descriptor, descriptor._model_data
+ self.assertEqual(descriptor.lms.due, v)
- # Check that the child inherits graceperiod correctly
+ # Check that the child inherits due correctly
child = descriptor.get_children()[0]
- self.assertEqual(child.metadata['graceperiod'], v)
-
- # check that the child does _not_ inherit any unicorns
- self.assertTrue('unicorn' not in child.metadata)
+ self.assertEqual(child.lms.due, v)
# Now export and check things
resource_fs = MemoryFS()
@@ -169,12 +167,12 @@ class ImportTestCase(unittest.TestCase):
# did we successfully strip the url_name from the definition contents?
self.assertTrue('url_name' not in course_xml.attrib)
- # Does the chapter tag now have a graceperiod attribute?
+ # Does the chapter tag now have a due attribute?
# hardcoded path to child
with resource_fs.open('chapter/ch.xml') as f:
chapter_xml = etree.fromstring(f.read())
self.assertEqual(chapter_xml.tag, 'chapter')
- self.assertFalse('graceperiod' in chapter_xml.attrib)
+ self.assertFalse('due' in chapter_xml.attrib)
def test_is_pointer_tag(self):
"""
@@ -216,7 +214,7 @@ class ImportTestCase(unittest.TestCase):
def check_for_key(key, node):
"recursive check for presence of key"
print "Checking {0}".format(node.location.url())
- self.assertTrue(key in node.metadata)
+ self.assertTrue(key in node._model_data)
for c in node.get_children():
check_for_key(key, c)
@@ -244,15 +242,15 @@ class ImportTestCase(unittest.TestCase):
toy_ch = toy.get_children()[0]
two_toys_ch = two_toys.get_children()[0]
- self.assertEqual(toy_ch.display_name, "Overview")
- self.assertEqual(two_toys_ch.display_name, "Two Toy Overview")
+ self.assertEqual(toy_ch.lms.display_name, "Overview")
+ self.assertEqual(two_toys_ch.lms.display_name, "Two Toy Overview")
# Also check that the grading policy loaded
self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999)
# Also check that keys from policy are run through the
# appropriate attribute maps -- 'graded' should be True, not 'true'
- self.assertEqual(toy.metadata['graded'], True)
+ self.assertEqual(toy.lms.graded, True)
def test_definition_loading(self):
@@ -271,8 +269,8 @@ class ImportTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
- self.assertEqual(toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh8")
- self.assertEqual(two_toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh9")
+ self.assertEqual(toy_video.youtube, "1.0:p2Q6BrNhdh8")
+ self.assertEqual(two_toy_video.youtube, "1.0:p2Q6BrNhdh9")
def test_colon_in_url_name(self):
@@ -306,7 +304,7 @@ class ImportTestCase(unittest.TestCase):
cloc = course.location
loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab')
html = modulestore.get_instance(course_id, loc)
- self.assertEquals(html.display_name, "Toy lab")
+ self.assertEquals(html.lms.display_name, "Toy lab")
def test_url_name_mangling(self):
"""
@@ -351,4 +349,4 @@ class ImportTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
- self.assertEqual(sa_sample.metadata['attempts'], '10')
+ self.assertEqual(sa_sample.max_attempts, '10')
diff --git a/common/lib/xmodule/xmodule/tests/test_model.py b/common/lib/xmodule/xmodule/tests/test_model.py
new file mode 100644
index 0000000000..0e42df80a1
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_model.py
@@ -0,0 +1,8 @@
+
+class ModelMetaclassTester(object):
+ __metaclass__ = ModelMetaclass
+
+ field_a = Int(scope=Scope.settings)
+ field_b = Int(scope=Scope.content)
+
+def test_model_metaclass():
diff --git a/common/lib/xmodule/xmodule/tests/test_runtime.py b/common/lib/xmodule/xmodule/tests/test_runtime.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index a2393aa235..f03fbe6183 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -1,7 +1,6 @@
import logging
import yaml
import os
-import time
from lxml import etree
from pprint import pprint
@@ -10,38 +9,14 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
-from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass, ModelType
+from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass
from .plugin import Plugin
-class Date(ModelType):
- time_format = "%Y-%m-%dT%H:%M"
-
- def from_json(self, value):
- """
- Parse an optional metadata key containing a time: if present, complain
- if it doesn't parse.
- Return None if not present or invalid.
- """
- try:
- return time.strptime(value, self.time_format)
- except ValueError as e:
- msg = "Field {0} has bad value '{1}': '{2}'".format(
- self._name, value, e)
- log.warning(msg)
- return None
-
- def to_json(self, value):
- """
- Convert a time struct to a string
- """
- return time.strftime(self.time_format, value)
-
-
class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
pass
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
def dummy_track(event_type, event):
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 36bea6edb2..50c4f7aa6d 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -311,11 +311,13 @@ class XmlDescriptor(XModuleDescriptor):
# Set/override any metadata specified by policy
k = policy_key(location)
if k in system.policy:
+ if k == 'video/labintro': print k, metadata, system.policy[k]
cls.apply_policy(metadata, system.policy[k])
model_data = {}
model_data.update(metadata)
model_data.update(definition)
+ if k == 'video/labintro': print model_data
return cls(
system,
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index f6fd7bda88..032378f863 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
format_scores = []
for section in sections:
section_descriptor = section['section_descriptor']
- section_name = section_descriptor.display_name
+ section_name = section_descriptor.lms.display_name
should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
@@ -276,15 +276,13 @@ def progress_summary(student, request, course, student_module_cache):
# Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden
- hidden = chapter_module._model_data.get('hide_from_toc','false')
- if hidden.lower() == 'true':
+ if chapter_module.lms.hide_from_toc:
continue
sections = []
for section_module in chapter_module.get_display_items():
# Skip if the section is hidden
- hidden = section_module._model_data.get('hide_from_toc','false')
- if hidden.lower() == 'true':
+ if section_module.lms.hide_from_toc:
continue
# Same for sections
@@ -309,17 +307,17 @@ def progress_summary(student, request, course, student_module_cache):
format = section_module.lms.format
sections.append({
- 'display_name': section_module.display_name,
+ 'display_name': section_module.lms.display_name,
'url_name': section_module.url_name,
'scores': scores,
'section_total': section_total,
'format': format,
- 'due': section_module.due,
+ 'due': section_module.lms.due,
'graded': graded,
})
- chapters.append({'course': course.display_name,
- 'display_name': chapter_module.display_name,
+ chapters.append({'course': course.lms.display_name,
+ 'display_name': chapter_module.lms.display_name,
'url_name': chapter_module.url_name,
'sections': sections})
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index f83ff35f1f..be207730b3 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -92,6 +92,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list()
for chapter in course_module.get_display_items():
+ print chapter, chapter._model_data, chapter._model_data.get('hide_from_toc'), chapter.lms.hide_from_toc
if chapter.lms.hide_from_toc:
continue
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 0b6392a7fc..0e05ad5bc5 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -76,7 +76,7 @@ def instructor_dashboard(request, course_id):
data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]]
data += compute_course_stats(course).items()
if request.user.is_staff:
- data.append(['metadata', escape(str(course.metadata))])
+ data.append(['metadata', escape(str(course._model_data))])
datatable['data'] = data
def return_csv(fn, datatable):
diff --git a/lms/lib/comment_client/utils.py b/lms/lib/comment_client/utils.py
index f50797d5e0..9a7043565a 100644
--- a/lms/lib/comment_client/utils.py
+++ b/lms/lib/comment_client/utils.py
@@ -3,7 +3,7 @@ import logging
import requests
import settings
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
def strip_none(dic):
return dict([(k, v) for k, v in dic.iteritems() if v is not None])
diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html
index ad4852335f..e0c957a05c 100644
--- a/lms/templates/staff_problem_info.html
+++ b/lms/templates/staff_problem_info.html
@@ -46,9 +46,22 @@ github = ${edit_link | h}
%if source_file:
source_url = ${source_file | h}
%endif
-%for name, field in fields:
-${name} =
%if render_histogram:
@@ -75,18 +71,21 @@ category = ${category | h}
From 8991cdd3b5560a13680706d6d510f53d97819e74 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:12:39 -0500
Subject: [PATCH 021/285] Stop raising BaseExceptions
---
cms/djangoapps/contentstore/utils.py | 4 ++--
common/djangoapps/static_replace.py | 2 +-
common/lib/xmodule/xmodule/modulestore/mongo.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index da2993e463..d6dcc3811d 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -37,10 +37,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
- raise BaseException('Could not find course at {0}'.format(course_search_location))
+ raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
- raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
+ raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
location = courses[0].location
diff --git a/common/djangoapps/static_replace.py b/common/djangoapps/static_replace.py
index e75362d784..3bc8181126 100644
--- a/common/djangoapps/static_replace.py
+++ b/common/djangoapps/static_replace.py
@@ -49,7 +49,7 @@ def replace(static_url, prefix=None, course_namespace=None):
# use the utility functions in StaticContent.py
if static_url.group('prefix') == '/static/' and not isinstance(modulestore(), XMLModuleStore):
if course_namespace is None:
- raise BaseException('You must pass in course_namespace when remapping static content urls with MongoDB stores')
+ raise Exception('You must pass in course_namespace when remapping static content urls with MongoDB stores')
url = StaticContent.convert_legacy_static_url(static_url.group('rest'), course_namespace)
else:
url = try_staticfiles_lookup(prefix + static_url.group('rest'))
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index c0ba73423c..c40bde1072 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -314,10 +314,10 @@ class MongoModuleStore(ModuleStoreBase):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
- raise BaseException('Could not find course at {0}'.format(course_search_location))
+ raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
- raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
+ raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
return courses[0]
From d11a9b1d2104551baf115f19ce7b84da89319fd9 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:14:48 -0500
Subject: [PATCH 022/285] Convert ABTest module to new property based format.
Doesn't account for definition_to_xml needing redefinition
---
common/lib/xmodule/xmodule/abtest_module.py | 65 +++++++++++----------
1 file changed, 34 insertions(+), 31 deletions(-)
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 0f655ded6c..4ee74eb29c 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -1,4 +1,3 @@
-import json
import random
import logging
from lxml import etree
@@ -7,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
-from .model import String, Scope
+from .model import String, Scope, Object, ModuleScope
DEFAULT = "_DEFAULT_GROUP"
@@ -37,25 +36,34 @@ class ABTestModule(XModule):
Implements an A/B test with an aribtrary number of competing groups
"""
- def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
+ group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
+ group_assignments = Object(help="What group this user belongs to", scope=Scope(student=True, module=ModuleScope.TYPE), default={})
+ group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
- if shared_state is None:
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
+ if self.group is None:
self.group = group_from_value(
- self.definition['data']['group_portions'].items(),
+ self.group_portions,
random.uniform(0, 1)
)
- else:
- shared_state = json.loads(shared_state)
- self.group = shared_state['group']
- def get_shared_state(self):
- return json.dumps({'group': self.group})
-
+ @property
+ def group(self):
+ return self.group_assignments.get(self.experiment)
+
+ @group.setter
+ def group(self, value):
+ self.group_assigments[self.experiment] = value
+
+ @group.deleter
+ def group(self):
+ del self.group_assignments[self.experiment]
+
def get_children_locations(self):
- return self.definition['data']['group_content'][self.group]
-
+ return self.group_content[self.group]
+
def displayable_items(self):
# Most modules return "self" as the displayable_item. We never display ourself
# (which is why we don't implement get_html). We only display our children.
@@ -70,6 +78,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
template_dir_name = "abtest"
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
+ group_portions = Object(help="What proportions of students should go in each group", default={})
+ group_assignments = Object(help="What group this user belongs to", scope=Scope(student=True, module=ModuleScope.TYPE), default={})
+ group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
@classmethod
def definition_from_xml(cls, xml_object, system):
@@ -88,19 +99,12 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
"ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True)))
- definition = {
- 'data': {
- 'experiment': experiment,
- 'group_portions': {},
- 'group_content': {DEFAULT: []},
- },
- 'children': []}
for group in xml_object:
if group.tag == 'default':
name = DEFAULT
else:
name = group.get('name')
- definition['data']['group_portions'][name] = float(group.get('portion', 0))
+ self.group_portions[name] = float(group.get('portion', 0))
child_content_urls = []
for child in group:
@@ -110,8 +114,8 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
log.exception("Unable to load child when parsing ABTest. Continuing...")
continue
- definition['data']['group_content'][name] = child_content_urls
- definition['children'].extend(child_content_urls)
+ self.group_content[name] = child_content_urls
+ self.children.extend(child_content_urls)
default_portion = 1 - sum(
portion for (name, portion) in definition['data']['group_portions'].items())
@@ -119,20 +123,20 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
- definition['data']['group_portions'][DEFAULT] = default_portion
- definition['children'].sort()
+ self.group_portions[DEFAULT] = default_portion
+ self.children.sort()
return definition
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('abtest')
- xml_object.set('experiment', self.definition['data']['experiment'])
- for name, group in self.definition['data']['group_content'].items():
+ xml_object.set('experiment', self.experiment)
+ for name, group in self.group_content.items():
if name == DEFAULT:
group_elem = etree.SubElement(xml_object, 'default')
else:
group_elem = etree.SubElement(xml_object, 'group', attrib={
- 'portion': str(self.definition['data']['group_portions'][name]),
+ 'portion': str(self.group_portions[name]),
'name': name,
})
@@ -141,7 +145,6 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object
-
-
+
def has_dynamic_children(self):
return True
From 040abfcc6820b44e38393638a36d6d14f9cb1a4d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:18:27 -0500
Subject: [PATCH 023/285] Don't call update_metadata anymore when updating
course tabs, because the updates are implicit
---
common/lib/xmodule/xmodule/modulestore/mongo.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index c40bde1072..d145523d16 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -380,7 +380,6 @@ class MongoModuleStore(ModuleStoreBase):
tab['name'] = metadata.get('display_name')
break
course.tabs = existing_tabs
- self.update_metadata(course.location, course.metadata)
self._update_single_item(location, {'metadata': metadata})
From 8c42e0f52ed3b8c051c79504bac61c085a6ad100 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:18:53 -0500
Subject: [PATCH 024/285] Import course objects first, so that later updates
that try and edit the course object don't fail
---
.../xmodule/modulestore/xml_importer.py | 171 ++++++++++--------
1 file changed, 91 insertions(+), 80 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index f90291aaa2..1132fe77bb 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -89,6 +89,91 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
return link
+
+def import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None):
+ # remap module to the new namespace
+ if target_location_namespace is not None:
+ # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
+ # the caller passed in
+ if module.location.category != 'course':
+ module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
+ course=target_location_namespace.course)
+ else:
+ module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
+ course=target_location_namespace.course, name=target_location_namespace.name)
+
+ # then remap children pointers since they too will be re-namespaced
+ children_locs = module.definition.get('children')
+ if children_locs is not None:
+ new_locs = []
+ for child in children_locs:
+ child_loc = Location(child)
+ new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
+ course=target_location_namespace.course)
+
+ new_locs.append(new_child_loc.url())
+
+ module.definition['children'] = new_locs
+
+ if hasattr(module, 'data'):
+ # cdodge: now go through any link references to '/static/' and make sure we've imported
+ # it as a StaticContent asset
+ try:
+ remap_dict = {}
+
+ # use the rewrite_links as a utility means to enumerate through all links
+ # in the module data. We use that to load that reference into our asset store
+ # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
+ # do the rewrites natively in that code.
+ # For example, what I'm seeing is ->
+ # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
+ # no good, so we have to do this kludge
+ if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
+ lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path,
+ static_content_store, link, remap_dict))
+
+ for key in remap_dict.keys():
+ module.data = module.data.replace(key, remap_dict[key])
+
+ except Exception:
+ logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
+
+ modulestore.update_item(module.location, module.data)
+
+ if module.has_children:
+ modulestore.update_children(module.location, module.children)
+
+ metadata = {}
+ for field in module.fields + module.lms.fields:
+ # Only save metadata that wasn't inherited
+ if (field.scope == Scope.settings and
+ field.name in module._inherited_metadata and
+ field.name in module._model_data):
+
+ metadata[field.name] = module._model_data[field.name]
+ modulestore.update_metadata(module.location, metadata)
+
+
+def import_course_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None):
+ # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
+ module.hide_progress_tab = True
+
+ # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
+ # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
+ # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
+ # if there is *any* tabs - then there at least needs to be some predefined ones
+ if module.tabs is None or len(module.tabs) == 0:
+ module.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
+
+ # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
+ # so let's make sure we import in case there are no other references to it in the modules
+ verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
+ import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace)
+
+
def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True, static_content_store=None, target_location_namespace=None):
@@ -134,89 +219,15 @@ def import_from_xml(store, data_dir, course_dirs=None,
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath='static')
+ # Import course modules first, because importing some of the children requires the course to exist
for module in module_store.modules[course_id].itervalues():
-
- # remap module to the new namespace
- if target_location_namespace is not None:
- # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
- # the caller passed in
- if module.location.category != 'course':
- module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
- course=target_location_namespace.course)
- else:
- module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
- course=target_location_namespace.course, name=target_location_namespace.name)
-
- # then remap children pointers since they too will be re-namespaced
- children_locs = module.definition.get('children')
- if children_locs is not None:
- new_locs = []
- for child in children_locs:
- child_loc = Location(child)
- new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
- course=target_location_namespace.course)
-
- new_locs.append(new_child_loc.url())
-
- module.definition['children'] = new_locs
-
if module.category == 'course':
- # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
- module.hide_progress_tab = True
+ import_course_from_xml(store, static_content_store, course_data_path, module, target_location_namespace)
- # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
- # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
- # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
- # if there is *any* tabs - then there at least needs to be some predefined ones
- if module.tabs is None or len(module.tabs) == 0:
- module.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
-
- # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
- # so let's make sure we import in case there are no other references to it in the modules
- verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
-
- course_items.append(module)
-
- if hasattr(module, 'data'):
- # cdodge: now go through any link references to '/static/' and make sure we've imported
- # it as a StaticContent asset
- try:
- remap_dict = {}
-
- # use the rewrite_links as a utility means to enumerate through all links
- # in the module data. We use that to load that reference into our asset store
- # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
- # do the rewrites natively in that code.
- # For example, what I'm seeing is ->
- # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
- # no good, so we have to do this kludge
- if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
- lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path,
- static_content_store, link, remap_dict))
-
- for key in remap_dict.keys():
- module.data = module.data.replace(key, remap_dict[key])
-
- except Exception:
- logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
-
- store.update_item(module.location, module.data)
-
- if module.has_children:
- store.update_children(module.location, module.children)
-
- metadata = {}
- for field in module.fields + module.lms.fields:
- # Only save metadata that wasn't inherited
- if (field.scope == Scope.settings and
- field.name in module._inherited_metadata and
- field.name in module._model_data):
-
- metadata[field.name] = module._model_data[field.name]
- store.update_metadata(module.location, metadata)
+ # Import the rest of the modules
+ for module in module_store.modules[course_id].itervalues():
+ if module.category != 'course':
+ import_module_from_xml(store, static_content_store, course_data_path, module, target_location_namespace)
return module_store, course_items
From 5cb31c0e325054c294999c2fc920f7d57a97b8a9 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:19:18 -0500
Subject: [PATCH 025/285] Make load_from_json on descriptors pass the right
sort of arguments
---
common/lib/xmodule/xmodule/x_module.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index f03fbe6183..a38b8e5549 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -456,7 +456,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system: A DescriptorSystem for interacting with external resources
"""
- return cls(system=system, **json_data)
+ return cls(system=system, location=json_data['location'], model_data=json_data)
# ================================= XML PARSING ============================
@staticmethod
From 3adb1e71090adcdf1b64872198d4d2c4dd903602 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:19:38 -0500
Subject: [PATCH 026/285] Make grading not require get_instance_module
---
lms/djangoapps/courseware/grades.py | 39 ++++++++++++++++-------------
1 file changed, 21 insertions(+), 18 deletions(-)
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 032378f863..b81147f905 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -9,7 +9,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from models import StudentModuleCache
-from module_render import get_module, get_instance_module
+from module_render import get_module
from xmodule import graders
from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor
@@ -338,6 +338,9 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
Can return None if user doesn't have access, or if something else went wrong.
cache: A StudentModuleCache
"""
+ if not user.is_authenticated():
+ return (None, None)
+
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)
@@ -347,29 +350,29 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
instance_module = student_module_cache.lookup(
course_id, problem_descriptor.category, problem_descriptor.location.url())
- if not instance_module:
+ if instance_module:
+ if instance_module.max_grade is None:
+ return (None, None)
+
+ correct = instance_module.grade if instance_module.grade is not None else 0
+ total = instance_module.max_grade
+ else:
# If the problem was not in the cache, we need to instantiate the problem.
# Otherwise, the max score (cached in instance_module) won't be available
problem = module_creator(problem_descriptor)
if problem is None:
return (None, None)
- instance_module = get_instance_module(course_id, user, problem, student_module_cache)
- # If this problem is ungraded/ungradable, bail
- if not instance_module or instance_module.max_grade is None:
- return (None, None)
+ correct = 0
+ total = problem.max_score()
- correct = instance_module.grade if instance_module.grade is not None else 0
- total = instance_module.max_grade
-
- if correct is not None and total is not None:
- #Now we re-weight the problem, if specified
- weight = getattr(problem_descriptor, 'weight', None)
- if weight is not None:
- if total == 0:
- log.exception("Cannot reweight a problem with zero total points. Problem: " + str(instance_module))
- return (correct, total)
- correct = correct * weight / total
- total = weight
+ #Now we re-weight the problem, if specified
+ weight = getattr(problem_descriptor, 'weight', None)
+ if weight is not None:
+ if total == 0:
+ log.exception("Cannot reweight a problem with zero total points. Problem: " + str(instance_module))
+ return (correct, total)
+ correct = correct * weight / total
+ total = weight
return (correct, total)
From 306dbcff9c708f89572148f0b1d661d2193da68c Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:19:53 -0500
Subject: [PATCH 027/285] Rationalize StudentModule unicode and repr strings
---
lms/djangoapps/courseware/models.py | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 2b7b12ac45..9c318427ec 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -60,13 +60,17 @@ class StudentModule(models.Model):
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]])
-
def __repr__(self):
- return 'StudentModule%r' % ((self.course_id, self.module_type, self.student, self.module_state_key, str(self.state)[:20]),)
+ return 'StudentModule<%r>' % ({
+ 'course_id': self.course_id,
+ 'module_type': self.module_type,
+ 'student': self.student.username,
+ 'module_state_key': self.module_state_key,
+ 'state': str(self.state)[:20],
+ },)
+ def __unicode__(self):
+ return unicode(repr(self))
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
From 13cab34a7d8c384bc92fda9101a90d36ed260f2e Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:20:33 -0500
Subject: [PATCH 028/285] Always use the url form of the location when making
StudentModules
---
lms/djangoapps/courseware/module_render.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index be207730b3..c9e46c7ce0 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -186,7 +186,7 @@ class LmsKeyValueStore(KeyValueStore):
course_id=self._course_id,
student=self._user,
module_type=key.module_scope_id.category,
- module_state_key=key.module_scope_id,
+ module_state_key=key.module_scope_id.url(),
state=json.dumps({})
)
self._student_module_cache.append(student_module)
From 64848a32ee53f23adb27237e9aff7663df6e35e7 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:20:55 -0500
Subject: [PATCH 029/285] Delete get_instance_module and
get_shared_instance_module, as they are obsolete with the new properties
---
lms/djangoapps/courseware/module_render.py | 59 ----------------------
lms/djangoapps/courseware/views.py | 2 +-
2 files changed, 1 insertion(+), 60 deletions(-)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index c9e46c7ce0..ec9c6b54eb 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -341,65 +341,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
return module
-# TODO (vshnayder): Rename this? It's very confusing.
-def get_instance_module(course_id, user, module, student_module_cache):
- """
- Returns the StudentModule specific to this module for this student,
- or None if this is an anonymous user
- """
- if user.is_authenticated():
- if not module.descriptor.stores_state:
- log.exception("Attempted to get the instance_module for a module "
- + str(module.id) + " which does not store state.")
- return None
-
- instance_module = student_module_cache.lookup(
- course_id, module.category, module.location.url())
-
- if not instance_module:
- instance_module = StudentModule(
- course_id=course_id,
- student=user,
- module_type=module.category,
- module_state_key=module.id,
- state=module.get_instance_state(),
- max_grade=module.max_score())
- instance_module.save()
- student_module_cache.append(instance_module)
-
- return instance_module
- else:
- return None
-
-def get_shared_instance_module(course_id, user, module, student_module_cache):
- """
- Return shared_module is a StudentModule specific to all modules with the same
- 'shared_state_key' attribute, or None if the module does not elect to
- share state
- """
- if user.is_authenticated():
- # To get the shared_state_key, we need to descriptor
- descriptor = modulestore().get_instance(course_id, module.location)
-
- shared_state_key = getattr(module, 'shared_state_key', None)
- if shared_state_key is not None:
- shared_module = student_module_cache.lookup(module.category,
- shared_state_key)
- if not shared_module:
- shared_module = StudentModule(
- course_id=course_id,
- student=user,
- module_type=descriptor.category,
- module_state_key=shared_state_key,
- state=module.get_shared_state())
- shared_module.save()
- student_module_cache.append(shared_module)
- else:
- shared_module = None
-
- return shared_module
- else:
- return None
@csrf_exempt
def xqueue_callback(request, course_id, userid, id, dispatch):
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index fffbd2fc18..0c6e2245b9 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -24,7 +24,7 @@ from courseware.access import has_access
from courseware.courses import (get_course_with_access, get_courses_by_university)
import courseware.tabs as tabs
from courseware.models import StudentModuleCache
-from module_render import toc_for_course, get_module, get_instance_module
+from module_render import toc_for_course, get_module
from student.models import UserProfile
from multicourse import multicourse_settings
From fbd9499c5130d0bdf512cb812ec1c5be01f2b0ee Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:21:24 -0500
Subject: [PATCH 030/285] Make debug message use the available request.user
object
---
lms/djangoapps/courseware/module_render.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index ec9c6b54eb..0aac05568c 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -452,7 +452,7 @@ def modx_dispatch(request, dispatch, location, course_id):
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
- log.debug("No module {0} for user {1}--access denied?".format(location, user))
+ log.debug("No module {0} for user {1}--access denied?".format(location, request.user))
raise Http404
# Let the module handle the AJAX
From 2879853eee15fe8e600034b57af3f1b9d77a3ff9 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Mon, 17 Dec 2012 13:23:21 -0500
Subject: [PATCH 031/285] Pep8 fixes
---
lms/djangoapps/courseware/tests/tests.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index 7a6d498660..6b2988d1db 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -742,11 +742,11 @@ class TestCourseGrader(PageLoader):
"""
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
- modx_url = reverse('modx_dispatch',
+ modx_url = reverse('modx_dispatch',
kwargs={
- 'course_id' : self.graded_course.id,
- 'location' : problem_location,
- 'dispatch' : 'problem_check', }
+ 'course_id': self.graded_course.id,
+ 'location': problem_location,
+ 'dispatch': 'problem_check', }
)
resp = self.client.post(modx_url, {
From 8693d288c86d4813d731ec258cbbbb7d6cd7864c Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 09:32:36 -0500
Subject: [PATCH 032/285] Fix errors from running unit tests (some tests still
fail)
---
common/lib/xmodule/xmodule/abtest_module.py | 23 +++++---
common/lib/xmodule/xmodule/course_module.py | 4 +-
common/lib/xmodule/xmodule/html_module.py | 4 +-
common/lib/xmodule/xmodule/raw_module.py | 2 +-
.../xmodule/xmodule/self_assessment_module.py | 2 +-
common/lib/xmodule/xmodule/seq_module.py | 2 +-
common/lib/xmodule/xmodule/x_module.py | 54 ++++++++++++++++---
common/lib/xmodule/xmodule/xml_module.py | 9 ++--
lms/djangoapps/courseware/access.py | 4 +-
lms/djangoapps/courseware/grades.py | 2 +
lms/djangoapps/courseware/module_render.py | 2 +
11 files changed, 78 insertions(+), 30 deletions(-)
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 4ee74eb29c..9c639e8f30 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -79,7 +79,6 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
group_portions = Object(help="What proportions of students should go in each group", default={})
- group_assignments = Object(help="What group this user belongs to", scope=Scope(student=True, module=ModuleScope.TYPE), default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
@classmethod
@@ -99,12 +98,16 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
"ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True)))
+ group_portions = {}
+ group_content = {}
+ children = []
+
for group in xml_object:
if group.tag == 'default':
name = DEFAULT
else:
name = group.get('name')
- self.group_portions[name] = float(group.get('portion', 0))
+ group_portions[name] = float(group.get('portion', 0))
child_content_urls = []
for child in group:
@@ -114,19 +117,23 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
log.exception("Unable to load child when parsing ABTest. Continuing...")
continue
- self.group_content[name] = child_content_urls
- self.children.extend(child_content_urls)
+ group_content[name] = child_content_urls
+ children.extend(child_content_urls)
default_portion = 1 - sum(
- portion for (name, portion) in definition['data']['group_portions'].items())
+ portion for (name, portion) in group_portions.items()
+ )
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
- self.group_portions[DEFAULT] = default_portion
- self.children.sort()
+ group_portions[DEFAULT] = default_portion
+ children.sort()
- return definition
+ return {
+ 'group_portions': group_portions,
+ 'group_content': group_content,
+ }, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('abtest')
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 596344d934..510857303a 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -270,12 +270,12 @@ class CourseDescriptor(SequenceDescriptor):
wiki_slug = wiki_tag.attrib.get("slug", default=None)
xml_object.remove(wiki_tag)
- definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
+ definition, children = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
definition.setdefault('data', {})['textbooks'] = textbooks
definition['data']['wiki_slug'] = wiki_slug
- return definition
+ return definition, children
def has_ended(self):
"""
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 6d86fb90a8..562eeeb361 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -87,7 +87,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
if filename is None:
definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml)
- return {'data': stringify_children(definition_xml)}
+ return {'data': stringify_children(definition_xml)}, []
else:
# html is special. cls.filename_extension is 'xml', but
# if 'filename' is in the definition, that means to load
@@ -131,7 +131,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [ filepath, filename ]
- return definition
+ return definition, []
except (ResourceNotFoundError) as err:
msg = 'Unable to load file contents at path {0}: {1} '.format(
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index 5ff16098ac..bdbc049712 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
@classmethod
def definition_from_xml(cls, xml_object, system):
- return {'data': etree.tostring(xml_object, pretty_print=True)}
+ return {'data': etree.tostring(xml_object, pretty_print=True)}, []
def definition_to_xml(self, resource_fs):
try:
diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py
index 2f4674cf7c..ca6eae9913 100644
--- a/common/lib/xmodule/xmodule/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/self_assessment_module.py
@@ -428,7 +428,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
'prompt': parse('prompt'),
'submitmessage': parse('submitmessage'),
'hintprompt': parse('hintprompt'),
- }
+ }, []
def definition_to_xml(self, resource_fs):
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 6e80d1cf61..a136473653 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -136,7 +136,7 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
continue
- return {'children': children}
+ return {}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('sequential')
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index a38b8e5549..6c499aa611 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -165,11 +165,8 @@ class XModule(HTMLSnippet):
'''
Return module instances for all the children of this module.
'''
- if not self.has_children:
- return []
-
if self._loaded_children is None:
- children = [self.system.get_module(loc) for loc in self.children]
+ children = [self.system.get_module(loc) for loc in self.get_children_locations()]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
@@ -179,6 +176,24 @@ class XModule(HTMLSnippet):
def __unicode__(self):
return ''.format(self.id)
+ def get_children_locations(self):
+ '''
+ Returns the locations of each of child modules.
+
+ Overriding this changes the behavior of get_children and
+ anything that uses get_children, such as get_display_items.
+
+ This method will not instantiate the modules of the children
+ unless absolutely necessary, so it is cheaper to call than get_children
+
+ These children will be the same children returned by the
+ descriptor unless descriptor.has_dynamic_children() is true.
+ '''
+ if not self.has_children:
+ return []
+
+ return self.children
+
def get_display_items(self):
'''
Returns a list of descendent module instances that will display
@@ -437,7 +452,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
on the contents of json_data.
json_data must contain a 'location' element, and must be suitable to be
- passed into the subclasses `from_json` method.
+ passed into the subclasses `from_json` method as model_data
"""
class_ = XModuleDescriptor.load_class(
json_data['location']['category'],
@@ -451,12 +466,35 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses
- json_data: A json object specifying the definition and any optional
- keyword arguments for the XModuleDescriptor
+ json_data: A json object with the keys 'definition' and 'metadata',
+ definition: A json object with the keys 'data' and 'children'
+ data: A json value
+ children: A list of edX Location urls
+ metadata: A json object with any keys
+
+ This json_data is transformed to model_data using the following rules:
+ 1) The model data contains all of the fields from metadata
+ 2) The model data contains the 'children' array
+ 3) If 'definition.data' is a json object, model data contains all of its fields
+ Otherwise, it contains the single field 'data'
+ 4) Any value later in this list overrides a value earlier in this list
system: A DescriptorSystem for interacting with external resources
"""
- return cls(system=system, location=json_data['location'], model_data=json_data)
+ model_data = {}
+ model_data.update(json_data.get('metadata', {}))
+
+ definition = json_data.get('definition', {})
+ if 'children' in definition:
+ model_data['children'] = definition['children']
+
+ if 'data' in definition:
+ if isinstance(definition['data'], dict):
+ model_data.update(definition['data'])
+ else:
+ model_data['data'] = definition['data']
+
+ return cls(system=system, location=json_data['location'], model_data=model_data)
# ================================= XML PARSING ============================
@staticmethod
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 50c4f7aa6d..754d9b523e 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -220,7 +220,7 @@ class XmlDescriptor(XModuleDescriptor):
definition_metadata = get_metadata_from_xml(definition_xml)
cls.clean_metadata_from_xml(definition_xml)
- definition = cls.definition_from_xml(definition_xml, system)
+ definition, children = cls.definition_from_xml(definition_xml, system)
if definition_metadata:
definition['definition_metadata'] = definition_metadata
@@ -228,7 +228,7 @@ class XmlDescriptor(XModuleDescriptor):
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [ filepath, filename ]
- return definition
+ return definition, children
@classmethod
def load_metadata(cls, xml_object):
@@ -289,7 +289,7 @@ class XmlDescriptor(XModuleDescriptor):
else:
definition_xml = xml_object # this is just a pointer, not the real definition content
- definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
+ definition, children = cls.load_definition(definition_xml, system, location) # note this removes metadata
# VS[compat] -- make Ike's github preview links work in both old and
# new file layouts
if is_pointer_tag(xml_object):
@@ -311,13 +311,12 @@ class XmlDescriptor(XModuleDescriptor):
# Set/override any metadata specified by policy
k = policy_key(location)
if k in system.policy:
- if k == 'video/labintro': print k, metadata, system.policy[k]
cls.apply_policy(metadata, system.policy[k])
model_data = {}
model_data.update(metadata)
model_data.update(definition)
- if k == 'video/labintro': print model_data
+ model_data['children'] = children
return cls(
system,
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index c6342e9d13..4318bf81bf 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -226,9 +226,9 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
return True
# Check start date
- if descriptor.start is not None:
+ if descriptor.lms.start is not None:
now = time.gmtime()
- if now > descriptor.start:
+ if now > descriptor.lms.start:
# after start date, everyone can see it
debug("Allow: now > start date")
return True
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index b81147f905..1d894d3707 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -36,6 +36,8 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
def get_dynamic_descriptor_children(descriptor):
if descriptor.has_dynamic_children():
module = module_creator(descriptor)
+ if module is None:
+ print "FOO", descriptor
child_locations = module.get_children_locations()
return [descriptor.system.load_item(child_location) for child_location in child_locations ]
else:
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 0aac05568c..3fd1abec55 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -313,9 +313,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
import_system = descriptor.system
if has_access(user, location, 'staff', course_id):
err_descriptor = ErrorDescriptor.from_xml(str(descriptor), import_system,
+ org=descriptor.location.org, course=descriptor.location.course,
error_msg=exc_info_to_str(sys.exc_info()))
else:
err_descriptor = NonStaffErrorDescriptor.from_xml(str(descriptor), import_system,
+ org=descriptor.location.org, course=descriptor.location.course,
error_msg=exc_info_to_str(sys.exc_info()))
# Make an error module
From d61c91c139e58d45723862d15c5d5459bee46b11 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 09:54:25 -0500
Subject: [PATCH 033/285] Fix errors around error descriptors and custom tag
modules
---
common/lib/xmodule/xmodule/error_module.py | 8 ++++++--
common/lib/xmodule/xmodule/raw_module.py | 8 ++++++--
common/lib/xmodule/xmodule/template_module.py | 9 ++-------
lms/djangoapps/courseware/views.py | 11 +++--------
4 files changed, 17 insertions(+), 19 deletions(-)
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index 67b52d383c..37e98b5b77 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -22,6 +22,10 @@ log = logging.getLogger(__name__)
class ErrorModule(XModule):
+
+ contents = String(scope=Scope.content)
+ error_msg = String(scope=Scope.content)
+
def get_html(self):
'''Show an error to staff.
TODO (vshnayder): proper style, divs, etc.
@@ -29,8 +33,8 @@ class ErrorModule(XModule):
# staff get to see all the details
return self.system.render_template('module-error.html', {
'staff_access': True,
- 'data': self.definition['data']['contents'],
- 'error': self.definition['data']['error_msg'],
+ 'data': self.contents,
+ 'error': self.error_msg,
})
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index bdbc049712..5e50bdf6a0 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -3,25 +3,29 @@ from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor
import logging
import sys
+from .model import String, Scope
log = logging.getLogger(__name__)
+
class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
Module that provides a raw editing view of its data and children. It
requires that the definition xml is valid.
"""
+ data = String(help="XML data for the module", scope=Scope.content)
+
@classmethod
def definition_from_xml(cls, xml_object, system):
return {'data': etree.tostring(xml_object, pretty_print=True)}, []
def definition_to_xml(self, resource_fs):
try:
- return etree.fromstring(self.definition['data'])
+ return etree.fromstring(self.data)
except etree.XMLSyntaxError as err:
# Can't recover here, so just add some info and
# re-raise
- lines = self.definition['data'].split('\n')
+ lines = self.data.split('\n')
line, offset = err.position
msg = ("Unable to create xml for problem {loc}. "
"Context: '{context}'".format(
diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py
index f14254c011..988aaaf7b7 100644
--- a/common/lib/xmodule/xmodule/template_module.py
+++ b/common/lib/xmodule/xmodule/template_module.py
@@ -27,11 +27,6 @@ class CustomTagModule(XModule):
More information given in the text
"""
- def __init__(self, system, location, definition, descriptor,
- instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor,
- instance_state, shared_state, **kwargs)
-
def get_html(self):
return self.descriptor.rendered_html
@@ -62,14 +57,14 @@ class CustomTagDescriptor(RawDescriptor):
template_loc = self.location._replace(category='custom_tag_template', name=template_name)
template_module = modulestore().get_instance(system.course_id, template_loc)
- template_module_data = template_module.definition['data']
+ template_module_data = template_module.data
template = Template(template_module_data)
return template.render(**params)
@property
def rendered_html(self):
- return self.render_template(self.system, self.definition['data'])
+ return self.render_template(self.system, self.data)
def export_to_file(self):
"""
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 0c6e2245b9..7ed0685704 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -143,10 +143,9 @@ def redirect_to_course_position(course_module, first_time):
'chapter': chapter.url_name,
'section': section.url_name}))
-def save_child_position(seq_module, child_name, instance_module):
+def save_child_position(seq_module, child_name):
"""
child_name: url_name of the child
- instance_module: the StudentModule object for the seq_module
"""
for i, c in enumerate(seq_module.get_display_items()):
if c.url_name == child_name:
@@ -155,8 +154,6 @@ def save_child_position(seq_module, child_name, instance_module):
# Only save if position changed
if position != seq_module.position:
seq_module.position = position
- instance_module.state = seq_module.get_instance_state()
- instance_module.save()
@login_required
@ensure_csrf_cookie
@@ -222,8 +219,7 @@ def index(request, course_id, chapter=None, section=None,
chapter_descriptor = course.get_child_by_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)
+ save_child_position(course_module, chapter)
else:
raise Http404
@@ -250,8 +246,7 @@ def index(request, course_id, chapter=None, section=None,
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)
+ save_child_position(chapter_module, section)
context['content'] = section_module.get_html()
From 81e065bdb6224d67368581fd5bce3b35f154e228 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 10:14:42 -0500
Subject: [PATCH 034/285] Fix more errors in tests
---
common/lib/xmodule/xmodule/capa_module.py | 16 +++++++++++-
common/lib/xmodule/xmodule/course_module.py | 28 ++-------------------
common/lib/xmodule/xmodule/html_module.py | 2 ++
3 files changed, 19 insertions(+), 27 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 36da929926..4728c713af 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -355,7 +355,11 @@ class CapaModule(XModule):
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
"
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
- return self.system.replace_urls(html, self.descriptor.data_dir, course_namespace=self.location)
+ return self.system.replace_urls(
+ html,
+ getattr(self.descriptor, 'data_dir', ''),
+ course_namespace=self.location
+ )
def handle_ajax(self, dispatch, get):
'''
@@ -461,11 +465,21 @@ class CapaModule(XModule):
new_answers = dict()
for answer_id in answers:
try:
+<<<<<<< HEAD
<<<<<<< HEAD
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
=======
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.descriptor.data_dir)}
>>>>>>> WIP: Save student state via StudentModule. Inheritance doesn't work
+=======
+ new_answer = {
+ answer_id: self.system.replace_urls(
+ answers[answer_id],
+ getattr(self, 'data_dir', ''),
+ course_namespace=self.location
+ )
+ }
+>>>>>>> Fix more errors in tests
except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]}
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 510857303a..c11024e406 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -290,30 +290,6 @@ class CourseDescriptor(SequenceDescriptor):
def has_started(self):
return time.gmtime() > self.start
- @property
- def end(self):
- return self._try_parse_time("end")
- @end.setter
- def end(self, value):
- if isinstance(value, time.struct_time):
- self.metadata['end'] = stringify_time(value)
- @property
- def enrollment_start(self):
- return self._try_parse_time("enrollment_start")
-
- @enrollment_start.setter
- def enrollment_start(self, value):
- if isinstance(value, time.struct_time):
- self.metadata['enrollment_start'] = stringify_time(value)
- @property
- def enrollment_end(self):
- return self._try_parse_time("enrollment_end")
-
- @enrollment_end.setter
- def enrollment_end(self, value):
- if isinstance(value, time.struct_time):
- self.metadata['enrollment_end'] = stringify_time(value)
-
@property
def grader(self):
return self._grading_policy['GRADER']
@@ -326,7 +302,7 @@ class CourseDescriptor(SequenceDescriptor):
def raw_grader(self, value):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
self._grading_policy['RAW_GRADER'] = value
- self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value
+ self.grading_policy['GRADER'] = value
@property
def grade_cutoffs(self):
@@ -335,7 +311,7 @@ class CourseDescriptor(SequenceDescriptor):
@grade_cutoffs.setter
def grade_cutoffs(self, value):
self._grading_policy['GRADE_CUTOFFS'] = value
- self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
+ self.grading_policy['GRADE_CUTOFFS'] = value
@lazyproperty
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 562eeeb361..6ec1061451 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -43,6 +43,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
module_class = HtmlModule
filename_extension = "xml"
template_dir_name = "html"
+
+ data = String(help="Html contents to display for this module", scope=Scope.content)
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
From 7679fda17229d1f6f367ea88d19ed3d735fe46f2 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 11:35:08 -0500
Subject: [PATCH 035/285] Remove debugging print statements
---
common/lib/xmodule/xmodule/course_module.py | 1 -
lms/djangoapps/courseware/grades.py | 2 --
lms/djangoapps/courseware/module_render.py | 1 -
lms/xmodule_namespace.py | 1 -
4 files changed, 5 deletions(-)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index c11024e406..ad68b9a1b3 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -401,7 +401,6 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
- print self.advertised_start, self.start
return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 1d894d3707..b81147f905 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -36,8 +36,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
def get_dynamic_descriptor_children(descriptor):
if descriptor.has_dynamic_children():
module = module_creator(descriptor)
- if module is None:
- print "FOO", descriptor
child_locations = module.get_children_locations()
return [descriptor.system.load_item(child_location) for child_location in child_locations ]
else:
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 3fd1abec55..085432cbd9 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -92,7 +92,6 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list()
for chapter in course_module.get_display_items():
- print chapter, chapter._model_data, chapter._model_data.get('hide_from_toc'), chapter.lms.hide_from_toc
if chapter.lms.hide_from_toc:
continue
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 976bd4483f..3a72a64dff 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -4,7 +4,6 @@ from xmodule.fields import Date
class StringyBoolean(Boolean):
def from_json(self, value):
- print "StringyBoolean ", value
if isinstance(value, basestring):
return value.lower() == 'true'
return value
From f3032ba7cf835f479a5254a1f08784aa860cfeee Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 11:35:25 -0500
Subject: [PATCH 036/285] Make course textbooks and wiki slugs work
---
common/lib/xmodule/xmodule/course_module.py | 132 +++++++++++---------
1 file changed, 73 insertions(+), 59 deletions(-)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index ad68b9a1b3..947d75eb97 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -4,7 +4,7 @@ from path import path # NOTE (THK): Only used for detecting presence of syllabus
from xmodule.graders import grader_from_conf
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
-from xmodule.timeparse import parse_time, stringify_time
+from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty
import json
import logging
@@ -20,10 +20,79 @@ log = logging.getLogger(__name__)
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True)
+class Textbook(object):
+ def __init__(self, title, book_url):
+ self.title = title
+ self.book_url = book_url
+ self.start_page = int(self.table_of_contents[0].attrib['page'])
+
+ # The last page should be the last element in the table of contents,
+ # but it may be nested. So recurse all the way down the last element
+ last_el = self.table_of_contents[-1]
+ while last_el.getchildren():
+ last_el = last_el[-1]
+
+ self.end_page = int(last_el.attrib['page'])
+
+ @lazyproperty
+ def table_of_contents(self):
+ """
+ Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
+
+ Returns XML tree representation of the table of contents
+ """
+ toc_url = self.book_url + 'toc.xml'
+
+ # Get the table of contents from S3
+ log.info("Retrieving textbook table of contents from %s" % toc_url)
+ try:
+ r = requests.get(toc_url)
+ except Exception as err:
+ msg = 'Error %s: Unable to retrieve textbook table of contents at %s' % (err, toc_url)
+ log.error(msg)
+ raise Exception(msg)
+
+ # TOC is XML. Parse it
+ try:
+ table_of_contents = etree.fromstring(r.text)
+ except Exception as err:
+ msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url)
+ log.error(msg)
+ raise Exception(msg)
+
+ return table_of_contents
+
+
+class TextbookList(ModelType):
+ def from_json(self, values):
+ textbooks = []
+ for title, book_url in values:
+ try:
+ textbooks.append(Textbook(title, book_url))
+ except:
+ # If we can't get to S3 (e.g. on a train with no internet), don't break
+ # the rest of the courseware.
+ log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url))
+ continue
+
+ return textbooks
+
+ def to_json(self, values):
+ json_data = []
+ for val in values:
+ if isinstance(val, Textbook):
+ json_data.append((textbook.title, textbook.book_url))
+ elif isinstance(val, tuple):
+ json_data.append(val)
+ else:
+ continue
+ return json_data
+
+
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
- textbooks = List(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content)
+ textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content)
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
@@ -70,65 +139,10 @@ class CourseDescriptor(SequenceDescriptor):
template_dir_name = 'course'
- class Textbook:
- def __init__(self, title, book_url):
- self.title = title
- self.book_url = book_url
- self.table_of_contents = self._get_toc_from_s3()
- self.start_page = int(self.table_of_contents[0].attrib['page'])
-
- # The last page should be the last element in the table of contents,
- # but it may be nested. So recurse all the way down the last element
- last_el = self.table_of_contents[-1]
- while last_el.getchildren():
- last_el = last_el[-1]
-
- self.end_page = int(last_el.attrib['page'])
-
- @property
- def table_of_contents(self):
- return self.table_of_contents
-
- def _get_toc_from_s3(self):
- """
- Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
-
- Returns XML tree representation of the table of contents
- """
- toc_url = self.book_url + 'toc.xml'
-
- # Get the table of contents from S3
- log.info("Retrieving textbook table of contents from %s" % toc_url)
- try:
- r = requests.get(toc_url)
- except Exception as err:
- msg = 'Error %s: Unable to retrieve textbook table of contents at %s' % (err, toc_url)
- log.error(msg)
- raise Exception(msg)
-
- # TOC is XML. Parse it
- try:
- table_of_contents = etree.fromstring(r.text)
- except Exception as err:
- msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url)
- log.error(msg)
- raise Exception(msg)
-
- return table_of_contents
def __init__(self, *args, **kwargs):
super(CourseDescriptor, self).__init__(*args, **kwargs)
- self.textbooks = []
- for title, book_url in self.textbooks:
- try:
- self.textbooks.append(self.Textbook(title, book_url))
- except:
- # If we can't get to S3 (e.g. on a train with no internet), don't break
- # the rest of the courseware.
- log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url))
- continue
-
if self.wiki_slug is None:
self.wiki_slug = self.location.course
@@ -272,8 +286,8 @@ class CourseDescriptor(SequenceDescriptor):
definition, children = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
- definition.setdefault('data', {})['textbooks'] = textbooks
- definition['data']['wiki_slug'] = wiki_slug
+ definition['textbooks'] = textbooks
+ definition['wiki_slug'] = wiki_slug
return definition, children
From 9d822fc359d068509ecdcfcc91326a3aab7ac887 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 11:35:55 -0500
Subject: [PATCH 037/285] Make sequences record whether they have been visited
or not (done somewhat implicitly now, would be nice to be more explicit)
---
common/lib/xmodule/xmodule/seq_module.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index a136473653..3fc3a5dbaa 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -37,7 +37,7 @@ class SequenceModule(XModule):
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
- position = Int(help="Last tab viewed in this sequence", default=1, scope=Scope.student_state)
+ position = Int(help="Last tab viewed in this sequence", scope=Scope.student_state)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
@@ -46,6 +46,13 @@ class SequenceModule(XModule):
if self.system.get('position'):
self.position = int(self.system.get('position'))
+ # Default to the first child
+ # Don't set 1 as the default in the property definition, because
+ # there is code that looks for the existance of the position value
+ # to determine if the student has visited the sequence before or not
+ if self.position is None:
+ self.position = 1
+
self.rendered = False
def get_instance_state(self):
From e1ca413b6f80dbb34e98553b90e43c7b4917bef9 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 18 Dec 2012 13:28:03 -0500
Subject: [PATCH 038/285] Fix import of metadata
---
common/lib/xmodule/xmodule/modulestore/xml_importer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index 1132fe77bb..bbbb60d8ed 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -147,7 +147,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
for field in module.fields + module.lms.fields:
# Only save metadata that wasn't inherited
if (field.scope == Scope.settings and
- field.name in module._inherited_metadata and
+ field.name not in module._inherited_metadata and
field.name in module._model_data):
metadata[field.name] = module._model_data[field.name]
From 7cb95aad47b3eeac72c9aaa725a0387cd464de70 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 19 Dec 2012 10:11:39 -0500
Subject: [PATCH 039/285] WIP: Add grade publishing functionality
---
common/lib/xmodule/xmodule/capa_module.py | 19 +++++++++++++++++-
common/lib/xmodule/xmodule/x_module.py | 7 +++++++
lms/djangoapps/courseware/module_render.py | 23 +++++++++++++++++++++-
3 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 4728c713af..8c2297af1b 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -103,7 +103,7 @@ class CapaModule(XModule):
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
- rerandomize = String(help="When to rerandomize the problem", default="always")
+ rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
@@ -442,6 +442,7 @@ class CapaModule(XModule):
score_msg = get['xqueue_body']
self.lcp.update_score(score_msg, queuekey)
self.set_state_from_lcp()
+ self.publish_grade()
return dict() # No AJAX return is needed
@@ -519,6 +520,19 @@ class CapaModule(XModule):
return answers
+ def publish_grade(self):
+ """
+ Publishes the student's current grade to the system as an event
+ """
+ score = self.lcp.get_score()
+ print score
+ self.system.publish({
+ 'event_name': 'grade',
+ 'value': score['score'],
+ 'max_value': score['total'],
+ })
+
+
def check_problem(self, get):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
@@ -570,6 +584,9 @@ class CapaModule(XModule):
self.attempts = self.attempts + 1
self.lcp.done = True
+ self.set_state_from_lcp()
+ self.publish_grade()
+
# success = correct if ALL questions in this problem are correct
success = 'correct'
for answer_id in correct_map:
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 6c499aa611..a05a191e5f 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -672,6 +672,7 @@ class ModuleSystem(object):
filestore=None,
debug=False,
xqueue=None,
+ publish=None,
node_path="",
anonymous_student_id=''):
'''
@@ -723,6 +724,12 @@ class ModuleSystem(object):
self.user_is_staff = user is not None and user.is_staff
self.xmodule_model_data = xmodule_model_data
+ if publish is None:
+ publish = lambda e: None
+
+ self.publish = publish
+
+
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
return self.__dict__.get(attr)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 085432cbd9..839ee80591 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -277,6 +277,26 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
LmsUsage(location, location)
)
+ def publish(event):
+ if event.get('event_name') != 'grade':
+ return
+
+ student_module = student_module_cache.lookup(
+ course_id, descriptor.location.category, descriptor.location.url()
+ )
+ if student_module is None:
+ student_module = StudentModule(
+ course_id=course_id,
+ student=user,
+ module_type=descriptor.location.category,
+ module_state_key=descriptor.location.url(),
+ state=json.dumps({})
+ )
+ student_module_cache.append(student_module)
+ student_module.grade = event.get('value')
+ student_module.max_grade = event.get('max_value')
+ student_module.save()
+
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from
@@ -294,7 +314,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
replace_urls=replace_urls,
node_path=settings.NODE_PATH,
anonymous_student_id=anonymous_student_id,
- xmodule_model_data=xmodule_model_data
+ xmodule_model_data=xmodule_model_data,
+ publish=publish,
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)
From 6e933ae756bf40b21a4568576e11f1271001287f Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 19 Dec 2012 10:11:53 -0500
Subject: [PATCH 040/285] Fix typo
---
common/lib/xmodule/xmodule/abtest_module.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 9c639e8f30..e805d5efa6 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -55,7 +55,7 @@ class ABTestModule(XModule):
@group.setter
def group(self, value):
- self.group_assigments[self.experiment] = value
+ self.group_assignments[self.experiment] = value
@group.deleter
def group(self):
From 9c5a922eee46ddc9879b7e556baf26ee518b1f32 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 20 Dec 2012 16:53:29 -0500
Subject: [PATCH 041/285] Create tables for all known scopes, and add tests of
the LmsKeyValueStore
---
.../migrations/0005_add_xmodule_storage.py | 185 +++++++++
.../migrations/0006_add_field_default.py | 128 +++++++
lms/djangoapps/courseware/model_data.py | 145 +++++++
lms/djangoapps/courseware/models.py | 126 ++++++
lms/djangoapps/courseware/module_render.py | 70 +---
.../courseware/tests/test_model_data.py | 360 ++++++++++++++++++
6 files changed, 946 insertions(+), 68 deletions(-)
create mode 100644 lms/djangoapps/courseware/migrations/0005_add_xmodule_storage.py
create mode 100644 lms/djangoapps/courseware/migrations/0006_add_field_default.py
create mode 100644 lms/djangoapps/courseware/model_data.py
create mode 100644 lms/djangoapps/courseware/tests/test_model_data.py
diff --git a/lms/djangoapps/courseware/migrations/0005_add_xmodule_storage.py b/lms/djangoapps/courseware/migrations/0005_add_xmodule_storage.py
new file mode 100644
index 0000000000..0d89471fff
--- /dev/null
+++ b/lms/djangoapps/courseware/migrations/0005_add_xmodule_storage.py
@@ -0,0 +1,185 @@
+# -*- 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 'XModuleStudentInfoField'
+ db.create_table('courseware_xmodulestudentinfofield', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('field_name', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('value', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('courseware', ['XModuleStudentInfoField'])
+
+ # Adding unique constraint on 'XModuleStudentInfoField', fields ['student', 'field_name']
+ db.create_unique('courseware_xmodulestudentinfofield', ['student_id', 'field_name'])
+
+ # Adding model 'XModuleContentField'
+ db.create_table('courseware_xmodulecontentfield', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('field_name', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('definition_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('value', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('courseware', ['XModuleContentField'])
+
+ # Adding unique constraint on 'XModuleContentField', fields ['definition_id', 'field_name']
+ db.create_unique('courseware_xmodulecontentfield', ['definition_id', 'field_name'])
+
+ # Adding model 'XModuleSettingsField'
+ db.create_table('courseware_xmodulesettingsfield', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('field_name', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('usage_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('value', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('courseware', ['XModuleSettingsField'])
+
+ # Adding unique constraint on 'XModuleSettingsField', fields ['usage_id', 'field_name']
+ db.create_unique('courseware_xmodulesettingsfield', ['usage_id', 'field_name'])
+
+ # Adding model 'XModuleStudentPrefsField'
+ db.create_table('courseware_xmodulestudentprefsfield', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('field_name', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('module_type', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('value', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('courseware', ['XModuleStudentPrefsField'])
+
+ # Adding unique constraint on 'XModuleStudentPrefsField', fields ['student', 'module_type', 'field_name']
+ db.create_unique('courseware_xmodulestudentprefsfield', ['student_id', 'module_type', 'field_name'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'XModuleStudentPrefsField', fields ['student', 'module_type', 'field_name']
+ db.delete_unique('courseware_xmodulestudentprefsfield', ['student_id', 'module_type', 'field_name'])
+
+ # Removing unique constraint on 'XModuleSettingsField', fields ['usage_id', 'field_name']
+ db.delete_unique('courseware_xmodulesettingsfield', ['usage_id', 'field_name'])
+
+ # Removing unique constraint on 'XModuleContentField', fields ['definition_id', 'field_name']
+ db.delete_unique('courseware_xmodulecontentfield', ['definition_id', 'field_name'])
+
+ # Removing unique constraint on 'XModuleStudentInfoField', fields ['student', 'field_name']
+ db.delete_unique('courseware_xmodulestudentinfofield', ['student_id', 'field_name'])
+
+ # Deleting model 'XModuleStudentInfoField'
+ db.delete_table('courseware_xmodulestudentinfofield')
+
+ # Deleting model 'XModuleContentField'
+ db.delete_table('courseware_xmodulecontentfield')
+
+ # Deleting model 'XModuleSettingsField'
+ db.delete_table('courseware_xmodulesettingsfield')
+
+ # Deleting model 'XModuleStudentPrefsField'
+ db.delete_table('courseware_xmodulestudentprefsfield')
+
+
+ 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.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.xmodulecontentfield': {
+ 'Meta': {'unique_together': "(('definition_id', 'field_name'),)", 'object_name': 'XModuleContentField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'definition_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'courseware.xmodulesettingsfield': {
+ 'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleSettingsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'usage_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'courseware.xmodulestudentinfofield': {
+ 'Meta': {'unique_together': "(('student', 'field_name'),)", 'object_name': 'XModuleStudentInfoField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'courseware.xmodulestudentprefsfield': {
+ 'Meta': {'unique_together': "(('student', 'module_type', 'field_name'),)", 'object_name': 'XModuleStudentPrefsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['courseware']
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/migrations/0006_add_field_default.py b/lms/djangoapps/courseware/migrations/0006_add_field_default.py
new file mode 100644
index 0000000000..cd885ee7a6
--- /dev/null
+++ b/lms/djangoapps/courseware/migrations/0006_add_field_default.py
@@ -0,0 +1,128 @@
+# -*- 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):
+
+ # Changing field 'XModuleContentField.value'
+ db.alter_column('courseware_xmodulecontentfield', 'value', self.gf('django.db.models.fields.TextField')())
+
+ # Changing field 'XModuleStudentInfoField.value'
+ db.alter_column('courseware_xmodulestudentinfofield', 'value', self.gf('django.db.models.fields.TextField')())
+
+ # Changing field 'XModuleSettingsField.value'
+ db.alter_column('courseware_xmodulesettingsfield', 'value', self.gf('django.db.models.fields.TextField')())
+
+ # Changing field 'XModuleStudentPrefsField.value'
+ db.alter_column('courseware_xmodulestudentprefsfield', 'value', self.gf('django.db.models.fields.TextField')())
+
+ def backwards(self, orm):
+
+ # Changing field 'XModuleContentField.value'
+ db.alter_column('courseware_xmodulecontentfield', 'value', self.gf('django.db.models.fields.TextField')(null=True))
+
+ # Changing field 'XModuleStudentInfoField.value'
+ db.alter_column('courseware_xmodulestudentinfofield', 'value', self.gf('django.db.models.fields.TextField')(null=True))
+
+ # Changing field 'XModuleSettingsField.value'
+ db.alter_column('courseware_xmodulesettingsfield', 'value', self.gf('django.db.models.fields.TextField')(null=True))
+
+ # Changing field 'XModuleStudentPrefsField.value'
+ db.alter_column('courseware_xmodulestudentprefsfield', 'value', self.gf('django.db.models.fields.TextField')(null=True))
+
+ 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.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.xmodulecontentfield': {
+ 'Meta': {'unique_together': "(('definition_id', 'field_name'),)", 'object_name': 'XModuleContentField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'definition_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmodulesettingsfield': {
+ 'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleSettingsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'usage_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmodulestudentinfofield': {
+ 'Meta': {'unique_together': "(('student', 'field_name'),)", 'object_name': 'XModuleStudentInfoField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmodulestudentprefsfield': {
+ 'Meta': {'unique_together': "(('student', 'module_type', 'field_name'),)", 'object_name': 'XModuleStudentPrefsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ }
+ }
+
+ complete_apps = ['courseware']
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
new file mode 100644
index 0000000000..e8dfb025ed
--- /dev/null
+++ b/lms/djangoapps/courseware/model_data.py
@@ -0,0 +1,145 @@
+import json
+from collections import namedtuple
+from .models import (
+ StudentModule,
+ XModuleContentField,
+ XModuleSettingsField,
+ XModuleStudentPrefsField,
+ XModuleStudentInfoField
+)
+
+from xmodule.runtime import DbModel, KeyValueStore
+from xmodule.model import Scope
+
+
+class InvalidScopeError(Exception):
+ pass
+
+class InvalidWriteError(Exception):
+ pass
+
+
+class LmsKeyValueStore(KeyValueStore):
+ """
+ This KeyValueStore will read data from descriptor_model_data if it exists,
+ but will not overwrite any keys set in descriptor_model_data. Attempts to do so will
+ raise an InvalidWriteError.
+
+ If the scope to write to is not one of the 5 named scopes:
+ Scope.content
+ Scope.settings
+ Scope.student_state
+ Scope.student_preferences
+ Scope.student_info
+ then an InvalidScopeError will be raised.
+
+ Data for Scope.student_state is stored as StudentModule objects via the django orm.
+
+ Data for the other scopes is stored in individual objects that are named for the
+ scope involved and have the field name as a key
+
+ If the key isn't found in the expected table during a read or a delete, then a KeyError will be raised
+ """
+ def __init__(self, course_id, user, descriptor_model_data, student_module_cache):
+ self._course_id = course_id
+ self._user = user
+ self._descriptor_model_data = descriptor_model_data
+ self._student_module_cache = student_module_cache
+
+ def _student_module(self, key):
+ student_module = self._student_module_cache.lookup(
+ self._course_id, key.module_scope_id.category, key.module_scope_id.url()
+ )
+ return student_module
+
+ def _field_object(self, key):
+ if key.scope == Scope.content:
+ return XModuleContentField, {'field_name': key.field_name, 'definition_id': key.module_scope_id}
+ elif key.scope == Scope.settings:
+ return XModuleSettingsField, {
+ 'field_name': key.field_name,
+ 'usage_id': '%s-%s' % (self._course_id, key.module_scope_id)
+ }
+ elif key.scope == Scope.student_preferences:
+ return XModuleStudentPrefsField, {'field_name': key.field_name, 'student': self._user, 'module_type': key.module_scope_id}
+ elif key.scope == Scope.student_info:
+ return XModuleStudentInfoField, {'field_name': key.field_name, 'student': self._user}
+
+ raise InvalidScopeError(key.scope)
+
+ def get(self, key):
+ if key.field_name in self._descriptor_model_data:
+ return self._descriptor_model_data[key.field_name]
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+
+ if student_module is None:
+ raise KeyError(key.field_name)
+
+ return json.loads(student_module.state)[key.field_name]
+
+ scope_field_cls, search_kwargs = self._field_object(key)
+ try:
+ return json.loads(scope_field_cls.objects.get(**search_kwargs).value)
+ except scope_field_cls.DoesNotExist:
+ raise KeyError(key.field_name)
+
+ def set(self, key, value):
+ if key.field_name in self._descriptor_model_data:
+ raise InvalidWriteError("Not allowed to overwrite descriptor model data", key.field_name)
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+ if student_module is None:
+ student_module = StudentModule(
+ course_id=self._course_id,
+ student=self._user,
+ module_type=key.module_scope_id.category,
+ module_state_key=key.module_scope_id.url(),
+ state=json.dumps({})
+ )
+ self._student_module_cache.append(student_module)
+ state = json.loads(student_module.state)
+ state[key.field_name] = value
+ student_module.state = json.dumps(state)
+ student_module.save()
+ return
+
+ scope_field_cls, search_kwargs = self._field_object(key)
+ json_value = json.dumps(value)
+ field, created = scope_field_cls.objects.select_for_update().get_or_create(
+ defaults={'value': json_value},
+ **search_kwargs
+ )
+ if not created:
+ field.value = json_value
+ field.save()
+
+ def delete(self, key):
+ if key.field_name in self._descriptor_model_data:
+ raise InvalidWriteError("Not allowed to deleted descriptor model data", key.field_name)
+
+ if key.scope == Scope.student_state:
+ student_module = self._student_module(key)
+
+ if student_module is None:
+ raise KeyError(key.field_name)
+
+ state = json.loads(student_module.state)
+ del state[key.field_name]
+ student_module.state = json.dumps(state)
+ student_module.save()
+ return
+
+ scope_field_cls, search_kwargs = self._field_object(key)
+ print scope_field_cls, search_kwargs
+ query = scope_field_cls.objects.filter(**search_kwargs)
+ if not query.exists():
+ raise KeyError(key.field_name)
+
+ query.delete()
+
+
+LmsUsage = namedtuple('LmsUsage', 'id, def_id')
+
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 9c318427ec..d6161e9f34 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -72,6 +72,132 @@ class StudentModule(models.Model):
def __unicode__(self):
return unicode(repr(self))
+
+class XModuleContentField(models.Model):
+ """
+ Stores data set in the Scope.content scope by an xmodule field
+ """
+
+ class Meta:
+ unique_together = (('definition_id', 'field_name'),)
+
+ # The name of the field
+ field_name = models.CharField(max_length=64, db_index=True)
+
+ # The definition id for the module
+ definition_id = models.CharField(max_length=255, db_index=True)
+
+ # The value of the field. Defaults to None dumped as json
+ value = models.TextField(default='null')
+
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified = models.DateTimeField(auto_now=True, db_index=True)
+
+ def __repr__(self):
+ return 'XModuleContentField<%r>' % ({
+ 'field_name': self.field_name,
+ 'definition_id': self.definition_id,
+ 'value': self.value,
+ },)
+
+ def __unicode__(self):
+ return unicode(repr(self))
+
+
+class XModuleSettingsField(models.Model):
+ """
+ Stores data set in the Scope.settings scope by an xmodule field
+ """
+
+ class Meta:
+ unique_together = (('usage_id', 'field_name'),)
+
+ # The name of the field
+ field_name = models.CharField(max_length=64, db_index=True)
+
+ # The usage id for the module
+ usage_id = models.CharField(max_length=255, db_index=True)
+
+ # The value of the field. Defaults to None, dumped as json
+ value = models.TextField(default='null')
+
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified = models.DateTimeField(auto_now=True, db_index=True)
+
+ def __repr__(self):
+ return 'XModuleSettingsField<%r>' % ({
+ 'field_name': self.field_name,
+ 'usage_id': self.usage_id,
+ 'value': self.value,
+ },)
+
+ def __unicode__(self):
+ return unicode(repr(self))
+
+
+class XModuleStudentPrefsField(models.Model):
+ """
+ Stores data set in the Scope.student_preferences scope by an xmodule field
+ """
+
+ class Meta:
+ unique_together = (('student', 'module_type', 'field_name'),)
+
+ # The name of the field
+ field_name = models.CharField(max_length=64, db_index=True)
+
+ # The type of the module for these preferences
+ module_type = models.CharField(max_length=64, db_index=True)
+
+ # The value of the field. Defaults to None dumped as json
+ value = models.TextField(default='null')
+
+ student = models.ForeignKey(User, db_index=True)
+
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified = models.DateTimeField(auto_now=True, db_index=True)
+
+ def __repr__(self):
+ return 'XModuleStudentPrefsField<%r>' % ({
+ 'field_name': self.field_name,
+ 'module_type': self.module_type,
+ 'student': self.student.username,
+ 'value': self.value,
+ },)
+
+ def __unicode__(self):
+ return unicode(repr(self))
+
+
+class XModuleStudentInfoField(models.Model):
+ """
+ Stores data set in the Scope.student_preferences scope by an xmodule field
+ """
+
+ class Meta:
+ unique_together = (('student', 'field_name'),)
+
+ # The name of the field
+ field_name = models.CharField(max_length=64, db_index=True)
+
+ # The value of the field. Defaults to None dumped as json
+ value = models.TextField(default='null')
+
+ student = models.ForeignKey(User, db_index=True)
+
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified = models.DateTimeField(auto_now=True, db_index=True)
+
+ def __repr__(self):
+ return 'XModuleStudentInfoField<%r>' % ({
+ 'field_name': self.field_name,
+ 'student': self.student.username,
+ 'value': self.value,
+ },)
+
+ def __unicode__(self):
+ return unicode(repr(self))
+
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 839ee80591..04dee7de0f 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -11,8 +11,6 @@ from django.http import Http404
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
-from collections import namedtuple
-
from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface
@@ -28,9 +26,9 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
-from xmodule.runtime import DbModel, KeyValueStore
-from xmodule.model import Scope
+from xmodule.runtime import DbModel
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
+from .model_data import LmsKeyValueStore, LmsUsage
from xmodule.modulestore.exceptions import ItemNotFoundError
from statsd import statsd
@@ -149,70 +147,6 @@ def get_module(user, request, location, student_module_cache, course_id, positio
return None
-class LmsKeyValueStore(KeyValueStore):
- def __init__(self, course_id, user, descriptor_model_data, student_module_cache):
- self._course_id = course_id
- self._user = user
- self._descriptor_model_data = descriptor_model_data
- self._student_module_cache = student_module_cache
-
- def _student_module(self, key):
- student_module = self._student_module_cache.lookup(
- self._course_id, key.module_scope_id.category, key.module_scope_id.url()
- )
- return student_module
-
- def get(self, key):
- if not key.scope.student:
- return self._descriptor_model_data[key.field_name]
-
- if key.scope == Scope.student_state:
- student_module = self._student_module(key)
-
- if student_module is None:
- raise KeyError(key.field_name)
-
- return json.loads(student_module.state)[key.field_name]
-
- def set(self, key, value):
- if not key.scope.student:
- self._descriptor_model_data[key.field_name] = value
-
- if key.scope == Scope.student_state:
- student_module = self._student_module(key)
- if student_module is None:
- student_module = StudentModule(
- course_id=self._course_id,
- student=self._user,
- module_type=key.module_scope_id.category,
- module_state_key=key.module_scope_id.url(),
- state=json.dumps({})
- )
- self._student_module_cache.append(student_module)
- state = json.loads(student_module.state)
- state[key.field_name] = value
- student_module.state = json.dumps(state)
- student_module.save()
-
- def delete(self, key):
- if not key.scope.student:
- del self._descriptor_model_data[key.field_name]
-
- if key.scope == Scope.student_state:
- student_module = self._student_module(key)
-
- if student_module is None:
- raise KeyError(key.field_name)
-
- state = json.loads(student_module.state)
- del state[key.field_name]
- student_module.state = json.dumps(state)
- student_module.save()
-
-
-LmsUsage = namedtuple('LmsUsage', 'id, def_id')
-
-
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display=True):
"""
Actually implement get_module. See docstring there for details.
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
new file mode 100644
index 0000000000..ab9067ce71
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -0,0 +1,360 @@
+import factory
+import json
+from mock import Mock
+from django.contrib.auth.models import User
+
+from functools import partial
+
+from courseware.model_data import LmsKeyValueStore, InvalidWriteError, InvalidScopeError
+from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField, XModuleStudentInfoField, XModuleStudentPrefsField, StudentModuleCache
+from xmodule.model import Scope, ModuleScope
+from xmodule.modulestore import Location
+
+from django.test import TestCase
+
+
+def mock_descriptor():
+ descriptor = Mock()
+ descriptor.stores_state = True
+ descriptor.location = location('def_id')
+ return descriptor
+
+location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
+course_id = 'edX/test_course/test'
+
+content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
+settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
+student_state_key = partial(LmsKeyValueStore.Key, Scope.student_state, 'user', location('def_id'))
+student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem')
+student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None)
+
+
+class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ username = 'user'
+
+
+class StudentModuleFactory(factory.Factory):
+ FACTORY_FOR = StudentModule
+
+ module_type = 'problem'
+ module_state_key = location('def_id').url()
+ student = factory.SubFactory(UserFactory)
+ course_id = course_id
+ state = None
+
+
+class ContentFactory(factory.Factory):
+ FACTORY_FOR = XModuleContentField
+
+ field_name = 'content_field'
+ value = json.dumps('content_value')
+ definition_id = location('def_id').url()
+
+
+class SettingsFactory(factory.Factory):
+ FACTORY_FOR = XModuleSettingsField
+
+ field_name = 'settings_field'
+ value = json.dumps('settings_value')
+ usage_id = '%s-%s' % (course_id, location('def_id').url())
+
+
+class StudentPrefsFactory(factory.Factory):
+ FACTORY_FOR = XModuleStudentPrefsField
+
+ field_name = 'student_pref_field'
+ value = json.dumps('student_pref_value')
+ student = factory.SubFactory(UserFactory)
+ module_type = 'problem'
+
+
+class StudentInfoFactory(factory.Factory):
+ FACTORY_FOR = XModuleStudentInfoField
+
+ field_name = 'student_info_field'
+ value = json.dumps('student_info_value')
+ student = factory.SubFactory(UserFactory)
+
+
+class TestDescriptorFallback(TestCase):
+
+ def setUp(self):
+ self.desc_md = {
+ 'field_a': 'content',
+ 'field_b': 'settings',
+ }
+ self.kvs = LmsKeyValueStore(course_id, UserFactory.build(), self.desc_md, None)
+
+ def test_get_from_descriptor(self):
+ self.assertEquals('content', self.kvs.get(content_key('field_a')))
+ self.assertEquals('settings', self.kvs.get(settings_key('field_b')))
+
+ def test_write_to_descriptor(self):
+ self.assertRaises(InvalidWriteError, self.kvs.set, content_key('field_a'), 'foo')
+ self.assertEquals('content', self.desc_md['field_a'])
+ self.assertRaises(InvalidWriteError, self.kvs.set, settings_key('field_b'), 'foo')
+ self.assertEquals('settings', self.desc_md['field_b'])
+
+ self.assertRaises(InvalidWriteError, self.kvs.delete, content_key('field_a'))
+ self.assertEquals('content', self.desc_md['field_a'])
+ self.assertRaises(InvalidWriteError, self.kvs.delete, settings_key('field_b'))
+ self.assertEquals('settings', self.desc_md['field_b'])
+
+
+class TestStudentStateFields(TestCase):
+ pass
+
+class TestInvalidScopes(TestCase):
+ def setUp(self):
+ self.desc_md = {}
+ self.kvs = LmsKeyValueStore(course_id, UserFactory.build(), self.desc_md, None)
+
+ def test_invalid_scopes(self):
+ for scope in (Scope(student=True, module=ModuleScope.DEFINITION),
+ Scope(student=False, module=ModuleScope.TYPE),
+ Scope(student=False, module=ModuleScope.ALL)):
+ self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
+ self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
+ self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
+
+
+class TestStudentModuleStorage(TestCase):
+
+ def setUp(self):
+ student_module = StudentModuleFactory.create(state=json.dumps({'a_field': 'a_value'}))
+ self.user = student_module.student
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [mock_descriptor()])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+
+ def test_get_existing_field(self):
+ "Test that getting an existing field in an existing StudentModule works"
+ self.assertEquals('a_value', self.kvs.get(student_state_key('a_field')))
+
+ def test_get_missing_field(self):
+ "Test that getting a missing field from an existing StudentModule raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
+
+ def test_set_existing_field(self):
+ "Test that setting an existing student_state field changes the value"
+ self.kvs.set(student_state_key('a_field'), 'new_value')
+ self.assertEquals(1, StudentModule.objects.all().count())
+ self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
+
+ def test_set_missing_field(self):
+ "Test that setting a new student_state field changes the value"
+ self.kvs.set(student_state_key('not_a_field'), 'new_value')
+ self.assertEquals(1, StudentModule.objects.all().count())
+ self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
+
+ def test_delete_existing_field(self):
+ "Test that deleting an existing field removes it from the StudentModule"
+ self.kvs.delete(student_state_key('a_field'))
+ self.assertEquals(1, StudentModule.objects.all().count())
+ self.assertEquals({}, json.loads(StudentModule.objects.all()[0].state))
+
+ def test_delete_missing_field(self):
+ "Test that deleting a missing field from an existing StudentModule raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, student_state_key('not_a_field'))
+ self.assertEquals(1, StudentModule.objects.all().count())
+ self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
+
+
+class TestMissingStudentModule(TestCase):
+ def setUp(self):
+ self.user = UserFactory.create()
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [mock_descriptor()])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+ def test_get_field_from_missing_student_module(self):
+ "Test that getting a field from a missing StudentModule raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, student_state_key('a_field'))
+
+ def test_set_field_in_missing_student_module(self):
+ "Test that setting a field in a missing StudentModule creates the student module"
+ self.assertEquals(0, len(self.smc.cache))
+ self.assertEquals(0, StudentModule.objects.all().count())
+
+ self.kvs.set(student_state_key('a_field'), 'a_value')
+
+ self.assertEquals(1, len(self.smc.cache))
+ self.assertEquals(1, StudentModule.objects.all().count())
+
+ student_module = StudentModule.objects.all()[0]
+ self.assertEquals({'a_field': 'a_value'}, json.loads(student_module.state))
+ self.assertEquals(self.user, student_module.student)
+ self.assertEquals(location('def_id').url(), student_module.module_state_key)
+ self.assertEquals(course_id, student_module.course_id)
+
+ def test_delete_field_from_missing_student_module(self):
+ "Test that deleting a field from a missing StudentModule raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, student_state_key('a_field'))
+
+
+class TestSettingsStorage(TestCase):
+
+ def setUp(self):
+ settings = SettingsFactory.create()
+ self.user = UserFactory.create()
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+ def test_get_existing_field(self):
+ "Test that getting an existing field in an existing SettingsField works"
+ self.assertEquals('settings_value', self.kvs.get(settings_key('settings_field')))
+
+ def test_get_missing_field(self):
+ "Test that getting a missing field from an existing SettingsField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, settings_key('not_settings_field'))
+
+ def test_set_existing_field(self):
+ "Test that setting an existing field changes the value"
+ self.kvs.set(settings_key('settings_field'), 'new_value')
+ self.assertEquals(1, XModuleSettingsField.objects.all().count())
+ self.assertEquals('new_value', json.loads(XModuleSettingsField.objects.all()[0].value))
+
+ def test_set_missing_field(self):
+ "Test that setting a new field changes the value"
+ self.kvs.set(settings_key('not_settings_field'), 'new_value')
+ self.assertEquals(2, XModuleSettingsField.objects.all().count())
+ self.assertEquals('settings_value', json.loads(XModuleSettingsField.objects.get(field_name='settings_field').value))
+ self.assertEquals('new_value', json.loads(XModuleSettingsField.objects.get(field_name='not_settings_field').value))
+
+ def test_delete_existing_field(self):
+ "Test that deleting an existing field removes it"
+ self.kvs.delete(settings_key('settings_field'))
+ self.assertEquals(0, XModuleSettingsField.objects.all().count())
+
+ def test_delete_missing_field(self):
+ "Test that deleting a missing field from an existing SettingsField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, settings_key('not_settings_field'))
+ self.assertEquals(1, XModuleSettingsField.objects.all().count())
+
+
+class TestContentStorage(TestCase):
+
+ def setUp(self):
+ content = ContentFactory.create()
+ self.user = UserFactory.create()
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+ def test_get_existing_field(self):
+ "Test that getting an existing field in an existing ContentField works"
+ self.assertEquals('content_value', self.kvs.get(content_key('content_field')))
+
+ def test_get_missing_field(self):
+ "Test that getting a missing field from an existing ContentField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, content_key('not_content_field'))
+
+ def test_set_existing_field(self):
+ "Test that setting an existing field changes the value"
+ self.kvs.set(content_key('content_field'), 'new_value')
+ self.assertEquals(1, XModuleContentField.objects.all().count())
+ self.assertEquals('new_value', json.loads(XModuleContentField.objects.all()[0].value))
+
+ def test_set_missing_field(self):
+ "Test that setting a new field changes the value"
+ self.kvs.set(content_key('not_content_field'), 'new_value')
+ self.assertEquals(2, XModuleContentField.objects.all().count())
+ self.assertEquals('content_value', json.loads(XModuleContentField.objects.get(field_name='content_field').value))
+ self.assertEquals('new_value', json.loads(XModuleContentField.objects.get(field_name='not_content_field').value))
+
+ def test_delete_existing_field(self):
+ "Test that deleting an existing field removes it"
+ self.kvs.delete(content_key('content_field'))
+ self.assertEquals(0, XModuleContentField.objects.all().count())
+
+ def test_delete_missing_field(self):
+ "Test that deleting a missing field from an existing ContentField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, content_key('not_content_field'))
+ self.assertEquals(1, XModuleContentField.objects.all().count())
+
+
+class TestStudentPrefsStorage(TestCase):
+
+ def setUp(self):
+ student_pref = StudentPrefsFactory.create()
+ self.user = student_pref.student
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+ def test_get_existing_field(self):
+ "Test that getting an existing field in an existing StudentPrefsField works"
+ self.assertEquals('student_pref_value', self.kvs.get(student_prefs_key('student_pref_field')))
+
+ def test_get_missing_field(self):
+ "Test that getting a missing field from an existing StudentPrefsField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, student_prefs_key('not_student_pref_field'))
+
+ def test_set_existing_field(self):
+ "Test that setting an existing field changes the value"
+ self.kvs.set(student_prefs_key('student_pref_field'), 'new_value')
+ self.assertEquals(1, XModuleStudentPrefsField.objects.all().count())
+ self.assertEquals('new_value', json.loads(XModuleStudentPrefsField.objects.all()[0].value))
+
+ def test_set_missing_field(self):
+ "Test that setting a new field changes the value"
+ self.kvs.set(student_prefs_key('not_student_pref_field'), 'new_value')
+ self.assertEquals(2, XModuleStudentPrefsField.objects.all().count())
+ self.assertEquals('student_pref_value', json.loads(XModuleStudentPrefsField.objects.get(field_name='student_pref_field').value))
+ self.assertEquals('new_value', json.loads(XModuleStudentPrefsField.objects.get(field_name='not_student_pref_field').value))
+
+ def test_delete_existing_field(self):
+ "Test that deleting an existing field removes it"
+ print list(XModuleStudentPrefsField.objects.all())
+ self.kvs.delete(student_prefs_key('student_pref_field'))
+ self.assertEquals(0, XModuleStudentPrefsField.objects.all().count())
+
+ def test_delete_missing_field(self):
+ "Test that deleting a missing field from an existing StudentPrefsField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, student_prefs_key('not_student_pref_field'))
+ self.assertEquals(1, XModuleStudentPrefsField.objects.all().count())
+
+
+class TestStudentInfoStorage(TestCase):
+
+ def setUp(self):
+ student_info = StudentInfoFactory.create()
+ self.user = student_info.student
+ self.desc_md = {}
+ self.smc = StudentModuleCache(course_id, self.user, [])
+ self.kvs = LmsKeyValueStore(course_id, self.user, self.desc_md, self.smc)
+
+ def test_get_existing_field(self):
+ "Test that getting an existing field in an existing StudentInfoField works"
+ self.assertEquals('student_info_value', self.kvs.get(student_info_key('student_info_field')))
+
+ def test_get_missing_field(self):
+ "Test that getting a missing field from an existing StudentInfoField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.get, student_info_key('not_student_info_field'))
+
+ def test_set_existing_field(self):
+ "Test that setting an existing field changes the value"
+ self.kvs.set(student_info_key('student_info_field'), 'new_value')
+ self.assertEquals(1, XModuleStudentInfoField.objects.all().count())
+ self.assertEquals('new_value', json.loads(XModuleStudentInfoField.objects.all()[0].value))
+
+ def test_set_missing_field(self):
+ "Test that setting a new field changes the value"
+ self.kvs.set(student_info_key('not_student_info_field'), 'new_value')
+ self.assertEquals(2, XModuleStudentInfoField.objects.all().count())
+ self.assertEquals('student_info_value', json.loads(XModuleStudentInfoField.objects.get(field_name='student_info_field').value))
+ self.assertEquals('new_value', json.loads(XModuleStudentInfoField.objects.get(field_name='not_student_info_field').value))
+
+ def test_delete_existing_field(self):
+ "Test that deleting an existing field removes it"
+ self.kvs.delete(student_info_key('student_info_field'))
+ self.assertEquals(0, XModuleStudentInfoField.objects.all().count())
+
+ def test_delete_missing_field(self):
+ "Test that deleting a missing field from an existing StudentInfoField raises a KeyError"
+ self.assertRaises(KeyError, self.kvs.delete, student_info_key('not_student_info_field'))
+ self.assertEquals(1, XModuleStudentInfoField.objects.all().count())
From db55001bf68b76baaa82785160c28f1c754bafb6 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 20 Dec 2012 16:53:50 -0500
Subject: [PATCH 042/285] Use named scope for student_preferences in abtests
---
common/lib/xmodule/xmodule/abtest_module.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index e805d5efa6..7143eee08d 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -37,7 +37,7 @@ class ABTestModule(XModule):
"""
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
- group_assignments = Object(help="What group this user belongs to", scope=Scope(student=True, module=ModuleScope.TYPE), default={})
+ group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
def __init__(self, *args, **kwargs):
From 4b6ec85dcbbba59042a2f9bfe23ba783e6d499d4 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 20 Dec 2012 16:54:15 -0500
Subject: [PATCH 043/285] Minor cleanups
---
common/lib/xmodule/xmodule/capa_module.py | 1 -
common/lib/xmodule/xmodule/course_module.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 8c2297af1b..6fcbc00797 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -525,7 +525,6 @@ class CapaModule(XModule):
Publishes the student's current grade to the system as an event
"""
score = self.lcp.get_score()
- print score
self.system.publish({
'event_name': 'grade',
'value': score['score'],
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 947d75eb97..444fcaf52d 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -96,11 +96,11 @@ class CourseDescriptor(SequenceDescriptor):
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
+ start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = Date(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
- start = Date(help="Start time when this module is visible", scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
From 9bd42278e95b45a04932bb87849cfe8530adfdcf Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 20 Dec 2012 16:54:30 -0500
Subject: [PATCH 044/285] Don't hide the logs in tests
---
lms/envs/test.py | 7 -------
1 file changed, 7 deletions(-)
diff --git a/lms/envs/test.py b/lms/envs/test.py
index b15e3acb4c..fe4f3e0c1b 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -8,7 +8,6 @@ sessions. Assumes structure:
/log # Where we're going to write log files
"""
from .common import *
-from logsettings import get_logger_config
import os
from path import path
@@ -44,12 +43,6 @@ STATUS_MESSAGE_PATH = TEST_ROOT / "status_message.json"
COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT
-LOGGING = get_logger_config(TEST_ROOT / "log",
- logging_env="dev",
- tracking_filename="tracking.log",
- dev_env=True,
- debug=True)
-
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# Where the content data is checked out. This may not exist on jenkins.
GITHUB_REPO_ROOT = ENV_ROOT / "data"
From e4c06fab4a368cdda845149e82db41bca458e0b7 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 11:12:03 -0500
Subject: [PATCH 045/285] Cleaning up ABTest properties
---
common/lib/xmodule/xmodule/abtest_module.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 7143eee08d..f33a3db91c 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -39,13 +39,15 @@ class ABTestModule(XModule):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
+ experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
+ has_children = True
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
if self.group is None:
self.group = group_from_value(
- self.group_portions,
+ self.group_portions.items(),
random.uniform(0, 1)
)
@@ -80,6 +82,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
group_portions = Object(help="What proportions of students should go in each group", default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
+ has_children = True
@classmethod
def definition_from_xml(cls, xml_object, system):
From c7069be2a4cdeaeaa4ebf7bdb877722d0762f66d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 11:12:19 -0500
Subject: [PATCH 046/285] Centralize logic for standardizing rerandomize values
---
common/lib/xmodule/xmodule/capa_module.py | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 6fcbc00797..5ad2a8bffa 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -83,6 +83,16 @@ class Timedelta(ModelType):
return ' '.join(values)
+class Randomization(String):
+ def from_json(self, value):
+ if value in ("", "true"):
+ return "always"
+ elif value == "false":
+ return "per_student"
+
+ to_json = from_json
+
+
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
@@ -103,7 +113,7 @@ class CapaModule(XModule):
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
- rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
+ rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
@@ -174,11 +184,6 @@ class CapaModule(XModule):
# add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
- if self.rerandomize in ("", "true"):
- self.rerandomize = "always"
- elif self.rerandomize == "false":
- self.rerandomize = "per_student"
-
def new_lcp(self, state, text=None):
if text is None:
text = self.data
From 48a1e09133d352903169af2f37c7d3afeabfe6e4 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 11:12:31 -0500
Subject: [PATCH 047/285] Pass in state to LCP on reset
---
common/lib/xmodule/xmodule/capa_module.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 5ad2a8bffa..4ee648b241 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -677,7 +677,7 @@ class CapaModule(XModule):
self.lcp.seed = None
self.set_state_from_lcp()
- self.lcp = self.new_lcp()
+ self.lcp = self.new_lcp(self.get_state_for_lcp())
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
From 7f8b79694c17cd8cbe1ed6c641809c77a1d8811e Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 11:12:58 -0500
Subject: [PATCH 048/285] Grade problems that have ungraded student modules
---
lms/djangoapps/courseware/grades.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index b81147f905..0c6220e561 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -345,27 +345,28 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
# These are not problems, and do not have a score
return (None, None)
- correct = 0.0
-
instance_module = student_module_cache.lookup(
course_id, problem_descriptor.category, problem_descriptor.location.url())
- if instance_module:
- if instance_module.max_grade is None:
- return (None, None)
-
+ if instance_module is not None and instance_module.max_grade is not None:
correct = instance_module.grade if instance_module.grade is not None else 0
total = instance_module.max_grade
else:
- # If the problem was not in the cache, we need to instantiate the problem.
+ # If the problem was not in the cache, or hasn't been graded yet,
+ # we need to instantiate the problem.
# Otherwise, the max score (cached in instance_module) won't be available
problem = module_creator(problem_descriptor)
if problem is None:
return (None, None)
- correct = 0
+ correct = 0.0
total = problem.max_score()
+ # Problem may be an error module (if something in the problem builder failed)
+ # In which case total might be None
+ if total is None:
+ return (None, None)
+
#Now we re-weight the problem, if specified
weight = getattr(problem_descriptor, 'weight', None)
if weight is not None:
From 84cb0ce99bbca77efa9bd39bd3aa79c2b06c28a2 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:15:37 -0500
Subject: [PATCH 049/285] Move inheritance logic out into a separate file in
the modulestore
---
.../contentstore/tests/factories.py | 5 +-
cms/djangoapps/contentstore/views.py | 5 +-
.../models/settings/course_details.py | 3 +-
.../xmodule/modulestore/inheritance.py | 48 ++++++++++++++
common/lib/xmodule/xmodule/modulestore/xml.py | 25 +-------
.../xmodule/modulestore/xml_importer.py | 12 +---
common/lib/xmodule/xmodule/x_module.py | 63 ++-----------------
common/lib/xmodule/xmodule/xml_module.py | 5 +-
.../management/commands/metadata_to_json.py | 2 +-
9 files changed, 68 insertions(+), 100 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/modulestore/inheritance.py
diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py
index 3274477098..24897bf795 100644
--- a/cms/djangoapps/contentstore/tests/factories.py
+++ b/cms/djangoapps/contentstore/tests/factories.py
@@ -1,6 +1,7 @@
from factory import Factory
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.inheritance import own_metadata
from time import gmtime
from uuid import uuid4
from xmodule.timeparse import stringify_time
@@ -48,7 +49,7 @@ class XModuleCourseFactory(Factory):
{"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore
- store.update_metadata(new_course.location.url(), new_course.own_metadata)
+ store.update_metadata(new_course.location.url(), own_metadata(new_course))
return new_course
@@ -95,7 +96,7 @@ class XModuleItemFactory(Factory):
if display_name is not None:
new_item.metadata['display_name'] = display_name
- store.update_metadata(new_item.location.url(), new_item.own_metadata)
+ store.update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index bf706f2996..22b46dc4be 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -28,6 +28,7 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
@@ -712,7 +713,7 @@ def clone_item(request):
if display_name is not None:
new_item.metadata['display_name'] = display_name
- get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
+ get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
@@ -1231,7 +1232,7 @@ def initialize_course_tabs(course):
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
- modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(new_course))
@ensure_csrf_cookie
@login_required
diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py
index 2bb9d98be7..59cadf6962 100644
--- a/cms/djangoapps/models/settings/course_details.py
+++ b/cms/djangoapps/models/settings/course_details.py
@@ -1,6 +1,7 @@
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
import time
@@ -117,7 +118,7 @@ class CourseDetails:
descriptor.enrollment_end = converted
if dirty:
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
new file mode 100644
index 0000000000..ca88aab8d8
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -0,0 +1,48 @@
+from xmodule.model import Scope
+
+
+def compute_inherited_metadata(descriptor):
+ """Given a descriptor, traverse all of its descendants and do metadata
+ inheritance. Should be called on a CourseDescriptor after importing a
+ course.
+
+ NOTE: This means that there is no such thing as lazy loading at the
+ moment--this accesses all the children."""
+ for child in descriptor.get_children():
+ inherit_metadata(child, descriptor._model_data)
+ compute_inherited_metadata(child)
+
+
+def inherit_metadata(descriptor, model_data):
+ """
+ Updates this module with metadata inherited from a containing module.
+ Only metadata specified in self.inheritable_metadata will
+ be inherited
+ """
+ if not hasattr(descriptor, '_inherited_metadata'):
+ setattr(descriptor, '_inherited_metadata', set())
+
+ # Set all inheritable metadata from kwargs that are
+ # in self.inheritable_metadata and aren't already set in metadata
+ for attr in descriptor.inheritable_metadata:
+ if attr not in descriptor._model_data and attr in model_data:
+ descriptor._inherited_metadata.add(attr)
+ descriptor._model_data[attr] = model_data[attr]
+
+
+def own_metadata(module):
+ """
+ Return a dictionary that contains only non-inherited field keys,
+ mapped to their values
+ """
+ inherited_metadata = getattr(module, '_inherited_metadata', {})
+ metadata = {}
+ for field in module.fields + module.lms.fields:
+ # Only save metadata that wasn't inherited
+ if (field.scope == Scope.settings and
+ field.name not in inherited_metadata and
+ field.name in module._model_data):
+
+ metadata[field.name] = module._model_data[field.name]
+
+ return metadata
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 72c9093bbf..ed1bfdb30f 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -22,6 +22,7 @@ from xmodule.html_module import HtmlDescriptor
from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError
+from .inheritance import compute_inherited_metadata
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True)
@@ -421,30 +422,6 @@ class XMLModuleStore(ModuleStoreBase):
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
- def compute_inherited_metadata(descriptor):
- """Given a descriptor, traverse all of its descendants and do metadata
- inheritance. Should be called on a CourseDescriptor after importing a
- course.
-
- NOTE: This means that there is no such thing as lazy loading at the
- moment--this accesses all the children."""
- for child in descriptor.get_children():
- inherit_metadata(child, descriptor._model_data)
- compute_inherited_metadata(child)
-
- def inherit_metadata(descriptor, model_data):
- """
- Updates this module with metadata inherited from a containing module.
- Only metadata specified in self.inheritable_metadata will
- be inherited
- """
- # Set all inheritable metadata from kwargs that are
- # in self.inheritable_metadata and aren't already set in metadata
- for attr in descriptor.inheritable_metadata:
- if attr not in descriptor._model_data and attr in model_data:
- descriptor._inherited_metadata.add(attr)
- descriptor._model_data[attr] = model_data[attr]
-
compute_inherited_metadata(course_descriptor)
# now import all pieces of course_info which is expected to be stored
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index bbbb60d8ed..356ca772f4 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -8,7 +8,7 @@ from .xml import XMLModuleStore
from .exceptions import DuplicateItemError
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
-from xmodule.model import Scope
+from .inheritance import own_metadata
log = logging.getLogger(__name__)
@@ -143,15 +143,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
if module.has_children:
modulestore.update_children(module.location, module.children)
- metadata = {}
- for field in module.fields + module.lms.fields:
- # Only save metadata that wasn't inherited
- if (field.scope == Scope.settings and
- field.name not in module._inherited_metadata and
- field.name in module._model_data):
-
- metadata[field.name] = module._model_data[field.name]
- modulestore.update_metadata(module.location, metadata)
+ modulestore.update_metadata(module.location, own_metadata(module))
def import_course_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None):
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index a05a191e5f..43d6f58a16 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -114,43 +114,12 @@ class XModule(HTMLSnippet):
location: Something Location-like that identifies this xmodule
- definition: A dictionary containing 'data' and 'children'. Both are
- optional
-
- 'data': is JSON-like (string, dictionary, list, bool, or None,
- optionally nested).
-
- This defines all of the data necessary for a problem to display
- that is intrinsic to the problem. It should not include any
- data that would vary between two courses using the same problem
- (due dates, grading policy, randomization, etc.)
-
- 'children': is a list of Location-like values for child modules that
- this module depends on
-
descriptor: the XModuleDescriptor that this module is an instance of.
TODO (vshnayder): remove the definition parameter and location--they
can come from the descriptor.
- instance_state: A string of serialized json that contains the state of
- this module for current student accessing the system, or None if
- no state has been saved
-
- shared_state: A string of serialized json that contains the state that
- is shared between this module and any modules of the same type with
- the same shared_state_key. This state is only shared per-student,
- not across different students
-
- kwargs: Optional arguments. Subclasses should always accept kwargs and
- pass them to the parent class constructor.
-
- Current known uses of kwargs:
-
- metadata: SCAFFOLDING - This dictionary will be split into
- several different types of metadata in the future (course
- policy, modification history, etc). A dictionary containing
- data that specifies information that is particular to a
- problem in the context of a course
+ model_data: A dictionary-like object that maps field names to values
+ for those fields.
'''
self.system = system
self.location = Location(location)
@@ -357,31 +326,10 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system: A DescriptorSystem for interacting with external resources
- definition: A dict containing `data` and `children` representing the
- problem definition
+ location: Something Location-like that identifies this xmodule
- Current arguments passed in kwargs:
-
- location: A xmodule.modulestore.Location object indicating the name
- and ownership of this problem
-
- shared_state_key: The key to use for sharing StudentModules with
- other modules of this type
-
- metadata: A dictionary containing the following optional keys:
- goals: A list of strings of learning goals associated with this
- module
- url_name: The name to use for this module in urls and other places
- where a unique name is needed.
- format: The format of this module ('Homework', 'Lab', etc)
- graded (bool): Whether this module is should be graded or not
- start (string): The date for which this module will be available
- due (string): The due date for this module
- graceperiod (string): The amount of grace period to allow when
- enforcing the due date
- showanswer (string): When to show answers for this module
- rerandomize (string): When to generate a newly randomized
- instance of the module data
+ model_data: A dictionary-like object that maps field names to values
+ for those fields.
"""
self.system = system
self.location = Location(location)
@@ -390,7 +338,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._model_data = model_data
self._child_instances = None
- self._inherited_metadata = set()
self._child_instances = None
def get_children(self):
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 754d9b523e..d19fc26157 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -1,5 +1,6 @@
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
+from xmodule.modulestore.inheritance import own_metadata
from lxml import etree
import json
import copy
@@ -368,10 +369,10 @@ class XmlDescriptor(XModuleDescriptor):
(Possible format conversion through an AttrMap).
"""
attr_map = self.xml_attribute_map.get(attr, AttrMap())
- return attr_map.to_xml(self.own_metadata[attr])
+ return attr_map.to_xml(self._model_data[attr])
# Add the non-inherited metadata
- for attr in sorted(self.own_metadata):
+ for attr in sorted(own_metadata(self)):
# don't want e.g. data_dir
if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
val = val_for_xml(attr)
diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
index 8ef0dee7b3..3d399ef115 100644
--- a/lms/djangoapps/courseware/management/commands/metadata_to_json.py
+++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
@@ -51,7 +51,7 @@ def node_metadata(node):
'start', 'due', 'graded', 'hide_from_toc',
'ispublic', 'xqa_key')
- orig = node.own_metadata
+ orig = own_metadata(node)
d = {k: orig[k] for k in to_export if k in orig}
return d
From 6ece7fd541fa4763390dfb54c53db36f0edf5989 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:15:58 -0500
Subject: [PATCH 050/285] Fieldify the video module
---
common/lib/xmodule/xmodule/video_module.py | 81 +++++++++++++---------
1 file changed, 48 insertions(+), 33 deletions(-)
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index 34ce353afd..ecc7d7fdad 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -28,41 +28,13 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
- data = String(help="XML data for the problem", scope=Scope.content)
- position = Int(help="Current position in the video", scope=Scope.student_state)
+ youtube = String(help="Youtube ids for each speed, in the format :[,: ...]", scope=Scope.content)
+ show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
+ source = String(help="External source for this video", scope=Scope.content)
+ track = String(help="Subtitle file", scope=Scope.content)
+ position = Int(help="Current position in the video", scope=Scope.student_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings)
- def __init__(self, *args, **kwargs):
- XModule.__init__(self, *args, **kwargs)
-
- xmltree = etree.fromstring(self.data)
- self.youtube = xmltree.get('youtube')
- self.position = 0
- self.show_captions = xmltree.get('show_captions', 'true')
- self.source = self._get_source(xmltree)
- self.track = self._get_track(xmltree)
-
- def _get_source(self, xmltree):
- # find the first valid source
- return self._get_first_external(xmltree, 'source')
-
- def _get_track(self, xmltree):
- # find the first valid track
- return self._get_first_external(xmltree, 'track')
-
- def _get_first_external(self, xmltree, tag):
- """
- Will return the first valid element
- of the given tag.
- 'valid' means has a non-empty 'src' attribute
- """
- result = None
- for element in xmltree.findall(tag):
- src = element.get('src')
- if src:
- result = src
- break
- return result
def handle_ajax(self, dispatch, get):
'''
@@ -115,7 +87,50 @@ class VideoModule(XModule):
})
+
class VideoDescriptor(RawDescriptor):
module_class = VideoModule
stores_state = True
template_dir_name = "video"
+
+ youtube = String(help="Youtube ids for each speed, in the format :[,: ...]", scope=Scope.content)
+ show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
+ source = String(help="External source for this video", scope=Scope.content)
+ track = String(help="Subtitle file", scope=Scope.content)
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ return {
+ 'youtube': xml_object.get('youtube'),
+ 'show_captions': xml_object.get('show_captions', 'true'),
+ 'source': _get_first_external(xml_object, 'source'),
+ 'track': _get_first_external(xml_object, 'track'),
+ }, []
+
+ def definition_to_xml(self, resource_fs):
+ xml_object = etree.Element('video', {
+ 'youtube': self.youtube,
+ 'show_captions': self.show_captions,
+ })
+
+ if self.source is not None:
+ SubElement(xml_object, 'source', {'src': self.source})
+
+ if self.track is not None:
+ SubElement(xml_object, 'track', {'src': self.track})
+
+ return xml_object
+
+def _get_first_external(xmltree, tag):
+ """
+ Will return the first valid element
+ of the given tag.
+ 'valid' means has a non-empty 'src' attribute
+ """
+ result = None
+ for element in xmltree.findall(tag):
+ src = element.get('src')
+ if src:
+ result = src
+ break
+ return result
From 16d5a769d2317c43641f68a3b6d4734ae3245e90 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:21:31 -0500
Subject: [PATCH 051/285] Add a fasttest rake command that runs both
fasttest_cms and fasttest_lms
---
rakefile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/rakefile b/rakefile
index adf16cc462..312ad90124 100644
--- a/rakefile
+++ b/rakefile
@@ -193,6 +193,8 @@ TEST_TASK_DIRS = []
run_tests(system, report_dir, args.stop_on_failure)
end
+ task :fasttest => "fasttest_#{system}"
+
TEST_TASK_DIRS << system
desc <<-desc
From fa75245e8a6f2358d24398c79eeb7327ee6668a0 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:22:53 -0500
Subject: [PATCH 052/285] WIP: Start cleaning up CMS to work with new field
format
---
.../contentstore/tests/factories.py | 12 ++--
cms/djangoapps/contentstore/views.py | 14 ++---
.../models/settings/course_details.py | 8 +--
.../models/settings/course_grading.py | 26 ++++++---
common/lib/xmodule/xmodule/capa_module.py | 56 +------------------
common/lib/xmodule/xmodule/fields.py | 39 +++++++++++++
common/lib/xmodule/xmodule/html_module.py | 4 +-
.../xmodule/xmodule/self_assessment_module.py | 6 +-
.../lib/xmodule/xmodule/tests/test_export.py | 15 +----
lms/xmodule_namespace.py | 3 +-
10 files changed, 79 insertions(+), 104 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py
index 24897bf795..1ae300daed 100644
--- a/cms/djangoapps/contentstore/tests/factories.py
+++ b/cms/djangoapps/contentstore/tests/factories.py
@@ -38,10 +38,9 @@ class XModuleCourseFactory(Factory):
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
- new_course.metadata['display_name'] = display_name
+ new_course.lms.display_name = display_name
- new_course.metadata['data_dir'] = uuid4().hex
- new_course.metadata['start'] = stringify_time(gmtime())
+ new_course.start = gmtime()
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
@@ -89,17 +88,14 @@ class XModuleItemFactory(Factory):
new_item = store.clone_item(template, dest_location)
- # TODO: This needs to be deleted when we have proper storage for static content
- new_item.metadata['data_dir'] = parent.metadata['data_dir']
-
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
- new_item.metadata['display_name'] = display_name
+ new_item.lms.display_name = display_name
store.update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
- store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+ store.update_children(parent_location, parent.children + [new_item.location.url()])
return new_item
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 22b46dc4be..809e43dea3 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -706,17 +706,14 @@ def clone_item(request):
new_item = get_modulestore(template).clone_item(template, dest_location)
- # TODO: This needs to be deleted when we have proper storage for static content
- new_item.metadata['data_dir'] = parent.metadata['data_dir']
-
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
- new_item.metadata['display_name'] = display_name
+ new_item.lms.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
- get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+ get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@@ -1206,13 +1203,10 @@ def create_new_course(request):
new_course = modulestore('direct').clone_item(template, dest_location)
if display_name is not None:
- new_course.metadata['display_name'] = display_name
-
- # we need a 'data_dir' for legacy reasons
- new_course.metadata['data_dir'] = uuid4().hex
+ new_course.display_name = display_name
# set a default start date to now
- new_course.metadata['start'] = stringify_time(time.gmtime())
+ new_course.start = time.gmtime()
initialize_course_tabs(new_course)
diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py
index 59cadf6962..b1250862f6 100644
--- a/cms/djangoapps/models/settings/course_details.py
+++ b/cms/djangoapps/models/settings/course_details.py
@@ -44,25 +44,25 @@ class CourseDetails:
temploc = course_location._replace(category='about', name='syllabus')
try:
- course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
- course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
- course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
- raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
+ raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index e0bab1f225..9ddbe87727 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -201,7 +201,7 @@ class CourseGradingModel:
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
- if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
+ if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
@staticmethod
@@ -226,20 +226,30 @@ class CourseGradingModel:
descriptor.metadata['format'] = jsondict.get('graderType')
descriptor.metadata['graded'] = True
else:
- if 'format' in descriptor.metadata: del descriptor.metadata['format']
- if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
+ if 'format' in descriptor.metadata: del descriptor.metadata['format']
+ if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
- get_modulestore(location).update_metadata(location, descriptor.metadata)
+ get_modulestore(location).update_metadata(location, descriptor.metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format
- rawgrace = descriptor.metadata.get('graceperiod', None)
+ rawgrace = descriptor.lms.graceperiod
if rawgrace:
- parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
- return parsedgrace
- else: return None
+ hours_from_day = rawgrace.days*24
+ seconds = rawgrace.seconds
+ hours_from_seconds = int(seconds / 3600)
+ seconds -= hours_from_seconds * 3600
+ minutes = int(seconds / 60)
+ seconds -= minutes * 60
+ return {
+ 'hours': hourse_from_days + hours_from_seconds,
+ 'minutes': minutes_from_seconds,
+ 'seconds': seconds,
+ }
+ else:
+ return None
@staticmethod
def parse_grader(json_grader):
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 4ee648b241..f83c31fb5c 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -5,10 +5,8 @@ import dateutil.parser
import json
import logging
import traceback
-import re
import sys
-from datetime import timedelta
from lxml import etree
from pkg_resources import resource_string
@@ -20,6 +18,9 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
+from .fields import Timedelta
+
+log = logging.getLogger("mitx.courseware")
class StringyInt(Int):
@@ -31,57 +32,6 @@ class StringyInt(Int):
return int(value)
return value
-log = logging.getLogger("mitx.courseware")
-
-#-----------------------------------------------------------------------------
-TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$')
-
-
-def only_one(lst, default="", process=lambda x: x):
- """
- If lst is empty, returns default
-
- If lst has a single element, applies process to that element and returns it.
-
- Otherwise, raises an exception.
- """
- if len(lst) == 0:
- return default
- elif len(lst) == 1:
- return process(lst[0])
- else:
- raise Exception('Malformed XML: expected at most one element in list.')
-
-
-class Timedelta(ModelType):
- def from_json(self, time_str):
- """
- time_str: A string with the following components:
- day[s] (optional)
- hour[s] (optional)
- minute[s] (optional)
- second[s] (optional)
-
- Returns a datetime.timedelta parsed from the string
- """
- parts = TIMEDELTA_REGEX.match(time_str)
- if not parts:
- return
- parts = parts.groupdict()
- time_params = {}
- for (name, param) in parts.iteritems():
- if param:
- time_params[name] = int(param)
- return timedelta(**time_params)
-
- def to_json(self, value):
- values = []
- for attr in ('days', 'hours', 'minutes', 'seconds'):
- cur_value = getattr(value, attr, 0)
- if cur_value > 0:
- values.append("%d %s" % (cur_value, attr))
- return ' '.join(values)
-
class Randomization(String):
def from_json(self, value):
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 715aea0c7c..21c360f914 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -1,6 +1,8 @@
import time
import logging
+import re
+from datetime import timedelta
from .model import ModelType
log = logging.getLogger(__name__)
@@ -15,6 +17,9 @@ class Date(ModelType):
if it doesn't parse.
Return None if not present or invalid.
"""
+ if value is None:
+ return None
+
try:
return time.strptime(value, self.time_format)
except ValueError as e:
@@ -27,4 +32,38 @@ class Date(ModelType):
"""
Convert a time struct to a string
"""
+ if value is None:
+ return None
+
return time.strftime(self.time_format, value)
+
+
+TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$')
+class Timedelta(ModelType):
+ def from_json(self, time_str):
+ """
+ time_str: A string with the following components:
+ day[s] (optional)
+ hour[s] (optional)
+ minute[s] (optional)
+ second[s] (optional)
+
+ Returns a datetime.timedelta parsed from the string
+ """
+ parts = TIMEDELTA_REGEX.match(time_str)
+ if not parts:
+ return
+ parts = parts.groupdict()
+ time_params = {}
+ for (name, param) in parts.iteritems():
+ if param:
+ time_params[name] = int(param)
+ return timedelta(**time_params)
+
+ def to_json(self, value):
+ values = []
+ for attr in ('days', 'hours', 'minutes', 'seconds'):
+ cur_value = getattr(value, attr, 0)
+ if cur_value > 0:
+ values.append("%d %s" % (cur_value, attr))
+ return ' '.join(values)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 6ec1061451..55155810e9 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -149,7 +149,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
string to filename.html.
'''
try:
- return etree.fromstring(self.definition['data'])
+ return etree.fromstring(self.data)
except etree.XMLSyntaxError:
pass
@@ -161,7 +161,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
- file.write(self.definition['data'])
+ file.write(self.data)
# write out the relative name
relname = path(pathname).basename()
diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py
index ca6eae9913..034ec01253 100644
--- a/common/lib/xmodule/xmodule/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/self_assessment_module.py
@@ -18,13 +18,11 @@ from progress import Progress
from pkg_resources import resource_string
-from .capa_module import only_one, ComplexEncoder
+from .capa_module import ComplexEncoder
from .editing_module import EditingDescriptor
-from .html_checker import check_html
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
-from xmodule.modulestore import Location
from .model import List, String, Scope, Int
log = logging.getLogger("mitx.courseware")
@@ -436,7 +434,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
elt = etree.Element('selfassessment')
def add_child(k):
- child_str = '<{tag}>{body}{tag}>'.format(tag=k, body=self.definition[k])
+ child_str = '<{tag}>{body}{tag}>'.format(tag=k, body=getattr(self, k))
child_node = etree.fromstring(child_str)
elt.append(child_node)
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index aeebc6da6b..c6ea617d70 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -18,21 +18,12 @@ TEST_DIR = TEST_DIR / 'test'
DATA_DIR = TEST_DIR / 'data'
-def strip_metadata(descriptor, key):
- """
- Recursively strips tag from all children.
- """
- print "strip {key} from {desc}".format(key=key, desc=descriptor.location.url())
- descriptor.metadata.pop(key, None)
- for d in descriptor.get_children():
- strip_metadata(d, key)
-
def strip_filenames(descriptor):
"""
Recursively strips 'filename' from all children's definitions.
"""
print "strip filename from {desc}".format(desc=descriptor.location.url())
- descriptor.definition.pop('filename', None)
+ descriptor._model_data.pop('filename', None)
for d in descriptor.get_children():
strip_filenames(d)
@@ -73,10 +64,6 @@ class RoundTripTestCase(unittest.TestCase):
exported_course = courses2[0]
print "Checking course equality"
- # HACK: data_dir metadata tags break equality because they
- # aren't real metadata, and depend on paths. Remove them.
- strip_metadata(initial_course, 'data_dir')
- strip_metadata(exported_course, 'data_dir')
# HACK: filenames change when changing file formats
# during imports from old-style courses. Ignore them.
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 3a72a64dff..81a84edeaa 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,5 +1,5 @@
from xmodule.model import Namespace, Boolean, Scope, String, List
-from xmodule.fields import Date
+from xmodule.fields import Date, Timedelta
class StringyBoolean(Boolean):
@@ -39,3 +39,4 @@ class LmsNamespace(Namespace):
giturl = String(help="DO NOT USE", scope=Scope.settings, default='https://github.com/MITx')
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
+ graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
From 265a2e7630addb6facfc170cde85eee464d4d4ed Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:23:09 -0500
Subject: [PATCH 053/285] Give children the None scope
---
common/lib/xmodule/xmodule/model.py | 2 +-
common/lib/xmodule/xmodule/tests/test_model.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
index 380c81dbe9..9d3e96534e 100644
--- a/common/lib/xmodule/xmodule/model.py
+++ b/common/lib/xmodule/xmodule/model.py
@@ -121,7 +121,7 @@ class ParentModelMetaclass(type):
"""
def __new__(cls, name, bases, attrs):
if attrs.get('has_children', False):
- attrs['children'] = List(help='The children of this XModule', default=[], scope=Scope.settings)
+ attrs['children'] = List(help='The children of this XModule', default=[], scope=None)
else:
attrs['has_children'] = False
diff --git a/common/lib/xmodule/xmodule/tests/test_model.py b/common/lib/xmodule/xmodule/tests/test_model.py
index b0ae9e5844..20f1207701 100644
--- a/common/lib/xmodule/xmodule/tests/test_model.py
+++ b/common/lib/xmodule/xmodule/tests/test_model.py
@@ -36,7 +36,7 @@ def test_parent_metaclass():
assert not hasattr(WithoutChildren, 'children')
assert isinstance(HasChildren.children, List)
- assert_equals(Scope.settings, HasChildren.children.scope)
+ assert_equals(None, HasChildren.children.scope)
def test_field_access():
From 2509308ce924e71c89e11a461726becadc045f11 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:23:21 -0500
Subject: [PATCH 054/285] Remove reference to shared_state_key
---
lms/djangoapps/courseware/models.py | 11 +----------
1 file changed, 1 insertion(+), 10 deletions(-)
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index d6161e9f34..3bfb29fd5a 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -282,16 +282,7 @@ class StudentModuleCache(object):
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
+ return [descriptor.location.url() for descriptor in descriptors if descriptor.stores_state]
def lookup(self, course_id, module_type, module_state_key):
'''
From 6a612a6e024c3fd1b091c72301f7387861507d9d Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 13:23:34 -0500
Subject: [PATCH 055/285] Trim down list of equality attributes for descriptors
---
common/lib/xmodule/xmodule/x_module.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 43d6f58a16..3fc5cb538e 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -303,8 +303,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# A list of descriptor attributes that must be equal for the descriptors to
# be equal
- equality_attributes = ('definition', 'metadata', 'location',
- 'shared_state_key', '_inherited_metadata')
+ equality_attributes = ('_model_data', 'location')
# Name of resource directory to load templates from
template_dir_name = "default"
From 344cb133aab2c55cf61144e433ee417fa090a368 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 14:16:56 -0500
Subject: [PATCH 056/285] Make import/export tests pass
---
common/lib/xmodule/xmodule/capa_module.py | 16 ++++++++--------
.../xmodule/xmodule/modulestore/inheritance.py | 11 ++++++++++-
.../xmodule/xmodule/self_assessment_module.py | 12 ++++++------
.../lib/xmodule/xmodule/tests/test_import.py | 2 ++
common/lib/xmodule/xmodule/x_module.py | 18 ++++++------------
common/lib/xmodule/xmodule/xml_module.py | 18 ++++++++++++++++--
lms/xmodule_namespace.py | 3 +++
7 files changed, 51 insertions(+), 29 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index f83c31fb5c..7e66ef534e 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -61,7 +61,7 @@ class CapaModule(XModule):
max_attempts = StringyInt(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
- show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
+ showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
@@ -359,26 +359,26 @@ class CapaModule(XModule):
def answer_available(self):
''' Is the user allowed to see an answer?
'''
- if self.show_answer == '':
+ if self.showanswer == '':
return False
- if self.show_answer == "never":
+ if self.showanswer == "never":
return False
# Admins can see the answer, unless the problem explicitly prevents it
if self.system.user_is_staff:
return True
- if self.show_answer == 'attempted':
+ if self.showanswer == 'attempted':
return self.attempts > 0
- if self.show_answer == 'answered':
+ if self.showanswer == 'answered':
return self.done
- if self.show_answer == 'closed':
+ if self.showanswer == 'closed':
return self.closed()
- if self.show_answer == 'always':
+ if self.showanswer == 'always':
return True
return False
@@ -409,7 +409,7 @@ class CapaModule(XModule):
'''
event_info = dict()
event_info['problem_id'] = self.location.url()
- self.system.track_function('show_answer', event_info)
+ self.system.track_function('showanswer', event_info)
if not self.answer_available():
raise NotFoundError('Answer is not available')
else:
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
index ca88aab8d8..38c592564d 100644
--- a/common/lib/xmodule/xmodule/modulestore/inheritance.py
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -1,5 +1,14 @@
from xmodule.model import Scope
+# A list of metadata that this module can inherit from its parent module
+INHERITABLE_METADATA = (
+ 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
+ # TODO (ichuang): used for Fall 2012 xqa server access
+ 'xqa_key',
+ # TODO: This is used by the XMLModuleStore to provide for locations for
+ # static files, and will need to be removed when that code is removed
+ 'data_dir'
+)
def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata
@@ -24,7 +33,7 @@ def inherit_metadata(descriptor, model_data):
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
- for attr in descriptor.inheritable_metadata:
+ for attr in INHERITABLE_METADATA:
if attr not in descriptor._model_data and attr in model_data:
descriptor._inherited_metadata.add(attr)
descriptor._model_data[attr] = model_data[attr]
diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py
index 034ec01253..ffcd86ba52 100644
--- a/common/lib/xmodule/xmodule/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/self_assessment_module.py
@@ -73,8 +73,8 @@ class SelfAssessmentModule(XModule):
max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
- submit_message = String(scope=Scope.content)
- hint_prompt = String(scope=Scope.content)
+ submitmessage = String(scope=Scope.content)
+ hintprompt = String(scope=Scope.content)
def _allow_reset(self):
"""Can the module be reset?"""
@@ -209,7 +209,7 @@ class SelfAssessmentModule(XModule):
else:
hint = ''
- context = {'hint_prompt': self.hint_prompt,
+ context = {'hint_prompt': self.hintprompt,
'hint': hint}
if self.state == self.REQUEST_HINT:
@@ -228,7 +228,7 @@ class SelfAssessmentModule(XModule):
if self.state != self.DONE:
return ""
- return """
{0}
""".format(self.submit_message)
+ return """
{0}
""".format(self.submitmessage)
def save_answer(self, get):
@@ -397,8 +397,8 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
- submit_message = String(scope=Scope.content)
- hint_prompt = String(scope=Scope.content)
+ submitmessage = String(scope=Scope.content)
+ hintprompt = String(scope=Scope.content)
@classmethod
def definition_from_xml(cls, xml_object, system):
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index d243ca1609..803aceef68 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -12,6 +12,7 @@ from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.modulestore.inheritance import compute_inherited_metadata
from .test_export import DATA_DIR
@@ -134,6 +135,7 @@ class ImportTestCase(unittest.TestCase):
'''.format(due=v, org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml)
+ compute_inherited_metadata(descriptor)
print descriptor, descriptor._model_data
self.assertEqual(descriptor.lms.due, v)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 3fc5cb538e..dc1be3eb08 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -281,21 +281,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
__metaclass__ = XModuleMetaclass
# Attributes for inspection of the descriptor
- stores_state = False # Indicates whether the xmodule state should be
+
+ # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
- has_score = False # This indicates whether the xmodule is a problem-type.
+ stores_state = False
+
+ # This indicates whether the xmodule is a problem-type.
# It should respond to max_score() and grade(). It can be graded or ungraded
# (like a practice problem).
-
- # A list of metadata that this module can inherit from its parent module
- inheritable_metadata = (
- 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
- # TODO (ichuang): used for Fall 2012 xqa server access
- 'xqa_key',
- # TODO: This is used by the XMLModuleStore to provide for locations for
- # static files, and will need to be removed when that code is removed
- 'data_dir'
- )
+ has_score = False
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index d19fc26157..a7c1087db7 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -1,6 +1,7 @@
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
+from xmodule.model import Object, Scope
from lxml import etree
import json
import copy
@@ -78,6 +79,8 @@ class XmlDescriptor(XModuleDescriptor):
Mixin class for standardized parsing of from xml
"""
+ xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export", default={}, scope=Scope.settings)
+
# Extension to append to filename paths
filename_extension = 'xml'
@@ -102,7 +105,9 @@ class XmlDescriptor(XModuleDescriptor):
'tabs', 'grading_policy', 'is_draft', 'published_by', 'published_date',
'discussion_blackouts',
# VS[compat] -- remove the below attrs once everything is in the CMS
- 'course', 'org', 'url_name', 'filename')
+ 'course', 'org', 'url_name', 'filename',
+ # Used for storing xml attributes between import and export, for roundtrips
+ 'xml_attributes')
metadata_to_export_to_policy = ('discussion_topics')
@@ -319,6 +324,11 @@ class XmlDescriptor(XModuleDescriptor):
model_data.update(definition)
model_data['children'] = children
+ model_data['xml_attributes'] = {}
+ for key, value in metadata.items():
+ if key not in set(f.name for f in cls.fields + cls.lms.fields):
+ model_data['xml_attributes'][key] = value
+
return cls(
system,
location,
@@ -343,7 +353,7 @@ class XmlDescriptor(XModuleDescriptor):
def export_to_xml(self, resource_fs):
"""
- Returns an xml string representing this module, and all modules
+ Returns an xml string representign this module, and all modules
underneath it. May also write required resources out to resource_fs
Assumes that modules have single parentage (that no module appears twice
@@ -379,6 +389,10 @@ class XmlDescriptor(XModuleDescriptor):
#logging.debug('location.category = {0}, attr = {1}'.format(self.location.category, attr))
xml_object.set(attr, val)
+ for key, value in self.xml_attributes.items():
+ if key not in self.metadata_to_strip:
+ xml_object.set(key, value)
+
if self.export_to_file():
# Write the definition to a file
url_path = name_to_pathname(self.url_name)
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 81a84edeaa..14a19c6714 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -40,3 +40,6 @@ class LmsNamespace(Namespace):
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
+ showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
+ rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
+
From 85e109da5793e8163ef8f883612f2b2e2952fb20 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 21 Dec 2012 14:31:53 -0500
Subject: [PATCH 057/285] Allow field scope of None
---
common/lib/xmodule/xmodule/runtime.py | 30 +++++++++++++++------------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
index 6f20997894..6dee5b0b00 100644
--- a/common/lib/xmodule/xmodule/runtime.py
+++ b/common/lib/xmodule/xmodule/runtime.py
@@ -59,21 +59,25 @@ class DbModel(MutableMapping):
def _key(self, name):
field = self._getfield(name)
- module = field.scope.module
-
- if module == ModuleScope.ALL:
+ if field.scope is None:
module_id = None
- elif module == ModuleScope.USAGE:
- module_id = self._usage.id
- elif module == ModuleScope.DEFINITION:
- module_id = self._usage.def_id
- elif module == ModuleScope.TYPE:
- module_id = self._module_cls.__name__
-
- if field.scope.student:
- student_id = self._student_id
- else:
student_id = None
+ else:
+ module = field.scope.module
+
+ if module == ModuleScope.ALL:
+ module_id = None
+ elif module == ModuleScope.USAGE:
+ module_id = self._usage.id
+ elif module == ModuleScope.DEFINITION:
+ module_id = self._usage.def_id
+ elif module == ModuleScope.TYPE:
+ module_id = self._module_cls.__name__
+
+ if field.scope.student:
+ student_id = self._student_id
+ else:
+ student_id = None
key = KeyValueStore.Key(
scope=field.scope,
From 6427dd6742a6da2dc3a834c4e575f38dd125bdb5 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 28 Dec 2012 14:33:33 -0500
Subject: [PATCH 058/285] WIP: Get the cms running. Component previews work
---
cms/djangoapps/contentstore/utils.py | 4 +-
cms/djangoapps/contentstore/views.py | 123 +++++++++---------
.../models/settings/course_grading.py | 6 +-
cms/templates/edit_subsection.html | 10 +-
cms/templates/overview.html | 4 +-
cms/xmodule_namespace.py | 20 +++
common/lib/xmodule/xmodule/editing_module.py | 5 +-
common/lib/xmodule/xmodule/error_module.py | 2 +-
common/lib/xmodule/xmodule/mako_module.py | 4 +-
.../lib/xmodule/xmodule/modulestore/draft.py | 16 +--
.../xmodule/modulestore/inheritance.py | 7 +-
.../lib/xmodule/xmodule/modulestore/mongo.py | 79 ++++++++++-
common/lib/xmodule/xmodule/runtime.py | 11 ++
common/lib/xmodule/xmodule/video_module.py | 81 +++++-------
lms/djangoapps/courseware/model_data.py | 5 +-
setup.py | 3 +-
16 files changed, 241 insertions(+), 139 deletions(-)
create mode 100644 cms/xmodule_namespace.py
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index d6dcc3811d..cb42c5a1fc 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -122,7 +122,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
- if unit.metadata.get('is_draft', False):
+ if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
@@ -142,4 +142,4 @@ def update_item(location, value):
if value is None:
get_modulestore(location).delete_item(location)
else:
- get_modulestore(location).update_item(location, value)
\ No newline at end of file
+ get_modulestore(location).update_item(location, value)
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 809e43dea3..a6957e6107 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -29,11 +29,14 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
+from xmodule.model import Scope
+from xmodule.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from static_replace import replace_urls
from external_auth.views import ssl_login_shortcut
+from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
@@ -214,8 +217,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
- policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
- if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
+ policy_metadata = dict(
+ (key,value)
+ for field
+ in item.fields
+ if field.name not in ['display_name', 'start', 'due', 'format'] and
+ field.scope == Scope.settings
+ )
can_view_live = False
subsection_units = item.get_children()
@@ -312,11 +320,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item)
- try:
- published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
- except TypeError:
- published_date = None
-
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
@@ -327,11 +330,11 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
- 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
+ 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
- 'published_date': published_date,
+ 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@@ -395,9 +398,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute
"""
- instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(location)
- instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
+ instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
@@ -408,42 +410,11 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
- save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
+ print request.session.items()
+
return HttpResponse(ajax_return)
-def load_preview_state(request, preview_id, location):
- """
- Load the state of a preview module from the request
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- """
- if 'preview_states' not in request.session:
- request.session['preview_states'] = defaultdict(dict)
-
- instance_state = request.session['preview_states'][preview_id, location].get('instance')
- shared_state = request.session['preview_states'][preview_id, location].get('shared')
-
- return instance_state, shared_state
-
-
-def save_preview_state(request, preview_id, location, instance_state, shared_state):
- """
- Save the state of a preview module to the request
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- instance_state: The instance state to save
- shared_state: The shared state to save
- """
- if 'preview_states' not in request.session:
- request.session['preview_states'] = defaultdict(dict)
-
- request.session['preview_states'][preview_id, location]['instance'] = instance_state
- request.session['preview_states'][preview_id, location]['shared'] = shared_state
-
-
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
@@ -451,6 +422,30 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
+class SessionKeyValueStore(KeyValueStore):
+ def __init__(self, request, model_data):
+ self._model_data = model_data
+ self._session = request.session
+
+ def get(self, key):
+ try:
+ return self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ return self._session[tuple(key)]
+
+ def set(self, key, value):
+ try:
+ self._model_data[key.field_name] = value
+ except (KeyError, InvalidScopeError):
+ self._session[tuple(key)] = value
+
+ def delete(self, key):
+ try:
+ del self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ del self._session[tuple(key)]
+
+
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -461,6 +456,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
+ def preview_model_data(model_data):
+ return DbModel(
+ SessionKeyValueStore(request, model_data),
+ descriptor.module_class,
+ preview_id,
+ MongoUsage(preview_id, descriptor.location.url()),
+ )
+
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
@@ -471,6 +474,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=replace_urls,
user=request.user,
+ xmodule_model_data=preview_model_data,
)
@@ -484,11 +488,10 @@ def get_preview_module(request, preview_id, location):
location: A Location
"""
descriptor = modulestore().get_item(location)
- instance_state, shared_state = descriptor.get_sample_state()[0]
- return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
+ return load_preview_module(request, preview_id, descriptor)
-def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
+def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
@@ -502,10 +505,11 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
try:
module = descriptor.xmodule(system)
except:
+ log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
- ).xmodule_constructor(system)(None, None)
+ ).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
@@ -523,11 +527,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls(
module.get_html,
- module.metadata.get('data_dir', module.location.course),
+ getattr(module, 'data_dir', module.location.course),
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])
)
- save_preview_state(request, preview_id, descriptor.location.url(),
- module.get_instance_state(), module.get_shared_state())
return module
@@ -541,7 +543,7 @@ def get_module_previews(request, descriptor):
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
- module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
+ module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
@@ -625,23 +627,26 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key in posted_metadata.keys():
+ for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
+ print "DELETING", metadata_key, value
+ print metadata_key in existing_item._model_data
# remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in existing_item.metadata:
- del existing_item.metadata[metadata_key]
+ if metadata_key in existing_item._model_data:
+ del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
-
- # overlay the new metadata over the modulestore sourced collection to support partial updates
- existing_item.metadata.update(posted_metadata)
+ else:
+ existing_item._model_data[metadata_key] = value
# commit to datastore
- store.update_metadata(item_location, existing_item.metadata)
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ print existing_item._model_data._kvs._metadata
+ store.update_metadata(item_location, existing_item._model_data._kvs._metadata)
return HttpResponse()
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index 9ddbe87727..ce229dd196 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -237,15 +237,15 @@ class CourseGradingModel:
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
- hours_from_day = rawgrace.days*24
+ hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
return {
- 'hours': hourse_from_days + hours_from_seconds,
- 'minutes': minutes_from_seconds,
+ 'hours': hours_from_days + hours_from_seconds,
+ 'minutes': minutes,
'seconds': seconds,
}
else:
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index 53567c73e1..e98ad526ed 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -25,7 +25,7 @@
-
+
@@ -54,13 +54,13 @@
<%
- start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
- parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
+ start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
+ parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
- % if subsection.start != parent_item.start and subsection.start:
+ % if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None:
The date above differs from the release date of ${parent_item.lms.display_name}, which is unset.
% else:
@@ -83,7 +83,7 @@
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
- due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
+ due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due is not None else None
%>
diff --git a/cms/templates/overview.html b/cms/templates/overview.html
index fad28dbf07..099cfb1a5b 100644
--- a/cms/templates/overview.html
+++ b/cms/templates/overview.html
@@ -141,7 +141,7 @@
<%
- start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
+ start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%>
@@ -178,7 +178,7 @@
-
+
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
new file mode 100644
index 0000000000..0ea729e27b
--- /dev/null
+++ b/cms/xmodule_namespace.py
@@ -0,0 +1,20 @@
+import datetime
+
+from xmodule.model import Namespace, Boolean, Scope, ModelType, String
+
+
+class DateTuple(ModelType):
+ """
+ ModelType that stores datetime objects as time tuples
+ """
+ def from_json(self, value):
+ return datetime.datetime(*value)
+
+ def to_json(self, value):
+ return list(value.timetuple())
+
+
+class CmsNamespace(Namespace):
+ is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
+ published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
+ published_by = String(help="Id of the user who published this module", scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py
index e025179b63..531fd7d8b9 100644
--- a/common/lib/xmodule/xmodule/editing_module.py
+++ b/common/lib/xmodule/xmodule/editing_module.py
@@ -1,5 +1,6 @@
from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.model import Scope, String
import logging
log = logging.getLogger(__name__)
@@ -14,13 +15,15 @@ class EditingDescriptor(MakoModuleDescriptor):
"""
mako_template = "widgets/raw-edit.html"
+ data = String(scope=Scope.content, default='')
+
# cdodge: a little refactoring here, since we're basically doing the same thing
# here as with our parent class, let's call into it to get the basic fields
# set and then add our additional fields. Trying to keep it DRY.
def get_context(self):
_context = MakoModuleDescriptor.get_context(self)
# Add our specific template information (the raw data body)
- _context.update({'data': self.definition.get('data', '')})
+ _context.update({'data': self.data})
return _context
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index 37e98b5b77..6a06b3ad3a 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -106,7 +106,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
- json.dumps(descriptor._model_data, indent=4),
+ descriptor._model_data,
error_msg,
location=descriptor.location,
)
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index bdf3cb4749..8ae68051a0 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -32,9 +32,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
"""
Return the context to render the mako template with
"""
- return {'module': self,
- 'editable_metadata_fields': self.editable_fields
- }
+ return {'module': self}
def get_html(self):
return self.system.render_template(
diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py
index 5fbf05ed9b..100bdb1dc6 100644
--- a/common/lib/xmodule/xmodule/modulestore/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/draft.py
@@ -15,11 +15,11 @@ def as_draft(location):
def wrap_draft(item):
"""
- Sets `item.metadata['is_draft']` to `True` if the item is a
- draft, and false otherwise. Sets the item's location to the
+ Sets `item.cms.is_draft` to `True` if the item is a
+ draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case
"""
- item.metadata['is_draft'] = item.location.revision == DRAFT
+ item.cms.is_draft = item.location.revision == DRAFT
item.location = item.location._replace(revision=None)
return item
@@ -112,7 +112,7 @@ class DraftModuleStore(ModuleStoreBase):
"""
draft_loc = as_draft(location)
draft_item = self.get_item(location)
- if not draft_item.metadata['is_draft']:
+ if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data)
@@ -127,7 +127,7 @@ class DraftModuleStore(ModuleStoreBase):
"""
draft_loc = as_draft(location)
draft_item = self.get_item(location)
- if not draft_item.metadata['is_draft']:
+ if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children)
@@ -143,7 +143,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location)
draft_item = self.get_item(location)
- if not draft_item.metadata['is_draft']:
+ if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
if 'is_draft' in metadata:
@@ -175,8 +175,8 @@ class DraftModuleStore(ModuleStoreBase):
draft = self.get_item(location)
metadata = {}
metadata.update(draft.metadata)
- metadata['published_date'] = tuple(datetime.utcnow().timetuple())
- metadata['published_by'] = published_by_id
+ metadata.cms.published_date = datetime.utcnow()
+ metadata.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {}))
super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
super(DraftModuleStore, self).update_metadata(location, metadata)
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
index 38c592564d..18cf0b3351 100644
--- a/common/lib/xmodule/xmodule/modulestore/inheritance.py
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -52,6 +52,11 @@ def own_metadata(module):
field.name not in inherited_metadata and
field.name in module._model_data):
- metadata[field.name] = module._model_data[field.name]
+ try:
+ metadata[field.name] = module._model_data[field.name]
+ except KeyError:
+ # Ignore any missing keys in _model_data
+ pass
+
return metadata
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index d145523d16..5c55c4d507 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -1,7 +1,9 @@
import pymongo
import sys
+import logging
from bson.son import SON
+from collections import namedtuple
from fs.osfs import OSFS
from itertools import repeat
from path import path
@@ -11,17 +13,79 @@ from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor
+from xmodule.runtime import DbModel, KeyValueStore, InvalidScopeError
+from xmodule.model import Scope
from . import ModuleStoreBase, Location
from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError,
DuplicateItemError)
+
+log = logging.getLogger(__name__)
+
# TODO (cpennington): This code currently operates under the assumption that
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
+class MongoKeyValueStore(KeyValueStore):
+ """
+ A KeyValueStore that maps keyed data access to one of the 3 data areas
+ known to the MongoModuleStore (data, children, and metadata)
+ """
+ def __init__(self, data, children, metadata):
+ self._data = data
+ self._children = children
+ self._metadata = metadata
+
+ def get(self, key):
+ print "GET", key
+ if key.field_name == 'children':
+ return self._children
+ elif key.scope == Scope.settings:
+ return self._metadata[key.field_name]
+ elif key.scope == Scope.content:
+ if key.field_name == 'data' and not isinstance(self._data, dict):
+ return self._data
+ else:
+ return self._data[key.field_name]
+ else:
+ raise InvalidScopeError(key.scope)
+
+ def set(self, key, value):
+ print "SET", key, value
+ if key.field_name == 'children':
+ self._children = value
+ elif key.scope == Scope.settings:
+ self._metadata[key.field_name] = value
+ elif key.scope == Scope.content:
+ if key.field_name == 'data' and not isinstance(self._data, dict):
+ self._data = value
+ else:
+ self._data[key.field_name] = value
+ else:
+ raise InvalidScopeError(key.scope)
+
+ def delete(self, key):
+ print "DELETE", key
+ if key.field_name == 'children':
+ self._children = []
+ elif key.scope == Scope.settings:
+ if key.field_name in self._metadata:
+ del self._metadata[key.field_name]
+ elif key.scope == Scope.content:
+ if key.field_name == 'data' and not isinstance(self._data, dict):
+ self._data = None
+ else:
+ del self._data[key.field_name]
+ else:
+ raise InvalidScopeError(key.scope)
+
+
+MongoUsage = namedtuple('MongoUsage', 'id, def_id')
+
+
class CachingDescriptorSystem(MakoDescriptorSystem):
"""
A system that has a cache of module json that it will use to load modules
@@ -64,8 +128,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# always load an entire course. We're punting on this until after launch, and then
# will build a proper course policy framework.
try:
- return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
+ class_ = XModuleDescriptor.load_class(
+ json_data['location']['category'],
+ self.default_class
+ )
+ definition = json_data.get('definition', {})
+ kvs = MongoKeyValueStore(
+ definition.get('data', {}),
+ definition.get('children', []),
+ json_data.get('metadata', {}),
+ )
+
+ model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
+ return class_(self, location, model_data)
except:
+ log.debug("Failed to load descriptor", exc_info=True)
return ErrorDescriptor.from_json(
json_data,
self,
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
index 6dee5b0b00..5a5533133b 100644
--- a/common/lib/xmodule/xmodule/runtime.py
+++ b/common/lib/xmodule/xmodule/runtime.py
@@ -3,6 +3,13 @@ from collections import MutableMapping, namedtuple
from .model import ModuleScope, ModelType
+class InvalidScopeError(Exception):
+ """
+ Raised to indicated that operating on the supplied scope isn't allowed by a KeyValueStore
+ """
+ pass
+
+
class KeyValueStore(object):
"""The abstract interface for Key Value Stores."""
@@ -102,8 +109,12 @@ class DbModel(MutableMapping):
def __len__(self):
return len(self.keys())
+ def __contains__(self, item):
+ return item in self.keys()
+
def keys(self):
fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces:
fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
+ print fields
return fields
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index ecc7d7fdad..34ce353afd 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -28,13 +28,41 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
- youtube = String(help="Youtube ids for each speed, in the format :[,: ...]", scope=Scope.content)
- show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
- source = String(help="External source for this video", scope=Scope.content)
- track = String(help="Subtitle file", scope=Scope.content)
- position = Int(help="Current position in the video", scope=Scope.student_state, default=0)
+ data = String(help="XML data for the problem", scope=Scope.content)
+ position = Int(help="Current position in the video", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
+ def __init__(self, *args, **kwargs):
+ XModule.__init__(self, *args, **kwargs)
+
+ xmltree = etree.fromstring(self.data)
+ self.youtube = xmltree.get('youtube')
+ self.position = 0
+ self.show_captions = xmltree.get('show_captions', 'true')
+ self.source = self._get_source(xmltree)
+ self.track = self._get_track(xmltree)
+
+ def _get_source(self, xmltree):
+ # find the first valid source
+ return self._get_first_external(xmltree, 'source')
+
+ def _get_track(self, xmltree):
+ # find the first valid track
+ return self._get_first_external(xmltree, 'track')
+
+ def _get_first_external(self, xmltree, tag):
+ """
+ Will return the first valid element
+ of the given tag.
+ 'valid' means has a non-empty 'src' attribute
+ """
+ result = None
+ for element in xmltree.findall(tag):
+ src = element.get('src')
+ if src:
+ result = src
+ break
+ return result
def handle_ajax(self, dispatch, get):
'''
@@ -87,50 +115,7 @@ class VideoModule(XModule):
})
-
class VideoDescriptor(RawDescriptor):
module_class = VideoModule
stores_state = True
template_dir_name = "video"
-
- youtube = String(help="Youtube ids for each speed, in the format :[,: ...]", scope=Scope.content)
- show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
- source = String(help="External source for this video", scope=Scope.content)
- track = String(help="Subtitle file", scope=Scope.content)
-
- @classmethod
- def definition_from_xml(cls, xml_object, system):
- return {
- 'youtube': xml_object.get('youtube'),
- 'show_captions': xml_object.get('show_captions', 'true'),
- 'source': _get_first_external(xml_object, 'source'),
- 'track': _get_first_external(xml_object, 'track'),
- }, []
-
- def definition_to_xml(self, resource_fs):
- xml_object = etree.Element('video', {
- 'youtube': self.youtube,
- 'show_captions': self.show_captions,
- })
-
- if self.source is not None:
- SubElement(xml_object, 'source', {'src': self.source})
-
- if self.track is not None:
- SubElement(xml_object, 'track', {'src': self.track})
-
- return xml_object
-
-def _get_first_external(xmltree, tag):
- """
- Will return the first valid element
- of the given tag.
- 'valid' means has a non-empty 'src' attribute
- """
- result = None
- for element in xmltree.findall(tag):
- src = element.get('src')
- if src:
- result = src
- break
- return result
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index e8dfb025ed..b2f2d3ef48 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -8,13 +8,10 @@ from .models import (
XModuleStudentInfoField
)
-from xmodule.runtime import DbModel, KeyValueStore
+from xmodule.runtime import KeyValueStore, InvalidScopeError
from xmodule.model import Scope
-class InvalidScopeError(Exception):
- pass
-
class InvalidWriteError(Exception):
pass
diff --git a/setup.py b/setup.py
index 84f242cf3d..a07f836413 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,8 @@ setup(
# for a description of entry_points
entry_points={
'xmodule.namespace': [
- 'lms = lms.xmodule_namespace:LmsNamespace'
+ 'lms = lms.xmodule_namespace:LmsNamespace',
+ 'cms = cms.xmodule_namespace:CmsNamespace',
],
}
)
\ No newline at end of file
From 60fa8619cbe10b13144a627a0b6d7c25371af4ba Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 2 Jan 2013 10:37:49 -0500
Subject: [PATCH 059/285] Fixing tests
---
.../contentstore/module_info_model.py | 121 +++++++++---------
.../tests/test_course_settings.py | 4 +-
cms/djangoapps/contentstore/views.py | 7 +-
.../models/settings/course_grading.py | 43 +++++--
common/lib/xmodule/xmodule/capa_module.py | 8 --
common/lib/xmodule/xmodule/course_module.py | 4 +-
.../lib/xmodule/xmodule/modulestore/mongo.py | 7 +-
.../xmodule/modulestore/store_utilities.py | 18 +--
common/lib/xmodule/xmodule/runtime.py | 1 -
.../lib/xmodule/xmodule/tests/test_import.py | 4 +-
10 files changed, 111 insertions(+), 106 deletions(-)
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index 0017010885..c6e97eb73d 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -8,77 +8,78 @@ import re
from django.http import HttpResponseBadRequest, Http404
def get_module_info(store, location, parent_location = None, rewrite_static_links = False):
- try:
- if location.revision is None:
- module = store.get_item(location)
- else:
- module = store.get_item(location)
- except ItemNotFoundError:
- raise Http404
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except ItemNotFoundError:
+ raise Http404
- data = module.definition['data']
- if rewrite_static_links:
- data = replace_urls(module.definition['data'], course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]))
+ data = module.data
+ if rewrite_static_links:
+ data = replace_urls(module.data, course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]))
- return {
+ return {
'id': module.location.url(),
'data': data,
- 'metadata': module.metadata
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ 'metadata': module._model_data._kvs._metadata
}
def set_module_info(store, location, post_data):
- module = None
- isNew = False
- try:
- if location.revision is None:
- module = store.get_item(location)
- else:
- module = store.get_item(location)
- except:
- pass
+ module = None
+ isNew = False
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except:
+ pass
- if module is None:
- # new module at this location
- # presume that we have an 'Empty' template
- template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
- module = store.clone_item(template_location, location)
- isNew = True
+ if module is None:
+ # new module at this location
+ # presume that we have an 'Empty' template
+ template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
+ module = store.clone_item(template_location, location)
+ isNew = True
- if post_data.get('data') is not None:
- data = post_data['data']
- store.update_item(location, data)
-
- # cdodge: note calling request.POST.get('children') will return None if children is an empty array
- # so it lead to a bug whereby the last component to be deleted in the UI was not actually
- # deleting the children object from the children collection
- if 'children' in post_data and post_data['children'] is not None:
- children = post_data['children']
- store.update_children(location, children)
+ if post_data.get('data') is not None:
+ data = post_data['data']
+ store.update_item(location, data)
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if post_data.get('metadata') is not None:
- posted_metadata = post_data['metadata']
+ # cdodge: note calling request.POST.get('children') will return None if children is an empty array
+ # so it lead to a bug whereby the last component to be deleted in the UI was not actually
+ # deleting the children object from the children collection
+ if 'children' in post_data and post_data['children'] is not None:
+ children = post_data['children']
+ store.update_children(location, children)
- # update existing metadata with submitted metadata (which can be partial)
- # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key in posted_metadata.keys():
+ # cdodge: also commit any metadata which might have been passed along in the
+ # POST from the client, if it is there
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
+ if post_data.get('metadata') is not None:
+ posted_metadata = post_data['metadata']
- # let's strip out any metadata fields from the postback which have been identified as system metadata
- # and therefore should not be user-editable, so we should accept them back from the client
- if metadata_key in module.system_metadata_fields:
- del posted_metadata[metadata_key]
- elif posted_metadata[metadata_key] is None:
- # remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in module.metadata:
- del module.metadata[metadata_key]
- del posted_metadata[metadata_key]
+ # update existing metadata with submitted metadata (which can be partial)
+ # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
+ for metadata_key in posted_metadata.keys():
- # overlay the new metadata over the modulestore sourced collection to support partial updates
- module.metadata.update(posted_metadata)
+ # let's strip out any metadata fields from the postback which have been identified as system metadata
+ # and therefore should not be user-editable, so we should accept them back from the client
+ if metadata_key in module.system_metadata_fields:
+ del posted_metadata[metadata_key]
+ elif posted_metadata[metadata_key] is None:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ if metadata_key in module._model_data:
+ del module._model_data[metadata_key]
+ del posted_metadata[metadata_key]
+ else:
+ module._model_data[metadata_key] = value
- # commit to datastore
- store.update_metadata(location, module.metadata)
+ # commit to datastore
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ store.update_metadata(item_location, module._model_data._kvs._metadata)
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 74eff6e9cc..a9ac6f9248 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -255,8 +255,10 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
- test_grader.grace_period = {'hours' : '4'}
+ test_grader.grace_period = {'hours': '4'}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
+ print test_grader.__dict__
+ print altered_grader.__dict__
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self):
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index a6957e6107..e027efc1bf 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -410,8 +410,6 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
- print request.session.items()
-
return HttpResponse(ajax_return)
@@ -634,8 +632,6 @@ def save_item(request):
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
- print "DELETING", metadata_key, value
- print metadata_key in existing_item._model_data
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
@@ -645,7 +641,6 @@ def save_item(request):
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- print existing_item._model_data._kvs._metadata
store.update_metadata(item_location, existing_item._model_data._kvs._metadata)
return HttpResponse()
@@ -1231,7 +1226,7 @@ def initialize_course_tabs(course):
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
- modulestore('direct').update_metadata(course.location.url(), own_metadata(new_course))
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie
@login_required
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index ce229dd196..e7c98908f8 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import re
from util import converters
+from datetime import timedelta
class CourseGradingModel:
@@ -91,7 +92,7 @@ class CourseGradingModel:
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@@ -119,7 +120,7 @@ class CourseGradingModel:
else:
descriptor.raw_grader.append(grader)
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
@@ -155,11 +156,17 @@ class CourseGradingModel:
if 'grace_period' in graceperiodjson:
graceperiodjson = graceperiodjson['grace_period']
- grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()])
+ timedelta_kwargs = dict(
+ (key, float(val))
+ for key, val
+ in graceperiodjson.items()
+ if key in ('days', 'seconds', 'minutes', 'hours')
+ )
+ grace_rep = timedelta(**timedelta_kwargs)
descriptor = get_modulestore(course_location).get_item(course_location)
- descriptor.metadata['graceperiod'] = grace_rep
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ descriptor.lms.graceperiod = grace_rep
+ get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def delete_grader(course_location, index):
@@ -170,12 +177,12 @@ class CourseGradingModel:
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
- index = int(index)
+ index = int(index)
if index < len(descriptor.raw_grader):
del descriptor.raw_grader[index]
# force propagation to definition
descriptor.raw_grader = descriptor.raw_grader
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
@@ -188,7 +195,7 @@ class CourseGradingModel:
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
@@ -202,7 +209,7 @@ class CourseGradingModel:
descriptor = get_modulestore(course_location).get_item(course_location)
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._data)
@staticmethod
def get_section_grader_type(location):
@@ -240,14 +247,22 @@ class CourseGradingModel:
hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
+ hours = hours_from_days + hours_from_seconds
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
- return {
- 'hours': hours_from_days + hours_from_seconds,
- 'minutes': minutes,
- 'seconds': seconds,
- }
+
+ graceperiod = {}
+ if hours > 0:
+ graceperiod['hours'] = str(hours)
+
+ if minutes > 0:
+ graceperiod['minutes'] = str(minutes)
+
+ if seconds > 0:
+ graceperiod['seconds'] = str(seconds)
+
+ return graceperiod
else:
return None
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 7e66ef534e..a41e634c36 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -421,13 +421,6 @@ class CapaModule(XModule):
new_answers = dict()
for answer_id in answers:
try:
-<<<<<<< HEAD
-<<<<<<< HEAD
- new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
-=======
- new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.descriptor.data_dir)}
->>>>>>> WIP: Save student state via StudentModule. Inheritance doesn't work
-=======
new_answer = {
answer_id: self.system.replace_urls(
answers[answer_id],
@@ -435,7 +428,6 @@ class CapaModule(XModule):
course_namespace=self.location
)
}
->>>>>>> Fix more errors in tests
except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]}
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 444fcaf52d..38cf81f3af 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -13,7 +13,7 @@ import time
import copy
from .model import Scope, ModelType, List, String, Object, Boolean
-from .x_module import Date
+from .fields import Date
log = logging.getLogger(__name__)
@@ -99,7 +99,7 @@ class CourseDescriptor(SequenceDescriptor):
start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = Date(help="Date that this course is advertised to start", scope=Scope.settings)
- grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
+ grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content, default={})
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 5c55c4d507..b59e6c50fb 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -20,6 +20,7 @@ from . import ModuleStoreBase, Location
from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError,
DuplicateItemError)
+from .inheritance import own_metadata
log = logging.getLogger(__name__)
@@ -40,7 +41,6 @@ class MongoKeyValueStore(KeyValueStore):
self._metadata = metadata
def get(self, key):
- print "GET", key
if key.field_name == 'children':
return self._children
elif key.scope == Scope.settings:
@@ -54,7 +54,6 @@ class MongoKeyValueStore(KeyValueStore):
raise InvalidScopeError(key.scope)
def set(self, key, value):
- print "SET", key, value
if key.field_name == 'children':
self._children = value
elif key.scope == Scope.settings:
@@ -68,7 +67,6 @@ class MongoKeyValueStore(KeyValueStore):
raise InvalidScopeError(key.scope)
def delete(self, key):
- print "DELETE", key
if key.field_name == 'children':
self._children = []
elif key.scope == Scope.settings:
@@ -457,6 +455,7 @@ class MongoModuleStore(ModuleStoreBase):
tab['name'] = metadata.get('display_name')
break
course.tabs = existing_tabs
+ self.update_metadata(course.location, own_metadata(course))
self._update_single_item(location, {'metadata': metadata})
@@ -474,7 +473,7 @@ class MongoModuleStore(ModuleStoreBase):
course = self.get_course_for_item(item.location)
existing_tabs = course.tabs or []
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
- self.update_metadata(course.location, course.metadata)
+ self.update_metadata(course.location, own_metadata(course))
self.collection.remove({'_id': Location(location).dict()})
diff --git a/common/lib/xmodule/xmodule/modulestore/store_utilities.py b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
index af346dbb7e..bab92c6c2d 100644
--- a/common/lib/xmodule/xmodule/modulestore/store_utilities.py
+++ b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
@@ -40,22 +40,24 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
print "Cloning module {0} to {1}....".format(original_loc, module.location)
- if 'data' in module.definition:
- modulestore.update_item(module.location, module.definition['data'])
+ modulestore.update_item(module.location, module._model_data._kvs._data)
# repoint children
- if 'children' in module.definition:
+ if module.has_children:
new_children = []
- for child_loc_url in module.definition['children']:
+ for child_loc_url in module.children:
child_loc = Location(child_loc_url)
- child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org,
- course = dest_location.course)
- new_children = new_children + [child_loc.url()]
+ child_loc = child_loc._replace(
+ tag = dest_location.tag,
+ org = dest_location.org,
+ course = dest_location.course
+ )
+ new_children.append(child_loc.url())
modulestore.update_children(module.location, new_children)
# save metadata
- modulestore.update_metadata(module.location, module.metadata)
+ modulestore.update_metadata(module.location, module._model_data._kvs._metadata)
# now iterate through all of the assets and clone them
# first the thumbnails
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
index 5a5533133b..8cbfc3ae36 100644
--- a/common/lib/xmodule/xmodule/runtime.py
+++ b/common/lib/xmodule/xmodule/runtime.py
@@ -116,5 +116,4 @@ class DbModel(MutableMapping):
fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces:
fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
- print fields
return fields
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 803aceef68..31dbb131f9 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -271,8 +271,8 @@ class ImportTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
- self.assertEqual(toy_video.youtube, "1.0:p2Q6BrNhdh8")
- self.assertEqual(two_toy_video.youtube, "1.0:p2Q6BrNhdh9")
+ self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8")
+ self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9")
def test_colon_in_url_name(self):
From 8e0d218c7dd1b891fd9f54721bed271774bd9efb Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Wed, 2 Jan 2013 15:06:43 -0500
Subject: [PATCH 060/285] Stop using .definition and .metadata directly
---
.../contentstore/course_info_model.py | 14 ++++-----
cms/djangoapps/contentstore/views.py | 2 +-
.../models/settings/course_grading.py | 10 +++---
cms/templates/edit_subsection.html | 2 +-
cms/templates/widgets/metadata-edit.html | 16 +++++-----
common/lib/xmodule/xmodule/mako_module.py | 30 +++++++++++++++---
common/lib/xmodule/xmodule/model.py | 31 +++++++++++++++++++
.../lib/xmodule/xmodule/modulestore/draft.py | 12 +++----
.../lib/xmodule/xmodule/modulestore/mongo.py | 6 ++--
common/lib/xmodule/xmodule/x_module.py | 1 +
10 files changed, 87 insertions(+), 37 deletions(-)
diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index c2e8348a66..23fad96caa 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -24,7 +24,7 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.definition['data'])
+ course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("")
@@ -61,7 +61,7 @@ def update_course_updates(location, update, passed_id=None):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.definition['data'])
+ course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("")
@@ -82,8 +82,8 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
- course_updates.definition['data'] = etree.tostring(course_html_parsed)
- modulestore('direct').update_item(location, course_updates.definition['data'])
+ course_updates.data = etree.tostring(course_html_parsed)
+ modulestore('direct').update_item(location, course_updates.data)
return {"id" : passed_id,
"date" : update['date'],
@@ -105,7 +105,7 @@ def delete_course_update(location, update, passed_id):
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.definition['data'])
+ course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("")
@@ -118,9 +118,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
- course_updates.definition['data'] = etree.tostring(course_html_parsed)
+ course_updates.data = etree.tostring(course_html_parsed)
store = modulestore('direct')
- store.update_item(location, course_updates.definition['data'])
+ store.update_item(location, course_updates.data)
return get_course_updates(location)
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index e027efc1bf..9ca8e5e8ec 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -218,7 +218,7 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict(
- (key,value)
+ (field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index e7c98908f8..f2795e80da 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -230,13 +230,13 @@ class CourseGradingModel:
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
- descriptor.metadata['format'] = jsondict.get('graderType')
- descriptor.metadata['graded'] = True
+ descriptor.lms.format = jsondict.get('graderType')
+ descriptor.lms.graded = True
else:
- if 'format' in descriptor.metadata: del descriptor.metadata['format']
- if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
+ del descriptor.lms.format
+ del descriptor.lms.graded
- get_modulestore(location).update_metadata(location, descriptor.metadata)
+ get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._data)
@staticmethod
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index e98ad526ed..196aedac01 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -73,7 +73,7 @@
\ No newline at end of file
From 789ac3fc875aa26380fc7f0865dc5c89a7359473 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 4 Jan 2013 16:19:58 -0500
Subject: [PATCH 067/285] Use the XBlock library as the base for XModule, so
that we can incrementally rely on more and more of the XBlock api
---
cms/djangoapps/contentstore/views.py | 6 +-
cms/xmodule_namespace.py | 2 +-
common/lib/xmodule/xmodule/abtest_module.py | 2 +-
common/lib/xmodule/xmodule/capa_module.py | 10 +-
common/lib/xmodule/xmodule/course_module.py | 2 +-
.../lib/xmodule/xmodule/discussion_module.py | 2 +-
common/lib/xmodule/xmodule/editing_module.py | 2 +-
common/lib/xmodule/xmodule/error_module.py | 2 +-
common/lib/xmodule/xmodule/fields.py | 2 +-
common/lib/xmodule/xmodule/html_module.py | 2 +-
common/lib/xmodule/xmodule/mako_module.py | 2 +-
common/lib/xmodule/xmodule/model.py | 217 ------------------
.../xmodule/modulestore/inheritance.py | 2 +-
.../lib/xmodule/xmodule/modulestore/mongo.py | 12 +-
common/lib/xmodule/xmodule/poll_module.py | 6 +-
common/lib/xmodule/xmodule/raw_module.py | 2 +-
common/lib/xmodule/xmodule/runtime.py | 119 ----------
.../xmodule/xmodule/self_assessment_module.py | 12 +-
common/lib/xmodule/xmodule/seq_module.py | 4 +-
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../lib/xmodule/xmodule/tests/test_model.py | 163 -------------
.../lib/xmodule/xmodule/tests/test_runtime.py | 105 ---------
common/lib/xmodule/xmodule/video_module.py | 4 +-
common/lib/xmodule/xmodule/x_module.py | 20 +-
common/lib/xmodule/xmodule/xml_module.py | 2 +-
github-requirements.txt | 1 +
lms/djangoapps/courseware/model_data.py | 19 +-
lms/djangoapps/courseware/module_render.py | 6 +-
.../courseware/tests/test_model_data.py | 8 +-
lms/xmodule_namespace.py | 2 +-
setup.py | 4 +-
31 files changed, 69 insertions(+), 675 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/model.py
delete mode 100644 common/lib/xmodule/xmodule/runtime.py
delete mode 100644 common/lib/xmodule/xmodule/tests/test_model.py
delete mode 100644 common/lib/xmodule/xmodule/tests/test_runtime.py
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 2b650a525b..2fd395cff9 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -29,8 +29,8 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
-from xmodule.model import Scope
-from xmodule.runtime import KeyValueStore, DbModel, InvalidScopeError
+from xblock.core import Scope
+from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
@@ -472,7 +472,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=replace_urls,
user=request.user,
- xmodule_model_data=preview_model_data,
+ xblock_model_data=preview_model_data,
)
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
index 3ade59532a..9e30105abc 100644
--- a/cms/xmodule_namespace.py
+++ b/cms/xmodule_namespace.py
@@ -1,6 +1,6 @@
import datetime
-from xmodule.model import Namespace, Boolean, Scope, ModelType, String
+from xblock.core import Namespace, Boolean, Scope, ModelType, String
class DateTuple(ModelType):
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index f33a3db91c..f511558693 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
-from .model import String, Scope, Object, ModuleScope
+from xblock.core import String, Scope, Object, BlockScope
DEFAULT = "_DEFAULT_GROUP"
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index a41e634c36..0d04b419a5 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -17,13 +17,13 @@ from progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
-from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
+from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float
from .fields import Timedelta
log = logging.getLogger("mitx.courseware")
-class StringyInt(Int):
+class StringyInteger(Integer):
"""
A model type that converts from strings to integers when reading from json
"""
@@ -57,8 +57,8 @@ class CapaModule(XModule):
'''
icon_class = 'problem'
- attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
- max_attempts = StringyInt(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
+ attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
+ max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
@@ -69,7 +69,7 @@ class CapaModule(XModule):
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
- seed = Int(help="Random seed for this student", scope=Scope.student_state)
+ seed = Integer(help="Random seed for this student", scope=Scope.student_state)
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 38cf81f3af..3b73b6a4d1 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -12,7 +12,7 @@ import requests
import time
import copy
-from .model import Scope, ModelType, List, String, Object, Boolean
+from xblock.core import Scope, ModelType, List, String, Object, Boolean
from .fields import Date
log = logging.getLogger(__name__)
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index b373f337fb..6a9edbbd70 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -3,7 +3,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
-from .model import String, Scope
+from xblock.core import String, Scope
class DiscussionModule(XModule):
js = {'coffee':
diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py
index 531fd7d8b9..9ff0124dc6 100644
--- a/common/lib/xmodule/xmodule/editing_module.py
+++ b/common/lib/xmodule/xmodule/editing_module.py
@@ -1,6 +1,6 @@
from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor
-from xmodule.model import Scope, String
+from xblock.core import Scope, String
import logging
log = logging.getLogger(__name__)
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index 6a06b3ad3a..d2f67d68a6 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -8,7 +8,7 @@ from xmodule.x_module import XModule
from xmodule.editing_module import JSONEditingDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
-from .model import String, Scope
+from xblock.core import String, Scope
log = logging.getLogger(__name__)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 21c360f914..fb80752e56 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -3,7 +3,7 @@ import logging
import re
from datetime import timedelta
-from .model import ModelType
+from xblock.core import ModelType
log = logging.getLogger(__name__)
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 55155810e9..f922181046 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -15,7 +15,7 @@ from .html_checker import check_html
from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
-from .model import Scope, String
+from xblock.core import Scope, String
log = logging.getLogger("mitx.courseware")
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index 66a4b647d3..adfe28387d 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -1,5 +1,5 @@
from .x_module import XModuleDescriptor, DescriptorSystem
-from .model import Scope
+from xblock.core import Scope
import logging
diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py
deleted file mode 100644
index b8228cb417..0000000000
--- a/common/lib/xmodule/xmodule/model.py
+++ /dev/null
@@ -1,217 +0,0 @@
-from collections import namedtuple
-from .plugin import Plugin
-
-
-class ModuleScope(object):
- USAGE, DEFINITION, TYPE, ALL = xrange(4)
-
-
-class Scope(namedtuple('ScopeBase', 'student module')):
- pass
-
-Scope.content = Scope(student=False, module=ModuleScope.DEFINITION)
-Scope.settings = Scope(student=False, module=ModuleScope.USAGE)
-Scope.student_state = Scope(student=True, module=ModuleScope.USAGE)
-Scope.student_preferences = Scope(student=True, module=ModuleScope.TYPE)
-Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
-
-
-class ModelType(object):
- """
- A field class that can be used as a class attribute to define what data the class will want
- to refer to.
-
- When the class is instantiated, it will be available as an instance attribute of the same
- name, by proxying through to self._model_data on the containing object.
- """
- sequence = 0
-
- def __init__(self, help=None, default=None, scope=Scope.content, computed_default=None):
- self._seq = self.sequence
- self._name = "unknown"
- self.help = help
- self.default = default
- self.computed_default = computed_default
- self.scope = scope
- ModelType.sequence += 1
-
- @property
- def name(self):
- return self._name
-
- def __get__(self, instance, owner):
- if instance is None:
- return self
-
- try:
- return self.from_json(instance._model_data[self.name])
- except KeyError:
- if self.default is None and self.computed_default is not None:
- return self.computed_default(instance)
-
- return self.default
-
- def __set__(self, instance, value):
- instance._model_data[self.name] = self.to_json(value)
-
- def __delete__(self, instance):
- del instance._model_data[self.name]
-
- def __repr__(self):
- return "<{0.__class__.__name__} {0._name}>".format(self)
-
- def __lt__(self, other):
- return self._seq < other._seq
-
- def to_json(self, value):
- """
- Return value in the form of nested lists and dictionaries (suitable
- for passing to json.dumps).
-
- This is called during field writes to convert the native python
- type to the value stored in the database
- """
- return value
-
- def from_json(self, value):
- """
- Return value as a native full featured python type (the inverse of to_json)
-
- Called during field reads to convert the stored value into a full featured python
- object
- """
- return value
-
- def read_from(self, model):
- """
- Retrieve the value for this field from the specified model object
- """
- return self.__get__(model, model.__class__)
-
- def write_to(self, model, value):
- """
- Set the value for this field to value on the supplied model object
- """
- self.__set__(model, value)
-
- def delete_from(self, model):
- """
- Delete the value for this field from the supplied model object
- """
- self.__delete__(model)
-
-Int = Float = Boolean = Object = List = String = Any = ModelType
-
-
-class ModelMetaclass(type):
- """
- A metaclass to be used for classes that want to use ModelTypes as class attributes
- to define data access.
-
- All class attributes that are ModelTypes will be added to the 'fields' attribute on
- the instance.
-
- Additionally, any namespaces registered in the `xmodule.namespace` will be added to
- the instance
- """
- def __new__(cls, name, bases, attrs):
- fields = []
- for n, v in attrs.items():
- if isinstance(v, ModelType):
- v._name = n
- fields.append(v)
- fields.sort()
- attrs['fields'] = sum([
- base.fields
- for base
- in bases
- if hasattr(base, 'fields')
- ], fields)
-
- return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs)
-
-
-class NamespacesMetaclass(type):
- """
- A metaclass to be used for classes that want to include namespaced fields in their
- instances.
-
- Any namespaces registered in the `xmodule.namespace` will be added to
- the instance
- """
- def __new__(cls, name, bases, attrs):
- namespaces = []
- for ns_name, namespace in Namespace.load_classes():
- if issubclass(namespace, Namespace):
- attrs[ns_name] = NamespaceDescriptor(namespace)
- namespaces.append(ns_name)
- attrs['namespaces'] = namespaces
-
- return super(NamespacesMetaclass, cls).__new__(cls, name, bases, attrs)
-
-
-class ParentModelMetaclass(type):
- """
- A ModelMetaclass that transforms the attribute `has_children = True`
- into a List field with an empty scope.
- """
- def __new__(cls, name, bases, attrs):
- if attrs.get('has_children', False):
- attrs['children'] = List(help='The children of this XModule', default=[], scope=None)
- else:
- attrs['has_children'] = False
-
- return super(ParentModelMetaclass, cls).__new__(cls, name, bases, attrs)
-
-
-class NamespaceDescriptor(object):
- def __init__(self, namespace):
- self._namespace = namespace
-
- def __get__(self, instance, owner):
- return self._namespace(instance)
-
-
-class Namespace(Plugin):
- """
- A baseclass that sets up machinery for ModelType fields that makes those fields be called
- with the container as the field instance
- """
- __metaclass__ = ModelMetaclass
-
- entry_point = 'xmodule.namespace'
-
- def __init__(self, container):
- self._container = container
-
- def __getattribute__(self, name):
- container = super(Namespace, self).__getattribute__('_container')
- namespace_attr = getattr(type(self), name, None)
-
- if namespace_attr is None or not isinstance(namespace_attr, ModelType):
- return super(Namespace, self).__getattribute__(name)
-
- return namespace_attr.__get__(container, type(container))
-
- def __setattr__(self, name, value):
- try:
- container = super(Namespace, self).__getattribute__('_container')
- except AttributeError:
- super(Namespace, self).__setattr__(name, value)
- return
-
- namespace_attr = getattr(type(self), name, None)
-
- if namespace_attr is None or not isinstance(namespace_attr, ModelType):
- return super(Namespace, self).__setattr__(name, value)
-
- return namespace_attr.__set__(container, value)
-
- def __delattr__(self, name):
- container = super(Namespace, self).__getattribute__('_container')
- namespace_attr = getattr(type(self), name, None)
-
- if namespace_attr is None or not isinstance(namespace_attr, ModelType):
- return super(Namespace, self).__detattr__(name)
-
- return namespace_attr.__delete__(container)
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
index 18cf0b3351..dd2ca7e346 100644
--- a/common/lib/xmodule/xmodule/modulestore/inheritance.py
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -1,4 +1,4 @@
-from xmodule.model import Scope
+from xblock.core import Scope
# A list of metadata that this module can inherit from its parent module
INHERITABLE_METADATA = (
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 52267d6734..201a214621 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -13,8 +13,8 @@ from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor
-from xmodule.runtime import DbModel, KeyValueStore, InvalidScopeError
-from xmodule.model import Scope
+from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
+from xblock.core import Scope
from . import ModuleStoreBase, Location
from .draft import DraftModuleStore
@@ -41,8 +41,10 @@ class MongoKeyValueStore(KeyValueStore):
self._metadata = metadata
def get(self, key):
- if key.field_name == 'children':
+ if key.scope == Scope.children:
return self._children
+ elif key.scope == Scope.parent:
+ return None
elif key.scope == Scope.settings:
return self._metadata[key.field_name]
elif key.scope == Scope.content:
@@ -54,7 +56,7 @@ class MongoKeyValueStore(KeyValueStore):
raise InvalidScopeError(key.scope)
def set(self, key, value):
- if key.field_name == 'children':
+ if key.scope == Scope.children:
self._children = value
elif key.scope == Scope.settings:
self._metadata[key.field_name] = value
@@ -67,7 +69,7 @@ class MongoKeyValueStore(KeyValueStore):
raise InvalidScopeError(key.scope)
def delete(self, key):
- if key.field_name == 'children':
+ if key.scope == Scope.children:
self._children = []
elif key.scope == Scope.settings:
if key.field_name in self._metadata:
diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py
index db0b3fd0c4..eb5bef9e6d 100644
--- a/common/lib/xmodule/xmodule/poll_module.py
+++ b/common/lib/xmodule/xmodule/poll_module.py
@@ -6,7 +6,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
-from .model import Int, Scope, Boolean
+from xblock.core import Integer, Scope, Boolean
log = logging.getLogger(__name__)
@@ -16,8 +16,8 @@ class PollModule(XModule):
js = {'coffee': [resource_string(__name__, 'js/src/poll/display.coffee')]}
js_module_name = "PollModule"
- upvotes = Int(help="Number of upvotes this poll has recieved", scope=Scope.content, default=0)
- downvotes = Int(help="Number of downvotes this poll has recieved", scope=Scope.content, default=0)
+ upvotes = Integer(help="Number of upvotes this poll has recieved", scope=Scope.content, default=0)
+ downvotes = Integer(help="Number of downvotes this poll has recieved", scope=Scope.content, default=0)
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
def handle_ajax(self, dispatch, get):
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index 5e50bdf6a0..c6d2ebf2b3 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -3,7 +3,7 @@ from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor
import logging
import sys
-from .model import String, Scope
+from xblock.core import String, Scope
log = logging.getLogger(__name__)
diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py
deleted file mode 100644
index 8cbfc3ae36..0000000000
--- a/common/lib/xmodule/xmodule/runtime.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from collections import MutableMapping, namedtuple
-
-from .model import ModuleScope, ModelType
-
-
-class InvalidScopeError(Exception):
- """
- Raised to indicated that operating on the supplied scope isn't allowed by a KeyValueStore
- """
- pass
-
-
-class KeyValueStore(object):
- """The abstract interface for Key Value Stores."""
-
- # Keys are structured to retain information about the scope of the data.
- # Stores can use this information however they like to store and retrieve
- # data.
- Key = namedtuple("Key", "scope, student_id, module_scope_id, field_name")
-
- def get(self, key):
- pass
-
- def set(self, key, value):
- pass
-
- def delete(self, key):
- pass
-
-
-class DbModel(MutableMapping):
- """A dictionary-like interface to the fields on a module."""
-
- def __init__(self, kvs, module_cls, student_id, usage):
- self._kvs = kvs
- self._student_id = student_id
- self._module_cls = module_cls
- self._usage = usage
-
- def __repr__(self):
- return "<{0.__class__.__name__} {0._module_cls!r}>".format(self)
-
- def _getfield(self, name):
- # First, get the field from the class, if defined
- module_field = getattr(self._module_cls, name, None)
- if module_field is not None and isinstance(module_field, ModelType):
- return module_field
-
- # If the class doesn't have the field, and it also
- # doesn't have any namespaces, then the the name isn't a field
- # so KeyError
- if not hasattr(self._module_cls, 'namespaces'):
- return KeyError(name)
-
- # Resolve the field name in the first namespace where it's
- # available
- for namespace_name in self._module_cls.namespaces:
- namespace = getattr(self._module_cls, namespace_name)
- namespace_field = getattr(type(namespace), name, None)
- if namespace_field is not None and isinstance(namespace_field, ModelType):
- return namespace_field
-
- # Not in the class or in any of the namespaces, so name
- # really doesn't name a field
- raise KeyError(name)
-
- def _key(self, name):
- field = self._getfield(name)
- if field.scope is None:
- module_id = None
- student_id = None
- else:
- module = field.scope.module
-
- if module == ModuleScope.ALL:
- module_id = None
- elif module == ModuleScope.USAGE:
- module_id = self._usage.id
- elif module == ModuleScope.DEFINITION:
- module_id = self._usage.def_id
- elif module == ModuleScope.TYPE:
- module_id = self._module_cls.__name__
-
- if field.scope.student:
- student_id = self._student_id
- else:
- student_id = None
-
- key = KeyValueStore.Key(
- scope=field.scope,
- student_id=student_id,
- module_scope_id=module_id,
- field_name=name
- )
- return key
-
- def __getitem__(self, name):
- return self._kvs.get(self._key(name))
-
- def __setitem__(self, name, value):
- self._kvs.set(self._key(name), value)
-
- def __delitem__(self, name):
- self._kvs.delete(self._key(name))
-
- def __iter__(self):
- return iter(self.keys())
-
- def __len__(self):
- return len(self.keys())
-
- def __contains__(self, item):
- return item in self.keys()
-
- def keys(self):
- fields = [field.name for field in self._module_cls.fields]
- for namespace_name in self._module_cls.namespaces:
- fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
- return fields
diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py
index ffcd86ba52..12f054eaa6 100644
--- a/common/lib/xmodule/xmodule/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/self_assessment_module.py
@@ -23,7 +23,7 @@ from .editing_module import EditingDescriptor
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
-from .model import List, String, Scope, Int
+from xblock.core import List, String, Scope, Integer
log = logging.getLogger("mitx.courseware")
@@ -67,10 +67,10 @@ class SelfAssessmentModule(XModule):
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
- max_score = Int(scope=Scope.settings, default=MAX_SCORE)
+ max_score = Integer(scope=Scope.settings, default=MAX_SCORE)
- attempts = Int(scope=Scope.student_state, default=0), Int
- max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
+ attempts = Integer(scope=Scope.student_state, default=0)
+ max_attempts = Integer(scope=Scope.settings, default=MAX_ATTEMPTS)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
submitmessage = String(scope=Scope.content)
@@ -392,9 +392,9 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
- max_score = Int(scope=Scope.settings, default=MAX_SCORE)
+ max_score = Integer(scope=Scope.settings, default=MAX_SCORE)
- max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
+ max_attempts = Integer(scope=Scope.settings, default=MAX_ATTEMPTS)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
submitmessage = String(scope=Scope.content)
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 3fc3a5dbaa..b3ceb7a229 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -8,7 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
-from .model import Int, Scope
+from xblock.core import Integer, Scope
from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module")
@@ -37,7 +37,7 @@ class SequenceModule(XModule):
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
- position = Int(help="Last tab viewed in this sequence", scope=Scope.student_state)
+ position = Integer(help="Last tab viewed in this sequence", scope=Scope.student_state)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index c16c6d7596..e8b71b53fc 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -31,7 +31,7 @@ i4xs = ModuleSystem(
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id = 'student',
- xmodule_model_data = lambda x: x,
+ xblock_model_data = lambda x: x,
)
diff --git a/common/lib/xmodule/xmodule/tests/test_model.py b/common/lib/xmodule/xmodule/tests/test_model.py
deleted file mode 100644
index c5eaaaefc7..0000000000
--- a/common/lib/xmodule/xmodule/tests/test_model.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from mock import patch
-from unittest import TestCase
-from nose.tools import assert_in, assert_equals, assert_raises
-
-from xmodule.model import *
-
-
-def test_model_metaclass():
- class ModelMetaclassTester(object):
- __metaclass__ = ModelMetaclass
-
- field_a = Int(scope=Scope.settings)
- field_b = Int(scope=Scope.content)
-
- def __init__(self, model_data):
- self._model_data = model_data
-
- class ChildClass(ModelMetaclassTester):
- pass
-
- assert hasattr(ModelMetaclassTester, 'field_a')
- assert hasattr(ModelMetaclassTester, 'field_b')
-
- assert_in(ModelMetaclassTester.field_a, ModelMetaclassTester.fields)
- assert_in(ModelMetaclassTester.field_b, ModelMetaclassTester.fields)
-
- assert hasattr(ChildClass, 'field_a')
- assert hasattr(ChildClass, 'field_b')
-
- assert_in(ChildClass.field_a, ChildClass.fields)
- assert_in(ChildClass.field_b, ChildClass.fields)
-
-
-def test_parent_metaclass():
-
- class HasChildren(object):
- __metaclass__ = ParentModelMetaclass
-
- has_children = True
-
- class WithoutChildren(object):
- __metaclass = ParentModelMetaclass
-
- assert hasattr(HasChildren, 'children')
- assert not hasattr(WithoutChildren, 'children')
-
- assert isinstance(HasChildren.children, List)
- assert_equals(None, HasChildren.children.scope)
-
-
-def test_field_access():
- class FieldTester(object):
- __metaclass__ = ModelMetaclass
-
- field_a = Int(scope=Scope.settings)
- field_b = Int(scope=Scope.content, default=10)
- field_c = Int(scope=Scope.student_state, computed_default=lambda s: s.field_a + s.field_b)
-
- def __init__(self, model_data):
- self._model_data = model_data
-
- field_tester = FieldTester({'field_a': 5, 'field_x': 15})
-
- assert_equals(5, field_tester.field_a)
- assert_equals(10, field_tester.field_b)
- assert_equals(15, field_tester.field_c)
- assert not hasattr(field_tester, 'field_x')
-
- field_tester.field_a = 20
- assert_equals(20, field_tester._model_data['field_a'])
- assert_equals(10, field_tester.field_b)
- assert_equals(30, field_tester.field_c)
-
- del field_tester.field_a
- assert_equals(None, field_tester.field_a)
- assert hasattr(FieldTester, 'field_a')
-
-
-class TestNamespace(Namespace):
- field_x = List(scope=Scope.content)
- field_y = String(scope=Scope.student_state, default="default_value")
-
-
-@patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)])
-def test_namespace_metaclass(mock_load_classes):
- class TestClass(object):
- __metaclass__ = NamespacesMetaclass
-
- assert hasattr(TestClass, 'test')
- assert hasattr(TestClass.test, 'field_x')
- assert hasattr(TestClass.test, 'field_y')
-
- assert_in(TestNamespace.field_x, TestClass.test.fields)
- assert_in(TestNamespace.field_y, TestClass.test.fields)
- assert isinstance(TestClass.test, Namespace)
-
-
-@patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)])
-def test_namespace_field_access(mock_load_classes):
- class Metaclass(ModelMetaclass, NamespacesMetaclass):
- pass
-
- class FieldTester(object):
- __metaclass__ = Metaclass
-
- field_a = Int(scope=Scope.settings)
- field_b = Int(scope=Scope.content, default=10)
- field_c = Int(scope=Scope.student_state, computed_default=lambda s: s.field_a + s.field_b)
-
- def __init__(self, model_data):
- self._model_data = model_data
-
- field_tester = FieldTester({
- 'field_a': 5,
- 'field_x': [1, 2, 3],
- })
-
- assert_equals(5, field_tester.field_a)
- assert_equals(10, field_tester.field_b)
- assert_equals(15, field_tester.field_c)
- assert_equals([1, 2, 3], field_tester.test.field_x)
- assert_equals('default_value', field_tester.test.field_y)
-
- field_tester.test.field_x = ['a', 'b']
- assert_equals(['a', 'b'], field_tester._model_data['field_x'])
-
- del field_tester.test.field_x
- assert_equals(None, field_tester.test.field_x)
-
- assert_raises(AttributeError, getattr, field_tester.test, 'field_z')
- assert_raises(AttributeError, delattr, field_tester.test, 'field_z')
-
- # Namespaces are created on the fly, so setting a new attribute on one
- # has no long-term effect
- field_tester.test.field_z = 'foo'
- assert_raises(AttributeError, getattr, field_tester.test, 'field_z')
- assert 'field_z' not in field_tester._model_data
-
-
-def test_field_serialization():
-
- class CustomField(ModelType):
- def from_json(self, value):
- return value['value']
-
- def to_json(self, value):
- return {'value': value}
-
- class FieldTester(object):
- __metaclass__ = ModelMetaclass
-
- field = CustomField()
-
- def __init__(self, model_data):
- self._model_data = model_data
-
- field_tester = FieldTester({
- 'field': {'value': 4}
- })
-
- assert_equals(4, field_tester.field)
- field_tester.field = 5
- assert_equals({'value': 5}, field_tester._model_data['field'])
diff --git a/common/lib/xmodule/xmodule/tests/test_runtime.py b/common/lib/xmodule/xmodule/tests/test_runtime.py
deleted file mode 100644
index e71c78e4af..0000000000
--- a/common/lib/xmodule/xmodule/tests/test_runtime.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from nose.tools import assert_equals
-from mock import patch
-
-from xmodule.model import *
-from xmodule.runtime import *
-
-
-class Metaclass(NamespacesMetaclass, ParentModelMetaclass, ModelMetaclass):
- pass
-
-
-class TestNamespace(Namespace):
- n_content = String(scope=Scope.content, default='nc')
- n_settings = String(scope=Scope.settings, default='ns')
- n_student_state = String(scope=Scope.student_state, default='nss')
- n_student_preferences = String(scope=Scope.student_preferences, default='nsp')
- n_student_info = String(scope=Scope.student_info, default='nsi')
- n_by_type = String(scope=Scope(False, ModuleScope.TYPE), default='nbt')
- n_for_all = String(scope=Scope(False, ModuleScope.ALL), default='nfa')
- n_student_def = String(scope=Scope(True, ModuleScope.DEFINITION), default='nsd')
-
-
-with patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)]):
- class TestModel(object):
- __metaclass__ = Metaclass
-
- content = String(scope=Scope.content, default='c')
- settings = String(scope=Scope.settings, default='s')
- student_state = String(scope=Scope.student_state, default='ss')
- student_preferences = String(scope=Scope.student_preferences, default='sp')
- student_info = String(scope=Scope.student_info, default='si')
- by_type = String(scope=Scope(False, ModuleScope.TYPE), default='bt')
- for_all = String(scope=Scope(False, ModuleScope.ALL), default='fa')
- student_def = String(scope=Scope(True, ModuleScope.DEFINITION), default='sd')
-
- def __init__(self, model_data):
- self._model_data = model_data
-
-
-class DictKeyValueStore(KeyValueStore):
- def __init__(self):
- self.db = {}
-
- def get(self, key):
- return self.db[key]
-
- def set(self, key, value):
- self.db[key] = value
-
- def delete(self, key):
- del self.db[key]
-
-
-Usage = namedtuple('Usage', 'id, def_id')
-
-
-def check_field(collection, field):
- print "Getting %s from %r" % (field.name, collection)
- assert_equals(field.default, getattr(collection, field.name))
- new_value = 'new ' + field.name
- print "Setting %s to %s on %r" % (field.name, new_value, collection)
- setattr(collection, field.name, new_value)
- print "Checking %s on %r" % (field.name, collection)
- assert_equals(new_value, getattr(collection, field.name))
- print "Deleting %s from %r" % (field.name, collection)
- delattr(collection, field.name)
- print "Back to defaults for %s in %r" % (field.name, collection)
- assert_equals(field.default, getattr(collection, field.name))
-
-
-def test_namespace_actions():
- tester = TestModel(DbModel(DictKeyValueStore(), TestModel, 's0', Usage('u0', 'd0')))
-
- for collection in (tester, tester.test):
- for field in collection.fields:
- yield check_field, collection, field
-
-
-def test_db_model_keys():
- key_store = DictKeyValueStore()
- tester = TestModel(DbModel(key_store, TestModel, 's0', Usage('u0', 'd0')))
-
- for collection in (tester, tester.test):
- for field in collection.fields:
- new_value = 'new ' + field.name
- setattr(collection, field.name, new_value)
-
- print key_store.db
- assert_equals('new content', key_store.db[KeyValueStore.Key(Scope.content, None, 'd0', 'content')])
- assert_equals('new settings', key_store.db[KeyValueStore.Key(Scope.settings, None, 'u0', 'settings')])
- assert_equals('new student_state', key_store.db[KeyValueStore.Key(Scope.student_state, 's0', 'u0', 'student_state')])
- assert_equals('new student_preferences', key_store.db[KeyValueStore.Key(Scope.student_preferences, 's0', 'TestModel', 'student_preferences')])
- assert_equals('new student_info', key_store.db[KeyValueStore.Key(Scope.student_info, 's0', None, 'student_info')])
- assert_equals('new by_type', key_store.db[KeyValueStore.Key(Scope(False, ModuleScope.TYPE), None, 'TestModel', 'by_type')])
- assert_equals('new for_all', key_store.db[KeyValueStore.Key(Scope(False, ModuleScope.ALL), None, None, 'for_all')])
- assert_equals('new student_def', key_store.db[KeyValueStore.Key(Scope(True, ModuleScope.DEFINITION), 's0', 'd0', 'student_def')])
-
- assert_equals('new n_content', key_store.db[KeyValueStore.Key(Scope.content, None, 'd0', 'n_content')])
- assert_equals('new n_settings', key_store.db[KeyValueStore.Key(Scope.settings, None, 'u0', 'n_settings')])
- assert_equals('new n_student_state', key_store.db[KeyValueStore.Key(Scope.student_state, 's0', 'u0', 'n_student_state')])
- assert_equals('new n_student_preferences', key_store.db[KeyValueStore.Key(Scope.student_preferences, 's0', 'TestModel', 'n_student_preferences')])
- assert_equals('new n_student_info', key_store.db[KeyValueStore.Key(Scope.student_info, 's0', None, 'n_student_info')])
- assert_equals('new n_by_type', key_store.db[KeyValueStore.Key(Scope(False, ModuleScope.TYPE), None, 'TestModel', 'n_by_type')])
- assert_equals('new n_for_all', key_store.db[KeyValueStore.Key(Scope(False, ModuleScope.ALL), None, None, 'n_for_all')])
- assert_equals('new n_student_def', key_store.db[KeyValueStore.Key(Scope(True, ModuleScope.DEFINITION), 's0', 'd0', 'n_student_def')])
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index 34ce353afd..e284c574d3 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -9,7 +9,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
-from .model import Int, Scope, String
+from xblock.core import Integer, Scope, String
log = logging.getLogger(__name__)
@@ -29,7 +29,7 @@ class VideoModule(XModule):
js_module_name = "Video"
data = String(help="XML data for the problem", scope=Scope.content)
- position = Int(help="Current position in the video", scope=Scope.student_state)
+ position = Integer(help="Current position in the video", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
def __init__(self, *args, **kwargs):
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 86f9f5f189..5ce74ffc46 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -10,12 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
-from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass
-from .plugin import Plugin
-
-
-class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
- pass
+from xblock.core import XBlock
log = logging.getLogger(__name__)
@@ -88,7 +83,7 @@ class HTMLSnippet(object):
.format(self.__class__))
-class XModule(HTMLSnippet):
+class XModule(HTMLSnippet, XBlock):
''' Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order
@@ -97,8 +92,6 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example.
'''
- __metaclass__ = XModuleMetaclass
-
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
#
@@ -266,7 +259,7 @@ class ResourceTemplates(object):
return templates
-class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
+class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XBlock):
"""
An XModuleDescriptor is a specification for an element of a course. This
could be a problem, an organizational element (a group of content), or a
@@ -279,7 +272,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
entry_point = "xmodule.v1"
module_class = XModule
- __metaclass__ = XModuleMetaclass
# Attributes for inspection of the descriptor
@@ -371,7 +363,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system,
self.location,
self,
- system.xmodule_model_data(self._model_data),
+ system.xblock_model_data(self._model_data),
)
def has_dynamic_children(self):
@@ -608,7 +600,7 @@ class ModuleSystem(object):
get_module,
render_template,
replace_urls,
- xmodule_model_data,
+ xblock_model_data,
user=None,
filestore=None,
debug=False,
@@ -663,7 +655,7 @@ class ModuleSystem(object):
self.node_path = node_path
self.anonymous_student_id = anonymous_student_id
self.user_is_staff = user is not None and user.is_staff
- self.xmodule_model_data = xmodule_model_data
+ self.xblock_model_data = xblock_model_data
if publish is None:
publish = lambda e: None
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index e50adeb364..f510a40cc7 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -1,7 +1,7 @@
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
-from xmodule.model import Object, Scope
+from xblock.core import Object, Scope
from lxml import etree
import json
import copy
diff --git a/github-requirements.txt b/github-requirements.txt
index 468d55ce65..cf6f097aee 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -3,3 +3,4 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
+-e git+ssh://git@github.com/MITx/xmodule-debugger@ada10f2991cdd61c60ec223b4e0b9b4e06d7cdc3#egg=XBlock
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index b2f2d3ef48..6ae75a2c71 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -8,8 +8,8 @@ from .models import (
XModuleStudentInfoField
)
-from xmodule.runtime import KeyValueStore, InvalidScopeError
-from xmodule.model import Scope
+from xblock.runtime import KeyValueStore, InvalidScopeError
+from xblock.core import Scope
class InvalidWriteError(Exception):
@@ -45,20 +45,20 @@ class LmsKeyValueStore(KeyValueStore):
def _student_module(self, key):
student_module = self._student_module_cache.lookup(
- self._course_id, key.module_scope_id.category, key.module_scope_id.url()
+ self._course_id, key.block_scope_id.category, key.block_scope_id.url()
)
return student_module
def _field_object(self, key):
if key.scope == Scope.content:
- return XModuleContentField, {'field_name': key.field_name, 'definition_id': key.module_scope_id}
+ return XModuleContentField, {'field_name': key.field_name, 'definition_id': key.block_scope_id}
elif key.scope == Scope.settings:
return XModuleSettingsField, {
'field_name': key.field_name,
- 'usage_id': '%s-%s' % (self._course_id, key.module_scope_id)
+ 'usage_id': '%s-%s' % (self._course_id, key.block_scope_id)
}
elif key.scope == Scope.student_preferences:
- return XModuleStudentPrefsField, {'field_name': key.field_name, 'student': self._user, 'module_type': key.module_scope_id}
+ return XModuleStudentPrefsField, {'field_name': key.field_name, 'student': self._user, 'module_type': key.block_scope_id}
elif key.scope == Scope.student_info:
return XModuleStudentInfoField, {'field_name': key.field_name, 'student': self._user}
@@ -68,6 +68,9 @@ class LmsKeyValueStore(KeyValueStore):
if key.field_name in self._descriptor_model_data:
return self._descriptor_model_data[key.field_name]
+ if key.scope == Scope.parent:
+ return None
+
if key.scope == Scope.student_state:
student_module = self._student_module(key)
@@ -92,8 +95,8 @@ class LmsKeyValueStore(KeyValueStore):
student_module = StudentModule(
course_id=self._course_id,
student=self._user,
- module_type=key.module_scope_id.category,
- module_state_key=key.module_scope_id.url(),
+ module_type=key.block_scope_id.category,
+ module_state_key=key.block_scope_id.url(),
state=json.dumps({})
)
self._student_module_cache.append(student_module)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index c2a8db0fab..4d599d128d 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -26,7 +26,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
-from xmodule.runtime import DbModel
+from xblock.runtime import DbModel
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
from .model_data import LmsKeyValueStore, LmsUsage
@@ -203,7 +203,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
return get_module(user, request, location,
student_module_cache, course_id, position)
- def xmodule_model_data(descriptor_model_data):
+ def xblock_model_data(descriptor_model_data):
return DbModel(
LmsKeyValueStore(course_id, user, descriptor_model_data, student_module_cache),
descriptor.module_class,
@@ -260,7 +260,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
replace_urls=replace_urls,
node_path=settings.NODE_PATH,
anonymous_student_id=anonymous_student_id,
- xmodule_model_data=xmodule_model_data,
+ xblock_model_data=xblock_model_data,
publish=publish,
)
# pass position specified in URL to module through ModuleSystem
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
index ab9067ce71..f18e128984 100644
--- a/lms/djangoapps/courseware/tests/test_model_data.py
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -7,7 +7,7 @@ from functools import partial
from courseware.model_data import LmsKeyValueStore, InvalidWriteError, InvalidScopeError
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField, XModuleStudentInfoField, XModuleStudentPrefsField, StudentModuleCache
-from xmodule.model import Scope, ModuleScope
+from xblock.core import Scope, BlockScope
from xmodule.modulestore import Location
from django.test import TestCase
@@ -112,9 +112,9 @@ class TestInvalidScopes(TestCase):
self.kvs = LmsKeyValueStore(course_id, UserFactory.build(), self.desc_md, None)
def test_invalid_scopes(self):
- for scope in (Scope(student=True, module=ModuleScope.DEFINITION),
- Scope(student=False, module=ModuleScope.TYPE),
- Scope(student=False, module=ModuleScope.ALL)):
+ for scope in (Scope(student=True, block=BlockScope.DEFINITION),
+ Scope(student=False, block=BlockScope.TYPE),
+ Scope(student=False, block=BlockScope.ALL)):
self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 5fd2c18bb7..8d587dfacb 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,4 +1,4 @@
-from xmodule.model import Namespace, Boolean, Scope, String, List
+from xblock.core import Namespace, Boolean, Scope, String, List
from xmodule.fields import Date, Timedelta
diff --git a/setup.py b/setup.py
index a07f836413..48572de6de 100644
--- a/setup.py
+++ b/setup.py
@@ -7,11 +7,11 @@ setup(
requires=[
'xmodule',
],
- py_modules=['lms.xmodule_namespace'],
+ py_modules=['lms.xmodule_namespace', 'cms.xmodule_namespace'],
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
entry_points={
- 'xmodule.namespace': [
+ 'xblock.namespace': [
'lms = lms.xmodule_namespace:LmsNamespace',
'cms = cms.xmodule_namespace:CmsNamespace',
],
From d230c1b03ce73896f7bb3e08d67ccba35a158d94 Mon Sep 17 00:00:00 2001
From: jmvt
Date: Fri, 11 Jan 2013 17:14:29 -0500
Subject: [PATCH 068/285] Added example of geographic student distribution over
world map.
---
.../jquery-jvectormap-1.1.1.css | 37 ++++++++++
.../jquery-jvectormap-1.1.1.min.js | 7 ++
.../jquery-jvectormap-world-mill-en.js | 1 +
lms/djangoapps/instructor/views.py | 3 +-
.../courseware/instructor_dashboard.html | 71 +++++++++++++++++++
5 files changed, 118 insertions(+), 1 deletion(-)
create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css
create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js
create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js
diff --git a/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css
new file mode 100644
index 0000000000..3d9c8844bb
--- /dev/null
+++ b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css
@@ -0,0 +1,37 @@
+.jvectormap-label {
+ position: absolute;
+ display: none;
+ border: solid 1px #CDCDCD;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ background: #292929;
+ color: white;
+ font-family: sans-serif, Verdana;
+ font-size: smaller;
+ padding: 3px;
+}
+
+.jvectormap-zoomin, .jvectormap-zoomout {
+ position: absolute;
+ left: 10px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ background: #292929;
+ padding: 3px;
+ color: white;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ line-height: 10px;
+ text-align: center;
+}
+
+.jvectormap-zoomin {
+ top: 10px;
+}
+
+.jvectormap-zoomout {
+ top: 30px;
+}
\ No newline at end of file
diff --git a/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js
new file mode 100644
index 0000000000..17450a0983
--- /dev/null
+++ b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js
@@ -0,0 +1,7 @@
+/**
+ * jVectorMap version 1.1
+ *
+ * Copyright 2011-2012, Kirill Lebedev
+ * Licensed under the MIT license.
+ *
+ */(function(e){var t={set:{colors:1,values:1,backgroundColor:1,scaleColors:1,normalizeFunction:1,focus:1},get:{selectedRegions:1,selectedMarkers:1,mapObject:1,regionName:1}};e.fn.vectorMap=function(e){var n,r,i,n=this.children(".jvectormap-container").data("mapObject");if(e==="addMap")jvm.WorldMap.maps[arguments[1]]=arguments[2];else{if(!(e!=="set"&&e!=="get"||!t[e][arguments[1]]))return r=arguments[1].charAt(0).toUpperCase()+arguments[1].substr(1),n[e+r].apply(n,Array.prototype.slice.call(arguments,2));e=e||{},e.container=this,n=new jvm.WorldMap(e)}return this}})(jQuery),function(e){function r(t){var n=t||window.event,r=[].slice.call(arguments,1),i=0,s=!0,o=0,u=0;return t=e.event.fix(n),t.type="mousewheel",n.wheelDelta&&(i=n.wheelDelta/120),n.detail&&(i=-n.detail/3),u=i,n.axis!==undefined&&n.axis===n.HORIZONTAL_AXIS&&(u=0,o=-1*i),n.wheelDeltaY!==undefined&&(u=n.wheelDeltaY/120),n.wheelDeltaX!==undefined&&(o=-1*n.wheelDeltaX/120),r.unshift(t,i,o,u),(e.event.dispatch||e.event.handle).apply(this,r)}var t=["DOMMouseScroll","mousewheel"];if(e.event.fixHooks)for(var n=t.length;n;)e.event.fixHooks[t[--n]]=e.event.mouseHooks;e.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],r,!1);else this.onmousewheel=r},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],r,!1);else this.onmousewheel=null}},e.fn.extend({mousewheel:function(e){return e?this.bind("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.unbind("mousewheel",e)}})}(jQuery);var jvm={inherits:function(e,t){function n(){}n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e,e.parentClass=t},mixin:function(e,t){var n;for(n in t.prototype)t.prototype.hasOwnProperty(n)&&(e.prototype[n]=t.prototype[n])},min:function(e){var t=Number.MAX_VALUE,n;if(e instanceof Array)for(n=0;nt&&(t=e[n]);else for(n in e)e[n]>t&&(t=e[n]);return t},keys:function(e){var t=[],n;for(n in e)t.push(n);return t},values:function(e){var t=[],n,r;for(r=0;r')}}catch(e){jvm.VMLElement.prototype.createElement=function(e){return document.createElement("<"+e+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}document.createStyleSheet().addRule(".rvml","behavior:url(#default#VML)"),jvm.VMLElement.VMLInitialized=!0},jvm.VMLElement.prototype.getElementCtr=function(e){return jvm["VML"+e]},jvm.VMLElement.prototype.addClass=function(e){jvm.$(this.node).addClass(e)},jvm.VMLElement.prototype.applyAttr=function(e,t){this.node[e]=t},jvm.VMLElement.prototype.getBBox=function(){var e=jvm.$(this.node);return{x:e.position().left/this.canvas.scale,y:e.position().top/this.canvas.scale,width:e.width()/this.canvas.scale,height:e.height()/this.canvas.scale}},jvm.VMLGroupElement=function(){jvm.VMLGroupElement.parentClass.call(this,"group"),this.node.style.left="0px",this.node.style.top="0px",this.node.coordorigin="0 0"},jvm.inherits(jvm.VMLGroupElement,jvm.VMLElement),jvm.VMLGroupElement.prototype.add=function(e){this.node.appendChild(e.node)},jvm.VMLCanvasElement=function(e,t,n){this.classPrefix="VML",jvm.VMLCanvasElement.parentClass.call(this,"group"),jvm.AbstractCanvasElement.apply(this,arguments),this.node.style.position="absolute"},jvm.inherits(jvm.VMLCanvasElement,jvm.VMLElement),jvm.mixin(jvm.VMLCanvasElement,jvm.AbstractCanvasElement),jvm.VMLCanvasElement.prototype.setSize=function(e,t){var n,r,i,s;this.width=e,this.height=t,this.node.style.width=e+"px",this.node.style.height=t+"px",this.node.coordsize=e+" "+t,this.node.coordorigin="0 0";if(this.rootElement){n=this.rootElement.node.getElementsByTagName("shape");for(i=0,s=n.length;i=0)e-=t[i],i++;return i==this.scale.length-1?e=this.vectorToNum(this.scale[i]):e=this.vectorToNum(this.vectorAdd(this.scale[i],this.vectorMult(this.vectorSubtract(this.scale[i+1],this.scale[i]),e/t[i]))),e},vectorToNum:function(e){var t=0,n;for(n=0;nt&&(t=e[i]),r").css({width:"100%",height:"100%"}).addClass("jvectormap-container"),this.params.container.append(this.container),this.container.data("mapObject",this),this.container.css({position:"relative",overflow:"hidden"}),this.defaultWidth=this.mapData.width,this.defaultHeight=this.mapData.height,this.setBackgroundColor(this.params.backgroundColor),this.onResize=function(){t.setSize()},jvm.$(window).resize(this.onResize);for(n in jvm.WorldMap.apiEvents)this.params[n]&&this.container.bind(jvm.WorldMap.apiEvents[n]+".jvectormap",this.params[n]);this.canvas=new jvm.VectorCanvas(this.container[0],this.width,this.height),"ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch?this.params.bindTouchEvents&&this.bindContainerTouchEvents():this.bindContainerEvents(),this.bindElementEvents(),this.createLabel(),this.bindZoomButtons(),this.createRegions(),this.createMarkers(this.params.markers||{}),this.setSize(),this.params.focusOn&&(typeof this.params.focusOn=="object"?this.setFocus.call(this,this.params.focusOn.scale,this.params.focusOn.x,this.params.focusOn.y):this.setFocus.call(this,this.params.focusOn)),this.params.selectedRegions&&this.setSelectedRegions(this.params.selectedRegions),this.params.selectedMarkers&&this.setSelectedMarkers(this.params.selectedMarkers),this.params.series&&this.createSeries()},jvm.WorldMap.prototype={transX:0,transY:0,scale:1,baseTransX:0,baseTransY:0,baseScale:1,width:0,height:0,setBackgroundColor:function(e){this.container.css("background-color",e)},resize:function(){var e=this.baseScale;this.width/this.height>this.defaultWidth/this.defaultHeight?(this.baseScale=this.height/this.defaultHeight,this.baseTransX=Math.abs(this.width-this.defaultWidth*this.baseScale)/(2*this.baseScale)):(this.baseScale=this.width/this.defaultWidth,this.baseTransY=Math.abs(this.height-this.defaultHeight*this.baseScale)/(2*this.baseScale)),this.scale*=this.baseScale/e,this.transX*=this.baseScale/e,this.transY*=this.baseScale/e},setSize:function(){this.width=this.container.width(),this.height=this.container.height(),this.resize(),this.canvas.setSize(this.width,this.height),this.applyTransform()},reset:function(){var e,t;for(e in this.series)for(t=0;tt?this.transY=t:this.transYe?this.transX=e:this.transXt[1].pageX?o=t[1].pageX+(t[0].pageX-t[1].pageX)/2:o=t[0].pageX+(t[1].pageX-t[0].pageX)/2,t[0].pageY>t[1].pageY?u=t[1].pageY+(t[0].pageY-t[1].pageY)/2:u=t[0].pageY+(t[1].pageY-t[0].pageY)/2),i=e.originalEvent.touches[0].pageX,s=e.originalEvent.touches[0].pageY}),jvm.$(this.container).bind("touchmove",function(e){var t;if(r.scale!=r.baseScale)return e.originalEvent.touches.length==1&&i&&s?(t=e.originalEvent.touches[0],r.transX-=(i-t.pageX)/r.scale,r.transY-=(s-t.pageY)/r.scale,r.applyTransform(),r.label.hide(),i=t.pageX,s=t.pageY):(i=!1,s=!1),!1})},bindElementEvents:function(){var e=this,t;this.container.mousemove(function(){t=!0}),this.container.delegate("[class~='jvectormap-element']","mouseover mouseout",function(t){var n=this,r=jvm.$(this).attr("class").indexOf("jvectormap-region")===-1?"marker":"region",i=r=="region"?jvm.$(this).attr("data-code"):jvm.$(this).attr("data-index"),s=r=="region"?e.regions[i].element:e.markers[i].element,o=r=="region"?e.mapData.paths[i].name:e.markers[i].config.name||"",u=jvm.$.Event(r+"LabelShow.jvectormap"),a=jvm.$.Event(r+"Over.jvectormap");t.type=="mouseover"?(e.container.trigger(a,[i]),a.isDefaultPrevented()||s.setHovered(!0),e.label.text(o),e.container.trigger(u,[e.label,i]),u.isDefaultPrevented()||(e.label.show(),e.labelWidth=e.label.width(),e.labelHeight=e.label.height())):(s.setHovered(!1),e.label.hide(),e.container.trigger(r+"Out.jvectormap",[i]))}),this.container.delegate("[class~='jvectormap-element']","mousedown",function(e){t=!1}),this.container.delegate("[class~='jvectormap-element']","mouseup",function(n){var r=this,i=jvm.$(this).attr("class").indexOf("jvectormap-region")===-1?"marker":"region",s=i=="region"?jvm.$(this).attr("data-code"):jvm.$(this).attr("data-index"),o=jvm.$.Event(i+"Click.jvectormap"),u=i=="region"?e.regions[s].element:e.markers[s].element;if(!t){e.container.trigger(o,[s]);if(i==="region"&&e.params.regionsSelectable||i==="marker"&&e.params.markersSelectable)o.isDefaultPrevented()||(e.params[i+"sSelectableOne"]&&e.clearSelected(i+"s"),u.setSelected(!u.isSelected))}})},bindZoomButtons:function(){var e=this;jvm.$("").addClass("jvectormap-zoomin").text("+").appendTo(this.container),jvm.$("").addClass("jvectormap-zoomout").html("−").appendTo(this.container),this.container.find(".jvectormap-zoomin").click(function(){e.setScale(e.scale*e.params.zoomStep,e.width/2,e.height/2)}),this.container.find(".jvectormap-zoomout").click(function(){e.setScale(e.scale/e.params.zoomStep,e.width/2,e.height/2)})},createLabel:function(){var e=this;this.label=jvm.$("").addClass("jvectormap-label").appendTo(jvm.$("body")),this.container.mousemove(function(t){var n=t.pageX-15-e.labelWidth,r=t.pageY-15-e.labelHeight;n<5&&(n=t.pageX+15),r<5&&(r=t.pageY+15),e.label.is(":visible")&&e.label.css({left:n,top:r})})},setScale:function(e,t,n,r){var i,s=jvm.$.Event("zoom.jvectormap");e>this.params.zoomMax*this.baseScale?e=this.params.zoomMax*this.baseScale:ei[0].x&&ei[0].y&&t
+
+
%block>
<%include file="/courseware/course_navigation.html" args="active_page='instructor'" />
@@ -40,6 +42,45 @@ table.stat_table td {
a.selectedmode { background-color: yellow; }
+
+.jvectormap-label {
+ position: absolute;
+ display: none;
+ border: solid 1px #CDCDCD;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ background: #292929;
+ color: white;
+ font-family: sans-serif, Verdana;
+ font-size: smaller;
+ padding: 3px;
+}
+
+.jvectormap-zoomin, .jvectormap-zoomout {
+ position: absolute;
+ left: 10px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ background: #292929;
+ padding: 3px;
+ color: white;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ line-height: 10px;
+ text-align: center;
+}
+
+.jvectormap-zoomin {
+ top: 10px;
+}
+
+.jvectormap-zoomout {
+ top: 30px;
+}
+
+
+
+
+
Number of active students per problems who have this problem graded as correct:
From a9d3736cfbbed09e3ad9b9bcee30530b7f63bc92 Mon Sep 17 00:00:00 2001
From: jmvt
Date: Wed, 16 Jan 2013 15:09:54 -0500
Subject: [PATCH 069/285] Replace all calls to analytics service to use the get
methods (pulls from mongodb). Added error handling.
---
lms/djangoapps/instructor/views.py | 36 ++--
.../courseware/instructor_dashboard.html | 166 +++++++++++-------
2 files changed, 129 insertions(+), 73 deletions(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 37fd133590..6c59200786 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -289,31 +289,43 @@ def instructor_dashboard(request, course_id):
from_day = to_day - timedelta(days=7)
# WARNING: do not use req.json because the preloaded json doesn't preserve the order of the original record
+ # use instead: json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students enrolled in this course
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id)
- students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsEnrolled&course_id=%s" % course_id)
+ if req.content != 'None':
+ students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students active in the past 7 days (including current day), i.e. with at least one activity for the period
#req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day))
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days
- students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days
+ if req.content != 'None':
+ students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students per problem who have problem graded correct
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
- students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s" % (course_id,))
+ if req.content != 'None':
+ students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict)
+
+ # number of students per problem who have problem graded correct <<< THIS IS FOR ACTIVE STUDENTS
+# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day))
+# if req.content != 'None':
+# students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict)
# grade distribution for the course +++ this is not the desired distribution +++
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,))
- overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=OverallGradeDistribution&course_id=%s" % (course_id,))
+ if req.content != 'None':
+ overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students distribution drop off per day
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day))
- dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day))
+ if req.content != 'None':
+ dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students per problem who attempted this problem at least once
- req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsAttemptedProblems&course_id=%s" % course_id)
- attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict)
+ req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsAttemptedProblems&course_id=%s" % course_id)
+ if req.content != 'None':
+ attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict)
# number of students active in the past 7 days (including current day) --- online version! experimental
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 94d05792f3..29f1602721 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -218,10 +218,29 @@ function goto( mode)
%if modeflag.get('Analytics'):
- Number of students enrolled: ${students_enrolled_json['data']['value']}
+ Number of students enrolled:
+ % if students_enrolled_json is not None:
+ % if students_enrolled_json['status'] == 'success':
+ ${students_enrolled_json['data']['value']}
+ % else:
+ ${students_enrolled_json['error']}
+ % endif
+ % else:
+ null data
+ % endif
+
- Number of active students for the past 7 days: ${students_active_json['data']['value']}
+ Number of active students for the past 7 days:
+ % if students_active_json is not None:
+ % if students_active_json['status'] == 'success':
+ ${students_active_json['data']['value']}
+ % else:
+ ${students_active_json['error']}
+ % endif
+ % else:
+ null data
+ % endif
@@ -253,78 +272,103 @@ function goto( mode)
-
Number of active students per problems who have this problem graded as correct:
+ % if students_per_problem_correct_json is not None:
+ % if students_per_problem_correct_json['status'] == 'success':
+
-
Problem
Number of students
+
Problem
Number of students
% for k,v in students_per_problem_correct_json['data'].items():
-
-
${k}
${v}
-
- % endfor
-
-
-
-
-
Grade distribution:
-
-
-
-
Grade
Number of students
- % for k,v in overall_grade_distribution['data'].items():
-
+## % for k,v in daily_activity_json['data'].items():
+##
+##
${k}
${v}
+##
+## % endfor
+##
+##
-
-
-
Module
Number of students
- % for k,v in attempted_problems['data'].items():
-
-
${k}
${v}
-
- % endfor
-
-
-
-
-
-
-
Daily activity (online version):
-
-
Day
Number of students
- % for k,v in daily_activity_json['data'].items():
-
-
${k}
${v}
-
- % endfor
-
-
%endif
From 85c37792869b4d0db1ddb1e6db85652d85a9527d Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 22 Jan 2013 15:21:18 -0500
Subject: [PATCH 070/285] Use the v0.1 tag in the XBlock repo.
---
github-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/github-requirements.txt b/github-requirements.txt
index cf6f097aee..883490da99 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -3,4 +3,4 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
--e git+ssh://git@github.com/MITx/xmodule-debugger@ada10f2991cdd61c60ec223b4e0b9b4e06d7cdc3#egg=XBlock
\ No newline at end of file
+-e git+ssh://git@github.com/MITx/xmodule-debugger@v0.1#egg=XBlock
From 532285e558d2bfa87c468974428abf1b26b23457 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 22 Jan 2013 15:22:30 -0500
Subject: [PATCH 071/285] Only need one of these lines.
---
common/lib/xmodule/xmodule/x_module.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 5ce74ffc46..3526990745 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -324,7 +324,6 @@ class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XBlock):
self._model_data = model_data
self._child_instances = None
- self._child_instances = None
def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of
From 4f005044d62cb557e916f6e10235df3d332ce776 Mon Sep 17 00:00:00 2001
From: jmvt
Date: Wed, 23 Jan 2013 10:17:17 -0500
Subject: [PATCH 072/285] Added time of query display.
---
.../courseware/instructor_dashboard.html | 52 ++++++++++---------
1 file changed, 27 insertions(+), 25 deletions(-)
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 3260ee569b..967088de19 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -348,7 +348,7 @@ function goto( mode)
Number of students enrolled:
% if students_enrolled_json is not None:
% if students_enrolled_json['status'] == 'success':
- ${students_enrolled_json['data']['value']}
+ ${students_enrolled_json['data']['value']} as of ${students_enrolled_json['time']}
% else:
${students_enrolled_json['error']}
% endif
@@ -361,7 +361,7 @@ function goto( mode)
Number of active students for the past 7 days:
% if students_active_json is not None:
% if students_active_json['status'] == 'success':
- ${students_active_json['data']['value']}
+ ${students_active_json['data']['value']} as of ${students_active_json['time']}
% else:
${students_active_json['error']}
% endif
@@ -400,7 +400,7 @@ function goto( mode)
-
Number of active students per problems who have this problem graded as correct:
+
Number of students per problem who have this problem graded as correct, as of ${students_per_problem_correct_json['time']}
% if students_per_problem_correct_json is not None:
% if students_per_problem_correct_json['status'] == 'success':
@@ -420,28 +420,30 @@ function goto( mode)
% endif
-##
-##
Students who attempted at least one exercise:
-##
-## % if attempted_problems is not None:
-## % if attempted_problems['status'] == 'success':
-##
-##
-##
Module
Number of students
-## % for k,v in attempted_problems['data'].items():
-##
+ Students per module who attempted at least one problem
+
+ % if attempted_problems is not None:
+ , as of ${attempted_problems['time']}
+ % if attempted_problems['status'] == 'success':
+
+
+
Module
Number of students
+ % for k,v in attempted_problems['data'].items():
+
- ${get_course_info_section(request, cache, course, 'guest_handouts')}
+ ${get_course_info_section(request, course, 'guest_handouts')}
% endif
From 27b57bb48b636ec29b9e592f4be89731fd7d3dd9 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Tue, 15 Jan 2013 08:47:08 -0500
Subject: [PATCH 086/285] Don't update settings based on data from content for
discussion modules during init
---
common/lib/xmodule/xmodule/discussion_module.py | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index 6a9edbbd70..e3161742df 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -17,21 +17,12 @@ class DiscussionModule(XModule):
discussion_target = String(scope=Scope.settings)
sort_key = String(scope=Scope.settings)
- data = String(help="XML definition of inline discussion", scope=Scope.content)
-
def get_html(self):
context = {
'discussion_id': self.discussion_id,
}
return self.system.render_template('discussion/_discussion_module.html', context)
- def __init__(self, *args, **kwargs):
- XModule.__init__(self, *args, **kwargs)
-
- xml_data = etree.fromstring(self.data)
- self.discussion_id = xml_data.attrib['id']
- self.title = xml_data.attrib['for']
- self.discussion_category = xml_data.attrib['discussion_category']
class DiscussionDescriptor(RawDescriptor):
module_class = DiscussionModule
From 037fe5f722c77c1a3c4144131c86665926d7b533 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 23 Jan 2013 17:11:02 -0500
Subject: [PATCH 087/285] When checking types to convert data, don't forget
about longs. 32-bit Pythons make longs from values that are ints on 64-bit
Pythons.
---
common/djangoapps/util/converters.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py
index 17c45114d1..dd4f47e70e 100644
--- a/common/djangoapps/util/converters.py
+++ b/common/djangoapps/util/converters.py
@@ -15,10 +15,13 @@ def jsdate_to_time(field):
"""
if field is None:
return field
- elif isinstance(field, unicode) or isinstance(field, str): # iso format but ignores time zone assuming it's Z
+ elif isinstance(field, (unicode, str)):
+ # ISO format but ignores time zone assuming it's Z.
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple()
- elif isinstance(field, int) or isinstance(field, float):
+ elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
- return field
\ No newline at end of file
+ return field
+ else:
+ raise ValueError("Couldn't convert %r to time" % field)
From ac9d162d86045b280003088dc35776b634fcb3f4 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 24 Jan 2013 13:07:55 -0500
Subject: [PATCH 088/285] Fix a failing test. Pennington's Law strikes again.
---
lms/djangoapps/courseware/model_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index 1e2b3323a1..383b4032bc 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -149,7 +149,7 @@ class ModelDataCache(object):
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.student_info:
- self._query(
+ return self._query(
XModuleStudentInfoField,
student=self.user,
field_name__in=set(field.name for field in fields),
From 7257a728aa9419ef3bc7d54126c582e5f842ec3b Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 24 Jan 2013 13:52:43 -0500
Subject: [PATCH 089/285] Get the latest version of XBlock automatically
---
github-requirements.txt | 1 -
local-requirements.txt | 5 +++++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/github-requirements.txt b/github-requirements.txt
index 883490da99..468d55ce65 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -3,4 +3,3 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
--e git+ssh://git@github.com/MITx/xmodule-debugger@v0.1#egg=XBlock
diff --git a/local-requirements.txt b/local-requirements.txt
index 201467d11e..1248ecf77d 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -2,3 +2,8 @@
-e common/lib/capa
-e common/lib/xmodule
-e .
+
+# XBlock:
+# Might change frequently, so put it in local-requirements.txt,
+# but conceptually is an external package, so it is in a separate repo.
+-e git+ssh://git@github.com/MITx/xmodule-debugger@8f82a3b7fc#egg=XBlock
From cad3a19b4dd3e5c0d82674e22dc207d3df06324e Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 24 Jan 2013 13:56:46 -0500
Subject: [PATCH 090/285] A script to run just one test, based on the test
failure description.
---
runone.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100755 runone.py
diff --git a/runone.py b/runone.py
new file mode 100755
index 0000000000..2227ae0adf
--- /dev/null
+++ b/runone.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+from django.core import management
+
+import argparse
+import os
+import sys
+
+# I want this:
+# ERROR: test_update_and_fetch (mitx.cms.djangoapps.contentstore.tests.test_course_settings.CourseDetailsViewTest)
+# to become:
+# test --settings=cms.envs.test --pythonpath=. -s cms/djangoapps/contentstore/tests/test_course_settings.py:CourseDetailsViewTest.test_update_and_fetch
+
+def find_full_path(path_to_file):
+ """Find the full path where we only have a relative path from somewhere in the tree."""
+ for subdir, dirs, files in os.walk("."):
+ full = os.path.relpath(os.path.join(subdir, path_to_file))
+ if os.path.exists(full):
+ return full
+
+def main(argv):
+ parser = argparse.ArgumentParser(description="Run just one test")
+ parser.add_argument('--nocapture', '-s', action='store_true', help="Don't capture stdout (any stdout output will be printed immediately)")
+ parser.add_argument('words', metavar="WORDS", nargs='+', help="The description of a test failure, like 'ERROR: test_set_missing_field (courseware.tests.test_model_data.TestStudentModuleStorage)'")
+
+ args = parser.parse_args(argv)
+ words = []
+ # Collect all the words, ignoring what was quoted together, and get rid of parens.
+ for argword in args.words:
+ words.extend(w.strip("()") for w in argword.split())
+ # If it starts with "ERROR:" or "FAIL:", just ignore that.
+ if words[0].endswith(':'):
+ del words[0]
+
+ test_method = words[0]
+ test_path = words[1].split('.')
+ if test_path[0] == 'mitx':
+ del test_path[0]
+ test_class = test_path[-1]
+ del test_path[-1]
+
+ test_py_path = "%s.py" % ("/".join(test_path))
+ test_py_path = find_full_path(test_py_path)
+ test_spec = "%s:%s.%s" % (test_py_path, test_class, test_method)
+
+ if test_py_path.startswith('cms'):
+ settings = 'cms.envs.test'
+ elif test_py_path.startswith('lms'):
+ settings = 'lms.envs.test'
+ else:
+ raise Exception("Couldn't determine settings to use!")
+
+ django_args = ["django-admin.py", "test", "--pythonpath=."]
+ django_args.append("--settings=%s" % settings)
+ if args.nocapture:
+ django_args.append("-s")
+ django_args.append(test_spec)
+
+ print " ".join(django_args)
+ management.execute_from_command_line(django_args)
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
From 9a3e122a4861eb7d1f9bbb4c26b8568f9d5916e4 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 24 Jan 2013 14:03:55 -0500
Subject: [PATCH 091/285] Fix a few failing tests
---
lms/djangoapps/courseware/tests/test_model_data.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
index 5d83d2763d..da89412238 100644
--- a/lms/djangoapps/courseware/tests/test_model_data.py
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -114,6 +114,7 @@ class TestDescriptorFallback(TestCase):
class TestInvalidScopes(TestCase):
def setUp(self):
self.desc_md = {}
+ self.user = UserFactory.create()
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
@@ -205,7 +206,7 @@ class TestSettingsStorage(TestCase):
settings = SettingsFactory.create()
self.user = UserFactory.create()
self.desc_md = {}
- self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user)
+ self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.settings, 'settings_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_get_existing_field(self):
@@ -246,7 +247,7 @@ class TestContentStorage(TestCase):
content = ContentFactory.create()
self.user = UserFactory.create()
self.desc_md = {}
- self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user)
+ self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.content, 'content_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_get_existing_field(self):
From 592d0448640c0051e7f0128ae767eeb7d189bcf2 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 24 Jan 2013 14:37:46 -0500
Subject: [PATCH 092/285] Fix more tests
---
lms/djangoapps/courseware/model_data.py | 18 ++++++++++++++++++
.../courseware/tests/test_model_data.py | 4 ++--
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index 383b4032bc..5d535bfdf5 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -267,6 +267,15 @@ class LmsKeyValueStore(KeyValueStore):
If the key isn't found in the expected table during a read or a delete, then a KeyError will be raised
"""
+
+ _allowed_scopes = (
+ Scope.content,
+ Scope.settings,
+ Scope.student_state,
+ Scope.student_preferences,
+ Scope.student_info,
+ Scope.children,
+ )
def __init__(self, descriptor_model_data, model_data_cache):
self._descriptor_model_data = descriptor_model_data
self._model_data_cache = model_data_cache
@@ -278,6 +287,9 @@ class LmsKeyValueStore(KeyValueStore):
if key.scope == Scope.parent:
return None
+ if key.scope not in self._allowed_scopes:
+ raise InvalidScopeError(key.scope)
+
field_object = self._model_data_cache.find(key)
if field_object is None:
raise KeyError(key.field_name)
@@ -293,6 +305,9 @@ class LmsKeyValueStore(KeyValueStore):
field_object = self._model_data_cache.find_or_create(key)
+ if key.scope not in self._allowed_scopes:
+ raise InvalidScopeError(key.scope)
+
if key.scope == Scope.student_state:
state = json.loads(field_object.state)
state[key.field_name] = value
@@ -306,6 +321,9 @@ class LmsKeyValueStore(KeyValueStore):
if key.field_name in self._descriptor_model_data:
raise InvalidWriteError("Not allowed to deleted descriptor model data", key.field_name)
+ if key.scope not in self._allowed_scopes:
+ raise InvalidScopeError(key.scope)
+
field_object = self._model_data_cache.find(key)
if field_object is None:
raise KeyError(key.field_name)
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
index da89412238..13ecf9429d 100644
--- a/lms/djangoapps/courseware/tests/test_model_data.py
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -131,8 +131,8 @@ class TestStudentModuleStorage(TestCase):
def setUp(self):
self.desc_md = {}
- self.mdc = Mock()
- self.mdc.find.return_value.state = json.dumps({'a_field': 'a_value'})
+ self.user = UserFactory.create()
+ self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_get_existing_field(self):
From 890f02a2324357d3ec56115dad3b7df6a3b6fe5b Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 25 Jan 2013 12:26:17 -0500
Subject: [PATCH 093/285] When installing the local requirements, don't upgrade
dependencies. Makes things faster.
---
rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rakefile b/rakefile
index 312ad90124..ac79ee6c6f 100644
--- a/rakefile
+++ b/rakefile
@@ -121,7 +121,7 @@ default_options = {
task :predjango do
sh("find . -type f -name *.pyc -delete")
- sh('pip install -q --upgrade -r local-requirements.txt')
+ sh('pip install -q --no-index -r local-requirements.txt')
sh('git submodule update --init')
end
From 97bb56b3020680df938e8580866d84144af2baf6 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 25 Jan 2013 12:33:30 -0500
Subject: [PATCH 094/285] Tweak the dev instructions.
---
doc/development.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/doc/development.md b/doc/development.md
index d68b49228e..91931e85f9 100644
--- a/doc/development.md
+++ b/doc/development.md
@@ -9,9 +9,8 @@ This will read the `Gemfile` and install all of the gems specified there.
### Python
-In order, run the following:
+Run the following::
- pip install -r pre-requirements.txt
pip install -r requirements.txt
pip install -r test-requirements.txt
From 08acf43575b34376c013f96180784b3885775dd3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 25 Jan 2013 15:49:29 -0500
Subject: [PATCH 095/285] Don't store first_time_user any more, instead
interested observers can examine whether .position is None or not.
---
common/lib/xmodule/xmodule/course_module.py | 14 +-----
common/lib/xmodule/xmodule/seq_module.py | 7 ---
lms/djangoapps/courseware/views.py | 56 ++++++++++++---------
3 files changed, 34 insertions(+), 43 deletions(-)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 2864aaba1d..6b0909ec2e 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -89,18 +89,8 @@ class TextbookList(ModelType):
return json_data
-class CourseModule(SequenceModule):
- first_time_user = Boolean(help="Whether this is the first time the user has visited this course", scope=Scope.student_state, default=True)
-
- def __init__(self, *args, **kwargs):
- super(CourseModule, self).__init__(*args, **kwargs)
-
- if self.first_time_user:
- self.first_time_user = False
-
-
class CourseDescriptor(SequenceDescriptor):
- module_class = CourseModule
+ module_class = SequenceModule
textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content)
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
@@ -123,7 +113,7 @@ class CourseDescriptor(SequenceDescriptor):
has_children = True
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
-
+
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
# courses to share the same css_class across runs even if they have
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index b3ceb7a229..17d722a52a 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -46,13 +46,6 @@ class SequenceModule(XModule):
if self.system.get('position'):
self.position = int(self.system.get('position'))
- # Default to the first child
- # Don't set 1 as the default in the property definition, because
- # there is code that looks for the existance of the position value
- # to determine if the student has visited the sequence before or not
- if self.position is None:
- self.position = 1
-
self.rendered = False
def get_instance_state(self):
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 201787db08..c425494260 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -103,16 +103,18 @@ def render_accordion(request, course, chapter, section, model_data_cache):
def get_current_child(xmodule):
"""
Get the xmodule.position's display item of an xmodule that has a position and
- children. Returns None if the xmodule doesn't have a position, or if there
- are no children. Otherwise, if position is out of bounds, returns the first child.
+ children. If xmodule has no position or is out of bounds, return the first child.
+ Returns None only if there are no children at all.
"""
- if not hasattr(xmodule, 'position'):
- return None
+ if xmodule.position is None:
+ pos = 0
+ else:
+ # position is 1-indexed.
+ pos = xmodule.position - 1
children = xmodule.get_display_items()
- # position is 1-indexed.
- if 0 <= xmodule.position - 1 < len(children):
- child = children[xmodule.position - 1]
+ if 0 <= pos < len(children):
+ child = children[pos]
elif len(children) > 0:
# Something is wrong. Default to first child
child = children[0]
@@ -121,36 +123,43 @@ def get_current_child(xmodule):
return child
-def redirect_to_course_position(course_module, first_time):
+def redirect_to_course_position(course_module):
"""
- Load the course state for the user, and return a redirect to the
- appropriate place in the course: either the first element if there
- is no state, or their previous place if there is.
+ Return a redirect to the user's current place in the course.
+
+ If this is the user's first time, redirects to COURSE/CHAPTER/SECTION.
+ If this isn't the users's first time, redirects to COURSE/CHAPTER,
+ and the view will find the current section and display a message
+ about reusing the stored position.
+
+ If there is no current position in the course or chapter, then selects
+ the first child.
- If this is the user's first time, send them to the first section instead.
"""
- course_id = course_module.descriptor.id
+ urlargs = {'course_id': course_module.descriptor.id}
chapter = get_current_child(course_module)
if chapter is None:
# oops. Something bad has happened.
raise Http404("No chapter found when loading current position in course")
- if not first_time:
- return redirect(reverse('courseware_chapter', kwargs={'course_id': course_id,
- 'chapter': chapter.url_name}))
+
+ urlargs['chapter'] = chapter.url_name
+ if course_module.position is not None:
+ return redirect(reverse('courseware_chapter', kwargs=urlargs))
+
# Relying on default of returning first child
section = get_current_child(chapter)
- return redirect(reverse('courseware_section', kwargs={'course_id': course_id,
- 'chapter': chapter.url_name,
- 'section': section.url_name}))
+ if section is None:
+ raise Http404("No section found when loading current position in course")
+
+ urlargs['section'] = section.url_name
+ return redirect(reverse('courseware_section', kwargs=urlargs))
def save_child_position(seq_module, child_name):
"""
child_name: url_name of the child
"""
- for i, c in enumerate(seq_module.get_display_items()):
+ for position, c in enumerate(seq_module.get_display_items(), start=1):
if c.url_name == child_name:
- # Position is 1-indexed
- position = i + 1
# Only save if position changed
if position != seq_module.position:
seq_module.position = position
@@ -201,7 +210,7 @@ def index(request, course_id, chapter=None, section=None,
return redirect(reverse('about_course', args=[course.id]))
if chapter is None:
- return redirect_to_course_position(course_module, course_module.first_time_user)
+ return redirect_to_course_position(course_module)
context = {
'csrf': csrf(request)['csrf_token'],
@@ -245,7 +254,6 @@ def index(request, course_id, chapter=None, section=None,
# Save where we are in the chapter
save_child_position(chapter_module, section)
-
context['content'] = section_module.get_html()
else:
# section is none, so display a message
From 9d34767e6afd998122e8bd5b74231888232d962f Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 1 Feb 2013 15:45:43 -0500
Subject: [PATCH 096/285] Fix the remaining module storage tests.
---
lms/djangoapps/courseware/model_data.py | 6 +++---
lms/djangoapps/courseware/tests/test_model_data.py | 5 +++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index 5d535bfdf5..d13bfe9bba 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -25,7 +25,7 @@ def chunks(items, chunk_size):
class ModelDataCache(object):
"""
A cache of django model objects needed to supply the data
- for a module and its decendents
+ for a module and its decendants
"""
def __init__(self, descriptors, course_id, user, select_for_update=False):
'''
@@ -35,10 +35,10 @@ class ModelDataCache(object):
state will have a StudentModule.
Arguments
- descriptors: An array of XModuleDescriptors.
+ descriptors: A list of XModuleDescriptors.
course_id: The id of the current course
user: The user for which to cache data
- select_for_update: Flag indicating whether the rows should be locked until end of transaction
+ select_for_update: True if rows should be locked until end of transaction
'''
self.cache = {}
self.descriptors = descriptors
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
index 13ecf9429d..33a14f3c61 100644
--- a/lms/djangoapps/courseware/tests/test_model_data.py
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -131,7 +131,8 @@ class TestStudentModuleStorage(TestCase):
def setUp(self):
self.desc_md = {}
- self.user = UserFactory.create()
+ student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
+ self.user = student_module.student
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
@@ -159,7 +160,7 @@ class TestStudentModuleStorage(TestCase):
"Test that deleting an existing field removes it from the StudentModule"
self.kvs.delete(student_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count())
- self.assertEquals({}, self.mdc.find.return_value.state)
+ self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
def test_delete_missing_field(self):
"Test that deleting a missing field from an existing StudentModule raises a KeyError"
From d6984cced8682d98d11658338aae6d166cf6059d Mon Sep 17 00:00:00 2001
From: jmvt
Date: Mon, 4 Feb 2013 16:08:37 -0500
Subject: [PATCH 097/285] Updated code to handle new analytics result format.
---
.../courseware/instructor_dashboard.html | 58 ++++++++++---------
1 file changed, 30 insertions(+), 28 deletions(-)
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 967088de19..8053f67a88 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -345,10 +345,10 @@ function goto( mode)
%if modeflag.get('Analytics'):
- Number of students enrolled:
+ Number of students enrolled for ${students_enrolled_json['data'][0]['course_id']}:
% if students_enrolled_json is not None:
% if students_enrolled_json['status'] == 'success':
- ${students_enrolled_json['data']['value']} as of ${students_enrolled_json['time']}
+ ${students_enrolled_json['data'][0]['count']} as of ${students_enrolled_json['time']}
% else:
${students_enrolled_json['error']}
% endif
@@ -358,10 +358,10 @@ function goto( mode)
- Number of active students for the past 7 days:
+ Number of students active for ${students_active_json['data'][0]['course_id']} for the past 7 days:
% if students_active_json is not None:
% if students_active_json['status'] == 'success':
- ${students_active_json['data']['value']} as of ${students_active_json['time']}
+ ${students_active_json['data'][0]['count']} as of ${students_active_json['time']}
% else:
${students_active_json['error']}
% endif
@@ -407,8 +407,8 @@ function goto( mode)
Problem
Number of students
- % for k,v in students_per_problem_correct_json['data'].items():
-
${k}
${v}
+ % for row in students_per_problem_correct_json['data']:
+
${row['module_id']}
${row['count']}
% endfor
@@ -420,6 +420,7 @@ function goto( mode)
% endif
+
Students per module who attempted at least one problem
@@ -429,8 +430,8 @@ function goto( mode)
Module
Number of students
- % for k,v in attempted_problems['data'].items():
-
${k}
${v}
+ % for row in attempted_problems['data']:
+
${row['module_id']}
${row['count']}
% endfor
@@ -444,6 +445,27 @@ function goto( mode)
+
+
Grade distribution:
+
+ % if overall_grade_distribution is not None:
+ % if overall_grade_distribution['status'] == 'success':
+
+
+
Grade
Number of students
+ % for row in overall_grade_distribution['data']:
+