Commit 1de8c4d3 by Peter Baratta

Merge branch 'master' into pbaratta/calc-tests

parents 70898a06 af8b509e
...@@ -26,6 +26,7 @@ Gemfile.lock ...@@ -26,6 +26,7 @@ Gemfile.lock
conf/locale/en/LC_MESSAGES/*.po conf/locale/en/LC_MESSAGES/*.po
!messages.po !messages.po
lms/static/sass/*.css lms/static/sass/*.css
lms/static/sass/application.scss
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
nosetests.xml nosetests.xml
......
REVIEWBOARD_URL = "https://rbcommons.com/s/edx/"
GUESS_FIELDS = True
...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] ...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes'] NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
This config file extends the test environment configuration This config file extends the test environment configuration
so that we can run the lettuce acceptance tests. so that we can run the lettuce acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
# You need to start the server in debug mode, # You need to start the server in debug mode,
...@@ -23,7 +28,7 @@ MODULESTORE_OPTIONS = { ...@@ -23,7 +28,7 @@ MODULESTORE_OPTIONS = {
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS 'OPTIONS': MODULESTORE_OPTIONS
}, },
'direct': { 'direct': {
......
""" """
This is the default template for our main set of AWS servers. This is the default template for our main set of AWS servers.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import json import json
from .common import * from .common import *
......
...@@ -19,6 +19,10 @@ Longer TODO: ...@@ -19,6 +19,10 @@ Longer TODO:
multiple sites, but we do need a way to map their data assets. multiple sites, but we do need a way to map their data assets.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import sys import sys
import lms.envs.common import lms.envs.common
from path import path from path import path
......
""" """
This config file runs the simplest dev environment""" This config file runs the simplest dev environment"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# dev environment for ichuang/mit # dev environment for ichuang/mit
# FORCE_SCRIPT_NAME = '/cms' # FORCE_SCRIPT_NAME = '/cms'
......
...@@ -8,6 +8,10 @@ The worker can be executed using: ...@@ -8,6 +8,10 @@ The worker can be executed using:
django_admin.py celery worker django_admin.py celery worker
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from dev import * from dev import *
################################# CELERY ###################################### ################################# CELERY ######################################
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
This configuration is used for running jasmine tests This configuration is used for running jasmine tests
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
import os import os
from path import path from path import path
......
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
// talbs: we need to slowly ween ourselves off of these // talbs: we need to slowly ween ourselves off of these
// ==================== // ====================
// line-height (old way)
@function lh($amount: 1) {
@return $body-line-height * $amount;
}
// inherited - vertical and horizontal centering // inherited - vertical and horizontal centering
@mixin vertically-and-horizontally-centered ($height, $width) { @mixin vertically-and-horizontally-centered ($height, $width) {
left: 50%; left: 50%;
......
...@@ -11,54 +11,54 @@ ...@@ -11,54 +11,54 @@
.t-title1 { .t-title1 {
@extend .t-title; @extend .t-title;
@include font-size(60); @include font-size(60);
@include lh(60); @include line-height(60);
} }
.t-title2 { .t-title2 {
@extend .t-title; @extend .t-title;
@include font-size(48); @include font-size(48);
@include lh(48); @include line-height(48);
} }
.t-title3 { .t-title3 {
@include font-size(36); @include font-size(36);
@include lh(36); @include line-height(36);
} }
.t-title4 { .t-title4 {
@extend .t-title; @extend .t-title;
@include font-size(24); @include font-size(24);
@include lh(24); @include line-height(24);
} }
.t-title5 { .t-title5 {
@extend .t-title; @extend .t-title;
@include font-size(18); @include font-size(18);
@include lh(18); @include line-height(18);
} }
.t-title6 { .t-title6 {
@extend .t-title; @extend .t-title;
@include font-size(16); @include font-size(16);
@include lh(16); @include line-height(16);
} }
.t-title7 { .t-title7 {
@extend .t-title; @extend .t-title;
@include font-size(14); @include font-size(14);
@include lh(14); @include line-height(14);
} }
.t-title8 { .t-title8 {
@extend .t-title; @extend .t-title;
@include font-size(12); @include font-size(12);
@include lh(12); @include line-height(12);
} }
.t-title9 { .t-title9 {
@extend .t-title; @extend .t-title;
@include font-size(11); @include font-size(11);
@include lh(11); @include line-height(11);
} }
// ==================== // ====================
...@@ -71,31 +71,31 @@ ...@@ -71,31 +71,31 @@
.t-copy-base { .t-copy-base {
@extend .t-copy; @extend .t-copy;
@include font-size(16); @include font-size(16);
@include lh(16); @include line-height(16);
} }
.t-copy-lead1 { .t-copy-lead1 {
@extend .t-copy; @extend .t-copy;
@include font-size(18); @include font-size(18);
@include lh(18); @include line-height(18);
} }
.t-copy-lead2 { .t-copy-lead2 {
@extend .t-copy; @extend .t-copy;
@include font-size(24); @include font-size(24);
@include lh(24); @include line-height(24);
} }
.t-copy-sub1 { .t-copy-sub1 {
@extend .t-copy; @extend .t-copy;
@include font-size(14); @include font-size(14);
@include lh(14); @include line-height(14);
} }
.t-copy-sub2 { .t-copy-sub2 {
@extend .t-copy; @extend .t-copy;
@include font-size(12); @include font-size(12);
@include lh(12); @include line-height(12);
} }
// ==================== // ====================
...@@ -103,22 +103,22 @@ ...@@ -103,22 +103,22 @@
// actions/labels // actions/labels
.t-action1 { .t-action1 {
@include font-size(18); @include font-size(18);
@include lh(18); @include line-height(18);
} }
.t-action2 { .t-action2 {
@include font-size(16); @include font-size(16);
@include lh(16); @include line-height(16);
} }
.t-action3 { .t-action3 {
@include font-size(14); @include font-size(14);
@include lh(14); @include line-height(14);
} }
.t-action4 { .t-action4 {
@include font-size(12); @include font-size(12);
@include lh(12); @include line-height(12);
} }
......
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="calc", name="calc",
version="0.1", version="0.1.1",
py_modules=["calc"], py_modules=["calc"],
install_requires=[ install_requires=[
"pyparsing==1.5.6", "pyparsing==1.5.6",
......
...@@ -475,12 +475,11 @@ class LoncapaProblem(object): ...@@ -475,12 +475,11 @@ class LoncapaProblem(object):
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;') msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
raise responsetypes.LoncapaProblemError(msg) raise responsetypes.LoncapaProblemError(msg)
# store code source in context # Store code source in context, along with the Python path needed to run it correctly.
context['script_code'] = all_code context['script_code'] = all_code
context['python_path'] = python_path
return context return context
def _extract_html(self, problemtree): # private def _extract_html(self, problemtree): # private
''' '''
Main (private) function which converts Problem XML tree to HTML. Main (private) function which converts Problem XML tree to HTML.
......
...@@ -286,7 +286,7 @@ class LoncapaResponse(object): ...@@ -286,7 +286,7 @@ class LoncapaResponse(object):
} }
try: try:
safe_exec.safe_exec(code, globals_dict) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
...@@ -972,7 +972,7 @@ class CustomResponse(LoncapaResponse): ...@@ -972,7 +972,7 @@ class CustomResponse(LoncapaResponse):
'ans': ans, 'ans': ans,
} }
globals_dict.update(kwargs) globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, cache=self.system.cache) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
return globals_dict['cfn_return'] return globals_dict['cfn_return']
return check_function return check_function
......
...@@ -39,6 +39,9 @@ class TestLazyMod(unittest.TestCase): ...@@ -39,6 +39,9 @@ class TestLazyMod(unittest.TestCase):
self.assertEqual(hsv[0], 0.25) self.assertEqual(hsv[0], 0.25)
def test_dotted(self): def test_dotted(self):
self.assertNotIn("email.utils", sys.modules) # wsgiref is a module with submodules that is not already imported.
email_utils = LazyModule("email.utils") # Any similar module would do. This test demonstrates that the module
self.assertEqual(email_utils.quote('"hi"'), r'\"hi\"') # is not already im
self.assertNotIn("wsgiref.util", sys.modules)
wsgiref_util = LazyModule("wsgiref.util")
self.assertEqual(wsgiref_util.guess_scheme({}), "http")
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="chem", name="chem",
version="0.1", version="0.1.1",
packages=["chem"], packages=["chem"],
install_requires=[ install_requires=[
"pyparsing==1.5.6", "pyparsing==1.5.6",
......
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="sandbox-packages", name="sandbox-packages",
version="0.1", version="0.1.1",
packages=[ packages=[
"verifiers", "verifiers",
], ],
......
...@@ -8,7 +8,7 @@ from .x_module import XModule ...@@ -8,7 +8,7 @@ from .x_module import XModule
from xblock.core import Integer, Scope, String, Boolean, List from xblock.core import Integer, Scope, String, Boolean, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, StringyFloat from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -49,19 +49,19 @@ class VersionInteger(Integer): ...@@ -49,19 +49,19 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state) current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) scope=Scope.user_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state) scope=Scope.user_state)
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state) scope=Scope.user_state)
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
scope=Scope.settings) scope=Scope.settings)
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
scope=Scope.settings) scope=Scope.settings)
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings) due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
......
...@@ -290,7 +290,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -290,7 +290,6 @@ class XMLModuleStore(ModuleStoreBase):
if course_dirs is None: if course_dirs is None:
course_dirs = sorted([d for d in os.listdir(self.data_dir) if course_dirs = sorted([d for d in os.listdir(self.data_dir) if
os.path.exists(self.data_dir / d / "course.xml")]) os.path.exists(self.data_dir / d / "course.xml")])
for course_dir in course_dirs: for course_dir in course_dirs:
self.try_load_course(course_dir) self.try_load_course(course_dir)
......
...@@ -6,7 +6,7 @@ log = logging.getLogger(__name__) ...@@ -6,7 +6,7 @@ log = logging.getLogger(__name__)
class ControllerQueryService(GradingService): class ControllerQueryService(GradingService):
""" """
Interface to staff grading backend. Interface to controller query backend.
""" """
def __init__(self, config, system): def __init__(self, config, system):
...@@ -77,6 +77,50 @@ class ControllerQueryService(GradingService): ...@@ -77,6 +77,50 @@ class ControllerQueryService(GradingService):
return response return response
class MockControllerQueryService(object):
"""
Mock controller query service for testing
"""
def __init__(self, config, system):
pass
def check_if_name_is_unique(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def check_for_eta(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def check_combined_notifications(self, **params):
combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}'
return combined_notifications
def get_grading_status_list(self, **params):
grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}'
return grading_status_list
def get_flagged_problem_list(self, **params):
flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}'
return flagged_problem_list
def take_action_on_flags(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def convert_seconds_to_human_readable(seconds): def convert_seconds_to_human_readable(seconds):
if seconds < 60: if seconds < 60:
human_string = "{0} seconds".format(seconds) human_string = "{0} seconds".format(seconds)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
} }
// mixins - line height // mixins - line height
@mixin lh($fontSize: auto){ @mixin line-height($fontSize: auto){
line-height: ($fontSize*1.48) + px; line-height: ($fontSize*1.48) + px;
line-height: (($fontSize/10)*1.48) + rem; line-height: (($fontSize/10)*1.48) + rem;
} }
......
...@@ -414,6 +414,11 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -414,6 +414,11 @@ def modx_dispatch(request, dispatch, location, course_id):
through the part before the first '?'. through the part before the first '?'.
- location -- the module location. Used to look up the XModule instance - location -- the module location. Used to look up the XModule instance
- course_id -- defines the course context for this request. - course_id -- defines the course context for this request.
Raises PermissionDenied if the user is not logged in. Raises Http404 if
the location and course_id do not identify a valid module, the module is
not accessible by the user, or the module raises NotFoundError. If the
module raises any other error, it will escape this function.
''' '''
# ''' (fix emacs broken parsing) # ''' (fix emacs broken parsing)
...@@ -442,8 +447,19 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -442,8 +447,19 @@ def modx_dispatch(request, dispatch, location, course_id):
return HttpResponse(json.dumps({'success': file_too_big_msg})) return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles p[fileinput_id] = inputfiles
try:
descriptor = modulestore().get_instance(course_id, location)
except ItemNotFoundError:
log.warn(
"Invalid location for course id {course_id}: {location}".format(
course_id=course_id,
location=location
)
)
raise Http404
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id,
request.user, modulestore().get_instance(course_id, location)) request.user, descriptor)
instance = get_module(request.user, request, location, model_data_cache, course_id, grade_bucket_type='ajax') instance = get_module(request.user, request, location, model_data_cache, course_id, grade_bucket_type='ajax')
if instance is None: if instance is None:
......
...@@ -69,19 +69,38 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): ...@@ -69,19 +69,38 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase):
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))})) (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
mock_request_3 = MagicMock() mock_request_3 = MagicMock()
mock_request_3.POST.copy.return_value = {} mock_request_3.POST.copy.return_value = {'position': 1}
mock_request_3.FILES = False mock_request_3.FILES = False
mock_request_3.user = UserFactory() mock_request_3.user = UserFactory()
inputfile_2 = Stub() inputfile_2 = Stub()
inputfile_2.size = 1 inputfile_2.size = 1
inputfile_2.name = 'name' inputfile_2.name = 'name'
self.assertRaises(ItemNotFoundError, render.modx_dispatch,
mock_request_3, 'dummy', self.location, 'toy')
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy',
self.location, self.course_id)
mock_request_3.POST.copy.return_value = {'position': 1}
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position', self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
self.location, self.course_id), HttpResponse) self.location, self.course_id), HttpResponse)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'goto_position',
self.location,
'bad_course_id'
)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'goto_position',
['i4x', 'edX', 'toy', 'chapter', 'bad_location'],
self.course_id
)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'bad_dispatch',
self.location,
self.course_id
)
def test_get_score_bucket(self): def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
......
...@@ -174,6 +174,9 @@ def forum_form_discussion(request, course_id): ...@@ -174,6 +174,9 @@ def forum_form_discussion(request, course_id):
try: try:
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
threads = [utils.safe_content(thread) for thread in unsafethreads] threads = [utils.safe_content(thread) for thread in unsafethreads]
except (cc.utils.CommentClientMaintenanceError) as err:
log.warning("Forum is in maintenance mode")
return render_to_response('discussion/maintenance.html', {})
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading forum discussion threads: %s" % str(err)) log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404 raise Http404
......
...@@ -13,7 +13,7 @@ class Migration(SchemaMigration): ...@@ -13,7 +13,7 @@ class Migration(SchemaMigration):
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('uri', self.gf('django.db.models.fields.CharField')(max_length=512, db_index=True)), ('uri', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('text', self.gf('django.db.models.fields.TextField')(default='')), ('text', self.gf('django.db.models.fields.TextField')(default='')),
('quote', self.gf('django.db.models.fields.TextField')(default='')), ('quote', self.gf('django.db.models.fields.TextField')(default='')),
('range_start', self.gf('django.db.models.fields.CharField')(max_length=2048)), ('range_start', self.gf('django.db.models.fields.CharField')(max_length=2048)),
...@@ -82,7 +82,7 @@ class Migration(SchemaMigration): ...@@ -82,7 +82,7 @@ class Migration(SchemaMigration):
'tags': ('django.db.models.fields.TextField', [], {'default': "''"}), 'tags': ('django.db.models.fields.TextField', [], {'default': "''"}),
'text': ('django.db.models.fields.TextField', [], {'default': "''"}), 'text': ('django.db.models.fields.TextField', [], {'default': "''"}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'uri': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}), 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
} }
} }
......
...@@ -9,7 +9,7 @@ import json ...@@ -9,7 +9,7 @@ import json
class Note(models.Model): class Note(models.Model):
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
uri = models.CharField(max_length=512, db_index=True) uri = models.CharField(max_length=255, db_index=True)
text = models.TextField(default="") text = models.TextField(default="")
quote = models.TextField(default="") quote = models.TextField(default="")
range_start = models.CharField(max_length=2048) # xpath string range_start = models.CharField(max_length=2048) # xpath string
......
...@@ -21,6 +21,7 @@ import open_ended_notifications ...@@ -21,6 +21,7 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search from xmodule.modulestore import search
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
...@@ -30,10 +31,10 @@ log = logging.getLogger(__name__) ...@@ -30,10 +31,10 @@ log = logging.getLogger(__name__)
system = ModuleSystem( system = ModuleSystem(
ajax_url=None, ajax_url=None,
track_function=None, track_function=None,
get_module = None, get_module=None,
render_template=render_to_string, render_template=render_to_string,
replace_urls = None, replace_urls=None,
xblock_model_data= {} xblock_model_data={}
) )
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
...@@ -90,40 +91,61 @@ def staff_grading(request, course_id): ...@@ -90,40 +91,61 @@ def staff_grading(request, course_id):
'staff_access': True, }) 'staff_access': True, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) def find_peer_grading_module(course):
def peer_grading(request, course_id): """
''' Given a course, finds the first peer grading module in it.
Show a peer grading interface @param course: A course object.
''' @return: boolean found_module, string problem_url
"""
#Get the current course
course = get_course_with_access(request.user, course_id, 'load')
course_id_parts = course.id.split("/")
false_dict = [False, "False", "false", "FALSE"]
#Reverse the base course url #Reverse the base course url
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: found_module = False
#TODO: This will not work with multiple runs of a course. Make it work. The last key in the Location passed problem_url = ""
#to get_items is called revision. Is this the same as run?
#Get the peer grading modules currently in the course #Get the course id and split it
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None]) course_id_parts = course.id.split("/")
log.info("COURSE ID PARTS")
log.info(course_id_parts)
#Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs.
items = modulestore().get_items(['i4x', course_id_parts[0], course_id_parts[1], 'peergrading', None],
course_id=course.id)
#See if any of the modules are centralized modules (ie display info from multiple problems) #See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict] items = [i for i in items if not getattr(i, "use_for_single_location", True)]
#Get the first one #Get the first one
if len(items) > 0:
item_location = items[0].location item_location = items[0].location
#Generate a url for the first module and redirect the user to it #Generate a url for the first module and redirect the user to it
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location) problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_url = generate_problem_url(problem_url_parts, base_course_url)
found_module = True
return HttpResponseRedirect(problem_url) return found_module, problem_url
except:
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id):
'''
When a student clicks on the "peer grading" button in the open ended interface, link them to a peer grading
xmodule in the course.
'''
#Get the current course
course = get_course_with_access(request.user, course_id, 'load')
found_module, problem_url = find_peer_grading_module(course)
if not found_module:
#This is a student_facing_error #This is a student_facing_error
error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff." error_message = """
Error with initializing peer grading.
There has not been a peer grading module created in the courseware that would allow you to grade others.
Please check back later for this.
"""
#This is a dev_facing_error #This is a dev_facing_error
log.exception(error_message + "Current course is: {0}".format(course_id)) log.exception(error_message + "Current course is: {0}".format(course_id))
return HttpResponse(error_message) return HttpResponse(error_message)
return HttpResponseRedirect(problem_url)
def generate_problem_url(problem_url_parts, base_course_url): def generate_problem_url(problem_url_parts, base_course_url):
""" """
...@@ -145,7 +167,8 @@ def generate_problem_url(problem_url_parts, base_course_url): ...@@ -145,7 +167,8 @@ def generate_problem_url(problem_url_parts, base_course_url):
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def student_problem_list(request, course_id): def student_problem_list(request, course_id):
''' '''
Show a student problem list Show a student problem list to a student. Fetch the list from the grading controller server, get some metadata,
and then show it to the student.
''' '''
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
student_id = unique_id_for_user(request.user) student_id = unique_id_for_user(request.user)
...@@ -157,6 +180,7 @@ def student_problem_list(request, course_id): ...@@ -157,6 +180,7 @@ def student_problem_list(request, course_id):
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: try:
#Get list of all open ended problems that the grading server knows about
problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json) problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success'] success = problem_list_dict['success']
...@@ -166,8 +190,22 @@ def student_problem_list(request, course_id): ...@@ -166,8 +190,22 @@ def student_problem_list(request, course_id):
else: else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
#A list of problems to remove (problems that can't be found in the course)
list_to_remove = []
for i in xrange(0, len(problem_list)): for i in xrange(0, len(problem_list)):
try:
#Try to load each problem in the courseware to get links to them
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
except ItemNotFoundError:
#If the problem cannot be found at the location received from the grading controller server, it has been deleted by the course author.
#Continue with the rest of the location to construct the list
error_message = "Could not find module for course {0} at location {1}".format(course.id,
problem_list[i][
'location'])
log.error(error_message)
#Mark the problem for removal from the list
list_to_remove.append(i)
continue
problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_url = generate_problem_url(problem_url_parts, base_course_url)
problem_list[i].update({'actual_url': problem_url}) problem_list[i].update({'actual_url': problem_url})
eta_available = problem_list[i]['eta_available'] eta_available = problem_list[i]['eta_available']
...@@ -197,6 +235,8 @@ def student_problem_list(request, course_id): ...@@ -197,6 +235,8 @@ def student_problem_list(request, course_id):
log.error("Problem with results from external grading service for open ended.") log.error("Problem with results from external grading service for open ended.")
success = False success = False
#Remove problems that cannot be found in the courseware from the list
problem_list = [problem_list[i] for i in xrange(0, len(problem_list)) if i not in list_to_remove]
ajax_url = _reverse_with_slash('open_ended_problems', course_id) ajax_url = _reverse_with_slash('open_ended_problems', course_id)
return render_to_response('open_ended_problems/open_ended_problems.html', { return render_to_response('open_ended_problems/open_ended_problems.html', {
...@@ -300,6 +340,15 @@ def combined_notifications(request, course_id): ...@@ -300,6 +340,15 @@ def combined_notifications(request, course_id):
'description': description, 'description': description,
'alert_message': alert_message 'alert_message': alert_message
} }
#The open ended panel will need to link the "peer grading" button in the panel to a peer grading
#xmodule defined in the course. This checks to see if the human name of the server notification
#that we are currently processing is "peer grading". If it is, it looks for a peer grading
#module in the course. If none exists, it removes the peer grading item from the panel.
if human_name == "Peer Grading":
found_module, problem_url = find_peer_grading_module(course)
if found_module:
notification_list.append(notification_item)
else:
notification_list.append(notification_item) notification_list.append(notification_item)
ajax_url = _reverse_with_slash('open_ended_notifications', course_id) ajax_url = _reverse_with_slash('open_ended_notifications', course_id)
...@@ -311,9 +360,7 @@ def combined_notifications(request, course_id): ...@@ -311,9 +360,7 @@ def combined_notifications(request, course_id):
'ajax_url': ajax_url, 'ajax_url': ajax_url,
} }
return render_to_response('open_ended_problems/combined_notifications.html', return render_to_response('open_ended_problems/combined_notifications.html', combined_dict)
combined_dict
)
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
This config file extends the test environment configuration This config file extends the test environment configuration
so that we can run the lettuce acceptance tests. so that we can run the lettuce acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
# You need to start the server in debug mode, # You need to start the server in debug mode,
......
...@@ -6,6 +6,11 @@ Common traits: ...@@ -6,6 +6,11 @@ Common traits:
* Use memcached, and cache-backed sessions * Use memcached, and cache-backed sessions
* Use a MySQL 5.1 database * Use a MySQL 5.1 database
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import json import json
from .common import * from .common import *
...@@ -109,6 +114,11 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA ...@@ -109,6 +114,11 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
if not THEME_NAME is None:
enable_theme(THEME_NAME)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -3,6 +3,11 @@ This config file is a copy of dev environment without the Debug ...@@ -3,6 +3,11 @@ This config file is a copy of dev environment without the Debug
Toolbar. I it suitable to run against acceptance tests. Toolbar. I it suitable to run against acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
# REMOVE DEBUG TOOLBAR # REMOVE DEBUG TOOLBAR
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..aws import * from ..aws import *
with open(ENV_ROOT / "cms.auth.json") as auth_file: with open(ENV_ROOT / "cms.auth.json") as auth_file:
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..dev import * from ..dev import *
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
MODULESTORE = { MODULESTORE = {
......
...@@ -18,6 +18,11 @@ Longer TODO: ...@@ -18,6 +18,11 @@ Longer TODO:
3. We need to handle configuration for multiple courses. This could be as 3. We need to handle configuration for multiple courses. This could be as
multiple sites, but we do need a way to map their data assets. multiple sites, but we do need a way to map their data assets.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import sys import sys
import os import os
...@@ -67,6 +72,7 @@ MITX_FEATURES = { ...@@ -67,6 +72,7 @@ MITX_FEATURES = {
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops)
'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False, 'ENABLE_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False, 'ENABLE_MANUAL_GIT_RELOAD': False,
...@@ -105,6 +111,9 @@ MITX_FEATURES = { ...@@ -105,6 +111,9 @@ MITX_FEATURES = {
# Enable URL that shows information about the status of variuous services # Enable URL that shows information about the status of variuous services
'ENABLE_SERVICE_STATUS': False, 'ENABLE_SERVICE_STATUS': False,
# Toggle to indicate use of a custom theme
'USE_CUSTOM_THEME': False
} }
# Used for A/B testing # Used for A/B testing
...@@ -162,12 +171,12 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', ...@@ -162,12 +171,12 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
# This is where Django Template lookup is defined. There are a few of these # This is where Django Template lookup is defined. There are a few of these
# still left lying around. # still left lying around.
TEMPLATE_DIRS = ( TEMPLATE_DIRS = [
PROJECT_ROOT / "templates", PROJECT_ROOT / "templates",
COMMON_ROOT / 'templates', COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates', COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates', COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
) ]
TEMPLATE_CONTEXT_PROCESSORS = ( TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
...@@ -709,3 +718,31 @@ MKTG_URL_LINK_MAP = { ...@@ -709,3 +718,31 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor', 'HONOR': 'honor',
'PRIVACY': 'privacy_edx', 'PRIVACY': 'privacy_edx',
} }
############################### THEME ################################
def enable_theme(theme_name):
"""
Enable the settings for a custom theme, whose files should be stored
in ENV_ROOT/themes/THEME_NAME (e.g., edx_all/themes/stanford).
The THEME_NAME setting should be configured separately since it can't
be set here (this function closes too early). An idiom for doing this
is:
THEME_NAME = "stanford"
enable_theme(THEME_NAME)
"""
MITX_FEATURES['USE_CUSTOM_THEME'] = True
# Calculate the location of the theme's files
theme_root = ENV_ROOT / "themes" / theme_name
# Include the theme's templates in the template search paths
TEMPLATE_DIRS.append(theme_root / 'templates')
MAKO_TEMPLATES['main'].append(theme_root / 'templates')
# Namespace the theme's static files to 'themes/<theme_name>' to
# avoid collisions with default edX static files
STATICFILES_DIRS.append((u'themes/%s' % theme_name,
theme_root / 'static'))
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
These are debug machines used for content creators, so they're kind of a cross These are debug machines used for content creators, so they're kind of a cross
between dev machines and AWS machines. between dev machines and AWS machines.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .aws import * from .aws import *
DEBUG = True DEBUG = True
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -8,6 +8,10 @@ sessions. Assumes structure: ...@@ -8,6 +8,10 @@ sessions. Assumes structure:
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import socket import socket
if 'eecs1' in socket.gethostname(): if 'eecs1' in socket.gethostname():
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
from .dev import * from .dev import *
......
...@@ -9,6 +9,11 @@ following domains to 127.0.0.1 in your /etc/hosts file: ...@@ -9,6 +9,11 @@ following domains to 127.0.0.1 in your /etc/hosts file:
Note that OS X has a bug where using *.local domains is excruciatingly slow, so Note that OS X has a bug where using *.local domains is excruciatingly slow, so
use *.dev domains instead for local testing. use *.dev domains instead for local testing.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
......
""" """
This config file runs the dev environment, but with mongo as the datastore This config file runs the dev environment, but with mongo as the datastore
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
GITHUB_REPO_ROOT = ENV_ROOT / "data" GITHUB_REPO_ROOT = ENV_ROOT / "data"
......
...@@ -8,6 +8,10 @@ The worker can be executed using: ...@@ -8,6 +8,10 @@ The worker can be executed using:
django_admin.py celery worker django_admin.py celery worker
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from dev import * from dev import *
################################# CELERY ###################################### ################################# CELERY ######################################
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..dev import * from ..dev import *
CLASSES_TO_DBS = { CLASSES_TO_DBS = {
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .courses import * from .courses import *
DATABASES = course_db_for('HarvardX/CS50x/2012') DATABASES = course_db_for('HarvardX/CS50x/2012')
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .courses import * from .courses import *
DATABASES = course_db_for('MITx/6.002x/2012_Fall') DATABASES = course_db_for('MITx/6.002x/2012_Fall')
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
Note that for this to work at all, you must have memcached running (or you won't Note that for this to work at all, you must have memcached running (or you won't
get shared sessions) get shared sessions)
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from courses import * from courses import *
# Move this to a shared file later: # Move this to a shared file later:
......
...@@ -13,6 +13,11 @@ Dir structure: ...@@ -13,6 +13,11 @@ Dir structure:
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
WIKI_ENABLED = True WIKI_ENABLED = True
......
# We intentionally define variables that aren't used
# pylint: disable=W0614
DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff')
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# Settings for edx4edx production instance # Settings for edx4edx production instance
from .aws import * from .aws import *
COURSE_NAME = "edx4edx" COURSE_NAME = "edx4edx"
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
This configuration is used for running jasmine tests This configuration is used for running jasmine tests
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
import os import os
from path import path from path import path
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
import os import os
......
...@@ -31,14 +31,9 @@ def merge_dict(dic1, dic2): ...@@ -31,14 +31,9 @@ def merge_dict(dic1, dic2):
def perform_request(method, url, data_or_params=None, *args, **kwargs): def perform_request(method, url, data_or_params=None, *args, **kwargs):
if data_or_params is None: if data_or_params is None:
data_or_params = {} data_or_params = {}
tags = [
"{k}:{v}".format(k=k, v=v)
for (k, v) in data_or_params.items() + [("method", method), ("url", url)]
if k != 'api_key'
]
data_or_params['api_key'] = settings.API_KEY data_or_params['api_key'] = settings.API_KEY
try: try:
with dog_stats_api.timer('comment_client.request.time', tags=tags): with dog_stats_api.timer('comment_client.request.time'):
if method in ['post', 'put', 'patch']: if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params, timeout=5) response = requests.request(method, url, data=data_or_params, timeout=5)
else: else:
...@@ -55,6 +50,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs): ...@@ -55,6 +50,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
if 200 < response.status_code < 500: if 200 < response.status_code < 500:
raise CommentClientError(response.text) raise CommentClientError(response.text)
# Heroku returns a 503 when an application is in maintenance mode
elif response.status_code == 503:
raise CommentClientMaintenanceError(response.text)
elif response.status_code == 500: elif response.status_code == 500:
raise CommentClientUnknownError(response.text) raise CommentClientUnknownError(response.text)
else: else:
...@@ -72,5 +70,9 @@ class CommentClientError(Exception): ...@@ -72,5 +70,9 @@ class CommentClientError(Exception):
return repr(self.message) return repr(self.message)
class CommentClientMaintenanceError(CommentClientError):
pass
class CommentClientUnknownError(CommentClientError): class CommentClientUnknownError(CommentClientError):
pass pass
...@@ -36,3 +36,16 @@ ...@@ -36,3 +36,16 @@
@import 'news'; @import 'news';
@import 'shame'; @import 'shame';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env.get('THEME_NAME') is not None:
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}'
% endif
<%inherit file="../main.html" />
<h1>We're sorry</h1>
<p>The forums are currently undergoing maintenance. We'll have them back up shortly!</p>
...@@ -8,7 +8,7 @@ from . import one_time_startup ...@@ -8,7 +8,7 @@ from . import one_time_startup
import django.contrib.auth.views import django.contrib.auth.views
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
if settings.DEBUG: if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
admin.autodiscover() admin.autodiscover()
urlpatterns = ('', # nopep8 urlpatterns = ('', # nopep8
...@@ -330,7 +330,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -330,7 +330,7 @@ if settings.COURSEWARE_ENABLED:
if settings.ENABLE_JASMINE: if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
if settings.DEBUG: if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin ## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),) urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
......
require 'json'
require 'rake/clean' require 'rake/clean'
require './rakefiles/helpers.rb' require './rakefiles/helpers.rb'
...@@ -7,6 +8,13 @@ end ...@@ -7,6 +8,13 @@ end
# Build Constants # Build Constants
REPO_ROOT = File.dirname(__FILE__) REPO_ROOT = File.dirname(__FILE__)
ENV_ROOT = File.dirname(REPO_ROOT)
REPORT_DIR = File.join(REPO_ROOT, "reports") REPORT_DIR = File.join(REPO_ROOT, "reports")
# Environment constants
SERVICE_VARIANT = ENV['SERVICE_VARIANT']
CONFIG_PREFIX = SERVICE_VARIANT ? SERVICE_VARIANT + "." : ""
ENV_FILE = File.join(ENV_ROOT, CONFIG_PREFIX + "env.json")
ENV_TOKENS = File.exists?(ENV_FILE) ? JSON.parse(File.read(ENV_FILE)) : {}
task :default => [:test, :pep8, :pylint] task :default => [:test, :pep8, :pylint]
# Theming constants
THEME_NAME = ENV_TOKENS['THEME_NAME']
USE_CUSTOM_THEME = !(THEME_NAME.nil? || THEME_NAME.empty?)
if USE_CUSTOM_THEME
THEME_ROOT = File.join(ENV_ROOT, "themes", THEME_NAME)
THEME_SASS = File.join(THEME_ROOT, "static", "sass")
end
# Run the specified file through the Mako templating engine, providing
# the ENV_TOKENS to the templating context.
def preprocess_with_mako(filename)
# simple command-line invocation of Mako engine
mako = "from mako.template import Template;" +
"print Template(filename=\"#{filename}\")" +
# Total hack. It works because a Python dict literal has
# the same format as a JSON object.
".render(env=#{ENV_TOKENS.to_json});"
# strip off the .mako extension
output_filename = filename.chomp(File.extname(filename))
# just pipe from stdout into the new file, exiting on failure
File.open(output_filename, 'w') do |file|
file.write(`python -c '#{mako}'`)
exit_code = $?.to_i
abort "#{mako} failed with #{exit_code}" if exit_code.to_i != 0
end
end
def xmodule_cmd(watch=false, debug=false) def xmodule_cmd(watch=false, debug=false)
xmodule_cmd = 'xmodule_assets common/static/xmodule' xmodule_cmd = 'xmodule_assets common/static/xmodule'
...@@ -32,10 +60,17 @@ def coffee_cmd(watch=false, debug=false) ...@@ -32,10 +60,17 @@ def coffee_cmd(watch=false, debug=false)
end end
def sass_cmd(watch=false, debug=false) def sass_cmd(watch=false, debug=false)
sass_load_paths = ["./common/static/sass"]
sass_watch_paths = ["*/static"]
if USE_CUSTOM_THEME
sass_load_paths << THEME_SASS
sass_watch_paths << THEME_SASS
end
"sass #{debug ? '--debug-info' : '--style compressed'} " + "sass #{debug ? '--debug-info' : '--style compressed'} " +
"--load-path ./common/static/sass " + "--load-path #{sass_load_paths.join(' ')} " +
"--require ./common/static/sass/bourbon/lib/bourbon.rb " + "--require ./common/static/sass/bourbon/lib/bourbon.rb " +
"#{watch ? '--watch' : '--update'} */static" "#{watch ? '--watch' : '--update'} #{sass_watch_paths.join(' ')}"
end end
desc "Compile all assets" desc "Compile all assets"
...@@ -46,6 +81,13 @@ namespace :assets do ...@@ -46,6 +81,13 @@ namespace :assets do
desc "Compile all assets in debug mode" desc "Compile all assets in debug mode"
multitask :debug multitask :debug
desc "Preprocess all static assets that have the .mako extension"
task :preprocess do
# Run assets through the Mako templating engine. Right now we
# just hardcode the asset filenames.
preprocess_with_mako("lms/static/sass/application.scss.mako")
end
desc "Watch all assets for changes and automatically recompile" desc "Watch all assets for changes and automatically recompile"
task :watch => 'assets:_watch' do task :watch => 'assets:_watch' do
puts "Press ENTER to terminate".red puts "Press ENTER to terminate".red
...@@ -54,9 +96,9 @@ namespace :assets do ...@@ -54,9 +96,9 @@ namespace :assets do
{:xmodule => :install_python_prereqs, {:xmodule => :install_python_prereqs,
:coffee => :install_node_prereqs, :coffee => :install_node_prereqs,
:sass => :install_ruby_prereqs}.each_pair do |asset_type, prereq_task| :sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
desc "Compile all #{asset_type} assets" desc "Compile all #{asset_type} assets"
task asset_type => prereq_task do task asset_type => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| sh(c)} cmd.each {|c| sh(c)}
...@@ -71,7 +113,7 @@ namespace :assets do ...@@ -71,7 +113,7 @@ namespace :assets do
namespace asset_type do namespace asset_type do
desc "Compile all #{asset_type} assets in debug mode" desc "Compile all #{asset_type} assets in debug mode"
task :debug => prereq_task do task :debug => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=true)
sh(cmd) sh(cmd)
end end
...@@ -82,7 +124,7 @@ namespace :assets do ...@@ -82,7 +124,7 @@ namespace :assets do
$stdin.gets $stdin.gets
end end
task :_watch => prereq_task do task :_watch => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| background_process(c)} cmd.each {|c| background_process(c)}
......
...@@ -14,16 +14,31 @@ def report_dir_path(dir) ...@@ -14,16 +14,31 @@ def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s) return File.join(REPORT_DIR, dir.to_s)
end end
def when_changed(unchanged_message, *files) def compute_fingerprint(files, dirs)
Rake::Task[PREREQS_MD5_DIR].invoke
cache_file = File.join(PREREQS_MD5_DIR, files.join('-').gsub(/\W+/, '-')) + '.md5'
digest = Digest::MD5.new() digest = Digest::MD5.new()
# Digest the contents of all the files.
Dir[*files].select{|file| File.file?(file)}.each do |file| Dir[*files].select{|file| File.file?(file)}.each do |file|
digest.file(file) digest.file(file)
end end
if !File.exists?(cache_file) or digest.hexdigest != File.read(cache_file)
# Digest the names of the files in all the dirs.
dirs.each do |dir|
file_names = Dir.entries(dir).sort.join(" ")
digest.update(file_names)
end
digest.hexdigest
end
# Hash the contents of all the files, and the names of files in the dirs.
# Run the block if they've changed.
def when_changed(unchanged_message, files, dirs=[])
Rake::Task[PREREQS_MD5_DIR].invoke
cache_file = File.join(PREREQS_MD5_DIR, files[0].gsub(/\W+/, '-').sub(/-+$/, '')) + '.md5'
if !File.exists?(cache_file) or compute_fingerprint(files, dirs) != File.read(cache_file)
yield yield
File.write(cache_file, digest.hexdigest) File.write(cache_file, compute_fingerprint(files, dirs))
elsif !unchanged_message.empty? elsif !unchanged_message.empty?
puts unchanged_message puts unchanged_message
end end
......
require './rakefiles/helpers.rb' require './rakefiles/helpers.rb'
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache') PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
CLOBBER.include(PREREQS_MD5_DIR) CLOBBER.include(PREREQS_MD5_DIR)
...@@ -13,7 +12,7 @@ task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install ...@@ -13,7 +12,7 @@ task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install
desc "Install all node prerequisites for the lms and cms" desc "Install all node prerequisites for the lms and cms"
task :install_node_prereqs => "ws:migrate" do task :install_node_prereqs => "ws:migrate" do
unchanged = 'Node requirements unchanged, nothing to install' unchanged = 'Node requirements unchanged, nothing to install'
when_changed(unchanged, 'package.json') do when_changed(unchanged, ['package.json']) do
sh('npm install') sh('npm install')
end unless ENV['NO_PREREQ_INSTALL'] end unless ENV['NO_PREREQ_INSTALL']
end end
...@@ -21,20 +20,21 @@ end ...@@ -21,20 +20,21 @@ end
desc "Install all ruby prerequisites for the lms and cms" desc "Install all ruby prerequisites for the lms and cms"
task :install_ruby_prereqs => "ws:migrate" do task :install_ruby_prereqs => "ws:migrate" do
unchanged = 'Ruby requirements unchanged, nothing to install' unchanged = 'Ruby requirements unchanged, nothing to install'
when_changed(unchanged, 'Gemfile') do when_changed(unchanged, ['Gemfile']) do
sh('bundle install') sh('bundle install')
end unless ENV['NO_PREREQ_INSTALL'] end unless ENV['NO_PREREQ_INSTALL']
end end
desc "Install all python prerequisites for the lms and cms" desc "Install all python prerequisites for the lms and cms"
task :install_python_prereqs => "ws:migrate" do task :install_python_prereqs => "ws:migrate" do
site_packages_dir = `python -c 'import os; import distutils.sysconfig as dusc; print dusc.get_python_lib()'`.chomp
unchanged = 'Python requirements unchanged, nothing to install' unchanged = 'Python requirements unchanged, nothing to install'
when_changed(unchanged, 'requirements/**/*') do when_changed(unchanged, ['requirements/**/*'], [site_packages_dir]) do
ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache' ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache'
sh('pip install --exists-action w -r requirements/edx/base.txt') sh('pip install --exists-action w -r requirements/edx/base.txt')
sh('pip install --exists-action w -r requirements/edx/post.txt') sh('pip install --exists-action w -r requirements/edx/post.txt')
# Check for private-requirements.txt: used to install our libs as working dirs, # requirements/private.txt is used to install our libs as
# or personal-use tools. # working dirs, or for personal-use tools.
if File.file?("requirements/private.txt") if File.file?("requirements/private.txt")
sh('pip install -r requirements/private.txt') sh('pip install -r requirements/private.txt')
end end
......
def run_pylint(system, report_dir, flags='')
apps = Dir["#{system}", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app|
File.basename(app)
end.select do |app|
app !=~ /.pyc$/
end.map do |app|
if app =~ /.py$/
app.gsub('.py', '')
else
app
end
end
pythonpath_prefix = "PYTHONPATH=#{system}:#{system}/djangoapps:#{system}/lib:common/djangoapps:common/lib"
sh("#{pythonpath_prefix} pylint #{flags} -f parseable #{apps.join(' ')} | tee #{report_dir}/pylint.report")
end
[:lms, :cms, :common].each do |system| [:lms, :cms, :common].each do |system|
report_dir = report_dir_path(system) report_dir = report_dir_path(system)
...@@ -11,21 +28,18 @@ ...@@ -11,21 +28,18 @@
desc "Run pylint on all #{system} code" desc "Run pylint on all #{system} code"
task "pylint_#{system}" => [report_dir, :install_python_prereqs] do task "pylint_#{system}" => [report_dir, :install_python_prereqs] do
apps = Dir["#{system}/*.py", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app| run_pylint(system, report_dir)
File.basename(app)
end.select do |app|
app !=~ /.pyc$/
end.map do |app|
if app =~ /.py$/
app.gsub('.py', '')
else
app
end end
namespace "pylint_#{system}" do
desc "Run pylint checking for errors only, and aborting if there are any"
task :errors do
run_pylint(system, report_dir, '-E')
end end
pythonpath_prefix = "PYTHONPATH=#{system}:#{system}/djangoapps:#{system}/lib:common/djangoapps:common/lib"
sh("#{pythonpath_prefix} pylint --rcfile=.pylintrc -f parseable #{apps.join(' ')} | tee #{report_dir}/pylint.report")
end end
namespace :pylint do
task :errors => "pylint_#{system}:errors"
end
task :pylint => "pylint_#{system}" task :pylint => "pylint_#{system}"
end end
\ No newline at end of file
# Install these packages from the edx-platform working tree
# NOTE: if you change code in these packages, you MUST change the version
# number in its setup.py or the code WILL NOT be installed during deploy.
common/lib/calc
common/lib/chem
common/lib/sandbox-packages
# Packages to install in the Python sandbox for secured execution. # Packages to install in the Python sandbox for secured execution.
scipy==0.11.0 scipy==0.11.0
lxml==3.0.1 lxml==3.0.1
-e common/lib/calc
-e common/lib/chem
-e common/lib/sandbox-packages
...@@ -74,6 +74,7 @@ lettuce==0.2.16 ...@@ -74,6 +74,7 @@ lettuce==0.2.16
mock==0.8.0 mock==0.8.0
nosexcover==1.0.7 nosexcover==1.0.7
pep8==1.4.5 pep8==1.4.5
pylint==0.28
rednose==0.3 rednose==0.3
selenium==2.31.0 selenium==2.31.0
splinter==0.5.0 splinter==0.5.0
...@@ -81,7 +82,3 @@ django_nose==1.1 ...@@ -81,7 +82,3 @@ django_nose==1.1
django-jasmine==0.3.2 django-jasmine==0.3.2
django_debug_toolbar django_debug_toolbar
django-debug-toolbar-mongo django-debug-toolbar-mongo
# Install pylint from a specific commit on trunk
# to get the fix for this issue: http://www.logilab.org/ticket/122793
https://bitbucket.org/logilab/pylint/get/e828cb5.zip
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@483e0cb1#egg=XBlock -e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
-e git+https://github.com/edx/codejail.git@07494f1#egg=codejail -e git+https://github.com/edx/codejail.git@07494f1#egg=codejail
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment