Merge branch 'master' into feature/server_split

This commit is contained in:
David Ormsbee
2012-08-13 00:36:31 -04:00
36 changed files with 738 additions and 239 deletions

View File

@@ -145,6 +145,8 @@ def has_staff_access_to_course(user, course):
'''
Returns True if the given user has staff access to the course.
This means that user is in the staff_* group, or is an overall admin.
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
course is the course field of the location being accessed.
'''
@@ -156,13 +158,18 @@ def has_staff_access_to_course(user, course):
# note this is the Auth group, not UserTestGroup
user_groups = [x[1] for x in user.groups.values_list()]
staff_group = course_staff_group_name(course)
log.debug('course %s, staff_group %s, user %s, groups %s' % (
course, staff_group, user, user_groups))
if staff_group in user_groups:
return True
return False
def has_access_to_course(user,course):
def has_staff_access_to_course_id(user, course_id):
"""Helper method that takes a course_id instead of a course name"""
loc = CourseDescriptor.id_to_location(course_id)
return has_staff_access_to_course(user, loc.course)
def has_access_to_course(user, course):
'''course is the .course element of a location'''
if course.metadata.get('ispublic'):
return True
return has_staff_access_to_course(user,course)

View File

@@ -1,3 +1,6 @@
# Compute grades using real division, with no integer truncation
from __future__ import division
import random
import logging
@@ -13,33 +16,33 @@ log = logging.getLogger("mitx.courseware")
def yield_module_descendents(module):
stack = module.get_display_items()
while len(stack) > 0:
next_module = stack.pop()
stack.extend( next_module.get_display_items() )
yield next_module
def grade(student, request, course, student_module_cache=None):
"""
This grades a student as quickly as possible. It retuns the
This grades a student as quickly as possible. It retuns the
output from the course grader, augmented with the final letter
grade. The keys in the output are:
- grade : A final letter grade.
- percent : The final percent for the class (rounded up).
- section_breakdown : A breakdown of each section that makes
up the grade. (For display)
- grade_breakdown : A breakdown of the major components that
make up the final grade. (For display)
More information on the format is in the docstring for CourseGrader.
"""
grading_context = course.grading_context
if student_module_cache == None:
student_module_cache = StudentModuleCache(student, grading_context['all_descriptors'])
totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is
# passed to the grader
@@ -48,91 +51,91 @@ def grade(student, request, course, student_module_cache=None):
for section in sections:
section_descriptor = section['section_descriptor']
section_name = section_descriptor.metadata.get('display_name')
should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for moduledescriptor in section['xmoduledescriptors']:
if student_module_cache.lookup(moduledescriptor.category, moduledescriptor.location.url() ):
should_grade_section = True
break
if should_grade_section:
scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
# Then, we may not need to instatiate any problems if they are already in the database
for module in yield_module_descendents(section_module):
for module in yield_module_descendents(section_module):
(correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None:
continue
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange(max(total - 2, 1), total + 1)
else:
correct = total
graded = module.metadata.get("graded", False)
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(scores, section_name)
else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores
if graded_total.possible > 0:
format_scores.append(graded_total)
else:
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location))
totaled_scores[section_format] = format_scores
grade_summary = course.grader.grade(totaled_scores)
# We round the grade here, to make sure that the grade is an whole percentage and
# doesn't get displayed differently than it gets grades
grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100
letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
grade_summary['grade'] = letter_grade
return grade_summary
def grade_for_percentage(grade_cutoffs, percentage):
"""
Returns a letter grade 'A' 'B' 'C' or None.
Arguments
- grade_cutoffs is a dictionary mapping a grade to the lowest
possible percentage to earn that grade.
- percentage is the final percent across all problems in a course
"""
letter_grade = None
for possible_grade in ['A', 'B', 'C']:
if percentage >= grade_cutoffs[possible_grade]:
letter_grade = possible_grade
break
return letter_grade
return letter_grade
def progress_summary(student, course, grader, student_module_cache):
"""
This pulls a summary of all problems in the course.
Returns
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
etc.
Arguments:
@@ -152,7 +155,7 @@ def progress_summary(student, course, grader, student_module_cache):
if correct is None and total is None:
continue
scores.append(Score(correct, total, graded,
scores.append(Score(correct, total, graded,
module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(
@@ -179,7 +182,7 @@ def progress_summary(student, course, grader, student_module_cache):
def get_score(user, problem, student_module_cache):
"""
Return the score for a user on a problem
Return the score for a user on a problem, as a tuple (correct, total).
user: a Student object
problem: an XModule
@@ -188,7 +191,7 @@ def get_score(user, problem, student_module_cache):
if not (problem.descriptor.stores_state and problem.descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)
correct = 0.0
# If the ID is not in the cache, add the item

View File

@@ -47,11 +47,12 @@ def toc_for_course(user, request, course, active_chapter, active_section):
'format': format, 'due': due, 'active' : bool}, ...]
active is set for the section and chapter corresponding to the passed
parameters. Everything else comes from the xml, or defaults to "".
parameters, which are expected to be url_names of the chapter+section.
Everything else comes from the xml, or defaults to "".
chapters with name 'hidden' are skipped.
'''
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
course = get_module(user, request, course.location, student_module_cache)
@@ -60,8 +61,8 @@ def toc_for_course(user, request, course, active_chapter, active_section):
sections = list()
for section in chapter.get_display_items():
active = (chapter.display_name == active_chapter and
section.display_name == active_section)
active = (chapter.url_name == active_chapter and
section.url_name == active_section)
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc:
@@ -74,7 +75,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters.append({'display_name': chapter.display_name,
'url_name': chapter.url_name,
'sections': sections,
'active': chapter.display_name == active_chapter})
'active': chapter.url_name == active_chapter})
return chapters
@@ -123,7 +124,7 @@ def get_module(user, request, location, student_module_cache, position=None):
position within module
Returns: xmodule instance
'''
descriptor = modulestore().get_item(location)
@@ -134,13 +135,13 @@ def get_module(user, request, location, student_module_cache, position=None):
if descriptor.stores_state:
instance_module = student_module_cache.lookup(descriptor.category,
descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category,
shared_state_key)
instance_state = instance_module.state if instance_module is not None else None
shared_state = shared_module.state if shared_module is not None else None
@@ -218,13 +219,13 @@ def get_instance_module(user, module, student_module_cache):
"""
if user.is_authenticated():
if not module.descriptor.stores_state:
log.exception("Attempted to get the instance_module for a module "
log.exception("Attempted to get the instance_module for a module "
+ str(module.id) + " which does not store state.")
return None
instance_module = student_module_cache.lookup(module.category,
module.location.url())
if not instance_module:
instance_module = StudentModule(
student=user,
@@ -234,11 +235,11 @@ def get_instance_module(user, module, student_module_cache):
max_grade=module.max_score())
instance_module.save()
student_module_cache.append(instance_module)
return instance_module
else:
return None
def get_shared_instance_module(user, module, student_module_cache):
"""
Return shared_module is a StudentModule specific to all modules with the same
@@ -248,7 +249,7 @@ def get_shared_instance_module(user, module, student_module_cache):
if user.is_authenticated():
# To get the shared_state_key, we need to descriptor
descriptor = modulestore().get_item(module.location)
shared_state_key = getattr(module, 'shared_state_key', None)
if shared_state_key is not None:
shared_module = student_module_cache.lookup(module.category,
@@ -263,7 +264,7 @@ def get_shared_instance_module(user, module, student_module_cache):
student_module_cache.append(shared_module)
else:
shared_module = None
return shared_module
else:
return None
@@ -271,7 +272,7 @@ def get_shared_instance_module(user, module, student_module_cache):
@csrf_exempt
def xqueue_callback(request, course_id, userid, id, dispatch):
'''
Entry point for graded results from the queueing system.
Entry point for graded results from the queueing system.
'''
# Test xqueue package, which we expect to be:
# xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
@@ -343,7 +344,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
instance_module = get_instance_module(request.user, instance, student_module_cache)
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
# Don't track state for anonymous users (who don't have student modules)
if instance_module is not None:
oldgrade = instance_module.grade

View File

@@ -13,8 +13,9 @@ from django.core.urlresolvers import reverse
from mock import patch, Mock
from override_settings import override_settings
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from student.models import Registration
from courseware.courses import course_staff_group_name
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
@@ -88,6 +89,13 @@ class ActivateLoginTestCase(TestCase):
self.assertTrue(data['success'])
return resp
def logout(self):
'''Logout, check that it worked.'''
resp = self.client.get(reverse('logout'), {})
# should redirect
self.assertEqual(resp.status_code, 302)
return resp
def _create_account(self, username, email, pw):
'''Try to create an account. No error checking'''
resp = self.client.post('/create_account', {
@@ -131,12 +139,16 @@ class ActivateLoginTestCase(TestCase):
'''The setup function does all the work'''
pass
def test_logout(self):
'''Setup function does login'''
self.logout()
class PageLoader(ActivateLoginTestCase):
''' Base class that adds a function to load all pages in a modulestore '''
def enroll(self, course):
"""Enroll the currently logged-in user, and check that it worked."""
resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll',
'course_id': course.id,
@@ -193,7 +205,99 @@ class TestCoursesLoadTestCase(PageLoader):
self.check_pages_load('full', TEST_DATA_DIR, modulestore())
# ========= TODO: check ajax interaction here too?
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class TestInstructorAuth(PageLoader):
"""Check that authentication works properly"""
# NOTE: setUpClass() runs before override_settings takes effect, so
# can't do imports there without manually hacking settings.
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
modulestore().collection.drop()
import_from_xml(modulestore(), TEST_DATA_DIR, ['toy'])
import_from_xml(modulestore(), TEST_DATA_DIR, ['full'])
courses = modulestore().get_courses()
# get the two courses sorted out
courses.sort(key=lambda c: c.location.course)
[self.full, self.toy] = courses
# 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 check_for_get_code(self, code, url):
resp = self.client.get(url)
# HACK: workaround the bug that returns 200 instead of 404.
# TODO (vshnayder): once we're returning 404s, get rid of this if.
if code != 404:
self.assertEqual(resp.status_code, code)
else:
# look for "page not found" instead of the status code
self.assertTrue(resp.content.lower().find('page not found') != -1)
def test_instructor_page(self):
"Make sure only instructors can load it"
# First, try with an enrolled student
self.login(self.student, self.password)
# shouldn't work before enroll
self.check_for_get_code(302, reverse('courseware', kwargs={'course_id': self.toy.id}))
self.enroll(self.toy)
self.enroll(self.full)
# should work now
self.check_for_get_code(200, reverse('courseware', kwargs={'course_id': self.toy.id}))
def instructor_urls(course):
"list of urls that only instructors/staff should be able to see"
urls = [reverse(name, kwargs={'course_id': course.id}) for name in (
'instructor_dashboard',
'gradebook',
'grade_summary',)]
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
# shouldn't be able to get to the instructor pages
for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
# Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy)
g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
# Now should be able to get to the toy course, but not the full course
for url in instructor_urls(self.toy):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
for url in instructor_urls(self.full):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
# now also make the instructor staff
u = user(self.instructor)
u.is_staff = True
u.save()
# and now should be able to load both
for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)

View File

@@ -27,7 +27,8 @@ from xmodule.course_module import CourseDescriptor
from util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment
from courseware import grades
from courseware.courses import check_course, get_courses_by_university
from courseware.courses import (check_course, get_courses_by_university,
has_staff_access_to_course_id)
log = logging.getLogger("mitx.courseware")
@@ -35,6 +36,9 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib}
def user_groups(user):
"""
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
"""
if not user.is_authenticated():
return []
@@ -64,64 +68,6 @@ def courses(request):
universities = get_courses_by_university(request.user)
return render_to_response("courses.html", {'universities': universities})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
if 'course_admin' not in user_groups(request.user):
raise Http404
course = check_course(course_id)
student_objects = User.objects.all()[:100]
student_info = []
#TODO: Only select students who are in the course
for student in student_objects:
student_info.append({
'username': student.username,
'id': student.id,
'email': student.email,
'grade_summary': grades.grade(student, request, course),
'realname': UserProfile.objects.get(user=student).name
})
return render_to_response('gradebook.html', {'students': student_info, 'course': course})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, course_id, student_id=None):
''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .'''
course = check_course(course_id)
if student_id is None:
student = request.user
else:
if 'course_admin' not in user_groups(request.user):
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module = get_module(request.user, request, course.location, student_module_cache)
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token'],
'courseware_summary' : courseware_summary,
'grade_summary' : grade_summary
}
context.update()
return render_to_response('profile.html', context)
def render_accordion(request, course, chapter, section):
''' Draws navigation bar. Takes current position in accordion as
@@ -129,19 +75,14 @@ def render_accordion(request, course, chapter, section):
If chapter and section are '' or None, renders a default accordion.
course, chapter, and section are the url_names.
Returns the html string'''
# grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
active_chapter = 1
for i in range(len(toc)):
if toc[i]['active']:
active_chapter = i
context = dict([('active_chapter', active_chapter),
('toc', toc),
('course_name', course.title),
context = dict([('toc', toc),
('course_id', course.id),
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
return render_to_string('accordion.html', context)
@@ -233,12 +174,10 @@ def jump_to(request, location):
'''
Show the page that contains a specific location.
If the location is invalid, return a 404.
If the location is invalid or not in any class, return a 404.
If the location is valid, but not present in a course, ?
If the location is valid, but in a course the current user isn't registered for, ?
TODO -- let the index view deal with it?
Otherwise, delegates to the index view to figure out whether this user
has access, and what they should see.
'''
# Complain if the location isn't valid
try:
@@ -254,16 +193,16 @@ def jump_to(request, location):
except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling
# Rely on index to do all error handling and access control.
return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie
def course_info(request, course_id):
'''
"""
Display the course's info.html, or 404 if there is no such course.
Assumes the course_id is in a valid format.
'''
"""
course = check_course(course_id)
return render_to_response('info.html', {'course': course})
@@ -289,7 +228,10 @@ def course_about(request, course_id):
@ensure_csrf_cookie
@cache_if_anonymous
def university_profile(request, org_id):
all_courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
"""
Return the profile for the particular org_id. 404 if it's not valid.
"""
all_courses = modulestore().get_courses()
valid_org_ids = set(c.org for c in all_courses)
if org_id not in valid_org_ids:
raise Http404("University Profile not found for {0}".format(org_id))
@@ -300,3 +242,104 @@ def university_profile(request, org_id):
template_file = "university_profile/{0}.html".format(org_id).lower()
return render_to_response(template_file, context)
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, course_id, student_id=None):
""" User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings.
Course staff are allowed to see the profiles of students in their class.
"""
course = check_course(course_id)
if student_id is None or student_id == request.user.id:
# always allowed to see your own profile
student = request.user
else:
# Requesting access to a different student's profile
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module = get_module(request.user, request, course.location, student_module_cache)
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token'],
'courseware_summary' : courseware_summary,
'grade_summary' : grade_summary
}
context.update()
return render_to_response('profile.html', context)
# ======== Instructor views =============================================================================
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
"""
Show the gradebook for this course:
- only displayed to course staff
- shows students who are enrolled.
"""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
# TODO (vshnayder): implement pagination.
enrolled_students = enrolled_students[:1000] # HACK!
student_info = [{'username': student.username,
'id': student.id,
'email': student.email,
'grade_summary': grades.grade(student, request, course),
'realname': UserProfile.objects.get(user=student).name
}
for student in enrolled_students]
return render_to_response('gradebook.html', {'students': student_info,
'course': course, 'course_id': course_id})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def grade_summary(request, course_id):
"""Display the grade summary for a course."""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
# For now, just a static page
context = {'course': course }
return render_to_response('grade_summary.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course."""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
# For now, just a static page
context = {'course': course }
return render_to_response('instructor_dashboard.html', context)