Commit 3cd414af by Calen Pennington

Merge pull request #1825 from MITx/fix/cale/gradebook

Fix/cale/gradebook
parents 24189378 904ec5d8
......@@ -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)
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,
self.assertContains(
resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
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,
self.assertContains(
resp,
'<span class="class-name">Robot Super Educational Course</span>',
status_code=200,
html=True)
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,
self.assertContains(
resp,
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200,
html=True)
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",
test_model = CourseMetadata.update_from_json(self.course_location, {
"advertised_start": "start A",
"testcenter_info": {"c": "test"},
"days_early_for_beta": 2})
"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)
)
\ No newline at end of file
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"""
......
......@@ -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 = []
......
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()
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))
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
......@@ -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
......
#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
......
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)
......
"""
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)
"""
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'}
......
"""
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
......@@ -961,11 +961,14 @@ def gradebook(request, course_id):
}
for student in enrolled_students]
return render_to_response('courseware/gradebook.html', {'students': student_info,
return render_to_response('courseware/gradebook.html', {
'students': student_info,
'course': course,
'course_id': course_id,
# Checked above
'staff_access': True, })
'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'
},
}
......
......@@ -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
......@@ -94,7 +97,7 @@
%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
coverage==3.6
pylint==0.27.0
pep8==1.4.5
lettuce==0.2.15
selenium==2.31.0
splinter==0.5.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment