Merge branch 'master' into feature/deena/testing

Conflicts:
	cms/djangoapps/contentstore/tests/factories.py
	lms/djangoapps/courseware/tests/test_access.py
This commit is contained in:
Will Daly
2013-03-04 16:30:41 -05:00
763 changed files with 76000 additions and 11842 deletions

View File

@@ -1,10 +1,10 @@
"""This file contains (or should), all access control logic for the courseware.
Ideally, it will be the only place that needs to know about any special settings
like DISABLE_START_DATES"""
import logging
import time
from datetime import datetime, timedelta
from functools import partial
from django.conf import settings
from django.contrib.auth.models import Group
@@ -186,9 +186,9 @@ def _get_access_group_name_course_desc(course, action):
'''
Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor'
'''
if action=='staff':
if action == 'staff':
return _course_staff_group_name(course.location)
elif action=='instructor':
elif action == 'instructor':
return _course_instructor_group_name(course.location)
return []
@@ -342,6 +342,51 @@ def _does_course_group_name_exist(name):
return len(Group.objects.filter(name=name)) > 0
def _course_org_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for an organization which corresponds
to the organization in the course id.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which
the location occurs.
Required if location doesn't have category 'course'
"""
loc = Location(location)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
return 'staff_%s' % course_id.split('/')[0]
def group_names_for(role, location, course_context=None):
"""Returns the group names for a given role with this location. Plural
because it will return both the name we expect now as well as the legacy
group name we support for backwards compatibility. This should not check
the DB for existence of a group (like some of its callers do) because that's
a DB roundtrip, and we expect this might be invoked many times as we crawl
an XModule tree."""
loc = Location(location)
legacy_group_name = '{0}_{1}'.format(role, loc.course)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
group_name = '{0}_{1}'.format(role, course_id)
return [group_name, legacy_group_name]
group_names_for_staff = partial(group_names_for, 'staff')
group_names_for_instructor = partial(group_names_for, 'instructor')
def _course_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for a location in the context of a course run.
@@ -354,31 +399,32 @@ def _course_staff_group_name(location, course_context=None):
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location)
legacy_name = 'staff_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
group_name, legacy_group_name = group_names_for_staff(location, course_context)
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def _course_org_instructor_group_name(location, course_context=None):
"""
Get the name of the instructor group for an organization which corresponds
to the organization in the course id.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which
the location occurs.
Required if location doesn't have category 'course'
"""
loc = Location(location)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
return 'staff_%s' % course_id
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
return 'instructor_%s' % course_id.split('/')[0]
def _course_instructor_group_name(location, course_context=None):
@@ -394,18 +440,26 @@ def _course_instructor_group_name(location, course_context=None):
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location)
legacy_name = 'instructor_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
group_name, legacy_group_name = group_names_for_instructor(location, course_context)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
return 'instructor_%s' % course_id
def _has_global_staff_access(user):
@@ -462,6 +516,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
return descriptor.start
def _has_instructor_access_to_location(user, location, course_context=None):
return _has_access_to_location(user, location, 'instructor', course_context)
@@ -496,19 +551,22 @@ def _has_access_to_location(user, location, access_level, course_context):
user_groups = [g.name for g in user.groups.all()]
if access_level == 'staff':
staff_group = _course_staff_group_name(location, course_context)
if staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
return True
debug("Deny: user not in group %s", staff_group)
staff_groups = group_names_for_staff(location, course_context) + \
[_course_org_staff_group_name(location, course_context)]
for staff_group in staff_groups:
if staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
return True
debug("Deny: user not in groups %s", staff_groups)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_group = _course_instructor_group_name(location, course_context)
if instructor_group in user_groups:
debug("Allow: user in group %s", instructor_group)
return True
debug("Deny: user not in group %s", instructor_group)
instructor_groups = group_names_for_instructor(location, course_context) + \
[_course_org_instructor_group_name(location, course_context)]
for instructor_group in instructor_groups:
if instructor_group in user_groups:
debug("Allow: user in group %s", instructor_group)
return True
debug("Deny: user not in groups %s", instructor_groups)
else:
log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level)

View File

@@ -11,4 +11,3 @@ admin.site.register(StudentModule)
admin.site.register(OfflineComputedGrade)
admin.site.register(OfflineComputedGradeLog)

View File

@@ -19,7 +19,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModule
from static_replace import replace_urls, try_staticfiles_lookup
from static_replace import replace_static_urls
from courseware.access import has_access
import branding
from courseware.models import StudentModuleCache
@@ -27,6 +27,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
def get_request_for_thread():
"""Walk up the stack, return the nearest first argument named "request"."""
frame = None
@@ -83,13 +84,12 @@ def get_opt_course_with_access(user, course_id, action):
return None
return get_course_with_access(user, course_id, action)
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if isinstance(modulestore(), XMLModuleStore):
path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
return '/static/' + course.metadata['data_dir'] + "/images/course_image.jpg"
else:
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
path = StaticContent.get_url_path_from_location(loc)
@@ -153,7 +153,7 @@ def get_course_about_section(course, section_key):
request = get_request_for_thread()
loc = course.location._replace(category='about', name=section_key)
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True, wrap_xmodule_display = False)
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok=True, wrap_xmodule_display=False)
html = ''
@@ -191,7 +191,7 @@ def get_course_info_section(request, cache, course, section_key):
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
course_module = get_module(request.user, request, loc, cache, course.id, wrap_xmodule_display = False)
course_module = get_module(request.user, request, loc, cache, course.id, wrap_xmodule_display=False)
html = ''
if course_module is not None:
@@ -224,8 +224,11 @@ def get_course_syllabus_section(course, section_key):
dirs = [path("syllabus") / course.url_name, path("syllabus")]
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'], course_namespace=course.location)
return replace_static_urls(
htmlFile.read().decode('utf-8'),
course.metadata['data_dir'],
course_namespace=course.location
)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url()))
@@ -257,7 +260,7 @@ def get_courses(user, domain=None):
courses = branding.get_visible_courses(domain)
courses = [c for c in courses if has_access(user, c, 'see_exists')]
courses = sorted(courses, key=lambda course:course.number)
courses = sorted(courses, key=lambda course: course.number)
return courses

View File

@@ -0,0 +1,99 @@
from lettuce import world, step
from django.core.management import call_command
from nose.tools import assert_equals, assert_in
from lettuce.django import django_url
from django.conf import settings
from django.contrib.auth.models import User
from student.models import CourseEnrollment
import time
from logging import getLogger
logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds):
time.sleep(float(seconds))
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
world.browser.visit(django_url('/'))
assert world.browser.is_element_present_by_css('header.global', 10)
@step(u'I (?:visit|access|open) the dashboard$')
def i_visit_the_dashboard(step):
world.browser.visit(django_url('/dashboard'))
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
def click_the_link_called(step, text):
world.browser.find_link_by_text(text).click()
@step('I should be on the dashboard page$')
def i_should_be_on_the_dashboard(step):
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
assert world.browser.title == 'Dashboard'
@step(u'I (?:visit|access|open) the courses page$')
def i_am_on_the_courses_page(step):
world.browser.visit(django_url('/courses'))
assert world.browser.is_element_present_by_css('section.courses')
@step('I should see that the path is "([^"]*)"$')
def i_should_see_that_the_path_is(step, path):
assert world.browser.url == django_url(path)
@step(u'the page title should be "([^"]*)"$')
def the_page_title_should_be(step, title):
assert world.browser.title == title
@step(r'should see that the url is "([^"]*)"$')
def should_have_the_url(step, url):
assert_equals(world.browser.url, url)
@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
def should_see_a_link_called(step, text):
assert len(world.browser.find_link_by_text(text)) > 0
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
def should_see_in_the_page(step, text):
assert_in(text, world.browser.html)
@step('I am logged in$')
def i_am_logged_in(step):
world.create_user('robot')
world.log_in('robot@edx.org', 'test')
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
@step(u'I am registered for a course$')
def i_am_registered_for_a_course(step):
world.create_user('robot')
u = User.objects.get(username='robot')
CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall')
world.log_in('robot@edx.org', 'test')
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)

View File

@@ -8,6 +8,7 @@ from logging import getLogger
logger = getLogger(__name__)
## support functions
def get_courses():
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
@@ -18,28 +19,6 @@ def get_courses():
courses = sorted(courses, key=lambda course: course.number)
return courses
# def get_courseware(course_id):
# """
# Given a course_id (string), return a courseware array of dictionaries for the
# top two levels of navigation. Example:
# [
# {'chapter_name': 'Overview',
# 'sections': ['Welcome', 'System Usage Sequence', 'Lab0: Using the tools', 'Circuit Sandbox']
# },
# {'chapter_name': 'Week 1',
# 'sections': ['Administrivia and Circuit Elements', 'Basic Circuit Analysis', 'Resistor Divider', 'Week 1 Tutorials']
# },
# {'chapter_name': 'Midterm Exam',
# 'sections': ['Midterm Exam']
# }
# ]
# """
# course = get_course_by_id(course_id)
# chapters = course.get_children()
# courseware = [ {'chapter_name':c.display_name, 'sections':[s.display_name for s in c.get_children()]} for c in chapters]
# return courseware
def get_courseware_with_tabs(course_id):
"""
@@ -101,18 +80,19 @@ def get_courseware_with_tabs(course_id):
"""
course = get_course_by_id(course_id)
chapters = [ chapter for chapter in course.get_children() if chapter.metadata.get('hide_from_toc','false').lower() != 'true' ]
courseware = [{'chapter_name':c.display_name,
'sections':[{'section_name':s.display_name,
'clickable_tab_count':len(s.get_children()) if (type(s)==seq_module.SequenceDescriptor) else 0,
'tabs':[{'children_count':len(t.get_children()) if (type(t)==vertical_module.VerticalDescriptor) else 0,
'class':t.__class__.__name__ }
for t in s.get_children() ]}
chapters = [chapter for chapter in course.get_children() if chapter.metadata.get('hide_from_toc', 'false').lower() != 'true']
courseware = [{'chapter_name': c.display_name,
'sections': [{'section_name': s.display_name,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
'class': t.__class__.__name__}
for t in s.get_children()]}
for s in c.get_children() if s.metadata.get('hide_from_toc', 'false').lower() != 'true']}
for c in chapters ]
for c in chapters]
return courseware
def process_section(element, num_tabs=0):
'''
Process section reads through whatever is in 'course-content' and classifies it according to sequence module type.

View File

@@ -9,10 +9,3 @@ Feature: View the Courseware Tab
And I click on View Courseware
When I click on the "Courseware" tab
Then the "Courseware" tab is active
# TODO: fix this one? Not sure whether you should get a 404.
# Scenario: I cannot get to the courseware tab when not logged in
# Given I am not logged in
# And I visit the homepage
# When I visit the courseware URL
# Then the login dialog is visible

View File

@@ -1,7 +1,8 @@
from lettuce import world, step
from lettuce.django import django_url
@step('I visit the courseware URL$')
def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
world.browser.visit(url)

View File

@@ -1,36 +1,43 @@
from lettuce import world, step
from lettuce.django import django_url
@step('I click on View Courseware')
def i_click_on_view_courseware(step):
css = 'p.enter-course'
css = 'a.enter-course'
world.browser.find_by_css(css).first.click()
@step('I click on the "([^"]*)" tab$')
def i_click_on_the_tab(step, tab):
world.browser.find_link_by_text(tab).first.click()
world.browser.find_link_by_partial_text(tab).first.click()
world.save_the_html()
@step('I visit the courseware URL$')
def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
@step(u'I do not see "([^"]*)" anywhere on the page')
def i_do_not_see_text_anywhere_on_the_page(step, text):
assert world.browser.is_text_not_present(text)
assert world.browser.is_text_not_present(text)
@step(u'I am on the dashboard page$')
def i_am_on_the_dashboard_page(step):
assert world.browser.is_element_present_by_css('section.courses')
assert world.browser.url == django_url('/dashboard')
@step('the "([^"]*)" tab is active$')
def the_tab_is_active(step, tab):
css = '.course-tabs a.active'
active_tab = world.browser.find_by_css(css)
assert (active_tab.text == tab)
@step('the login dialog is visible$')
def login_dialog_visible(step):
css = 'form#login_form.login_form'

View File

@@ -0,0 +1,47 @@
Feature: Homepage for web users
In order to get an idea what edX is about
As a an anonymous web user
I want to check the information on the home page
Scenario: User can see the "Login" button
Given I visit the homepage
Then I should see a link called "Log In"
Scenario: User can see the "Sign up" button
Given I visit the homepage
Then I should see a link called "Sign Up"
Scenario Outline: User can see main parts of the page
Given I visit the homepage
Then I should see a link called "<Link>"
When I click the link with the text "<Link>"
Then I should see that the path is "<Path>"
Examples:
| Link | Path |
| Find Courses | /courses |
| About | /about |
| Jobs | /jobs |
| Contact | /contact |
Scenario: User can visit the blog
Given I visit the homepage
When I click the link with the text "Blog"
Then I should see that the url is "http://blog.edx.org/"
# TODO: test according to domain or policy
Scenario: User can see the partner institutions
Given I visit the homepage
Then I should see "<Partner>" in the Partners section
Examples:
| Partner |
| MITx |
| HarvardX |
| BerkeleyX |
| UTx |
| WellesleyX |
| GeorgetownX |
# # TODO: Add scenario that tests the courses available
# # using a policy or a configuration file

View File

@@ -0,0 +1,9 @@
from lettuce import world, step
from nose.tools import assert_in
@step('I should see "([^"]*)" in the Partners section$')
def i_should_see_partner(step, partner):
partners = world.browser.find_by_css(".partner .name span")
names = set(span.text for span in partners)
assert_in(partner, names)

View File

@@ -0,0 +1,27 @@
Feature: Login in as a registered user
As a registered user
In order to access my content
I want to be able to login in to edX
Scenario: Login to an unactivated account
Given I am an edX user
And I am an unactivated user
And I visit the homepage
When I click the link with the text "Log In"
And I submit my credentials on the login form
Then I should see the login error message "This account has not been activated"
Scenario: Login to an activated account
Given I am an edX user
And I am an activated user
And I visit the homepage
When I click the link with the text "Log In"
And I submit my credentials on the login form
Then I should be on the dashboard page
Scenario: Logout of a signed in account
Given I am logged in
When I click the dropdown arrow
And I click the link with the text "Log Out"
Then I should see a link with the text "Log In"
And I should see that the path is "/"

View File

@@ -0,0 +1,52 @@
from lettuce import step, world
from django.contrib.auth.models import User
@step('I am an unactivated user$')
def i_am_an_unactivated_user(step):
user_is_an_unactivated_user('robot')
@step('I am an activated user$')
def i_am_an_activated_user(step):
user_is_an_activated_user('robot')
@step('I submit my credentials on the login form')
def i_submit_my_credentials_on_the_login_form(step):
fill_in_the_login_form('email', 'robot@edx.org')
fill_in_the_login_form('password', 'test')
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_value('Access My Courses').click()
@step(u'I should see the login error message "([^"]*)"$')
def i_should_see_the_login_error_message(step, msg):
login_error_div = world.browser.find_by_css('form#login_form #login_error')
assert (msg in login_error_div.text)
@step(u'click the dropdown arrow$')
def click_the_dropdown(step):
css = ".dropdown"
e = world.browser.find_by_css(css)
e.click()
#### helper functions
def user_is_an_unactivated_user(uname):
u = User.objects.get(username=uname)
u.is_active = False
u.save()
def user_is_an_activated_user(uname):
u = User.objects.get(username=uname)
u.is_active = True
u.save()
def fill_in_the_login_form(field, value):
login_form = world.browser.find_by_css('form#login_form')
form_field = login_form.find_by_name(field)
form_field.fill(value)

View File

@@ -3,31 +3,35 @@ Feature: Open ended grading
In order to complete the courseware questions
I want the machine learning grading to be functional
Scenario: An answer that is too short is rejected
Given I navigate to an openended question
And I enter the answer "z"
When I press the "Check" button
And I wait for "8" seconds
And I see the grader status "Submitted for grading"
And I press the "Recheck for Feedback" button
Then I see the red X
And I see the grader score "0"
# Commenting these all out right now until we can
# make a reference implementation for a course with
# an open ended grading problem that is always available
#
# Scenario: An answer that is too short is rejected
# Given I navigate to an openended question
# And I enter the answer "z"
# When I press the "Check" button
# And I wait for "8" seconds
# And I see the grader status "Submitted for grading"
# And I press the "Recheck for Feedback" button
# Then I see the red X
# And I see the grader score "0"
Scenario: An answer with too many spelling errors is rejected
Given I navigate to an openended question
And I enter the answer "az"
When I press the "Check" button
And I wait for "8" seconds
And I see the grader status "Submitted for grading"
And I press the "Recheck for Feedback" button
Then I see the red X
And I see the grader score "0"
When I click the link for full output
Then I see the spelling grading message "More spelling errors than average."
# Scenario: An answer with too many spelling errors is rejected
# Given I navigate to an openended question
# And I enter the answer "az"
# When I press the "Check" button
# And I wait for "8" seconds
# And I see the grader status "Submitted for grading"
# And I press the "Recheck for Feedback" button
# Then I see the red X
# And I see the grader score "0"
# When I click the link for full output
# Then I see the spelling grading message "More spelling errors than average."
Scenario: An answer makes its way to the instructor dashboard
Given I navigate to an openended question as staff
When I submit the answer "I love Chemistry."
And I wait for "8" seconds
And I visit the staff grading page
Then my answer is queued for instructor grading
# Scenario: An answer makes its way to the instructor dashboard
# Given I navigate to an openended question as staff
# When I submit the answer "I love Chemistry."
# And I wait for "8" seconds
# And I visit the staff grading page
# Then my answer is queued for instructor grading

View File

@@ -4,29 +4,33 @@ from nose.tools import assert_equals, assert_in
from logging import getLogger
logger = getLogger(__name__)
@step('I navigate to an openended question$')
def navigate_to_an_openended_question(step):
world.register_by_course_id('MITx/3.091x/2012_Fall')
world.log_in('robot@edx.org','test')
world.log_in('robot@edx.org', 'test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
@step('I navigate to an openended question as staff$')
def navigate_to_an_openended_question_as_staff(step):
world.register_by_course_id('MITx/3.091x/2012_Fall', True)
world.log_in('robot@edx.org','test')
world.log_in('robot@edx.org', 'test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
@step(u'I enter the answer "([^"]*)"$')
def enter_the_answer_text(step, text):
textarea_css = 'textarea'
world.browser.find_by_css(textarea_css).first.fill(text)
@step(u'I submit the answer "([^"]*)"$')
def i_submit_the_answer_text(step, text):
textarea_css = 'textarea'
@@ -34,53 +38,62 @@ def i_submit_the_answer_text(step, text):
check_css = 'input.check'
world.browser.find_by_css(check_css).click()
@step('I click the link for full output$')
def click_full_output_link(step):
link_css = 'a.full'
world.browser.find_by_css(link_css).first.click()
@step(u'I visit the staff grading page$')
def i_visit_the_staff_grading_page(step):
# course_u = '/courses/MITx/3.091x/2012_Fall'
# sg_url = '%s/staff_grading' % course_u
world.browser.click_link_by_text('Instructor')
world.browser.click_link_by_text('Staff grading')
world.browser.click_link_by_text('Staff grading')
# world.browser.visit(django_url(sg_url))
@step(u'I see the grader message "([^"]*)"$')
def see_grader_message(step, msg):
message_css = 'div.external-grader-message'
grader_msg = world.browser.find_by_css(message_css).text
assert_in(msg, grader_msg)
@step(u'I see the grader status "([^"]*)"$')
def see_the_grader_status(step, status):
status_css = 'div.grader-status'
grader_status = world.browser.find_by_css(status_css).text
assert_equals(status, grader_status)
@step('I see the red X$')
def see_the_red_x(step):
x_css = 'div.grader-status > span.incorrect'
assert world.browser.find_by_css(x_css)
@step(u'I see the grader score "([^"]*)"$')
def see_the_grader_score(step, score):
score_css = 'div.result-output > p'
score_text = world.browser.find_by_css(score_css).text
assert_equals(score_text, 'Score: %s' % score)
@step('I see the link for full output$')
def see_full_output_link(step):
link_css = 'a.full'
assert world.browser.find_by_css(link_css)
@step('I see the spelling grading message "([^"]*)"$')
def see_spelling_msg(step, msg):
spelling_css = 'div.spelling'
spelling_msg = world.browser.find_by_css(spelling_css).text
spelling_msg = world.browser.find_by_css(spelling_css).text
assert_equals('Spelling: %s' % msg, spelling_msg)
@step(u'my answer is queued for instructor grading$')
def answer_is_queued_for_instructor_grading(step):
list_css = 'ul.problem-list > li > a'

View File

@@ -0,0 +1,17 @@
Feature: Register for a course
As a registered user
In order to access my class content
I want to register for a class on the edX website
Scenario: I can register for a course
Given I am logged in
And I visit the courses page
When I register for the course numbered "6.002x"
Then I should see the course numbered "6.002x" in my dashboard
Scenario: I can unregister for a course
Given I am registered for a course
And I visit the dashboard
When I click the link with the text "Unregister"
And I press the "Unregister" button in the Unenroll dialog
Then I should see "Looks like you haven't registered for any courses yet." somewhere in the page

View File

@@ -0,0 +1,28 @@
from lettuce import world, step
@step('I register for the course numbered "([^"]*)"$')
def i_register_for_the_course(step, course):
courses_section = world.browser.find_by_css('section.courses')
course_link_css = 'article[id*="%s"] > div' % course
course_link = courses_section.find_by_css(course_link_css).first
course_link.click()
intro_section = world.browser.find_by_css('section.intro')
register_link = intro_section.find_by_css('a.register')
register_link.click()
assert world.browser.is_element_present_by_css('section.container.dashboard')
@step(u'I should see the course numbered "([^"]*)" in my dashboard$')
def i_should_see_that_course_in_my_dashboard(step, course):
course_link_css = 'section.my-courses a[href*="%s"]' % course
assert world.browser.is_element_present_by_css(course_link_css)
@step(u'I press the "([^"]*)" button in the Unenroll dialog')
def i_press_the_button_in_the_unenroll_dialog(step, value):
button_css = 'section#unenroll-modal input[value="%s"]' % value
world.browser.find_by_css(button_css).click()
assert world.browser.is_element_present_by_css('section.container.dashboard')

View File

@@ -0,0 +1,16 @@
Feature: Sign in
In order to use the edX content
As a new user
I want to signup for a student account
Scenario: Sign up from the homepage
Given I visit the homepage
When I click the link with the text "Sign Up"
And I fill in "email" on the registration form with "robot2@edx.org"
And I fill in "password" on the registration form with "test"
And I fill in "username" on the registration form with "robot2"
And I fill in "name" on the registration form with "Robot Two"
And I check the checkbox named "terms_of_service"
And I check the checkbox named "honor_code"
And I press the "Create My Account" button on the registration form
Then I should see "THANKS FOR REGISTERING!" in the dashboard banner

View File

@@ -0,0 +1,25 @@
from lettuce import world, step
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
register_form = world.browser.find_by_css('form#register_form')
form_field = register_form.find_by_name(field)
form_field.fill(value)
@step('I press the "([^"]*)" button on the registration form$')
def i_press_the_button_on_the_registration_form(step, button):
register_form = world.browser.find_by_css('form#register_form')
register_form.find_by_value(button).click()
@step('I check the checkbox named "([^"]*)"$')
def i_check_checkbox(step, checkbox):
world.browser.find_by_name(checkbox).check()
@step('I should see "([^"]*)" in the dashboard banner$')
def i_should_see_text_in_the_dashboard_banner_section(step, text):
css_selector = "section.dashboard-banner h2"
assert (text in world.browser.find_by_css(css_selector).text)

View File

@@ -23,37 +23,41 @@ Feature: There are courses on the homepage
As an acceptance test
I want to count all the chapters, sections, and tabs for each course
Scenario: Navigate through course MITx/3.091x/2012_Fall
Given I am registered for course "MITx/3.091x/2012_Fall"
And I log in
Then I verify all the content of each course
# Commenting these all out for now because they don't always run,
# they have too many prerequesites, e.g. the course exists, and
# is within the start and end dates, etc.
Scenario: Navigate through course MITx/6.002x/2012_Fall
Given I am registered for course "MITx/6.002x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course MITx/3.091x/2012_Fall
# Given I am registered for course "MITx/3.091x/2012_Fall"
# And I log in
# Then I verify all the content of each course
Scenario: Navigate through course MITx/6.00x/2012_Fall
Given I am registered for course "MITx/6.00x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course MITx/6.002x/2012_Fall
# Given I am registered for course "MITx/6.002x/2012_Fall"
# And I log in
# Then I verify all the content of each course
Scenario: Navigate through course HarvardX/PH207x/2012_Fall
Given I am registered for course "HarvardX/PH207x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course MITx/6.00x/2012_Fall
# Given I am registered for course "MITx/6.00x/2012_Fall"
# And I log in
# Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall
Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course HarvardX/PH207x/2012_Fall
# Given I am registered for course "HarvardX/PH207x/2012_Fall"
# And I log in
# Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall
Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall"
# And I log in
# Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
And I log in
Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall
# Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
# And I log in
# Then I verify all the content of each course

View File

@@ -7,6 +7,7 @@ from courses import *
from logging import getLogger
logger = getLogger(__name__)
def check_for_errors():
e = world.browser.find_by_css('.outside-app')
if len(e) > 0:
@@ -14,6 +15,7 @@ def check_for_errors():
else:
assert True
@step(u'I verify all the content of each course')
def i_verify_all_the_content_of_each_course(step):
all_possible_courses = get_courses()
@@ -34,11 +36,11 @@ def i_verify_all_the_content_of_each_course(step):
check_for_errors()
# Get the course. E.g. 'MITx/6.002x/2012_Fall'
current_course = sub('/info','', sub('.*/courses/', '', world.browser.url))
validate_course(current_course,ids)
current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url))
validate_course(current_course, ids)
world.browser.find_link_by_text('Courseware').click()
assert world.browser.is_element_present_by_id('accordion',wait_time=2)
assert world.browser.is_element_present_by_id('accordion', wait_time=2)
check_for_errors()
browse_course(current_course)
@@ -46,6 +48,7 @@ def i_verify_all_the_content_of_each_course(step):
world.browser.find_by_css('.user-link').click()
check_for_errors()
def browse_course(course_id):
## count chapters from xml and page and compare
@@ -91,7 +94,7 @@ def browse_course(course_id):
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
## sometimes the course-content takes a long time to load
assert world.browser.is_element_present_by_css('.course-content',wait_time=5)
assert world.browser.is_element_present_by_css('.course-content', wait_time=5)
## look for server error div
check_for_errors()
@@ -108,7 +111,7 @@ def browse_course(course_id):
rendered_tabs = 0
num_rendered_tabs = 0
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
#logger.debug(msg)
@@ -132,7 +135,7 @@ def browse_course(course_id):
tab_class = tabs[tab_it]['class']
if tab_children != 0:
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
num_rendered_items = len(rendered_items)
num_rendered_items = len(rendered_items)
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
#logger.debug(msg)

View File

@@ -18,15 +18,17 @@ from models import StudentModule
log = logging.getLogger("mitx.courseware")
def yield_module_descendents(module):
stack = module.get_display_items()
stack.reverse()
while len(stack) > 0:
next_module = stack.pop()
stack.extend( next_module.get_display_items() )
stack.extend(next_module.get_display_items())
yield next_module
def yield_dynamic_descriptor_descendents(descriptor, module_creator):
"""
This returns all of the descendants of a descriptor. If the descriptor
@@ -39,15 +41,15 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
return module.get_child_descriptors()
else:
return descriptor.get_children()
stack = [descriptor]
while len(stack) > 0:
next_descriptor = stack.pop()
stack.extend( get_dynamic_descriptor_children(next_descriptor) )
stack.extend(get_dynamic_descriptor_children(next_descriptor))
yield next_descriptor
def yield_problems(request, course, student):
"""
@@ -88,6 +90,7 @@ def yield_problems(request, course, student):
if isinstance(problem, CapaModule):
yield problem
def answer_distributions(request, course):
"""
Given a course_descriptor, compute frequencies of answers for each problem:
@@ -150,7 +153,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
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%
# 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(
course.id, moduledescriptor.category, moduledescriptor.location.url()):
@@ -159,20 +162,20 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
if should_grade_section:
scores = []
def create_module(descriptor):
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
return get_module(student, request, descriptor.location,
return get_module(student, request, descriptor.location,
student_module_cache, course.id)
for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module):
(correct, total) = get_score(course.id, student, module_descriptor, create_module, student_module_cache)
if correct is None and total is None:
continue
if settings.GENERATE_PROFILE_SCORES: # for debugging!
if settings.GENERATE_PROFILE_SCORES: # for debugging!
if total > 1:
correct = random.randrange(max(total - 2, 1), total + 1)
else:
@@ -208,12 +211,13 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
grade_summary['grade'] = letter_grade
grade_summary['totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging
grade_summary['totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging
if keep_raw_scores:
grade_summary['raw_scores'] = raw_scores # way to get all RAW scores out to instructor
# so grader can be double-checked
return grade_summary
def grade_for_percentage(grade_cutoffs, percentage):
"""
Returns a letter grade as defined in grading_policy (e.g. 'A' 'B' 'C' for 6.002x) or None.
@@ -225,7 +229,7 @@ def grade_for_percentage(grade_cutoffs, percentage):
"""
letter_grade = None
# Possible grades, sorted in descending order of score
descending_grades = sorted(grade_cutoffs, key=lambda x: grade_cutoffs[x], reverse=True)
for possible_grade in descending_grades:
@@ -255,13 +259,13 @@ def progress_summary(student, request, course, student_module_cache):
course: A Descriptor containing the course to grade
student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student
If the student does not have access to load the course module, this function
will return None.
"""
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
course_module = get_module(student, request,
@@ -270,30 +274,30 @@ def progress_summary(student, request, course, student_module_cache):
if not course_module:
# This student must not have access to the course.
return None
chapters = []
# Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden
hidden = chapter_module.metadata.get('hide_from_toc','false')
hidden = chapter_module.metadata.get('hide_from_toc', 'false')
if hidden.lower() == 'true':
continue
sections = []
for section_module in chapter_module.get_display_items():
# Skip if the section is hidden
hidden = section_module.metadata.get('hide_from_toc','false')
hidden = section_module.metadata.get('hide_from_toc', 'false')
if hidden.lower() == 'true':
continue
# Same for sections
graded = section_module.metadata.get('graded', False)
scores = []
module_creator = section_module.system.get_module
for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator):
course_id = course.id
(correct, total) = get_score(course_id, student, module_descriptor, module_creator, student_module_cache)
if correct is None and total is None:
@@ -339,6 +343,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
Can return None if user doesn't have access, or if something else went wrong.
cache: A StudentModuleCache
"""
if problem_descriptor.always_recalculate_grades:
problem = module_creator(problem_descriptor)
d = problem.get_score()
if d is not None:
return (d['score'], d['total'])
else:
return (None, None)
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)

View File

@@ -12,6 +12,7 @@ from django.core.management.base import BaseCommand
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.errortracker import make_error_tracker
def traverse_tree(course):
'''Load every descriptor in course. Return bool success value.'''
queue = [course]

View File

@@ -14,6 +14,7 @@ from django.core.management.base import BaseCommand
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import policy_key
def import_course(course_dir, verbose=True):
course_dir = path(course_dir)
data_dir = course_dir.dirname()
@@ -44,6 +45,7 @@ def import_course(course_dir, verbose=True):
return course
def node_metadata(node):
# make a copy
to_export = ('format', 'display_name',
@@ -55,6 +57,7 @@ def node_metadata(node):
d = {k: orig[k] for k in to_export if k in orig}
return d
def get_metadata(course):
d = OrderedDict({})
queue = [course]

View File

@@ -114,4 +114,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['courseware']
complete_apps = ['courseware']

View File

@@ -13,14 +13,8 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
"""
from django.db import models
#from django.core.cache import cache
from django.contrib.auth.models import User
#from cache_toolbox import cache_model, cache_relation
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
class StudentModule(models.Model):
"""
Keeps student state for a particular module in a particular course.
@@ -30,6 +24,7 @@ class StudentModule(models.Model):
MODULE_TYPES = (('problem', 'problem'),
('video', 'video'),
('html', 'html'),
('timelimit', 'timelimit'),
)
## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
@@ -113,6 +108,9 @@ class StudentModuleCache(object):
descriptor_filter=lambda descriptor: True,
select_for_update=False):
"""
obtain and return cache for descriptor descendents (ie children) AND modules required by the descriptor,
but which are not children of the module
course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules.
descriptor: An XModuleDescriptor
@@ -132,7 +130,7 @@ class StudentModuleCache(object):
if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children():
for child in descriptor.get_children() + descriptor.get_required_module_descriptors():
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
return descriptors
@@ -209,7 +207,7 @@ class OfflineComputedGradeLog(models.Model):
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
seconds = models.IntegerField(default=0) # seconds elapsed for computation
seconds = models.IntegerField(default=0) # seconds elapsed for computation
nstudents = models.IntegerField(default=0)
def __unicode__(self):

View File

@@ -2,6 +2,9 @@ import json
import logging
import pyparsing
import sys
import static_replace
from functools import partial
from django.conf import settings
from django.contrib.auth.models import User
@@ -18,7 +21,6 @@ from courseware.access import has_access
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from static_replace import replace_urls
from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError
@@ -31,7 +33,7 @@ from xmodule_modifiers import replace_course_urls, replace_static_urls, add_hist
from xmodule.modulestore.exceptions import ItemNotFoundError
from statsd import statsd
log = logging.getLogger("mitx.courseware")
log = logging.getLogger(__name__)
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
@@ -89,7 +91,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list()
for chapter in course_module.get_display_items():
hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
hide_from_toc = chapter.metadata.get('hide_from_toc', 'false').lower() == 'true'
if hide_from_toc:
continue
@@ -164,6 +166,7 @@ def get_module_for_descriptor(user, request, descriptor, student_module_cache, c
return _get_module(user, request, descriptor, student_module_cache, course_id,
position=position, wrap_xmodule_display=wrap_xmodule_display)
def _get_module(user, request, descriptor, student_module_cache, course_id,
position=None, wrap_xmodule_display=True):
"""
@@ -223,6 +226,30 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
}
def get_or_default(key, default):
getattr(settings, key, default)
#This is a hacky way to pass settings to the combined open ended xmodule
#It needs an S3 interface to upload images to S3
#It needs the open ended grading interface in order to get peer grading to be done
#TODO: refactor these settings into module-specific settings when possible.
#this first checks to see if the descriptor is the correct one, and only sends settings if it is
is_descriptor_combined_open_ended = (descriptor.__class__.__name__ == 'CombinedOpenEndedDescriptor')
is_descriptor_peer_grading = (descriptor.__class__.__name__ == 'PeerGradingDescriptor')
open_ended_grading_interface = None
s3_interface = None
if is_descriptor_combined_open_ended or is_descriptor_peer_grading:
open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE
open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING
open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING
if is_descriptor_combined_open_ended:
s3_interface = {
'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''),
'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''),
'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','')
}
def inner_get_module(descriptor):
"""
Delegate to get_module. It does an access check, so may return None
@@ -244,10 +271,16 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
# TODO (cpennington): This should be removed when all html from
# a module is coming through get_html and is therefore covered
# by the replace_static_urls code below
replace_urls=replace_urls,
replace_urls=partial(
static_replace.replace_static_urls,
data_directory=descriptor.metadata.get('data_dir', ''),
course_namespace=descriptor.location._replace(category=None, name=None),
),
node_path=settings.NODE_PATH,
anonymous_student_id=unique_id_for_user(user),
course_id=course_id,
open_ended_grading_interface=open_ended_grading_interface,
s3_interface=s3_interface,
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)
@@ -273,6 +306,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
# Make an error module
return err_descriptor.xmodule_constructor(system)(None, None)
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id))
_get_html = module.get_html
if wrap_xmodule_display == True:
@@ -280,8 +314,8 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
module.get_html = replace_static_urls(
_get_html,
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
course_namespace = module.location._replace(category=None, name=None))
module.metadata.get('data_dir', ''),
course_namespace=module.location._replace(category=None, name=None))
# Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course
@@ -294,6 +328,8 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
return module
# TODO (vshnayder): Rename this? It's very confusing.
def get_instance_module(course_id, user, module, student_module_cache):
"""
Returns the StudentModule specific to this module for this student,
@@ -323,6 +359,7 @@ def get_instance_module(course_id, user, module, student_module_cache):
else:
return None
def get_shared_instance_module(course_id, user, module, student_module_cache):
"""
Return shared_module is a StudentModule specific to all modules with the same
@@ -353,6 +390,7 @@ def get_shared_instance_module(course_id, user, module, student_module_cache):
else:
return None
@csrf_exempt
def xqueue_callback(request, course_id, userid, id, dispatch):
'''
@@ -409,8 +447,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
instance_module.save()
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
score_bucket = get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run = course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
@@ -450,9 +488,9 @@ def modx_dispatch(request, dispatch, location, course_id):
return HttpResponse(json.dumps({'success': too_many_files_msg}))
for inputfile in inputfiles:
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2))
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))
return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles
@@ -493,7 +531,7 @@ def modx_dispatch(request, dispatch, location, course_id):
# Don't track state for anonymous users (who don't have student modules)
if instance_module is not None:
instance_module.state = instance.get_instance_state()
instance_module.max_grade=instance.max_score()
instance_module.max_grade = instance.max_score()
if instance.get_score():
instance_module.grade = instance.get_score()['score']
if (instance_module.grade != oldgrade or
@@ -502,8 +540,8 @@ def modx_dispatch(request, dispatch, location, course_id):
instance_module.save()
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
score_bucket = get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run = course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
@@ -520,6 +558,7 @@ def modx_dispatch(request, dispatch, location, course_id):
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def preview_chemcalc(request):
"""
Render an html preview of a chemical formula or equation. The fact that
@@ -538,7 +577,7 @@ def preview_chemcalc(request):
raise Http404
result = {'preview': '',
'error': '' }
'error': ''}
formula = request.GET.get('formula')
if formula is None:
result['error'] = "No formula specified."
@@ -557,17 +596,15 @@ def preview_chemcalc(request):
return HttpResponse(json.dumps(result))
def get_score_bucket(grade,max_grade):
def get_score_bucket(grade, max_grade):
"""
Function to split arbitrary score ranges into 3 buckets.
Used with statsd tracking.
"""
score_bucket="incorrect"
if(grade>0 and grade<max_grade):
score_bucket="partial"
elif(grade==max_grade):
score_bucket="correct"
score_bucket = "incorrect"
if(grade > 0 and grade < max_grade):
score_bucket = "partial"
elif(grade == max_grade):
score_bucket = "correct"
return score_bucket

View File

@@ -18,24 +18,22 @@ from django.core.urlresolvers import reverse
from fs.errors import ResourceNotFoundError
from lxml.html import rewrite_links
from courseware.access import has_access
from lxml.html import rewrite_links
from module_render import get_module
from courseware.access import has_access
from static_replace import replace_urls
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import XModule
from open_ended_grading.peer_grading_service import PeerGradingService
from open_ended_grading.staff_grading_service import StaffGradingService
from student.models import unique_id_for_user
from open_ended_grading import open_ended_notifications
log = logging.getLogger(__name__)
class InvalidTabsException(Exception):
"""
A complaint about invalid tabs.
@@ -44,6 +42,7 @@ class InvalidTabsException(Exception):
CourseTabBase = namedtuple('CourseTab', 'name link is_active has_img img')
def CourseTab(name, link, is_active, has_img=False, img=""):
return CourseTabBase(name, link, is_active, has_img, img)
@@ -67,22 +66,26 @@ def _courseware(tab, user, course, active_page):
link = reverse('courseware', args=[course.id])
return [CourseTab('Courseware', link, active_page == "courseware")]
def _course_info(tab, user, course, active_page):
link = reverse('info', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "info")]
def _progress(tab, user, course, active_page):
if user.is_authenticated():
link = reverse('progress', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "progress")]
return []
def _wiki(tab, user, course, active_page):
if settings.WIKI_ENABLED:
link = reverse('course_wiki', args=[course.id])
return [CourseTab(tab['name'], link, active_page == 'wiki')]
return []
def _discussion(tab, user, course, active_page):
"""
This tab format only supports the new Berkeley discussion forums.
@@ -90,17 +93,19 @@ def _discussion(tab, user, course, active_page):
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
link = reverse('django_comment_client.forum.views.forum_form_discussion',
args=[course.id])
return [CourseTab(tab['name'], link, active_page=='discussion')]
return [CourseTab(tab['name'], link, active_page == 'discussion')]
return []
def _external_link(tab, user, course, active_page):
# external links are never active
return [CourseTab(tab['name'], tab['link'], False)]
def _static_tab(tab, user, course, active_page):
link = reverse('static_tab', args=[course.id, tab['url_slug']])
active_str = 'static_tab_{0}'.format(tab['url_slug'])
return [CourseTab(tab['name'], link, active_page==active_str)]
return [CourseTab(tab['name'], link, active_page == active_str)]
def _textbooks(tab, user, course, active_page):
@@ -110,57 +115,65 @@ def _textbooks(tab, user, course, active_page):
if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
# since there can be more than one textbook, active_page is e.g. "book/0".
return [CourseTab(textbook.title, reverse('book', args=[course.id, index]),
active_page=="textbook/{0}".format(index))
active_page == "textbook/{0}".format(index))
for index, textbook in enumerate(course.textbooks)]
return []
def _pdf_textbooks(tab, user, course, active_page):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
if user.is_authenticated():
# since there can be more than one textbook, active_page is e.g. "book/0".
return [CourseTab(textbook['tab_title'], reverse('pdf_book', args=[course.id, index]),
active_page == "pdftextbook/{0}".format(index))
for index, textbook in enumerate(course.pdf_textbooks)]
return []
def _staff_grading(tab, user, course, active_page):
if has_access(user, course, 'staff'):
link = reverse('staff_grading', args=[course.id])
staff_gs = StaffGradingService(settings.STAFF_GRADING_INTERFACE)
pending_grading=False
tab_name = "Staff grading"
img_path= ""
try:
notifications = json.loads(staff_gs.get_notifications(course.id))
if notifications['success']:
if notifications['staff_needs_to_grade']:
pending_grading=True
except:
#Non catastrophic error, so no real action
log.info("Problem with getting notifications from staff grading service.")
if pending_grading:
img_path = "/static/images/slider-handle.png"
tab_name = "Staff grading"
notifications = open_ended_notifications.staff_grading_notifications(course, user)
pending_grading = notifications['pending_grading']
img_path = notifications['img_path']
tab = [CourseTab(tab_name, link, active_page == "staff_grading", pending_grading, img_path)]
return tab
return []
def _peer_grading(tab, user, course, active_page):
if user.is_authenticated():
link = reverse('peer_grading', args=[course.id])
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
pending_grading=False
tab_name = "Peer grading"
img_path= ""
try:
notifications = json.loads(peer_gs.get_notifications(course.id,unique_id_for_user(user)))
if notifications['success']:
if notifications['student_needs_to_peer_grade']:
pending_grading=True
except:
#Non catastrophic error, so no real action
log.info("Problem with getting notifications from peer grading service.")
if pending_grading:
img_path = "/static/images/slider-handle.png"
notifications = open_ended_notifications.peer_grading_notifications(course, user)
pending_grading = notifications['pending_grading']
img_path = notifications['img_path']
tab = [CourseTab(tab_name, link, active_page == "peer_grading", pending_grading, img_path)]
return tab
return []
def _combined_open_ended_grading(tab, user, course, active_page):
if user.is_authenticated():
link = reverse('open_ended_notifications', args=[course.id])
tab_name = "Open Ended Panel"
notifications = open_ended_notifications.combined_notifications(course, user)
pending_grading = notifications['pending_grading']
img_path = notifications['img_path']
tab = [CourseTab(tab_name, link, active_page == "open_ended", pending_grading, img_path)]
return tab
return []
#### Validators
@@ -178,6 +191,7 @@ def key_checker(expected_keys):
need_name = key_checker(['name'])
def null_validator(d):
"""
Don't check anything--use for tabs that don't need any params. (e.g. textbook)
@@ -194,10 +208,12 @@ VALID_TAB_TYPES = {
'discussion': TabImpl(need_name, _discussion),
'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
'textbooks': TabImpl(null_validator, _textbooks),
'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
'progress': TabImpl(need_name, _progress),
'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
'peer_grading': TabImpl(null_validator, _peer_grading),
'staff_grading': TabImpl(null_validator, _staff_grading),
'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
}
@@ -241,7 +257,7 @@ def get_course_tabs(user, course, active_page):
"""
Return the tabs to show a particular user, as a list of CourseTab items.
"""
if not hasattr(course,'tabs') or not course.tabs:
if not hasattr(course, 'tabs') or not course.tabs:
return get_default_tabs(user, course, active_page)
# TODO (vshnayder): There needs to be a place to call this right after course
@@ -275,7 +291,7 @@ def get_default_tabs(user, course, active_page):
if hasattr(course, 'syllabus_present') and course.syllabus_present:
link = reverse('syllabus', args=[course.id])
tabs.append(CourseTab('Syllabus', link, active_page=='syllabus'))
tabs.append(CourseTab('Syllabus', link, active_page == 'syllabus'))
tabs.extend(_textbooks({}, user, course, active_page))
@@ -296,10 +312,11 @@ def get_default_tabs(user, course, active_page):
if has_access(user, course, 'staff'):
link = reverse('instructor_dashboard', args=[course.id])
tabs.append(CourseTab('Instructor', link, active_page=='instructor'))
tabs.append(CourseTab('Instructor', link, active_page == 'instructor'))
return tabs
def get_static_tab_by_slug(course, tab_slug):
"""
Look for a tab with type 'static_tab' and the specified 'tab_slug'. Returns
@@ -314,6 +331,7 @@ def get_static_tab_by_slug(course, tab_slug):
return None
def get_static_tab_contents(request, cache, course, tab):
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])

View File

@@ -1 +0,0 @@

View File

@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
from datetime import datetime
import uuid
class UserProfileFactory(factory.Factory):
FACTORY_FOR = UserProfile
@@ -12,12 +13,14 @@ class UserProfileFactory(factory.Factory):
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(factory.Factory):
FACTORY_FOR = Registration
user = None
activation_key = uuid.uuid4().hex
class UserFactory(factory.Factory):
FACTORY_FOR = User
@@ -32,11 +35,13 @@ class UserFactory(factory.Factory):
last_login = datetime.now()
date_joined = datetime.now()
class GroupFactory(factory.Factory):
FACTORY_FOR = Group
name = 'test_group'
class CourseEnrollmentAllowedFactory(factory.Factory):
FACTORY_FOR = CourseEnrollmentAllowed

View File

@@ -15,6 +15,7 @@ import courseware.access as access
from factories import CourseEnrollmentAllowedFactory
class AccessTestCase(TestCase):
def test__has_global_staff_access(self):
u = Mock(is_staff=False)
@@ -52,13 +53,13 @@ class AccessTestCase(TestCase):
self.assertTrue(access._has_access_to_location(u, location,
'instructor', None))
# A user does not have staff access if they are
# A user does not have staff access if they are
# not in either the staff or the the instructor group
g.name = 'student_only'
self.assertFalse(access._has_access_to_location(u, location,
'staff', None))
# A user does not have instructor access if they are
# A user does not have instructor access if they are
# not in the instructor group
g.name = 'student_only'
self.assertFalse(access._has_access_to_location(u, location,
@@ -77,7 +78,7 @@ class AccessTestCase(TestCase):
# TODO: override DISABLE_START_DATES and test the start date branch of the method
u = Mock()
d = Mock()
d.start = time.gmtime(time.time() - 86400) # make sure the start time is in the past
d.start = time.gmtime(time.time() - 86400) # make sure the start time is in the past
# Always returns true because DISABLE_START_DATES is set in test.py
self.assertTrue(access._has_access_descriptor(u, d, 'load'))
@@ -113,5 +114,5 @@ class AccessTestCase(TestCase):
c.metadata.get = 'is_public'
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
# TODO:
# TODO:
# Non-staff cannot enroll outside the open enrollment period if not specifically allowed

View File

@@ -11,7 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
from override_settings import override_settings
from django.test.utils import override_settings
import xmodule.modulestore.django
from xmodule.modulestore.mongo import MongoModuleStore
@@ -31,6 +31,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.timeparse import stringify_time
def parse_json(response):
"""Parse response, which is assumed to be json"""
return json.loads(response.content)
@@ -49,6 +50,7 @@ def registration(email):
# jump_to works with the xmlmodulestore or we have an even better solution
# NOTE: this means this test requires mongo to be running.
def mongo_store_config(data_dir):
return {
'default': {
@@ -64,6 +66,7 @@ def mongo_store_config(data_dir):
}
}
def draft_mongo_store_config(data_dir):
return {
'default': {
@@ -79,6 +82,7 @@ def draft_mongo_store_config(data_dir):
}
}
def xml_store_config(data_dir):
return {
'default': {
@@ -95,6 +99,7 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
class ActivateLoginTestCase(TestCase):
'''Check that we can activate and log in'''
@@ -286,13 +291,13 @@ class PageLoader(ActivateLoginTestCase):
all_ok = False
num_bad += 1
elif descriptor.location.category == 'static_tab':
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug' : descriptor.location.name}))
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug': descriptor.location.name}))
msg = str(resp.status_code)
if resp.status_code != 200:
msg = "ERROR " + msg
all_ok = False
num_bad += 1
num_bad += 1
elif descriptor.location.category == 'course_info':
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
msg = str(resp.status_code)
@@ -300,7 +305,7 @@ class PageLoader(ActivateLoginTestCase):
if resp.status_code != 200:
msg = "ERROR " + msg
all_ok = False
num_bad += 1
num_bad += 1
elif descriptor.location.category == 'custom_tag_template':
pass
else:
@@ -321,7 +326,7 @@ class PageLoader(ActivateLoginTestCase):
# check content to make sure there were no rendering failures
content = resp.content
if content.find("this module is temporarily unavailable")>=0:
if content.find("this module is temporarily unavailable") >= 0:
msg = "ERROR unavailable module "
all_ok = False
num_bad += 1
@@ -335,7 +340,7 @@ class PageLoader(ActivateLoginTestCase):
self.assertTrue(all_ok) # fail fast
print "{0}/{1} good".format(n - num_bad, n)
log.info( "{0}/{1} good".format(n - num_bad, n))
log.info("{0}/{1} good".format(n - num_bad, n))
self.assertTrue(all_ok)
@@ -347,7 +352,7 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoader):
def setUp(self):
ActivateLoginTestCase.setUp(self)
xmodule.modulestore.django._MODULESTORES = {}
def test_toy_course_loads(self):
module_store = XMLModuleStore(
TEST_DATA_DIR,
@@ -376,7 +381,7 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoader):
ActivateLoginTestCase.setUp(self)
xmodule.modulestore.django._MODULESTORES = {}
modulestore().collection.drop()
def test_toy_course_loads(self):
module_store = modulestore()
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
@@ -431,7 +436,7 @@ class TestNavigation(PageLoader):
# Now we directly navigate to a section in a different chapter
self.check_for_get_code(200, reverse('courseware_section',
kwargs={'course_id': self.toy.id,
'chapter':'secret:magic', 'section':'toyvideo'}))
'chapter': 'secret:magic', 'section': 'toyvideo'}))
# And now hitting the courseware tab should redirect to 'secret:magic'
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
@@ -565,7 +570,7 @@ class TestViewAuth(PageLoader):
"""Actually do the test, relying on settings to be right."""
# Make courses start in the future
tomorrow = time.time() + 24*3600
tomorrow = time.time() + 24 * 3600
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
@@ -603,7 +608,7 @@ class TestViewAuth(PageLoader):
def instructor_urls(course):
"""list of urls that only instructors/staff should be able to see"""
urls = reverse_urls(['instructor_dashboard','gradebook','grade_summary'],
urls = reverse_urls(['instructor_dashboard', 'gradebook', 'grade_summary'],
course)
return urls
@@ -770,7 +775,7 @@ class TestCourseGrader(PageLoader):
def find_course(course_id):
"""Assumes the course is present"""
return [c for c in courses if c.id==course_id][0]
return [c for c in courses if c.id == course_id][0]
self.graded_course = find_course("edX/graded/2012_Fall")
@@ -825,17 +830,17 @@ class TestCourseGrader(PageLoader):
modx_url = reverse('modx_dispatch',
kwargs={
'course_id' : self.graded_course.id,
'location' : problem_location,
'dispatch' : 'problem_check', }
'course_id': self.graded_course.id,
'location': problem_location,
'dispatch': 'problem_check', }
)
resp = self.client.post(modx_url, {
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
})
print "modx_url" , modx_url, "responses" , responses
print "resp" , resp
print "modx_url", modx_url, "responses", responses
print "resp", resp
return resp
@@ -847,9 +852,9 @@ class TestCourseGrader(PageLoader):
modx_url = reverse('modx_dispatch',
kwargs={
'course_id' : self.graded_course.id,
'location' : problem_location,
'dispatch' : 'problem_reset', }
'course_id': self.graded_course.id,
'location': problem_location,
'dispatch': 'problem_reset', }
)
resp = self.client.post(modx_url)
@@ -873,7 +878,7 @@ class TestCourseGrader(PageLoader):
# Only get half of the first problem correct
self.submit_question_answer('H1P1', ['Correct', 'Incorrect'])
self.check_grade_percent(0.06)
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0])
# Get both parts of the first problem correct
@@ -905,14 +910,13 @@ class TestCourseGrader(PageLoader):
# Third homework
self.submit_question_answer('H3P1', ['Correct', 'Correct'])
self.check_grade_percent(0.42) # Score didn't change
self.check_grade_percent(0.42) # Score didn't change
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
self.submit_question_answer('H3P2', ['Correct', 'Correct'])
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
# Now we answer the final question (worth half of the grade)
self.submit_question_answer('FinalQuestion', ['Correct', 'Correct'])
self.check_grade_percent(1.0) # Hooray! We got 100%
self.check_grade_percent(1.0) # Hooray! We got 100%

View File

@@ -8,7 +8,7 @@ from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
@@ -20,7 +20,7 @@ from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs
from courseware.models import StudentModuleCache
from courseware.models import StudentModule, StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
from django_comment_client.utils import get_discussion_title
@@ -86,7 +86,8 @@ def render_accordion(request, course, chapter, section):
Returns the html string'''
# grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
toc = toc_for_course(user, request, course, chapter, section)
context = dict([('toc', toc),
('course_id', course.id),
@@ -137,6 +138,7 @@ def redirect_to_course_position(course_module, first_time):
'chapter': chapter.url_name,
'section': section.url_name}))
def save_child_position(seq_module, child_name, instance_module):
"""
child_name: url_name of the child
@@ -152,6 +154,76 @@ def save_child_position(seq_module, child_name, instance_module):
instance_module.state = seq_module.get_instance_state()
instance_module.save()
def check_for_active_timelimit_module(request, course_id, course):
'''
Looks for a timing module for the given user and course that is currently active.
If found, returns a context dict with timer-related values to enable display of time remaining.
'''
context = {}
timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
if timelimit_student_modules:
for timelimit_student_module in timelimit_student_modules:
# get the corresponding section_descriptor for the given StudentModel entry:
module_state_key = timelimit_student_module.module_state_key
timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course.id, position=None)
if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
timelimit_module.has_begun and not timelimit_module.has_ended:
location = timelimit_module.location
# determine where to go when the timer expires:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
# Fetch the remaining time relative to the end time as stored in the module when it was started.
# This value should be in milliseconds.
remaining_time = timelimit_module.get_remaining_time_in_ms()
context['timer_expiration_duration'] = remaining_time
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location})
context['timer_navigation_return_url'] = return_url
return context
def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module):
'''
Updates the state of the provided timing module, starting it if it hasn't begun.
Returns dict with timer-related values to enable display of time remaining.
Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired.
'''
context = {}
# determine where to go when the exam ends:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', timelimit_module.location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
if not timelimit_module.has_ended:
if not timelimit_module.has_begun:
# user has not started the exam, so start it now.
if 'duration' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('duration', timelimit_module.location))
# The user may have an accommodation that has been granted to them.
# This accommodation information should already be stored in the module's state.
duration = int(timelimit_descriptor.metadata.get('duration'))
timelimit_module.begin(duration)
# we have changed state, so we need to persist the change:
instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache)
instance_module.state = timelimit_module.get_instance_state()
instance_module.save()
# the exam has been started, either because the student is returning to the
# exam page, or because they have just visited it. Fetch the remaining time relative to the
# end time as stored in the module when it was started.
context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms()
# also use the timed module to determine whether top-level navigation is visible:
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return context
@login_required
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@@ -179,23 +251,24 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
"""
course = get_course_with_access(request.user, course_id, 'load', depth=2)
staff_access = has_access(request.user, course, 'staff')
registered = registered_for_course(course, request.user)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
course = get_course_with_access(user, course_id, 'load', depth=2)
staff_access = has_access(user, course, 'staff')
registered = registered_for_course(course, user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url()))
return redirect(reverse('about_course', args=[course.id]))
try:
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
course.id, user, course, depth=2)
# Has this student been in this course before?
first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
# Load the module for the course
course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
course_module = get_module_for_descriptor(user, request, course, student_module_cache, course.id)
if course_module is None:
log.warning('If you see this, something went wrong: if we got this'
' far, should have gotten a course module for this user')
@@ -212,15 +285,15 @@ def index(request, course_id, chapter=None, section=None,
'init': '',
'content': '',
'staff_access': staff_access,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None:
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
instance_module = get_instance_module(course_id, user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module)
else:
raise Http404
raise Http404('No chapter descriptor found with name {}'.format(chapter))
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
if chapter_module is None:
@@ -236,9 +309,9 @@ def index(request, course_id, chapter=None, section=None,
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
section_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, section_descriptor, depth=None)
course.id, user, section_descriptor, depth=None)
section_module = get_module(request.user, request, section_descriptor.location,
section_module = get_module(user, request, section_descriptor.location,
section_module_cache, course.id, position=position, depth=None)
if section_module is None:
# User may be trying to be clever and access something
@@ -246,10 +319,23 @@ def index(request, course_id, chapter=None, section=None,
raise Http404
# Save where we are in the chapter
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
instance_module = get_instance_module(course_id, user, chapter_module, student_module_cache)
save_child_position(chapter_module, section, instance_module)
# check here if this section *is* a timed module.
if section_module.category == 'timelimit':
timer_context = update_timelimit_module(user, course_id, student_module_cache,
section_descriptor, section_module)
if 'timer_expiration_duration' in timer_context:
context.update(timer_context)
else:
# if there is no expiration defined, then we know the timer has expired:
return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
else:
# check here if this page is within a course that has an active timed module running. If so, then
# add in the appropriate timer information to the rendering context:
context.update(check_for_active_timelimit_module(request, course_id, course))
context['content'] = section_module.get_html()
else:
# section is none, so display a message
@@ -279,7 +365,7 @@ def index(request, course_id, chapter=None, section=None,
log.exception("Error in index view: user={user}, course={course},"
" chapter={chapter} section={section}"
"position={position}".format(
user=request.user,
user=user,
course=course,
chapter=chapter,
section=section,
@@ -288,7 +374,7 @@ def index(request, course_id, chapter=None, section=None,
try:
result = render_to_response('courseware/courseware-error.html',
{'staff_access': staff_access,
'course' : course})
'course': course})
except:
# Let the exception propagate, relying on global config to at
# at least return a nice error message
@@ -297,6 +383,7 @@ def index(request, course_id, chapter=None, section=None,
return result
@ensure_csrf_cookie
def jump_to(request, course_id, location):
'''
@@ -333,6 +420,7 @@ def jump_to(request, course_id, location):
else:
return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position)
@ensure_csrf_cookie
def course_info(request, course_id):
"""
@@ -343,9 +431,10 @@ def course_info(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
return render_to_response('courseware/info.html', {'request' : request, 'course_id' : course_id, 'cache' : None,
return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None,
'course': course, 'staff_access': staff_access})
@ensure_csrf_cookie
def static_tab(request, course_id, tab_slug):
"""
@@ -368,9 +457,11 @@ def static_tab(request, course_id, tab_slug):
{'course': course,
'tab': tab,
'tab_contents': contents,
'staff_access': staff_access,})
'staff_access': staff_access, })
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
@ensure_csrf_cookie
def syllabus(request, course_id):
"""
@@ -382,7 +473,7 @@ def syllabus(request, course_id):
staff_access = has_access(request.user, course, 'staff')
return render_to_response('courseware/syllabus.html', {'course': course,
'staff_access': staff_access,})
'staff_access': staff_access, })
def registered_for_course(course, user):
@@ -394,6 +485,7 @@ def registered_for_course(course, user):
else:
return False
@ensure_csrf_cookie
@cache_if_anonymous
def course_about(request, course_id):
@@ -412,7 +504,7 @@ def course_about(request, course_id):
{'course': course,
'registered': registered,
'course_target': course_target,
'show_courseware_link' : show_courseware_link})
'show_courseware_link': show_courseware_link})
@ensure_csrf_cookie
@@ -425,6 +517,7 @@ def static_university_profile(request, org_id):
context = dict(courses=[], org_id=org_id)
return render_to_response(template_file, context)
@ensure_csrf_cookie
@cache_if_anonymous
def university_profile(request, org_id):
@@ -446,6 +539,7 @@ def university_profile(request, org_id):
return render_to_response(template_file, context)
def render_notifications(request, course, notifications):
context = {
'notifications': notifications,
@@ -454,6 +548,7 @@ def render_notifications(request, course, notifications):
}
return render_to_string('courseware/notifications.html', context)
@login_required
def news(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
@@ -467,6 +562,7 @@ def news(request, course_id):
return render_to_response('courseware/news.html', context)
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def progress(request, course_id, student_id=None):