Commit 1de8c4d3 by Peter Baratta

Merge branch 'master' into pbaratta/calc-tests

parents 70898a06 af8b509e
......@@ -26,6 +26,7 @@ Gemfile.lock
conf/locale/en/LC_MESSAGES/*.po
!messages.po
lms/static/sass/*.css
lms/static/sass/application.scss
cms/static/sass/*.css
lms/lib/comment_client/python
nosetests.xml
......
REVIEWBOARD_URL = "https://rbcommons.com/s/edx/"
GUESS_FIELDS = True
......@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
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_POLICY_KEY = 'advanced_modules'
......
......@@ -2,6 +2,11 @@
This config file extends the test environment configuration
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 *
# You need to start the server in debug mode,
......@@ -23,7 +28,7 @@ MODULESTORE_OPTIONS = {
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
......
"""
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
from .common import *
......
......@@ -19,6 +19,10 @@ Longer TODO:
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 lms.envs.common
from path import path
......
"""
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 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
# FORCE_SCRIPT_NAME = '/cms'
......
......@@ -8,6 +8,10 @@ The worker can be executed using:
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 *
################################# CELERY ######################################
......
......@@ -2,6 +2,10 @@
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 logsettings import get_logger_config
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 *
import os
from path import path
......
......@@ -5,6 +5,12 @@
// 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
@mixin vertically-and-horizontally-centered ($height, $width) {
left: 50%;
......
......@@ -11,54 +11,54 @@
.t-title1 {
@extend .t-title;
@include font-size(60);
@include lh(60);
@include line-height(60);
}
.t-title2 {
@extend .t-title;
@include font-size(48);
@include lh(48);
@include line-height(48);
}
.t-title3 {
@include font-size(36);
@include lh(36);
@include line-height(36);
}
.t-title4 {
@extend .t-title;
@include font-size(24);
@include lh(24);
@include line-height(24);
}
.t-title5 {
@extend .t-title;
@include font-size(18);
@include lh(18);
@include line-height(18);
}
.t-title6 {
@extend .t-title;
@include font-size(16);
@include lh(16);
@include line-height(16);
}
.t-title7 {
@extend .t-title;
@include font-size(14);
@include lh(14);
@include line-height(14);
}
.t-title8 {
@extend .t-title;
@include font-size(12);
@include lh(12);
@include line-height(12);
}
.t-title9 {
@extend .t-title;
@include font-size(11);
@include lh(11);
@include line-height(11);
}
// ====================
......@@ -71,31 +71,31 @@
.t-copy-base {
@extend .t-copy;
@include font-size(16);
@include lh(16);
@include line-height(16);
}
.t-copy-lead1 {
@extend .t-copy;
@include font-size(18);
@include lh(18);
@include line-height(18);
}
.t-copy-lead2 {
@extend .t-copy;
@include font-size(24);
@include lh(24);
@include line-height(24);
}
.t-copy-sub1 {
@extend .t-copy;
@include font-size(14);
@include lh(14);
@include line-height(14);
}
.t-copy-sub2 {
@extend .t-copy;
@include font-size(12);
@include lh(12);
@include line-height(12);
}
// ====================
......@@ -103,22 +103,22 @@
// actions/labels
.t-action1 {
@include font-size(18);
@include lh(18);
@include line-height(18);
}
.t-action2 {
@include font-size(16);
@include lh(16);
@include line-height(16);
}
.t-action3 {
@include font-size(14);
@include lh(14);
@include line-height(14);
}
.t-action4 {
@include font-size(12);
@include lh(12);
@include line-height(12);
}
......
......@@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="calc",
version="0.1",
version="0.1.1",
py_modules=["calc"],
install_requires=[
"pyparsing==1.5.6",
......
......@@ -475,12 +475,11 @@ class LoncapaProblem(object):
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
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['python_path'] = python_path
return context
def _extract_html(self, problemtree): # private
'''
Main (private) function which converts Problem XML tree to HTML.
......
......@@ -286,7 +286,7 @@ class LoncapaResponse(object):
}
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:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr(
......@@ -972,7 +972,7 @@ class CustomResponse(LoncapaResponse):
'ans': ans,
}
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 check_function
......
......@@ -39,6 +39,9 @@ class TestLazyMod(unittest.TestCase):
self.assertEqual(hsv[0], 0.25)
def test_dotted(self):
self.assertNotIn("email.utils", sys.modules)
email_utils = LazyModule("email.utils")
self.assertEqual(email_utils.quote('"hi"'), r'\"hi\"')
# wsgiref is a module with submodules that is not already imported.
# Any similar module would do. This test demonstrates that the module
# 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
setup(
name="chem",
version="0.1",
version="0.1.1",
packages=["chem"],
install_requires=[
"pyparsing==1.5.6",
......
......@@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="sandbox-packages",
version="0.1",
version="0.1.1",
packages=[
"verifiers",
],
......
......@@ -8,7 +8,7 @@ from .x_module import XModule
from xblock.core import Integer, Scope, String, Boolean, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
from .fields import Date, StringyFloat
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
log = logging.getLogger("mitx.courseware")
......@@ -49,19 +49,19 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object):
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)
state = String(help="Which step within the current task that the student is on.", default="initial",
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)
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)
attempts = Integer(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)
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
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)
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,
......
......@@ -290,7 +290,6 @@ class XMLModuleStore(ModuleStoreBase):
if course_dirs is None:
course_dirs = sorted([d for d in os.listdir(self.data_dir) if
os.path.exists(self.data_dir / d / "course.xml")])
for course_dir in course_dirs:
self.try_load_course(course_dir)
......
......@@ -6,7 +6,7 @@ log = logging.getLogger(__name__)
class ControllerQueryService(GradingService):
"""
Interface to staff grading backend.
Interface to controller query backend.
"""
def __init__(self, config, system):
......@@ -77,6 +77,50 @@ class ControllerQueryService(GradingService):
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):
if seconds < 60:
human_string = "{0} seconds".format(seconds)
......
......@@ -8,7 +8,7 @@
}
// mixins - line height
@mixin lh($fontSize: auto){
@mixin line-height($fontSize: auto){
line-height: ($fontSize*1.48) + px;
line-height: (($fontSize/10)*1.48) + rem;
}
......
......@@ -414,6 +414,11 @@ def modx_dispatch(request, dispatch, location, course_id):
through the part before the first '?'.
- location -- the module location. Used to look up the XModule instance
- 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)
......@@ -442,8 +447,19 @@ def modx_dispatch(request, dispatch, location, course_id):
return HttpResponse(json.dumps({'success': file_too_big_msg}))
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,
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')
if instance is None:
......
......@@ -69,19 +69,38 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase):
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
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.user = UserFactory()
inputfile_2 = Stub()
inputfile_2.size = 1
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.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):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
......
......@@ -174,6 +174,9 @@ def forum_form_discussion(request, course_id):
try:
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
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:
log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404
......
......@@ -13,7 +13,7 @@ class Migration(SchemaMigration):
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('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)),
('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='')),
('quote', self.gf('django.db.models.fields.TextField')(default='')),
('range_start', self.gf('django.db.models.fields.CharField')(max_length=2048)),
......@@ -82,7 +82,7 @@ class Migration(SchemaMigration):
'tags': ('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'}),
'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']"})
}
}
......
......@@ -9,7 +9,7 @@ import json
class Note(models.Model):
user = models.ForeignKey(User, 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="")
quote = models.TextField(default="")
range_start = models.CharField(max_length=2048) # xpath string
......
......@@ -21,6 +21,7 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.http import HttpResponse, Http404, HttpResponseRedirect
from mitxmako.shortcuts import render_to_string
......@@ -30,10 +31,10 @@ log = logging.getLogger(__name__)
system = ModuleSystem(
ajax_url=None,
track_function=None,
get_module = None,
get_module=None,
render_template=render_to_string,
replace_urls = None,
xblock_model_data= {}
replace_urls=None,
xblock_model_data={}
)
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
......@@ -90,40 +91,61 @@ def staff_grading(request, course_id):
'staff_access': True, })
def find_peer_grading_module(course):
"""
Given a course, finds the first peer grading module in it.
@param course: A course object.
@return: boolean found_module, string problem_url
"""
#Reverse the base course url
base_course_url = reverse('courses')
found_module = False
problem_url = ""
#Get the course id and split it
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)
items = [i for i in items if not getattr(i, "use_for_single_location", True)]
#Get the first one
if len(items) > 0:
item_location = items[0].location
#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 = generate_problem_url(problem_url_parts, base_course_url)
found_module = True
return found_module, problem_url
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id):
'''
Show a peer grading interface
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')
course_id_parts = course.id.split("/")
false_dict = [False, "False", "false", "FALSE"]
#Reverse the base course url
base_course_url = reverse('courses')
try:
#TODO: This will not work with multiple runs of a course. Make it work. The last key in the Location passed
#to get_items is called revision. Is this the same as run?
#Get the peer grading modules currently in the course
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
#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]
#Get the first one
item_location = items[0].location
#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 = generate_problem_url(problem_url_parts, base_course_url)
return HttpResponseRedirect(problem_url)
except:
found_module, problem_url = find_peer_grading_module(course)
if not found_module:
#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
log.exception(error_message + "Current course is: {0}".format(course_id))
return HttpResponse(error_message)
return HttpResponseRedirect(problem_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):
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
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')
student_id = unique_id_for_user(request.user)
......@@ -157,6 +180,7 @@ def student_problem_list(request, course_id):
base_course_url = reverse('courses')
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_dict = json.loads(problem_list_json)
success = problem_list_dict['success']
......@@ -166,8 +190,22 @@ def student_problem_list(request, course_id):
else:
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)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
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'])
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_list[i].update({'actual_url': problem_url})
eta_available = problem_list[i]['eta_available']
......@@ -197,6 +235,8 @@ def student_problem_list(request, course_id):
log.error("Problem with results from external grading service for open ended.")
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)
return render_to_response('open_ended_problems/open_ended_problems.html', {
......@@ -300,7 +340,16 @@ def combined_notifications(request, course_id):
'description': description,
'alert_message': alert_message
}
notification_list.append(notification_item)
#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)
ajax_url = _reverse_with_slash('open_ended_notifications', course_id)
combined_dict = {
......@@ -311,9 +360,7 @@ def combined_notifications(request, course_id):
'ajax_url': ajax_url,
}
return render_to_response('open_ended_problems/combined_notifications.html',
combined_dict
)
return render_to_response('open_ended_problems/combined_notifications.html', combined_dict)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
......@@ -2,6 +2,11 @@
This config file extends the test environment configuration
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 *
# You need to start the server in debug mode,
......
......@@ -6,6 +6,11 @@ Common traits:
* Use memcached, and cache-backed sessions
* 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
from .common import *
......@@ -109,6 +114,11 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
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
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
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 *
# REMOVE DEBUG TOOLBAR
......
......@@ -2,6 +2,10 @@
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 *
with open(ENV_ROOT / "cms.auth.json") as auth_file:
......
......@@ -2,6 +2,10 @@
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 *
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
......
......@@ -2,6 +2,10 @@
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 *
MODULESTORE = {
......
......@@ -18,6 +18,11 @@ Longer TODO:
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.
"""
# 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 os
......@@ -67,6 +72,7 @@ MITX_FEATURES = {
'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_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False,
......@@ -105,6 +111,9 @@ MITX_FEATURES = {
# Enable URL that shows information about the status of variuous services
'ENABLE_SERVICE_STATUS': False,
# Toggle to indicate use of a custom theme
'USE_CUSTOM_THEME': False
}
# Used for A/B testing
......@@ -162,12 +171,12 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
# This is where Django Template lookup is defined. There are a few of these
# still left lying around.
TEMPLATE_DIRS = (
TEMPLATE_DIRS = [
PROJECT_ROOT / "templates",
COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
)
]
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
......@@ -709,3 +718,31 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor',
'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 @@
These are debug machines used for content creators, so they're kind of a cross
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 *
DEBUG = True
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 logsettings import get_logger_config
......
......@@ -8,6 +8,10 @@ sessions. Assumes structure:
/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
if 'eecs1' in socket.gethostname():
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 logsettings import get_logger_config
from .dev import *
......
......@@ -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
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 *
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
......
"""
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 *
GITHUB_REPO_ROOT = ENV_ROOT / "data"
......
......@@ -8,6 +8,10 @@ The worker can be executed using:
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 *
################################# 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 *
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 *
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 *
DATABASES = course_db_for('MITx/6.002x/2012_Fall')
......@@ -2,6 +2,11 @@
Note that for this to work at all, you must have memcached running (or you won't
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 *
# Move this to a shared file later:
......
......@@ -13,6 +13,11 @@ Dir structure:
/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 *
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')
# 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
from .aws import *
COURSE_NAME = "edx4edx"
......
......@@ -2,6 +2,10 @@
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 logsettings import get_logger_config
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 logsettings import get_logger_config
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 *
import os
from path import path
......
......@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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 logsettings import get_logger_config
import os
......
......@@ -31,14 +31,9 @@ def merge_dict(dic1, dic2):
def perform_request(method, url, data_or_params=None, *args, **kwargs):
if data_or_params is None:
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
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']:
response = requests.request(method, url, data=data_or_params, timeout=5)
else:
......@@ -55,6 +50,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
if 200 < response.status_code < 500:
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:
raise CommentClientUnknownError(response.text)
else:
......@@ -72,5 +70,9 @@ class CommentClientError(Exception):
return repr(self.message)
class CommentClientMaintenanceError(CommentClientError):
pass
class CommentClientUnknownError(CommentClientError):
pass
......@@ -36,3 +36,16 @@
@import 'news';
@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
import django.contrib.auth.views
# 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()
urlpatterns = ('', # nopep8
......@@ -330,7 +330,7 @@ if settings.COURSEWARE_ENABLED:
if settings.ENABLE_JASMINE:
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
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
......
require 'json'
require 'rake/clean'
require './rakefiles/helpers.rb'
......@@ -7,6 +8,13 @@ end
# Build Constants
REPO_ROOT = File.dirname(__FILE__)
ENV_ROOT = File.dirname(REPO_ROOT)
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]
# 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)
xmodule_cmd = 'xmodule_assets common/static/xmodule'
......@@ -32,10 +60,17 @@ def coffee_cmd(watch=false, debug=false)
end
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'} " +
"--load-path ./common/static/sass " +
"--load-path #{sass_load_paths.join(' ')} " +
"--require ./common/static/sass/bourbon/lib/bourbon.rb " +
"#{watch ? '--watch' : '--update'} */static"
"#{watch ? '--watch' : '--update'} #{sass_watch_paths.join(' ')}"
end
desc "Compile all assets"
......@@ -46,6 +81,13 @@ namespace :assets do
desc "Compile all assets in debug mode"
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"
task :watch => 'assets:_watch' do
puts "Press ENTER to terminate".red
......@@ -54,9 +96,9 @@ namespace :assets do
{:xmodule => :install_python_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"
task asset_type => prereq_task do
task asset_type => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
if cmd.kind_of?(Array)
cmd.each {|c| sh(c)}
......@@ -71,7 +113,7 @@ namespace :assets do
namespace asset_type do
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)
sh(cmd)
end
......@@ -82,7 +124,7 @@ namespace :assets do
$stdin.gets
end
task :_watch => prereq_task do
task :_watch => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
if cmd.kind_of?(Array)
cmd.each {|c| background_process(c)}
......
......@@ -14,16 +14,31 @@ def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s)
end
def when_changed(unchanged_message, *files)
Rake::Task[PREREQS_MD5_DIR].invoke
cache_file = File.join(PREREQS_MD5_DIR, files.join('-').gsub(/\W+/, '-')) + '.md5'
def compute_fingerprint(files, dirs)
digest = Digest::MD5.new()
# Digest the contents of all the files.
Dir[*files].select{|file| File.file?(file)}.each do |file|
digest.file(file)
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
File.write(cache_file, digest.hexdigest)
File.write(cache_file, compute_fingerprint(files, dirs))
elsif !unchanged_message.empty?
puts unchanged_message
end
......
require './rakefiles/helpers.rb'
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
CLOBBER.include(PREREQS_MD5_DIR)
......@@ -13,7 +12,7 @@ task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install
desc "Install all node prerequisites for the lms and cms"
task :install_node_prereqs => "ws:migrate" do
unchanged = 'Node requirements unchanged, nothing to install'
when_changed(unchanged, 'package.json') do
when_changed(unchanged, ['package.json']) do
sh('npm install')
end unless ENV['NO_PREREQ_INSTALL']
end
......@@ -21,20 +20,21 @@ end
desc "Install all ruby prerequisites for the lms and cms"
task :install_ruby_prereqs => "ws:migrate" do
unchanged = 'Ruby requirements unchanged, nothing to install'
when_changed(unchanged, 'Gemfile') do
when_changed(unchanged, ['Gemfile']) do
sh('bundle install')
end unless ENV['NO_PREREQ_INSTALL']
end
desc "Install all python prerequisites for the lms and cms"
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'
when_changed(unchanged, 'requirements/**/*') do
when_changed(unchanged, ['requirements/**/*'], [site_packages_dir]) do
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/post.txt')
# Check for private-requirements.txt: used to install our libs as working dirs,
# or personal-use tools.
# requirements/private.txt is used to install our libs as
# working dirs, or for personal-use tools.
if File.file?("requirements/private.txt")
sh('pip install -r requirements/private.txt')
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|
report_dir = report_dir_path(system)
......@@ -11,21 +28,18 @@
desc "Run pylint on all #{system} code"
task "pylint_#{system}" => [report_dir, :install_python_prereqs] do
apps = Dir["#{system}/*.py", "#{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
run_pylint(system, report_dir)
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
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
namespace :pylint do
task :errors => "pylint_#{system}:errors"
end
task :pylint => "pylint_#{system}"
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.
scipy==0.11.0
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
mock==0.8.0
nosexcover==1.0.7
pep8==1.4.5
pylint==0.28
rednose==0.3
selenium==2.31.0
splinter==0.5.0
......@@ -81,7 +82,3 @@ django_nose==1.1
django-jasmine==0.3.2
django_debug_toolbar
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 @@
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# 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
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