Merge pull request #1825 from MITx/fix/cale/gradebook
Fix/cale/gradebook
This commit is contained in:
20
.pylintrc
20
.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]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
104
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
Normal file
104
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
0
lms/djangoapps/instructor/tests/__init__.py
Normal file
0
lms/djangoapps/instructor/tests/__init__.py
Normal file
81
lms/djangoapps/instructor/tests/test_download_csv.py
Normal file
81
lms/djangoapps/instructor/tests/test_download_csv.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
154
lms/djangoapps/instructor/tests/test_gradebook.py
Normal file
154
lms/djangoapps/instructor/tests/test_gradebook.py
Normal file
@@ -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'))
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user