diff --git a/.pylintrc b/.pylintrc index 49fcf80eb947405af455359bf9ea7158dfeffb35..792079ce0329c3f15f7b80ac592a4bd22d3710fd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -34,10 +34,13 @@ load-plugins= # multiple time (only on the command line, not in the configuration file where # it should appear only once). disable= +# Never going to use these # C0301: Line too long -# C0302: Too many lines in module -# W0141: Used builtin function 'map' # W0142: Used * or ** magic +# W0141: Used builtin function 'map' + +# Might use these when the code is in better shape +# C0302: Too many lines in module # R0201: Method could be a function # R0901: Too many ancestors # R0902: Too many instance attributes @@ -96,7 +99,18 @@ zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. -generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size,content +generated-members= + REQUEST, + acl_users, + aq_parent, + objects, + DoesNotExist, + can_read, + can_write, + get_url, + size, + content, + status_code [BASIC] diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index c5fb0a231b1a71b785b54a973a8e0dc85fb33dde..e40d7c57b98de7eda854bcbfca181fd77c367f8e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -15,8 +15,9 @@ from datetime import timedelta from django.contrib.auth.models import User from django.dispatch import Signal from contentstore.utils import get_modulestore +from contentstore.tests.utils import parse_json -from .utils import ModuleStoreTestCase, parse_json +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore import Location @@ -48,6 +49,7 @@ class MongoCollectionFindWrapper(object): self.counter = self.counter+1 return self.original(query, *args, **kwargs) + @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -187,27 +189,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def test_get_depth_with_drafts(self): import_from_xml(modulestore(), 'common/test/data/', ['simple']) - course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', - 'course', '2012_Fall', None]), depth=None) + course = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), + depth=None + ) # make sure no draft items have been returned num_drafts = self._get_draft_counts(course) self.assertEqual(num_drafts, 0) - problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', - 'problem', 'ps01-simple', None])) + problem = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]) + ) # put into draft modulestore('draft').clone_item(problem.location, problem.location) # make sure we can query that item and verify that it is a draft - draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', - 'problem', 'ps01-simple', None])) + draft_problem = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]) + ) self.assertTrue(getattr(draft_problem, 'is_draft', False)) #now requery with depth - course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', - 'course', '2012_Fall', None]), depth=None) + course = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), + depth=None + ) # make sure just one draft item have been returned num_drafts = self._get_draft_counts(course) @@ -266,10 +274,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # make sure the parent no longer points to the child object which was deleted self.assertTrue(sequential.location.url() in chapter.children) - self.client.post(reverse('delete_item'), - json.dumps({'id': sequential.location.url(), 'delete_children': 'true', - 'delete_all_versions': 'true'}), - "application/json") + self.client.post( + reverse('delete_item'), + json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}), + "application/json" + ) found = False try: @@ -537,15 +546,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): print 'Exporting to tempdir = {0}'.format(root_dir) # export out to a tempdir - exported = False - try: - export_to_xml(module_store, content_store, location, root_dir, 'test_export') - exported = True - except Exception: - print 'Exception thrown: {0}'.format(traceback.format_exc()) - pass - - self.assertTrue(exported) + export_to_xml(module_store, content_store, location, root_dir, 'test_export') class ContentStoreTest(ModuleStoreTestCase): @@ -625,10 +626,12 @@ class ContentStoreTest(ModuleStoreTestCase): """Test viewing the index page with no courses""" # Create a course so there is something to view resp = self.client.get(reverse('index')) - self.assertContains(resp, - '<h1 class="title-1">My Courses</h1>', - status_code=200, - html=True) + self.assertContains( + resp, + '<h1 class="title-1">My Courses</h1>', + status_code=200, + html=True + ) def test_course_factory(self): """Test that the course factory works correctly.""" @@ -645,10 +648,12 @@ class ContentStoreTest(ModuleStoreTestCase): """Test viewing the index page with an existing course""" CourseFactory.create(display_name='Robot Super Educational Course') resp = self.client.get(reverse('index')) - self.assertContains(resp, - '<span class="class-name">Robot Super Educational Course</span>', - status_code=200, - html=True) + self.assertContains( + resp, + '<span class="class-name">Robot Super Educational Course</span>', + status_code=200, + html=True + ) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" @@ -661,10 +666,12 @@ class ContentStoreTest(ModuleStoreTestCase): } resp = self.client.get(reverse('course_index', kwargs=data)) - self.assertContains(resp, - '<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">', - status_code=200, - html=True) + self.assertContains( + resp, + '<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">', + status_code=200, + html=True + ) def test_clone_item(self): """Test cloning an item. E.g. creating a new section""" @@ -680,7 +687,10 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertEqual(resp.status_code, 200) data = parse_json(resp) - self.assertRegexpMatches(data['id'], '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$') + self.assertRegexpMatches( + data['id'], + r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$" + ) def test_capa_module(self): """Test that a problem treats markdown specially.""" diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index c3fa665b2c24f422679b9b44e19e97ae64f09e15..c9f6b2053ec996b4137cd063bf858a79c2cd5a36 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -12,7 +12,7 @@ from models.settings.course_details import (CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from contentstore.utils import get_modulestore -from .utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata @@ -47,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) - t = 'i4x://edx/templates/course/Empty' - o = 'MITx' - n = '999' - dn = 'Robot Super Course' - self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course') - CourseFactory.create(template=t, org=o, number=n, display_name=dn) + course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course') + self.course_location = course.location class CourseDetailsTestCase(CourseTestCase): @@ -86,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase): jsondetails = CourseDetails.fetch(self.course_location) jsondetails.syllabus = "<a href='foo'>bar</a>" # encode - decode to convert date fields and other data which changes form - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus, - jsondetails.syllabus, "After set syllabus") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).syllabus, + jsondetails.syllabus, "After set syllabus" + ) jsondetails.overview = "Overview" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview, - jsondetails.overview, "After set overview") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).overview, + jsondetails.overview, "After set overview" + ) jsondetails.intro_video = "intro_video" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video, - jsondetails.intro_video, "After set intro_video") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).intro_video, + jsondetails.intro_video, "After set intro_video" + ) jsondetails.effort = "effort" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, - jsondetails.effort, "After set effort") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).effort, + jsondetails.effort, "After set effort" + ) class CourseDetailsViewTest(CourseTestCase): @@ -150,9 +154,7 @@ class CourseDetailsViewTest(CourseTestCase): @staticmethod def struct_to_datetime(struct_time): - return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, - struct_time.tm_mday, struct_time.tm_hour, - struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) + return datetime.datetime(*struct_time[:6], tzinfo=UTC()) def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: @@ -271,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('xqa_key', test_model, 'xqa_key field ') def test_update_from_json(self): - test_model = CourseMetadata.update_from_json(self.course_location, - {"advertised_start": "start A", - "testcenter_info": {"c": "test"}, - "days_early_for_beta": 2}) + test_model = CourseMetadata.update_from_json(self.course_location, { + "advertised_start": "start A", + "testcenter_info": {"c": "test"}, + "days_early_for_beta": 2 + }) self.update_check(test_model) # try fresh fetch to ensure persistence test_model = CourseMetadata.fetch(self.course_location) self.update_check(test_model) # now change some of the existing metadata - test_model = CourseMetadata.update_from_json(self.course_location, - {"advertised_start": "start B", - "display_name": "jolly roger"}) + test_model = CourseMetadata.update_from_json(self.course_location, { + "advertised_start": "start B", + "display_name": "jolly roger"} + ) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index 6b1b6b7f6f3101877a2ae971a3e73035790294b0..eb7bfb6db9d4149410e33b318e2bdd11bd6192f1 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -3,7 +3,7 @@ from contentstore import utils import mock from django.test import TestCase from xmodule.modulestore.tests.factories import CourseFactory -from .utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class LMSLinksTestCase(TestCase): @@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase): class UrlReverseTestCase(ModuleStoreTestCase): """ Tests for get_url_reverse """ - def test_CoursePageNames(self): + def test_course_page_names(self): """ Test the defined course pages. """ course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') @@ -70,4 +70,3 @@ class UrlReverseTestCase(ModuleStoreTestCase): 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) ) - \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 72e30dca6437093f4d80348e6694f9291cb14c40..34e5da4b4dc41ca8acb592558943b8c4a40aae74 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,7 +1,8 @@ from django.test.client import Client from django.core.urlresolvers import reverse -from .utils import ModuleStoreTestCase, parse_json, user, registration +from .utils import parse_json, user, registration +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class ContentStoreTestCase(ModuleStoreTestCase): diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index bb7ac2bf0614e87ad05ec00f822ec4f49815e92f..3135e49a089e43bd42a151c9b61b4e5e65649cb6 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -2,112 +2,11 @@ Utilities for contentstore tests ''' -#pylint: disable=W0603 - import json -import copy -from uuid import uuid4 -from django.test import TestCase -from django.conf import settings from student.models import Registration from django.contrib.auth.models import User -import xmodule.modulestore.django -from xmodule.templates import update_templates - - -class ModuleStoreTestCase(TestCase): - """ Subclass for any test case that uses the mongodb - module store. This populates a uniquely named modulestore - collection with templates before running the TestCase - and drops it they are finished. """ - - @staticmethod - def flush_mongo_except_templates(): - ''' - Delete everything in the module store except templates - ''' - modulestore = xmodule.modulestore.django.modulestore() - - # This query means: every item in the collection - # that is not a template - query = {"_id.course": {"$ne": "templates"}} - - # Remove everything except templates - modulestore.collection.remove(query) - - @staticmethod - def load_templates_if_necessary(): - ''' - Load templates into the modulestore only if they do not already exist. - We need the templates, because they are copied to create - XModules such as sections and problems - ''' - modulestore = xmodule.modulestore.django.modulestore() - - # Count the number of templates - query = {"_id.course": "templates"} - num_templates = modulestore.collection.find(query).count() - - if num_templates < 1: - update_templates() - - @classmethod - def setUpClass(cls): - ''' - Flush the mongo store and set up templates - ''' - - # Use a uuid to differentiate - # the mongo collections on jenkins. - cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE) - test_modulestore = cls.orig_modulestore - test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - xmodule.modulestore.django._MODULESTORES = {} - - settings.MODULESTORE = test_modulestore - - TestCase.setUpClass() - - @classmethod - def tearDownClass(cls): - ''' - Revert to the old modulestore settings - ''' - - # Clean up by dropping the collection - modulestore = xmodule.modulestore.django.modulestore() - modulestore.collection.drop() - - # Restore the original modulestore settings - settings.MODULESTORE = cls.orig_modulestore - - def _pre_setup(self): - ''' - Remove everything but the templates before each test - ''' - - # Flush anything that is not a template - ModuleStoreTestCase.flush_mongo_except_templates() - - # Check that we have templates loaded; if not, load them - ModuleStoreTestCase.load_templates_if_necessary() - - # Call superclass implementation - super(ModuleStoreTestCase, self)._pre_setup() - - def _post_teardown(self): - ''' - Flush everything we created except the templates - ''' - # Flush anything that is not a template - ModuleStoreTestCase.flush_mongo_except_templates() - - # Call superclass implementation - super(ModuleStoreTestCase, self)._post_teardown() - def parse_json(response): """Parse response, which is assumed to be json""" diff --git a/cms/envs/test.py b/cms/envs/test.py index 99869bc8697b607808b2c6d082cefcd8d57829fc..f11ff9b56c1bb43c0af1323535039525da72ac3f 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -28,7 +28,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # Makes the tests run much faster... -SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead +SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead # TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing STATICFILES_DIRS = [ @@ -41,7 +41,7 @@ STATICFILES_DIRS += [ if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir) ] -modulestore_options = { +MODULESTORE_OPTIONS = { 'default_class': 'xmodule.raw_module.RawDescriptor', 'host': 'localhost', 'db': 'test_xmodule', @@ -53,15 +53,15 @@ modulestore_options = { MODULESTORE = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options + 'OPTIONS': MODULESTORE_OPTIONS }, 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options + 'OPTIONS': MODULESTORE_OPTIONS }, 'draft': { 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', - 'OPTIONS': modulestore_options + 'OPTIONS': MODULESTORE_OPTIONS } } @@ -76,7 +76,7 @@ CONTENTSTORE = { DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "cms.db", + 'NAME': TEST_ROOT / "db" / "cms.db", }, } diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index f74188725aadd79c41967ded3c89e0919ea4e86d..adb51954e8ccacd69c41f5e2f7ccaac8bfd9099a 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration, CourseEnrollmentAllowed, CourseEnrollment) from django.contrib.auth.models import Group from datetime import datetime -from factory import Factory, SubFactory +from factory import Factory, SubFactory, post_generation from uuid import uuid4 @@ -44,6 +44,17 @@ class UserFactory(Factory): last_login = datetime(2012, 1, 1) date_joined = datetime(2011, 1, 1) + @post_generation + def set_password(self, create, extracted, **kwargs): + self._raw_password = self.password + self.set_password(self.password) + if create: + self.save() + + +class AdminFactory(UserFactory): + is_staff = True + class CourseEnrollmentFactory(Factory): FACTORY_FOR = CourseEnrollment diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5d3f5761197307ab51920375a032b48a7d58a5de..1ea51bd7f1f0eb56b074696803b0003d9c0cfb4a 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -232,6 +232,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): # disable the syllabus content for courses that do not provide a syllabus self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) self._grading_policy = {} + self.set_grading_policy(self.grading_policy) self.test_center_exams = [] diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..753cbfac4f34acf8546909777ac843df7fb9673b --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -0,0 +1,104 @@ + +import copy +from uuid import uuid4 +from django.test import TestCase + +from django.conf import settings +import xmodule.modulestore.django +from xmodule.templates import update_templates + + +class ModuleStoreTestCase(TestCase): + """ Subclass for any test case that uses the mongodb + module store. This populates a uniquely named modulestore + collection with templates before running the TestCase + and drops it they are finished. """ + + @staticmethod + def flush_mongo_except_templates(): + ''' + Delete everything in the module store except templates + ''' + modulestore = xmodule.modulestore.django.modulestore() + + # This query means: every item in the collection + # that is not a template + query = {"_id.course": {"$ne": "templates"}} + + # Remove everything except templates + modulestore.collection.remove(query) + + @staticmethod + def load_templates_if_necessary(): + ''' + Load templates into the modulestore only if they do not already exist. + We need the templates, because they are copied to create + XModules such as sections and problems + ''' + modulestore = xmodule.modulestore.django.modulestore() + + # Count the number of templates + query = {"_id.course": "templates"} + num_templates = modulestore.collection.find(query).count() + + if num_templates < 1: + update_templates() + + @classmethod + def setUpClass(cls): + ''' + Flush the mongo store and set up templates + ''' + + # Use a uuid to differentiate + # the mongo collections on jenkins. + cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE) + if 'direct' not in settings.MODULESTORE: + settings.MODULESTORE['direct'] = settings.MODULESTORE['default'] + + settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex + settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex + xmodule.modulestore.django._MODULESTORES.clear() + + print settings.MODULESTORE + + TestCase.setUpClass() + + @classmethod + def tearDownClass(cls): + ''' + Revert to the old modulestore settings + ''' + + # Clean up by dropping the collection + modulestore = xmodule.modulestore.django.modulestore() + modulestore.collection.drop() + + xmodule.modulestore.django._MODULESTORES.clear() + + # Restore the original modulestore settings + settings.MODULESTORE = cls.orig_modulestore + + def _pre_setup(self): + ''' + Remove everything but the templates before each test + ''' + + # Flush anything that is not a template + ModuleStoreTestCase.flush_mongo_except_templates() + + # Check that we have templates loaded; if not, load them + ModuleStoreTestCase.load_templates_if_necessary() + + # Call superclass implementation + super(ModuleStoreTestCase, self)._pre_setup() + + def _post_teardown(self): + ''' + Flush everything we created except the templates + ''' + # Flush anything that is not a template + ModuleStoreTestCase.flush_mongo_except_templates() + + # Call superclass implementation + super(ModuleStoreTestCase, self)._post_teardown() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 1a82e1b7082d76e3ef9c20d2b50bb49ff013ce69..e49972a305dd5a2252215596ce8d6ea08ca9aeba 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,4 +1,4 @@ -from factory import Factory +from factory import Factory, lazy_attribute_sequence, lazy_attribute from time import gmtime from uuid import uuid4 from xmodule.modulestore import Location @@ -7,21 +7,12 @@ from xmodule.timeparse import stringify_time from xmodule.modulestore.inheritance import own_metadata -def XMODULE_COURSE_CREATION(class_to_create, **kwargs): - return XModuleCourseFactory._create(class_to_create, **kwargs) - - -def XMODULE_ITEM_CREATION(class_to_create, **kwargs): - return XModuleItemFactory._create(class_to_create, **kwargs) - - class XModuleCourseFactory(Factory): """ Factory for XModule courses. """ ABSTRACT_FACTORY = True - _creation_function = (XMODULE_COURSE_CREATION,) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -33,7 +24,10 @@ class XModuleCourseFactory(Factory): location = Location('i4x', org, number, 'course', Location.clean(display_name)) - store = modulestore('direct') + try: + store = modulestore('direct') + except KeyError: + store = modulestore() # Write the data to the mongo datastore new_course = store.clone_item(template, location) @@ -52,6 +46,11 @@ class XModuleCourseFactory(Factory): # Update the data in the mongo datastore store.update_metadata(new_course.location.url(), own_metadata(new_course)) + data = kwargs.get('data') + if data is not None: + store.update_item(new_course.location, data) + + return new_course @@ -74,7 +73,19 @@ class XModuleItemFactory(Factory): """ ABSTRACT_FACTORY = True - _creation_function = (XMODULE_ITEM_CREATION,) + + display_name = None + + @lazy_attribute + def category(attr): + template = Location(attr.template) + return template.category + + @lazy_attribute + def location(attr): + parent = Location(attr.parent_location) + dest_name = attr.display_name.replace(" ", "_") if attr.display_name is not None else uuid4().hex + return parent._replace(category=attr.category, name=dest_name) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -110,12 +121,7 @@ class XModuleItemFactory(Factory): # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) - # If a display name is set, use that - dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex - dest_location = parent_location._replace(category=template.category, - name=dest_name) - - new_item = store.clone_item(template, dest_location) + new_item = store.clone_item(template, kwargs.get('location')) # replace the display name with an optional parameter passed in from the caller if display_name is not None: @@ -145,4 +151,7 @@ class ItemFactory(XModuleItemFactory): parent_location = 'i4x://MITx/999/course/Robot_Super_Course' template = 'i4x://edx/templates/chapter/Empty' - display_name = 'Section One' + + @lazy_attribute_sequence + def display_name(attr, n): + return "{} {}".format(attr.category.title(), n) \ No newline at end of file diff --git a/jenkins/test.sh b/jenkins/test.sh index edcca840c84fa9fa5e5a00c7ad06a1a356944b14..b554e7a708c20009adc0e43bd8a5a0ba4c30ce59 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -36,7 +36,7 @@ export PIP_DOWNLOAD_CACHE=/mnt/pip-cache source /mnt/virtualenvs/"$JOB_NAME"/bin/activate pip install -q -r pre-requirements.txt -yes w | pip install -q -r test-requirements.txt -r requirements.txt +yes w | pip install -q -r requirements.txt rake clobber rake pep8 > pep8.log || cat pep8.log diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index f6256adfa17326d54e6b3c3a1c13a9861cd8735c..e81568ae4b354b11e15c314287c1fc29693ded5e 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -1,6 +1,8 @@ #pylint: disable=C0111 #pylint: disable=W0621 +from __future__ import absolute_import + from lettuce import world, step from nose.tools import assert_equals, assert_in from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/tests/factories.py b/lms/djangoapps/courseware/tests/factories.py index a84b2b8475f25a9c0db68be41fb91e77fecd41ca..df072c015c7f72aee52f952a39e1dfa243bfce26 100644 --- a/lms/djangoapps/courseware/tests/factories.py +++ b/lms/djangoapps/courseware/tests/factories.py @@ -1,6 +1,7 @@ import factory from student.models import (User, UserProfile, Registration, CourseEnrollmentAllowed) +from courseware.models import StudentModule from django.contrib.auth.models import Group from datetime import datetime import uuid @@ -47,3 +48,15 @@ class CourseEnrollmentAllowedFactory(factory.Factory): email = 'test@edx.org' course_id = 'edX/test/2012_Fall' + + +class StudentModuleFactory(factory.Factory): + FACTORY_FOR = StudentModule + + module_type = "problem" + student = factory.SubFactory(UserFactory) + course_id = "MITx/999/Robot_Super_Course" + state = None + grade = None + max_grade = None + done = 'na' diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 5613f8831fec21f3f7a034e661f2f1d503047665..4c9f592797c2395bb1aba0f3f840ccaec390e110 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -55,7 +55,7 @@ def mongo_store_config(data_dir): Use of this config requires mongo to be running ''' - return { + store = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'OPTIONS': { @@ -68,6 +68,8 @@ def mongo_store_config(data_dir): } } } + store['direct'] = store['default'] + return store def draft_mongo_store_config(data_dir): @@ -83,6 +85,17 @@ def draft_mongo_store_config(data_dir): 'fs_root': data_dir, 'render_template': 'mitxmako.shortcuts.render_to_string', } + }, + 'direct': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore', + 'fs_root': data_dir, + 'render_template': 'mitxmako.shortcuts.render_to_string', + } } } diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 023b355a29ebb58e0829b1e917342db561612318..e06aed1281e711f16b685762b4e8d6ed7177ff2d 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -38,7 +38,7 @@ class Role(models.Model): def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing, # since it's one-off and doesn't handle inheritance later if role.course_id and role.course_id != self.course_id: - logging.warning("{0} cannot inherit permissions from {1} due to course_id inconsistency", \ + logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency", \ self, role) for per in role.permissions.all(): self.add_permission(per) diff --git a/lms/djangoapps/instructor/tests/__init__.py b/lms/djangoapps/instructor/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lms/djangoapps/instructor/tests/test_download_csv.py b/lms/djangoapps/instructor/tests/test_download_csv.py new file mode 100644 index 0000000000000000000000000000000000000000..8e4c175faa0894338951cab861523039a780ecff --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_download_csv.py @@ -0,0 +1,81 @@ +""" +Unit tests for instructor dashboard + +Based on (and depends on) unit tests for courseware. + +Notes for running by hand: + +django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor +""" + +from django.test.utils import override_settings + +# Need access to internal func to put users in the right group +from django.contrib.auth.models import Group + +from django.core.urlresolvers import reverse + +from courseware.access import _course_staff_group_name +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from xmodule.modulestore.django import modulestore +import xmodule.modulestore.django + + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): + ''' + Check for download of csv + ''' + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") + + # Create two accounts + self.student = 'view@test.com' + self.instructor = 'view2@test.com' + self.password = 'foo' + self.create_account('u1', self.student, self.password) + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.student) + self.activate_user(self.instructor) + + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(get_user(self.instructor)) + + make_instructor(self.toy) + + self.logout() + self.login(self.instructor, self.password) + self.enroll(self.toy) + + def test_download_grades_csv(self): + course = self.toy + url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + msg = "url = {0}\n".format(url) + response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'}) + msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response) + + self.assertEqual(response['Content-Type'], 'text/csv', msg) + + cdisp = response['Content-Disposition'] + msg += "Content-Disposition = '%s'\n" % cdisp + self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg) + + body = response.content.replace('\r', '') + msg += "body = '{0}'\n".format(body) + + # All the not-actually-in-the-course hw and labs come from the + # default grading policy string in graders.py + expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" +"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" +''' + + self.assertEqual(body, expected_body, msg) + + + diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests/test_forum_admin.py similarity index 73% rename from lms/djangoapps/instructor/tests.py rename to lms/djangoapps/instructor/tests/test_forum_admin.py index fd8e6529976d7fde6c1ce8e0b574cdc234a9957e..d2d58fb61c97d6d4dd3e936b7de086b59da2df78 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests/test_forum_admin.py @@ -1,13 +1,8 @@ """ -Unit tests for instructor dashboard - -Based on (and depends on) unit tests for courseware. - -Notes for running by hand: - -django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor +Unit tests for instructor dashboard forum administration """ + from django.test.utils import override_settings # Need access to internal func to put users in the right group @@ -24,63 +19,6 @@ from xmodule.modulestore.django import modulestore import xmodule.modulestore.django -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): - ''' - Check for download of csv - ''' - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - self.toy = modulestore().get_course("edX/toy/2012_Fall") - - # Create two accounts - self.student = 'view@test.com' - self.instructor = 'view2@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.instructor, self.password) - self.activate_user(self.student) - self.activate_user(self.instructor) - - def make_instructor(course): - group_name = _course_staff_group_name(course.location) - g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) - - make_instructor(self.toy) - - self.logout() - self.login(self.instructor, self.password) - self.enroll(self.toy) - - def test_download_grades_csv(self): - course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) - msg = "url = {0}\n".format(url) - response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'}) - msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response) - - self.assertEqual(response['Content-Type'], 'text/csv', msg) - - cdisp = response['Content-Disposition'] - msg += "Content-Disposition = '%s'\n" % cdisp - self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg) - - body = response.content.replace('\r', '') - msg += "body = '{0}'\n".format(body) - - # All the not-actually-in-the-course hw and labs come from the - # default grading policy string in graders.py - expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" -"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" -''' - - self.assertEqual(body, expected_body, msg) - - FORUM_ROLES = [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] FORUM_ADMIN_ACTION_SUFFIX = {FORUM_ROLE_ADMINISTRATOR: 'admin', FORUM_ROLE_MODERATOR: 'moderator', FORUM_ROLE_COMMUNITY_TA: 'community TA'} FORUM_ADMIN_USER = {FORUM_ROLE_ADMINISTRATOR: 'forumadmin', FORUM_ROLE_MODERATOR: 'forummoderator', FORUM_ROLE_COMMUNITY_TA: 'forummoderator'} @@ -208,4 +146,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): added_roles.append(rolename) added_roles.sort() roles = ', '.join(added_roles) - self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) + self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) \ No newline at end of file diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py new file mode 100644 index 0000000000000000000000000000000000000000..2de5c18bcd298a9913cd70bba43c5468f7ca4d82 --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -0,0 +1,154 @@ +""" +Tests of the instructor dashboard gradebook +""" + +from django.test import TestCase +from django.test.utils import override_settings +from django.core.urlresolvers import reverse +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory, UserProfileFactory, AdminFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from mock import patch, DEFAULT +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from capa.tests.response_xml_factory import StringResponseXMLFactory +from courseware.tests.factories import StudentModuleFactory +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore + + +USER_COUNT = 11 + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestGradebook(ModuleStoreTestCase): + grading_policy = None + + def setUp(self): + instructor = AdminFactory.create() + self.client.login(username=instructor.username, password='test') + + modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None + + course_data = {} + if self.grading_policy is not None: + course_data['grading_policy'] = self.grading_policy + + self.course = CourseFactory.create(data=course_data) + chapter = ItemFactory.create( + parent_location=self.course.location, + template="i4x://edx/templates/sequential/Empty", + ) + section = ItemFactory.create( + parent_location=chapter.location, + template="i4x://edx/templates/sequential/Empty", + metadata={'graded': True, 'format': 'Homework'} + ) + + self.users = [ + UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i) + for i in xrange(USER_COUNT) + ] + + for user in self.users: + UserProfileFactory.create(user=user) + CourseEnrollmentFactory.create(user=user, course_id=self.course.id) + + for i in xrange(USER_COUNT-1): + template_name = "i4x://edx/templates/problem/Blank_Common_Problem" + item = ItemFactory.create( + parent_location=section.location, + template=template_name, + data=StringResponseXMLFactory().build_xml(answer='foo'), + metadata={'rerandomize': 'always'} + ) + + for j, user in enumerate(self.users): + StudentModuleFactory.create( + grade=1 if i < j else 0, + max_grade=1, + student=user, + course_id=self.course.id, + module_state_key=Location(item.location).url() + ) + + self.response = self.client.get(reverse('gradebook', args=(self.course.id,))) + + def test_response_code(self): + self.assertEquals(self.response.status_code, 200) + +class TestDefaultGradingPolicy(TestGradebook): + def test_all_users_listed(self): + for user in self.users: + self.assertIn(user.username, self.response.content) + + def test_default_policy(self): + # Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6] + # One use at the top of the page [1] + self.assertEquals(7, self.response.content.count('grade_Pass')) + + # Users 1-5 attempted Homework 1 (and get Fs) [4] + # Users 1-10 attempted any homework (and get Fs) [10] + # Users 4-10 scored enough to not get rounded to 0 for the class (and get Fs) [7] + # One use at top of the page [1] + self.assertEquals(22, self.response.content.count('grade_F')) + + # All other grades are None [29 categories * 11 users - 27 non-empty grades = 292] + # One use at the top of the page [1] + self.assertEquals(293, self.response.content.count('grade_None')) + +class TestLetterCutoffPolicy(TestGradebook): + grading_policy = { + "GRADER": [ + { + "type": "Homework", + "min_count": 1, + "drop_count": 0, + "short_label": "HW", + "weight": 1 + }, + ], + "GRADE_CUTOFFS": { + 'A': .9, + 'B': .8, + 'C': .7, + 'D': .6, + } + } + + def test_styles(self): + + self.assertIn("grade_A {color:green;}", self.response.content) + self.assertIn("grade_B {color:Chocolate;}", self.response.content) + self.assertIn("grade_C {color:DarkSlateGray;}", self.response.content) + self.assertIn("grade_D {color:DarkSlateGray;}", self.response.content) + + def test_assigned_grades(self): + print self.response.content + # Users 9-10 have >= 90% on Homeworks [2] + # Users 9-10 have >= 90% on the class [2] + # One use at the top of the page [1] + self.assertEquals(5, self.response.content.count('grade_A')) + + # User 8 has 80 <= Homeworks < 90 [1] + # User 8 has 80 <= class < 90 [1] + # One use at the top of the page [1] + self.assertEquals(3, self.response.content.count('grade_B')) + + # User 7 has 70 <= Homeworks < 80 [1] + # User 7 has 70 <= class < 80 [1] + # One use at the top of the page [1] + self.assertEquals(3, self.response.content.count('grade_C')) + + # User 6 has 60 <= Homeworks < 70 [1] + # User 6 has 60 <= class < 70 [1] + # One use at the top of the page [1] + self.assertEquals(3, self.response.content.count('grade_C')) + + # Users 1-5 have 60% > grades > 0 on Homeworks [5] + # Users 1-5 have 60% > grades > 0 on the class [5] + # One use at top of the page [1] + self.assertEquals(11, self.response.content.count('grade_F')) + + # User 0 has 0 on Homeworks [1] + # User 0 has 0 on the class [1] + # One use at the top of the page [1] + self.assertEquals(3, self.response.content.count('grade_None')) \ No newline at end of file diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index a3b4f42bf768c23e40e818cf4243b07676459705..dd6748e691e96e304bee5b5560d23ff5edeca5a5 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -961,11 +961,14 @@ def gradebook(request, course_id): } for student in enrolled_students] - return render_to_response('courseware/gradebook.html', {'students': student_info, - 'course': course, - 'course_id': course_id, - # Checked above - 'staff_access': True, }) + return render_to_response('courseware/gradebook.html', { + 'students': student_info, + 'course': course, + 'course_id': course_id, + # Checked above + 'staff_access': True, + 'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True), + }) @cache_control(no_cache=True, no_store=True, must_revalidate=True) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index 06f777f611067ce1e5ef5b6210d06e2cb96affa8..db24126a8e25b41570a605d5f439f16de5444f6f 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -73,7 +73,7 @@ def _create_license(user, software): license.save() except IndexError: # there are no free licenses - log.error('No serial numbers available for {0}', software) + log.error('No serial numbers available for %s', software) license = None # TODO [rocha]look if someone has unenrolled from the class # and already has a serial number diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py index 5289c31bc6663cfeea1440d380188c1b7d2fb545..5cf5e44dde8ad4907ea20c7469bd06e9d01515c0 100644 --- a/lms/djangoapps/licenses/tests.py +++ b/lms/djangoapps/licenses/tests.py @@ -8,10 +8,14 @@ from tempfile import NamedTemporaryFile from factory import Factory, SubFactory from django.test import TestCase +from django.test.utils import override_settings from django.core.management import call_command from django.core.urlresolvers import reverse +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from licenses.models import CourseSoftware, UserLicense from courseware.tests.tests import LoginEnrollmentTestCase, get_user +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase COURSE_1 = 'edX/toy/2012_Fall' @@ -130,20 +134,24 @@ class LicenseTestCase(LoginEnrollmentTestCase): self.assertEqual(302, response.status_code) -class CommandTest(TestCase): +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class CommandTest(ModuleStoreTestCase): '''Test management command for importing serial numbers''' + def setUp(self): + course = CourseFactory.create() + self.course_id = course.id def test_import_serial_numbers(self): size = 20 log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1)) with generate_serials_file(size) as temp_file: - args = [COURSE_1, SOFTWARE_1, temp_file.name] + args = [self.course_id, SOFTWARE_1, temp_file.name] call_command('import_serial_numbers', *args) log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2)) with generate_serials_file(size) as temp_file: - args = [COURSE_1, SOFTWARE_2, temp_file.name] + args = [self.course_id, SOFTWARE_2, temp_file.name] call_command('import_serial_numbers', *args) log.debug('There should be only 2 course-software entries') @@ -156,7 +164,7 @@ class CommandTest(TestCase): log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1)) with generate_serials_file(size) as temp_file: - args = [COURSE_1, SOFTWARE_1, temp_file.name] + args = [self.course_id, SOFTWARE_1, temp_file.name] call_command('import_serial_numbers', *args) log.debug('There should be still only 2 course-software entries') @@ -179,7 +187,7 @@ class CommandTest(TestCase): with NamedTemporaryFile() as tmpfile: tmpfile.write('\n'.join(known_serials)) tmpfile.flush() - args = [COURSE_1, SOFTWARE_1, tmpfile.name] + args = [self.course_id, SOFTWARE_1, tmpfile.name] call_command('import_serial_numbers', *args) log.debug('Check if we added only the new ones') diff --git a/lms/envs/test.py b/lms/envs/test.py index 5eb96c8df0b20dadeb76d9343d9886cd4e4f1c56..58b49f744bf3bbc2c5c1fde990d9f302547e2fa8 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -91,7 +91,7 @@ MODULESTORE = { DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': PROJECT_ROOT / "db" / "mitx.db", + 'NAME': TEST_ROOT / 'db' / 'mitx.db' }, } @@ -122,7 +122,7 @@ CACHES = { 'LOCATION': '/var/tmp/mongo_metadata_inheritance', 'TIMEOUT': 300, 'KEY_FUNCTION': 'util.memcache.safe_key', - } + } } # Dummy secret key for dev diff --git a/lms/templates/courseware/gradebook.html b/lms/templates/courseware/gradebook.html index fb750aed1947859835e75b1c501a2cdd3ba5b930..015004ee1cdf4501cd6804fa7622a0972a054617 100644 --- a/lms/templates/courseware/gradebook.html +++ b/lms/templates/courseware/gradebook.html @@ -13,9 +13,12 @@ <%static:css group='course'/> <style type="text/css"> - .grade_A {color:green;} - .grade_B {color:Chocolate;} - .grade_C {color:DarkSlateGray;} + % for (grade, _), color in zip(ordered_grades, ['green', 'Chocolate']): + .grade_${grade} {color:${color};} + % endfor + % for (grade, _) in ordered_grades[2:]: + .grade_${grade} {color:DarkSlateGray;} + % endfor .grade_F {color:DimGray;} .grade_None {color:LightGray;} </style> @@ -78,8 +81,8 @@ letter_grade = 'None' if fraction > 0: letter_grade = 'F' - for grade in ['A', 'B', 'C']: - if fraction >= course.grade_cutoffs[grade]: + for (grade, cutoff) in ordered_grades: + if fraction >= cutoff: letter_grade = grade break @@ -90,11 +93,11 @@ <tbody> %for student in students: - <tr> + <tr> %for section in student['grade_summary']['section_breakdown']: ${percent_data( section['percent'] )} %endfor - <td>${percent_data( student['grade_summary']['percent'])}</td> + ${percent_data( student['grade_summary']['percent'])} </tr> %endfor </tbody> diff --git a/requirements.txt b/requirements.txt index 1a383e6cc0c6c8cc38d9ac78d40c9e700dfd041c..a626ac1944d186f9bdd5ecf3b3cd89e52dfa1bac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,7 @@ beautifulsoup==3.2.1 boto==2.6.0 django-celery==3.0.11 django-countries==1.5 -django-debug-toolbar-mongo django-followit==0.0.3 -django-jasmine==0.3.2 django-keyedcache==1.4-6 django-kombu==0.9.4 django-mako==0.1.5pre @@ -19,29 +17,20 @@ django-ses==0.4.1 django-storages==1.1.5 django-threaded-multihost==1.4-1 django==1.4.3 -django_debug_toolbar -django_nose==1.1 -dogapi==1.2.1 -dogstatsd-python==0.2.1 -factory_boy feedparser==5.1.3 fs==0.4.0 GitPython==0.3.2.RC1 glob2==0.3 http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz -ipython==0.13.1 lxml==3.0.1 mako==0.7.3 Markdown==2.2.1 -mock==0.8.0 MySQL-python==1.2.4c1 networkx==1.7 -newrelic==1.8.0.13 nltk==2.0.4 -nosexcover==1.0.7 numpy==1.6.2 paramiko==1.9.0 -path.py +path.py==3.0.1 Pillow==1.7.8 pip pygments==1.5 @@ -51,11 +40,37 @@ python-memcached==1.48 python-openid==2.2.5 pytz==2012h PyYAML==3.10 -rednose==0.3 requests==0.14.2 scipy==0.11.0 Shapely==1.2.16 sorl-thumbnail==11.12 South==0.7.6 -sphinx==1.1.3 xmltodict==0.4.1 + +# Used for debugging +ipython==0.13.1 + + +# Metrics gathering and monitoring +dogapi==1.2.1 +dogstatsd-python==0.2.1 +newrelic==1.8.0.13 + +# Used for documentation gathering +sphinx==1.1.3 + +# Used for testing +coverage==3.6 +factory_boy==1.3.0 +lettuce==0.2.15 +mock==0.8.0 +nosexcover==1.0.7 +pep8==1.4.5 +pylint==0.27.0 +rednose==0.3 +selenium==2.31.0 +splinter==0.5.0 +django_nose==1.1 +django-jasmine==0.3.2 +django_debug_toolbar +django-debug-toolbar-mongo diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index d9db89f1077d0d38513306bc1134f4ca7691bf62..0000000000000000000000000000000000000000 --- a/test-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -coverage==3.6 -pylint==0.27.0 -pep8==1.4.5 -lettuce==0.2.15 -selenium==2.31.0 -splinter==0.5.0 -