Merge pull request #2552 from edx/giulio/edit-in-cms-links
Add "View in Studio" links throughout LMS
This commit is contained in:
@@ -17,6 +17,8 @@ from xmodule.seq_module import SequenceModule
|
||||
from xmodule.vertical_module import VerticalModule
|
||||
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -152,17 +154,33 @@ def grade_histogram(module_id):
|
||||
return grades
|
||||
|
||||
|
||||
def add_staff_debug_info(user, block, view, frag, context): # pylint: disable=unused-argument
|
||||
def add_staff_markup(user, block, view, frag, context): # pylint: disable=unused-argument
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the output of the old get_html function with additional information
|
||||
for admin users only, including a histogram of student answers and the
|
||||
definition of the xmodule
|
||||
for admin users only, including a histogram of student answers, the
|
||||
definition of the xmodule, and a link to view the module in Studio
|
||||
if it is a Studio edited, mongo stored course.
|
||||
|
||||
Does nothing if module is a SequenceModule or a VerticalModule.
|
||||
Does nothing if module is a SequenceModule.
|
||||
"""
|
||||
# TODO: make this more general, eg use an XModule attribute instead
|
||||
if isinstance(block, (SequenceModule, VerticalModule)):
|
||||
if isinstance(block, VerticalModule):
|
||||
# check that the course is a mongo backed Studio course before doing work
|
||||
is_mongo_course = modulestore().get_modulestore_type(block.course_id) == MONGO_MODULESTORE_TYPE
|
||||
is_studio_course = block.course_edit_method == "Studio"
|
||||
|
||||
if is_studio_course and is_mongo_course:
|
||||
# get relative url/location of unit in Studio
|
||||
locator = loc_mapper().translate_location(block.course_id, block.location, False, True)
|
||||
# build edit link to unit in CMS
|
||||
edit_link = "//" + settings.CMS_BASE + locator.url_reverse('unit', '')
|
||||
# return edit link in rendered HTML for display
|
||||
return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link}))
|
||||
else:
|
||||
return frag
|
||||
|
||||
if isinstance(block, SequenceModule):
|
||||
return frag
|
||||
|
||||
block_id = block.id
|
||||
|
||||
@@ -224,6 +224,7 @@ class CourseFields(object):
|
||||
scope=Scope.content)
|
||||
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
|
||||
display_name = String(help="Display name for this module", default="Empty", display_name="Display Name", scope=Scope.settings)
|
||||
course_edit_method = String(help="Method with which this course is edited.", default="Studio", scope=Scope.settings)
|
||||
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
|
||||
tabs = CourseTabList(help="List of tabs to enable in this course", scope=Scope.settings, default=[])
|
||||
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
|
||||
|
||||
@@ -36,6 +36,10 @@ class InheritanceMixin(XBlockMixin):
|
||||
default=None,
|
||||
scope=Scope.user_state,
|
||||
)
|
||||
course_edit_method = String(
|
||||
help="Method with which this course is edited.",
|
||||
default="Studio", scope=Scope.settings
|
||||
)
|
||||
giturl = String(
|
||||
help="url root for course data git repository",
|
||||
scope=Scope.settings,
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.conf import settings
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location, XML_MODULESTORE_TYPE
|
||||
from xmodule.modulestore import Location, XML_MODULESTORE_TYPE, MONGO_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
@@ -341,3 +341,27 @@ def get_cms_course_link(course):
|
||||
course.location.course_id, course.location, False, True
|
||||
)
|
||||
return "//" + settings.CMS_BASE + locator.url_reverse('course/', '')
|
||||
|
||||
|
||||
def get_cms_block_link(block, page):
|
||||
"""
|
||||
Returns a link to block_index for editing the course in cms,
|
||||
assuming that the block is actually cms-backed.
|
||||
"""
|
||||
locator = loc_mapper().translate_location(
|
||||
block.location.course_id, block.location, False, True
|
||||
)
|
||||
return "//" + settings.CMS_BASE + locator.url_reverse(page, '')
|
||||
|
||||
|
||||
def get_studio_url(course_id, page):
|
||||
"""
|
||||
Get the Studio URL of the page that is passed in.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
is_studio_course = course.course_edit_method == "Studio"
|
||||
is_mongo_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE
|
||||
studio_link = None
|
||||
if is_studio_course and is_mongo_course:
|
||||
studio_link = get_cms_block_link(course, page)
|
||||
return studio_link
|
||||
|
||||
@@ -37,7 +37,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore, ModuleI18nService
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.util.duedate import get_extended_due_date
|
||||
from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, replace_static_urls, add_staff_debug_info, wrap_xblock
|
||||
from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, replace_static_urls, add_staff_markup, wrap_xblock
|
||||
from xmodule.lti_module import LTIModule
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
@@ -371,7 +371,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
|
||||
|
||||
if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
|
||||
if has_access(user, descriptor, 'staff', course_id):
|
||||
block_wrappers.append(partial(add_staff_debug_info, user))
|
||||
block_wrappers.append(partial(add_staff_markup, user))
|
||||
|
||||
# These modules store data using the anonymous_student_id as a key.
|
||||
# To prevent loss of data, we will continue to provide old modules with
|
||||
|
||||
@@ -14,8 +14,13 @@ from xmodule.tests.xml import factories as xml
|
||||
from xmodule.tests.xml import XModuleXmlImportTest
|
||||
|
||||
from courseware.courses import (
|
||||
get_course_by_id, get_course, get_cms_course_link, course_image_url,
|
||||
get_course_info_section, get_course_about_section
|
||||
get_course_by_id,
|
||||
get_course,
|
||||
get_cms_course_link,
|
||||
get_cms_block_link,
|
||||
course_image_url,
|
||||
get_course_info_section,
|
||||
get_course_about_section
|
||||
)
|
||||
from courseware.tests.helpers import get_request_for_user
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_MIXED_MODULESTORE
|
||||
@@ -52,21 +57,18 @@ class CoursesTest(ModuleStoreTestCase):
|
||||
@override_settings(
|
||||
MODULESTORE=TEST_DATA_MONGO_MODULESTORE, CMS_BASE=CMS_BASE_TEST
|
||||
)
|
||||
def test_get_cms_course_link(self):
|
||||
def test_get_cms_course_block_link(self):
|
||||
"""
|
||||
Tests that get_cms_course_link_by_id returns the right thing
|
||||
Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing
|
||||
"""
|
||||
|
||||
cms_url = u"//{}/course/org.num.name/branch/draft/block/name".format(CMS_BASE_TEST)
|
||||
self.course = CourseFactory.create(
|
||||
org='org', number='num', display_name='name'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
u"//{}/course/org.num.name/branch/draft/block/name".format(
|
||||
CMS_BASE_TEST
|
||||
),
|
||||
get_cms_course_link(self.course)
|
||||
)
|
||||
self.assertEqual(cms_url, get_cms_course_link(self.course))
|
||||
self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
|
||||
|
||||
@mock.patch(
|
||||
'xmodule.modulestore.django.get_current_request_hostname',
|
||||
|
||||
@@ -63,7 +63,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
|
||||
def test_staff_debug_for_staff(self):
|
||||
resp = self.get_cw_section()
|
||||
sdebug = '<div aria-hidden="true"><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
|
||||
sdebug = 'Staff Debug Info'
|
||||
|
||||
self.assertTrue(sdebug in resp.content)
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
self.assertEqual(togresp.content, '{"status": "student"}', '')
|
||||
|
||||
resp = self.get_cw_section()
|
||||
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
|
||||
sdebug = 'Staff Debug Info'
|
||||
|
||||
self.assertFalse(sdebug in resp.content)
|
||||
|
||||
|
||||
@@ -27,9 +27,12 @@ from xmodule.x_module import XModuleDescriptor
|
||||
from courseware import module_render as render
|
||||
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.tests.factories import StudentModuleFactory, UserFactory
|
||||
from courseware.tests.factories import StudentModuleFactory, UserFactory, GlobalStaffFactory
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase
|
||||
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE
|
||||
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
|
||||
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
@@ -509,6 +512,119 @@ class TestHtmlModifiers(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
|
||||
class ViewInStudioTest(ModuleStoreTestCase):
|
||||
"""Tests for the 'View in Studio' link visiblity."""
|
||||
|
||||
def setUp(self):
|
||||
""" Set up the user and request that will be used. """
|
||||
self.staff_user = GlobalStaffFactory.create()
|
||||
self.request = RequestFactory().get('/')
|
||||
self.request.user = self.staff_user
|
||||
self.request.session = {}
|
||||
self.module = None
|
||||
|
||||
def _get_module(self, course_id, descriptor, location):
|
||||
"""
|
||||
Get the module from the course from which to pattern match (or not) the 'View in Studio' buttons
|
||||
"""
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_id,
|
||||
self.staff_user,
|
||||
descriptor
|
||||
)
|
||||
|
||||
self.module = render.get_module(
|
||||
self.staff_user,
|
||||
self.request,
|
||||
location,
|
||||
field_data_cache,
|
||||
course_id,
|
||||
)
|
||||
|
||||
def setup_mongo_course(self, course_edit_method='Studio'):
|
||||
""" Create a mongo backed course. """
|
||||
course = CourseFactory.create(
|
||||
course_edit_method=course_edit_method
|
||||
)
|
||||
|
||||
descriptor = ItemFactory.create(
|
||||
category='vertical',
|
||||
)
|
||||
|
||||
self._get_module(course.id, descriptor, descriptor.location)
|
||||
|
||||
def setup_xml_course(self):
|
||||
"""
|
||||
Define the XML backed course to use.
|
||||
Toy courses are already loaded in XML and mixed modulestores.
|
||||
"""
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
|
||||
descriptor = modulestore().get_instance(course_id, location)
|
||||
|
||||
self._get_module(course_id, descriptor, location)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class MongoViewInStudioTest(ViewInStudioTest):
|
||||
"""Test the 'View in Studio' link visibility in a mongo backed course."""
|
||||
|
||||
def setUp(self):
|
||||
super(MongoViewInStudioTest, self).setUp()
|
||||
|
||||
def test_view_in_studio_link_studio_course(self):
|
||||
"""Regular Studio courses should see 'View in Studio' links."""
|
||||
self.setup_mongo_course()
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_xml_authored(self):
|
||||
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
|
||||
self.setup_mongo_course(course_edit_method='XML')
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class MixedViewInStudioTest(ViewInStudioTest):
|
||||
"""Test the 'View in Studio' link visibility in a mixed mongo backed course."""
|
||||
|
||||
def setUp(self):
|
||||
super(MixedViewInStudioTest, self).setUp()
|
||||
|
||||
def test_view_in_studio_link_mongo_backed(self):
|
||||
"""Mixed mongo courses that are mongo backed should see 'View in Studio' links."""
|
||||
self.setup_mongo_course()
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_xml_authored(self):
|
||||
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
|
||||
self.setup_mongo_course(course_edit_method='XML')
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_xml_backed(self):
|
||||
"""Course in XML only modulestore should not see 'View in Studio' links."""
|
||||
self.setup_xml_course()
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class XmlViewInStudioTest(ViewInStudioTest):
|
||||
"""Test the 'View in Studio' link visibility in an xml backed course."""
|
||||
|
||||
def setUp(self):
|
||||
super(XmlViewInStudioTest, self).setUp()
|
||||
|
||||
def test_view_in_studio_link_xml_backed(self):
|
||||
"""Course in XML only modulestore should not see 'View in Studio' links."""
|
||||
self.setup_xml_course()
|
||||
result_fragment = self.module.render('student_view')
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_DEBUG_INFO_TO_STAFF': True, 'DISPLAY_HISTOGRAMS_TO_STAFF': True})
|
||||
@patch('courseware.module_render.has_access', Mock(return_value=True))
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
Courseware views functions
|
||||
"""
|
||||
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
@@ -20,7 +24,8 @@ from markupsafe import escape
|
||||
|
||||
from courseware import grades
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_courses, get_course_with_access, sort_by_announcement
|
||||
from courseware.courses import get_courses, get_course_with_access, get_studio_url, sort_by_announcement
|
||||
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache
|
||||
from .module_render import toc_for_course, get_module_for_descriptor, get_module
|
||||
@@ -46,6 +51,7 @@ log = logging.getLogger("edx.courseware")
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
"""
|
||||
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
|
||||
@@ -95,7 +101,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
|
||||
|
||||
# grab the table of contents
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
request.user = user # keep just one instance of User
|
||||
request.user = user # keep just one instance of User
|
||||
toc = toc_for_course(user, request, course, chapter, section, field_data_cache)
|
||||
|
||||
context = dict([
|
||||
@@ -257,6 +263,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
u' far, should have gotten a course module for this user')
|
||||
return redirect(reverse('about_course', args=[course.id]))
|
||||
|
||||
studio_url = get_studio_url(course_id, 'course')
|
||||
|
||||
if chapter is None:
|
||||
return redirect_to_course_position(course_module)
|
||||
|
||||
@@ -268,6 +276,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'init': '',
|
||||
'fragment': Fragment(),
|
||||
'staff_access': staff_access,
|
||||
'studio_url': studio_url,
|
||||
'masquerade': masq,
|
||||
'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
|
||||
'reverifications': fetch_reverify_banner_info(request, course_id),
|
||||
@@ -294,7 +303,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_module is None:
|
||||
# User may be trying to access a chapter that isn't live yet
|
||||
if masq=='student': # if staff is masquerading as student be kinder, don't 404
|
||||
if masq == 'student': # if staff is masquerading as student be kinder, don't 404
|
||||
log.debug('staff masq as student: no chapter %s' % chapter)
|
||||
return redirect(reverse('courseware', args=[course.id]))
|
||||
raise Http404
|
||||
@@ -303,7 +312,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
|
||||
if section_descriptor is None:
|
||||
# Specifically asked-for section doesn't exist
|
||||
if masq=='student': # if staff is masquerading as student be kinder, don't 404
|
||||
if masq == 'student': # if staff is masquerading as student be kinder, don't 404
|
||||
log.debug('staff masq as student: no section %s' % section)
|
||||
return redirect(reverse('courseware', args=[course.id]))
|
||||
raise Http404
|
||||
@@ -317,7 +326,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_id, user, section_descriptor, depth=None)
|
||||
|
||||
section_module = get_module_for_descriptor(request.user,
|
||||
section_module = get_module_for_descriptor(
|
||||
request.user,
|
||||
request,
|
||||
section_descriptor,
|
||||
section_field_data_cache,
|
||||
@@ -336,6 +346,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
context['section_title'] = section_descriptor.display_name_with_default
|
||||
else:
|
||||
# section is none, so display a message
|
||||
studio_url = get_studio_url(course_id, 'course')
|
||||
prev_section = get_current_child(chapter_module)
|
||||
if prev_section is None:
|
||||
# Something went wrong -- perhaps this chapter has no sections visible to the user
|
||||
@@ -347,6 +358,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'courseware/welcome-back.html',
|
||||
{
|
||||
'course': course,
|
||||
'studio_url': studio_url,
|
||||
'chapter_module': chapter_module,
|
||||
'prev_section': prev_section,
|
||||
'prev_section_url': prev_section_url
|
||||
@@ -461,6 +473,7 @@ def course_info(request, course_id):
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
|
||||
studio_url = get_studio_url(course_id, 'course_info')
|
||||
reverifications = fetch_reverify_banner_info(request, course_id)
|
||||
|
||||
context = {
|
||||
@@ -470,6 +483,7 @@ def course_info(request, course_id):
|
||||
'course': course,
|
||||
'staff_access': staff_access,
|
||||
'masquerade': masq,
|
||||
'studio_url': studio_url,
|
||||
'reverifications': reverifications,
|
||||
}
|
||||
|
||||
@@ -537,6 +551,11 @@ def registered_for_course(course, user):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_about(request, course_id):
|
||||
"""
|
||||
Display the course's about page.
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
|
||||
if microsite.get_value(
|
||||
'ENABLE_MKTG_SITE',
|
||||
@@ -546,6 +565,8 @@ def course_about(request, course_id):
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'see_exists')
|
||||
registered = registered_for_course(course, request.user)
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
studio_url = get_studio_url(course_id, 'settings/details')
|
||||
|
||||
if has_access(request.user, course, 'load'):
|
||||
course_target = reverse('info', args=[course.id])
|
||||
@@ -575,6 +596,8 @@ def course_about(request, course_id):
|
||||
|
||||
return render_to_response('courseware/course_about.html', {
|
||||
'course': course,
|
||||
'staff_access': staff_access,
|
||||
'studio_url': studio_url,
|
||||
'registered': registered,
|
||||
'course_target': course_target,
|
||||
'registration_price': registration_price,
|
||||
@@ -664,7 +687,7 @@ def _progress(request, course_id, student_id):
|
||||
student = User.objects.prefetch_related("groups").get(id=student.id)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, request, course)
|
||||
|
||||
studio_url = get_studio_url(course_id, 'settings/grading')
|
||||
grade_summary = grades.grade(student, request, course)
|
||||
|
||||
if courseware_summary is None:
|
||||
@@ -674,6 +697,7 @@ def _progress(request, course_id, student_id):
|
||||
context = {
|
||||
'course': course,
|
||||
'courseware_summary': courseware_summary,
|
||||
'studio_url': studio_url,
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
'student': student,
|
||||
@@ -701,6 +725,7 @@ def fetch_reverify_banner_info(request, course_id):
|
||||
reverifications[info.status].append(info)
|
||||
return reverifications
|
||||
|
||||
|
||||
@login_required
|
||||
def submission_history(request, course_id, student_username, location):
|
||||
"""Render an HTML fragment (meant for inclusion elsewhere) that renders a
|
||||
|
||||
@@ -117,7 +117,7 @@ def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument
|
||||
# Go to the email section of the instructor dash
|
||||
world.visit('/courses/edx/888/Bulk_Email_Test_Course')
|
||||
world.css_click('a[href="/courses/edx/888/Bulk_Email_Test_Course/instructor"]')
|
||||
world.css_click('div.beta-button-wrapper>a')
|
||||
world.css_click('div.beta-button-wrapper>a.beta-button')
|
||||
world.css_click('a[data-section="send_email"]')
|
||||
|
||||
# Select the recipient
|
||||
|
||||
@@ -77,7 +77,7 @@ def go_to_section(section_name):
|
||||
# course_info, membership, student_admin, data_download, analytics, send_email
|
||||
world.visit('/courses/edx/999/Test_Course')
|
||||
world.css_click('a[href="/courses/edx/999/Test_Course/instructor"]')
|
||||
world.css_click('div.beta-button-wrapper>a')
|
||||
world.css_click('div.beta-button-wrapper>a.beta-button')
|
||||
world.css_click('a[data-section="{0}"]'.format(section_name))
|
||||
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@ def instructor_dashboard_2(request, course_id):
|
||||
sections.insert(3, _section_extensions(course))
|
||||
|
||||
# Gate access to course email by feature flag & by course-specific authorization
|
||||
if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \
|
||||
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
|
||||
if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
|
||||
sections.append(_section_send_email(course_id, access, course))
|
||||
|
||||
# Gate access to Metrics tab by featue flag and staff authorization
|
||||
|
||||
@@ -851,7 +851,6 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
# determine if this is a studio-backed course so we can provide a link to edit this course in studio
|
||||
is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE
|
||||
|
||||
studio_url = None
|
||||
if is_studio_course:
|
||||
studio_url = get_cms_course_link(course)
|
||||
|
||||
@@ -11,7 +11,36 @@ html.video-fullscreen{
|
||||
}
|
||||
}
|
||||
|
||||
.wrap-instructor-info {
|
||||
margin: ($baseline/2) ($baseline/4) 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.studio-view {
|
||||
position: relative;
|
||||
top: -($baseline/2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.instructor-info-action {
|
||||
@extend %t-copy-sub2;
|
||||
float: right;
|
||||
margin-left: ($baseline/2);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
border-radius: ($baseline/4);
|
||||
background-color: $shadow-l2;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
color: $lighter-base-font-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $link-hover;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.course-wrapper {
|
||||
position: relative;
|
||||
|
||||
section.course-content {
|
||||
@extend .content;
|
||||
|
||||
@@ -233,7 +233,31 @@
|
||||
|
||||
.container {
|
||||
@include clearfix;
|
||||
|
||||
|
||||
.wrap-instructor-info {
|
||||
&.studio-view {
|
||||
position: relative;
|
||||
margin: ($baseline/2) 0 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.instructor-info-action {
|
||||
@extend %t-copy-sub2;
|
||||
float: right;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
border-radius: ($baseline/4);
|
||||
background-color: $shadow-l2;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
color: $lighter-base-font-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $link-hover;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
@include box-sizing(border-box);
|
||||
|
||||
@@ -201,6 +201,12 @@
|
||||
|
||||
<section class="container">
|
||||
<section class="details">
|
||||
% if staff_access and studio_url is not None:
|
||||
<div class="wrap-instructor-info studio-view">
|
||||
<a class="instructor-info-action" href="${studio_url}">${_("View About Page in studio")}</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<nav>
|
||||
<a href="#" class="active">${_("Overview")}</a>
|
||||
## <a href="#">${_("FAQ")}</a>
|
||||
|
||||
@@ -49,7 +49,7 @@ def url_class(is_active):
|
||||
<%block name="extratabs" />
|
||||
% if masquerade is not UNDEFINED:
|
||||
% if staff_access and masquerade is not None:
|
||||
<li style="float:right"><a href="#" id="staffstatus">${_("Staff view")}</a></li>
|
||||
<li style="float:right"><a href="#" id="staffstatus">${_("Staff view")}</a></li>
|
||||
% endif
|
||||
% endif
|
||||
</ol>
|
||||
|
||||
@@ -29,6 +29,14 @@ $(document).ready(function(){
|
||||
<div class="info-wrapper">
|
||||
% if user.is_authenticated():
|
||||
<section class="updates">
|
||||
% if staff_access and masquerade is not UNDEFINED and studio_url is not None:
|
||||
% if masquerade == 'staff':
|
||||
<div class="wrap-instructor-info studio-view">
|
||||
<a class="instructor-info-action" href="${studio_url}">${_("View Updates in Studio")}</a>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
<h1>${_("Course Updates & News")}</h1>
|
||||
${get_course_info_section(request, course, 'updates')}
|
||||
</section>
|
||||
|
||||
@@ -117,16 +117,16 @@ function goto( mode)
|
||||
<section class="container">
|
||||
<div class="instructor-dashboard-wrapper">
|
||||
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
|
||||
<div class="beta-button-wrapper"><a href="${ beta_dashboard_url }">${_("Try New Beta Dashboard")}</a></div>
|
||||
%endif
|
||||
%if studio_url:
|
||||
## not checking access because if user can see this, they are at least course staff (with studio edit access)
|
||||
<div class="studio-edit-link"><a href="${studio_url}" target="_blank">${_('Edit Course In Studio')}</a></div>
|
||||
%endif
|
||||
|
||||
|
||||
<section class="instructor-dashboard-content" id="instructor-dashboard-content">
|
||||
<div class="wrap-instructor-info studio-view beta-button-wrapper">
|
||||
%if studio_url:
|
||||
<a class="instructor-info-action" href="${studio_url}">${_("View Course in Studio")}</a>
|
||||
%endif
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
|
||||
<a class="instructor-info-action beta-button" href="${ beta_dashboard_url }">${_("Try New Beta Dashboard")}</a>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
<h1>${_("Instructor Dashboard")}</h1>
|
||||
|
||||
<h2 class="navbar">[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
|
||||
|
||||
@@ -26,19 +26,26 @@ from django.conf import settings
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
|
||||
<script>
|
||||
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade)}
|
||||
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade)}
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='progress'" />
|
||||
|
||||
<div class="container">
|
||||
<div class="profile-wrapper">
|
||||
|
||||
<div class="course-info" id="course-info-progress" aria-label="${_('Course Progress')}">
|
||||
% if staff_access and studio_url is not None:
|
||||
<div class="wrap-instructor-info">
|
||||
<a class="instructor-info-action studio-view" href="${studio_url}">${_("View Grading in studio")}</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<header>
|
||||
<h1>${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email)}</h1>
|
||||
<h1>${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email)}</h1>
|
||||
</header>
|
||||
|
||||
%if not course.disable_progress_graph:
|
||||
|
||||
6
lms/templates/edit_unit_link.html
Normal file
6
lms/templates/edit_unit_link.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrap-instructor-info studio-view">
|
||||
<a class="instructor-info-action" href="${edit_link}">${_("View Unit in Studio")}</a>
|
||||
</div>
|
||||
${frag_content}
|
||||
@@ -53,34 +53,35 @@
|
||||
<script language="JavaScript" type="text/javascript"></script>
|
||||
|
||||
<section class="container">
|
||||
<div class="instructor-dashboard-wrapper-2">
|
||||
<div class="olddash-button-wrapper"><a href="${ old_dashboard_url }"> ${_("Back to Standard Dashboard")} </a></div>
|
||||
%if studio_url:
|
||||
## not checking access because if user can see this, they are at least course staff (with studio edit access)
|
||||
<div class="studio-edit-link"><a href="${studio_url}" target="_blank">${_('Edit Course In Studio')}</a></div>
|
||||
%endif
|
||||
<section class="instructor-dashboard-content-2" id="instructor-dashboard-content">
|
||||
|
||||
<h1>${_("Instructor Dashboard")}</h1>
|
||||
<hr />
|
||||
## links which are tied to idash-sections below.
|
||||
## the links are acativated and handled in instructor_dashboard.coffee
|
||||
## when the javascript loads, it clicks on the first section
|
||||
<h2 class="instructor-nav">
|
||||
% for section_data in sections:
|
||||
<a href="" data-section="${ section_data['section_key'] }">${_(section_data['section_display_name'])}</a>
|
||||
% endfor
|
||||
</h2>
|
||||
|
||||
## each section corresponds to a section_data sub-dictionary provided by the view
|
||||
## to keep this short, sections can be pulled out into their own files
|
||||
|
||||
% for section_data in sections:
|
||||
<section id="${ section_data['section_key'] }" class="idash-section">
|
||||
<%include file="${ section_data['section_key'] }.html" args="section_data=section_data" />
|
||||
<div class="instructor-dashboard-wrapper-2">
|
||||
<section class="instructor-dashboard-content-2" id="instructor-dashboard-content">
|
||||
<div class="wrap-instructor-info studio-view">
|
||||
%if studio_url:
|
||||
<a class="instructor-info-action" href="${studio_url}">${_("View Course in Studio")}</a>
|
||||
%endif
|
||||
<a class="instructor-info-action" href="${ old_dashboard_url }"> ${_("Back to Standard Dashboard")} </a>
|
||||
</div>
|
||||
|
||||
<h1>${_("Instructor Dashboard")}</h1>
|
||||
<hr />
|
||||
## links which are tied to idash-sections below.
|
||||
## the links are acativated and handled in instructor_dashboard.coffee
|
||||
## when the javascript loads, it clicks on the first section
|
||||
<h2 class="instructor-nav">
|
||||
% for section_data in sections:
|
||||
<a href="" data-section="${ section_data['section_key'] }">${_(section_data['section_display_name'])}</a>
|
||||
% endfor
|
||||
</h2>
|
||||
|
||||
## each section corresponds to a section_data sub-dictionary provided by the view
|
||||
## to keep this short, sections can be pulled out into their own files
|
||||
|
||||
% for section_data in sections:
|
||||
<section id="${ section_data['section_key'] }" class="idash-section">
|
||||
<%include file="${ section_data['section_key'] }.html" args="section_data=section_data" />
|
||||
</section>
|
||||
% endfor
|
||||
|
||||
</section>
|
||||
% endfor
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,24 +4,26 @@
|
||||
${block_content}
|
||||
%if location.category in ['problem','video','html','combinedopenended','graphical_slider_tool']:
|
||||
% if edit_link:
|
||||
<div>
|
||||
<a href="${edit_link}">Edit</a>
|
||||
% if xqa_key:
|
||||
/ <a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
|
||||
'location': '${location}',
|
||||
'xqa_key': '${xqa_key}',
|
||||
'category': '${category}',
|
||||
'user': '${user}'
|
||||
})" id="${element_id}_xqa_log">QA</a>
|
||||
% endif
|
||||
</div>
|
||||
<div>
|
||||
<a href="${edit_link}">Edit</a>
|
||||
% if xqa_key:
|
||||
/ <a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
|
||||
'location': '${location}',
|
||||
'xqa_key': '${xqa_key}',
|
||||
'category': '${category}',
|
||||
'user': '${user}'
|
||||
})" id="${element_id}_xqa_log">QA</a>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
<div aria-hidden="true"><a href="#${element_id}_debug" id="${element_id}_trig">${_("Staff Debug Info")}</a></div>
|
||||
<div aria-hidden="true" class="wrap-instructor-info">
|
||||
<a class="instructor-info-action" href="#${element_id}_debug" id="${element_id}_trig">${_("Staff Debug Info")}</a>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW') and \
|
||||
location.category == 'problem':
|
||||
<div aria-hidden="true"><a href="#${element_id}_history" id="${element_id}_history_trig">${_("Submission history")}</a></div>
|
||||
% endif
|
||||
% if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW') and \
|
||||
location.category == 'problem':
|
||||
<a class="instructor-info-action" href="#${element_id}_history" id="${element_id}_history_trig">${_("Submission history")}</a>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<section aria-hidden="true" id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" >
|
||||
<div class="inner-wrapper">
|
||||
|
||||
Reference in New Issue
Block a user