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:
@@ -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)
|
||||
|
||||
|
||||
@@ -11,4 +11,3 @@ admin.site.register(StudentModule)
|
||||
admin.site.register(OfflineComputedGrade)
|
||||
|
||||
admin.site.register(OfflineComputedGradeLog)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
99
lms/djangoapps/courseware/features/common.py
Normal file
99
lms/djangoapps/courseware/features/common.py
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
47
lms/djangoapps/courseware/features/homepage.feature
Normal file
47
lms/djangoapps/courseware/features/homepage.feature
Normal 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
|
||||
9
lms/djangoapps/courseware/features/homepage.py
Normal file
9
lms/djangoapps/courseware/features/homepage.py
Normal 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)
|
||||
27
lms/djangoapps/courseware/features/login.feature
Normal file
27
lms/djangoapps/courseware/features/login.feature
Normal 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 "/"
|
||||
52
lms/djangoapps/courseware/features/login.py
Normal file
52
lms/djangoapps/courseware/features/login.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
17
lms/djangoapps/courseware/features/registration.feature
Normal file
17
lms/djangoapps/courseware/features/registration.feature
Normal 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
|
||||
28
lms/djangoapps/courseware/features/registration.py
Normal file
28
lms/djangoapps/courseware/features/registration.py
Normal 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')
|
||||
16
lms/djangoapps/courseware/features/signup.feature
Normal file
16
lms/djangoapps/courseware/features/signup.feature
Normal 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
|
||||
25
lms/djangoapps/courseware/features/signup.py
Normal file
25
lms/djangoapps/courseware/features/signup.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -114,4 +114,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
complete_apps = ['courseware']
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user