Commit be6a4570 by Andy Armstrong Committed by GitHub

Merge pull request #14767 from edx/andya/new-course-tab-preview

Add the staff preview bar to the new course home page
parents 83dce1ea ec241774
...@@ -72,6 +72,9 @@ class CourseTab(object): ...@@ -72,6 +72,9 @@ class CourseTab(object):
# True if this tab should be displayed only for instructors # True if this tab should be displayed only for instructors
course_staff_only = False course_staff_only = False
# True if this tab supports showing staff users a preview menu
supports_preview_menu = False
def __init__(self, tab_dict): def __init__(self, tab_dict):
""" """
Initializes class members with values passed in by subclasses. Initializes class members with values passed in by subclasses.
......
...@@ -36,6 +36,7 @@ class CoursewareTab(EnrolledTab): ...@@ -36,6 +36,7 @@ class CoursewareTab(EnrolledTab):
view_name = 'courseware' view_name = 'courseware'
is_movable = False is_movable = False
is_default = False is_default = False
supports_preview_menu = True
@staticmethod @staticmethod
def main_course_url_name(request): def main_course_url_name(request):
......
...@@ -395,7 +395,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi ...@@ -395,7 +395,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
def test_masquerade_as_specific_student_progress(self): def test_masquerade_as_specific_student_progress(self):
""" """
Test masquesrading as a specific user for progress page. Test masquerading as a specific user for progress page.
""" """
# Give the student some correct answers, check their progress page # Give the student some correct answers, check their progress page
self.login_student() self.login_student()
......
...@@ -412,9 +412,9 @@ class CoursewareIndex(View): ...@@ -412,9 +412,9 @@ class CoursewareIndex(View):
'init': '', 'init': '',
'fragment': Fragment(), 'fragment': Fragment(),
'staff_access': self.is_staff, 'staff_access': self.is_staff,
'studio_url': get_studio_url(self.course, 'course'),
'masquerade': self.masquerade, 'masquerade': self.masquerade,
'real_user': self.real_user, 'supports_preview_menu': True,
'studio_url': get_studio_url(self.course, 'course'),
'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
'bookmarks_api_url': reverse('bookmarks'), 'bookmarks_api_url': reverse('bookmarks'),
'language_preference': self._get_language_preference(), 'language_preference': self._get_language_preference(),
......
...@@ -51,6 +51,7 @@ import survey.views ...@@ -51,6 +51,7 @@ import survey.views
from certificates import api as certs_api from certificates import api as certs_api
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from commerce.utils import EcommerceService from commerce.utils import EcommerceService
from enrollment.api import add_enrollment from enrollment.api import add_enrollment
from course_modes.models import CourseMode from course_modes.models import CourseMode
...@@ -383,10 +384,11 @@ def course_info(request, course_id): ...@@ -383,10 +384,11 @@ def course_info(request, course_id):
'course': course, 'course': course,
'staff_access': staff_access, 'staff_access': staff_access,
'masquerade': masquerade, 'masquerade': masquerade,
'supports_preview_menu': True,
'studio_url': studio_url, 'studio_url': studio_url,
'show_enroll_banner': show_enroll_banner, 'show_enroll_banner': show_enroll_banner,
'url_to_enroll': url_to_enroll, 'url_to_enroll': url_to_enroll,
'upgrade_link': upgrade_link 'upgrade_link': upgrade_link,
} }
# Get the URL of the user's last position in order to display the 'where you were last' message # Get the URL of the user's last position in order to display the 'where you were last' message
...@@ -449,7 +451,7 @@ def get_last_accessed_courseware(course, request, user): ...@@ -449,7 +451,7 @@ def get_last_accessed_courseware(course, request, user):
return (None, None) return (None, None)
class StaticCourseTabView(FragmentView): class StaticCourseTabView(EdxFragmentView):
""" """
View that displays a static course tab with a given name. View that displays a static course tab with a given name.
""" """
...@@ -486,7 +488,7 @@ class StaticCourseTabView(FragmentView): ...@@ -486,7 +488,7 @@ class StaticCourseTabView(FragmentView):
}) })
class CourseTabView(FragmentView): class CourseTabView(EdxFragmentView):
""" """
View that displays a course tab page. View that displays a course tab page.
""" """
...@@ -499,29 +501,46 @@ class CourseTabView(FragmentView): ...@@ -499,29 +501,46 @@ class CourseTabView(FragmentView):
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key) course = get_course_with_access(request.user, 'load', course_key)
tab = CourseTabList.get_tab_by_type(course.tabs, tab_type) tab = CourseTabList.get_tab_by_type(course.tabs, tab_type)
return super(CourseTabView, self).get(request, course=course, tab=tab, **kwargs) page_context = self.create_page_context(request, course=course, tab=tab, **kwargs)
return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs)
def render_to_fragment(self, request, course=None, tab=None, **kwargs): def create_page_context(self, request, course=None, tab=None, **kwargs):
"""
Creates the context for the fragment's template.
"""
staff_access = has_access(request.user, 'staff', course)
supports_preview_menu = tab.get('supports_preview_menu', False)
if supports_preview_menu:
masquerade, masquerade_user = setup_masquerade(request, course.id, staff_access, reset_masquerade_data=True)
request.user = masquerade_user
else:
masquerade = None
return {
'course': course,
'tab': tab,
'active_page': tab.get('type', None),
'staff_access': staff_access,
'masquerade': masquerade,
'supports_preview_menu': supports_preview_menu,
'uses_pattern_library': True,
'disable_courseware_js': True,
}
def render_to_fragment(self, request, course=None, page_context=None, **kwargs):
""" """
Renders the course tab to a fragment. Renders the course tab to a fragment.
""" """
tab = page_context['tab']
return tab.render_to_fragment(request, course, **kwargs) return tab.render_to_fragment(request, course, **kwargs)
def render_to_standalone_html(self, request, fragment, course=None, tab=None, **kwargs): def render_to_standalone_html(self, request, fragment, course=None, tab=None, page_context=None, **kwargs):
""" """
Renders this course tab's fragment to HTML for a standalone page. Renders this course tab's fragment to HTML for a standalone page.
""" """
return render_to_string( if not page_context:
'courseware/tab-view.html', page_context = self.create_page_context(request, course=course, tab=tab, **kwargs)
{ page_context['fragment'] = fragment
'course': course, return render_to_string('courseware/tab-view.html', page_context)
'active_page': tab['type'],
'tab': tab,
'fragment': fragment,
'uses_pattern_library': True,
'disable_courseware_js': True,
},
)
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -871,11 +890,12 @@ def _progress(request, course_key, student_id): ...@@ -871,11 +890,12 @@ def _progress(request, course_key, student_id):
'studio_url': studio_url, 'studio_url': studio_url,
'grade_summary': grade_summary, 'grade_summary': grade_summary,
'staff_access': staff_access, 'staff_access': staff_access,
'masquerade': masquerade,
'supports_preview_menu': True,
'student': student, 'student': student,
'passed': is_course_passed(course, grade_summary), 'passed': is_course_passed(course, grade_summary),
'credit_course_requirements': _credit_course_requirements(course_key, student), 'credit_course_requirements': _credit_course_requirements(course_key, student),
'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode), 'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode),
'masquerade': masquerade
} }
with outer_atomic(): with outer_atomic():
...@@ -1397,7 +1417,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): ...@@ -1397,7 +1417,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
'disable_header': True, 'disable_header': True,
'disable_footer': True, 'disable_footer': True,
'disable_window_wrap': True, 'disable_window_wrap': True,
'disable_preview_menu': True,
'staff_access': bool(has_access(request.user, 'staff', course)), 'staff_access': bool(has_access(request.user, 'staff', course)),
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'), 'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
} }
......
...@@ -21,14 +21,14 @@ ...@@ -21,14 +21,14 @@
display: inline-block; display: inline-block;
.action-preview-label { .action-preview-label {
@include margin-right($baseline/2);
display: inline-block; display: inline-block;
margin-right: ($baseline/2);
margin-bottom: 0; margin-bottom: 0;
vertical-align: middle; vertical-align: middle;
} }
.action-preview-select { .action-preview-select {
margin-right: $baseline; @include margin-right($baseline);
} }
.action-preview-username-container { .action-preview-username-container {
......
...@@ -92,3 +92,60 @@ ...@@ -92,3 +92,60 @@
} }
} }
} }
.wrapper-preview-menu {
@include clearfix();
@include box-sizing(border-box);
margin: 0 auto 0;
padding: ($baseline*0.75);
background-color: $lms-preview-menu-color;
@media print {
display: none;
}
.preview-menu {
max-width: $lms-max-width;
width: auto;
margin: 0 auto;
}
.preview-actions {
@include margin-left(0);
display: inline-block;
margin-bottom: 0;
.action-preview {
display: inline-block;
.action-preview-label {
@include margin-right($baseline/2);
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
}
.action-preview-select {
@include margin-right($baseline);
}
.action-preview-username-container {
display: none;
.action-preview-username {
vertical-align: middle;
height: 25px;
}
}
}
}
.preview-specific-student-notice {
margin-top: ($baseline/2);
font-size: 90%;
> p {
margin-bottom: 0;
}
}
}
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
margin: 0 auto; margin: 0 auto;
padding: 10px 10px 0; padding: 10px 10px 0;
width: 100%; width: 100%;
max-width: 1180px; max-width: $lms-max-width;
.left { .left {
@include float(left); @include float(left);
......
// LMS layouts // LMS layouts
.content-wrapper { .content-wrapper {
max-width: 1180px; max-width: $lms-max-width;
padding: { padding-bottom: $baseline*2;
top: $baseline/2;
bottom: $baseline*2;
}
.container { .container {
@include clearfix(); @include clearfix();
......
// LMS variables // LMS variables
$lms-max-width: 1180px;
$lms-gray: palette(grayscale, base); $lms-gray: palette(grayscale, base);
$lms-background-color: palette(grayscale, x-back); $lms-background-color: palette(grayscale, x-back);
$lms-container-background-color: $white; $lms-container-background-color: $white;
$lms-border-color: palette(grayscale, back); $lms-border-color: palette(grayscale, back);
$lms-label-color: palette(grayscale, black); $lms-label-color: palette(grayscale, black);
$lms-active-color: palette(primary, base); $lms-active-color: palette(primary, base);
$lms-preview-menu-color: #c8c8c8;
$white-transparent: rgba(255, 255, 255, 0); $white-transparent: rgba(255, 255, 255, 0);
$white-opacity-40: rgba(255, 255, 255, 0.4); $white-opacity-40: rgba(255, 255, 255, 0.4);
......
## mako ## mako
<%page args="active_page=None" expression_filter="h" /> <%page args="active_page=None" expression_filter="h" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from courseware.tabs import get_course_tab_list from courseware.tabs import get_course_tab_list
from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.markup import HTML, Text
from student.models import CourseEnrollment
%> %>
<% <%
...@@ -17,16 +14,6 @@ if active_page is None and active_page_context is not UNDEFINED: ...@@ -17,16 +14,6 @@ if active_page is None and active_page_context is not UNDEFINED:
# If active_page is not passed in as an argument, it may be in the context as active_page_context # If active_page is not passed in as an argument, it may be in the context as active_page_context
active_page = active_page_context active_page = active_page_context
def selected(is_selected):
return "selected" if is_selected else ""
show_preview_menu = not disable_preview_menu and staff_access and active_page in ["courseware", "info", "progress"]
cohorted_user_partition = get_cohorted_user_partition(course)
masquerade_user_name = masquerade.user_name if masquerade else None
masquerade_group_id = masquerade.group_id if masquerade else None
staff_selected = selected(not masquerade or masquerade.role != "student")
specific_student_selected = selected(not staff_selected and masquerade.user_name)
student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id)
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams) include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
%> %>
...@@ -39,47 +26,6 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and ...@@ -39,47 +26,6 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
% endfor % endfor
<div class="proctored_exam_status"></div> <div class="proctored_exam_status"></div>
% endif % endif
% if show_preview_menu:
<nav class="wrapper-preview-menu" aria-label="${_('Course View')}">
<div class="preview-menu">
<ol class="preview-actions">
<li class="action-preview">
<form action="#" class="action-preview-form" method="post">
<label for="action-preview-select" class="action-preview-label">${_("View this course as:")}</label>
<select class="action-preview-select" id="action-preview-select" name="select">
<option value="staff" ${staff_selected}>${_("Staff")}</option>
<option value="student" ${student_selected}>${_("Student")}</option>
<option value="specific student" ${specific_student_selected}>${_("Specific student")}</option>
% if cohorted_user_partition:
% for group in sorted(cohorted_user_partition.groups, key=lambda group: group.name):
<option value="group.id" data-group-id="${group.id}" ${selected(masquerade_group_id == group.id)}>
${_("Student in {content_group}").format(content_group=group.name)}
</option>
% endfor
% endif
</select>
<div class="action-preview-username-container">
<label for="action-preview-username" class="action-preview-label">${_("Username or email:")}</label>
<input type="text" class="action-preview-username" id="action-preview-username">
</div>
<button type="submit" class="sr" name="submit" value="submit">${_("Set preview mode")}</button>
</form>
</li>
</ol>
% if specific_student_selected:
<div class="preview-specific-student-notice">
<p>
${Text(_("You are now viewing the course as {i_start}{user_name}{i_end}.")).format(
user_name=masquerade_user_name,
i_start=HTML(u'<i>'),
i_end=HTML(u'</i>'),
)}
</p>
</div>
% endif
</div>
</nav>
% endif
% if disable_tabs is UNDEFINED or not disable_tabs: % if disable_tabs is UNDEFINED or not disable_tabs:
<nav class="${active_page} wrapper-course-material" aria-label="${_('Course Material')}"> <nav class="${active_page} wrapper-course-material" aria-label="${_('Course Material')}">
...@@ -95,18 +41,3 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and ...@@ -95,18 +41,3 @@ include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
</div> </div>
</nav> </nav>
%endif %endif
% if show_preview_menu:
<%
preview_options = {
"courseId": course.id,
"disableStudentAccess": disable_student_access if disable_student_access is not UNDEFINED else False,
"specificStudentSelected": specific_student_selected,
"cohortedUserPartitionId": cohorted_user_partition.id if cohorted_user_partition else None,
"masqueradeUsername" : masquerade_user_name if masquerade_user_name is not UNDEFINED else None,
}
%>
<%static:require_module_async module_name="lms/js/preview/preview_factory" class_name="PreviewFactory">
PreviewFactory(${preview_options | n, dump_js_escaped_json});
</%static:require_module_async>
% endif
...@@ -134,6 +134,7 @@ from pipeline_mako import render_require_js_path_overrides ...@@ -134,6 +134,7 @@ from pipeline_mako import render_require_js_path_overrides
% if not disable_header: % if not disable_header:
<%include file="${static.get_template_path('header.html')}" args="online_help_token=online_help_token" /> <%include file="${static.get_template_path('header.html')}" args="online_help_token=online_help_token" />
<%include file="/preview_menu.html" />
% endif % endif
<div class="content-wrapper" id="content"> <div class="content-wrapper" id="content">
......
## mako
<%page args="active_page=None" expression_filter="h" />
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.conf import settings
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.markup import HTML, Text
%>
<%
show_preview_menu = course and staff_access and supports_preview_menu
%>
% if show_preview_menu:
<%
def selected(is_selected):
return "selected" if is_selected else ""
cohorted_user_partition = get_cohorted_user_partition(course)
masquerade_user_name = masquerade.user_name if masquerade else None
masquerade_group_id = masquerade.group_id if masquerade else None
staff_selected = selected(not masquerade or masquerade.role != "student")
specific_student_selected = selected(not staff_selected and masquerade.user_name)
student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id)
%>
<nav class="wrapper-preview-menu" aria-label="${_('Course View')}">
<div class="preview-menu">
<ol class="preview-actions">
<li class="action-preview">
<form action="#" class="action-preview-form" method="post">
<label for="action-preview-select" class="action-preview-label">${_("View this course as:")}</label>
<select class="action-preview-select" id="action-preview-select" name="select">
<option value="staff" ${staff_selected}>${_("Staff")}</option>
<option value="student" ${student_selected}>${_("Student")}</option>
<option value="specific student" ${specific_student_selected}>${_("Specific student")}</option>
% if cohorted_user_partition:
% for group in sorted(cohorted_user_partition.groups, key=lambda group: group.name):
<option value="group.id" data-group-id="${group.id}" ${selected(masquerade_group_id == group.id)}>
${_("Student in {content_group}").format(content_group=group.name)}
</option>
% endfor
% endif
</select>
<div class="action-preview-username-container">
<label for="action-preview-username" class="action-preview-label">${_("Username or email:")}</label>
<input type="text" class="action-preview-username" id="action-preview-username">
</div>
<button type="submit" class="sr" name="submit" value="submit">${_("Set preview mode")}</button>
</form>
</li>
</ol>
% if specific_student_selected:
<div class="preview-specific-student-notice">
<p>
${Text(_("You are now viewing the course as {i_start}{user_name}{i_end}.")).format(
user_name=masquerade_user_name,
i_start=HTML(u'<i>'),
i_end=HTML(u'</i>'),
)}
</p>
</div>
% endif
</div>
</nav>
<%
preview_options = {
"courseId": course.id,
"disableStudentAccess": disable_student_access if disable_student_access is not UNDEFINED else False,
"specificStudentSelected": specific_student_selected,
"cohortedUserPartitionId": cohorted_user_partition.id if cohorted_user_partition else None,
"masqueradeUsername" : masquerade_user_name if masquerade_user_name is not UNDEFINED else None,
}
%>
<%static:require_module_async module_name="lms/js/preview/preview_factory" class_name="PreviewFactory">
PreviewFactory(${preview_options | n, dump_js_escaped_json});
</%static:require_module_async>
% endif
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
## This template should not use the target student's details when masquerading, see TNL-4895 ## This template should not use the target student's details when masquerading, see TNL-4895
<% <%
self.real_user = real_user if real_user != UNDEFINED else user self.real_user = getattr(user, 'real_user', user)
%> %>
<%! <%!
......
...@@ -91,6 +91,5 @@ class EdxFragmentView(FragmentView): ...@@ -91,6 +91,5 @@ class EdxFragmentView(FragmentView):
'disable_header': True, 'disable_header': True,
'disable_footer': True, 'disable_footer': True,
'disable_window_wrap': True, 'disable_window_wrap': True,
'disable_preview_menu': True,
} }
return render_to_response(settings.STANDALONE_FRAGMENT_VIEW_TEMPLATE, context) return render_to_response(settings.STANDALONE_FRAGMENT_VIEW_TEMPLATE, context)
...@@ -49,6 +49,7 @@ class CourseBookmarksView(View): ...@@ -49,6 +49,7 @@ class CourseBookmarksView(View):
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
'supports_preview_menu': True,
'course_url': course_url, 'course_url': course_url,
'bookmarks_fragment': bookmarks_fragment, 'bookmarks_fragment': bookmarks_fragment,
'disable_courseware_js': True, 'disable_courseware_js': True,
......
## mako ## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>
<%! <%!
import json import json
...@@ -21,20 +14,6 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str ...@@ -21,20 +14,6 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
%> %>
<%block name="bodyclass">course</%block>
<%block name="pagetitle">${course_name()}</%block>
<%include file="../courseware/course_navigation.html" args="active_page='courseware'" />
<%block name="headextra">
${HTML(outline_fragment.head_html())}
</%block>
<%block name="js_extra">
${HTML(outline_fragment.foot_html())}
</%block>
<%block name="content"> <%block name="content">
<div class="course-view container" id="course-container"> <div class="course-view container" id="course-container">
<header class="page-header has-secondary"> <header class="page-header has-secondary">
......
""" """
Tests for the Course Outline view and supporting views. Tests for the Course Outline view and supporting views.
""" """
import datetime
from mock import patch from mock import patch
import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.tests.factories import StaffFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_PASSWORD = 'test'
def course_home_url(course):
"""
Returns the URL for the course's home page
"""
return reverse(
'edx.course_experience.course_home',
kwargs={
'course_id': unicode(course.id),
}
)
class TestCourseOutlinePage(SharedModuleStoreTestCase): class TestCourseOutlinePage(SharedModuleStoreTestCase):
""" """
...@@ -43,8 +60,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): ...@@ -43,8 +60,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
"""Set up and enroll our fake user in the course.""" """Set up and enroll our fake user in the course."""
cls.password = 'test' cls.user = UserFactory(password=TEST_PASSWORD)
cls.user = UserFactory(password=cls.password)
for course in cls.courses: for course in cls.courses:
CourseEnrollment.enroll(cls.user, course.id) CourseEnrollment.enroll(cls.user, course.id)
...@@ -53,18 +69,13 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): ...@@ -53,18 +69,13 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
Set up for the tests. Set up for the tests.
""" """
super(TestCourseOutlinePage, self).setUp() super(TestCourseOutlinePage, self).setUp()
self.client.login(username=self.user.username, password=self.password) self.client.login(username=self.user.username, password=TEST_PASSWORD)
@patch('openedx.features.course_experience.views.course_outline.get_last_accessed_courseware') @patch('openedx.features.course_experience.views.course_outline.get_last_accessed_courseware')
def test_render(self, patched_get_last_accessed): def test_render(self, patched_get_last_accessed):
for course in self.courses: for course in self.courses:
patched_get_last_accessed.return_value = (None, course.last_accessed) patched_get_last_accessed.return_value = (None, course.last_accessed)
url = reverse( url = course_home_url(course)
'edx.course_experience.course_home',
kwargs={
'course_id': unicode(course.id),
}
)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_content = response.content.decode("utf-8") response_content = response.content.decode("utf-8")
...@@ -79,3 +90,67 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): ...@@ -79,3 +90,67 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
self.assertIn(section.display_name, response_content) self.assertIn(section.display_name, response_content)
for vertical in section.children: for vertical in section.children:
self.assertNotIn(vertical.display_name, response_content) self.assertNotIn(vertical.display_name, response_content)
class TestCourseOutlinePreview(SharedModuleStoreTestCase):
"""
Unit tests for staff preview of the course outline.
"""
def update_masquerade(self, course, role, group_id=None, user_name=None):
"""
Toggle masquerade state.
"""
masquerade_url = reverse(
'masquerade_update',
kwargs={
'course_key_string': unicode(course.id),
}
)
response = self.client.post(
masquerade_url,
json.dumps({'role': role, 'group_id': group_id, 'user_name': user_name}),
'application/json'
)
self.assertEqual(response.status_code, 200)
return response
def test_preview(self):
"""
Verify the behavior of preview for the course outline.
"""
course = CourseFactory.create(
start=datetime.datetime.now() - datetime.timedelta(days=30)
)
staff_user = StaffFactory(course_key=course.id, password=TEST_PASSWORD)
CourseEnrollment.enroll(staff_user, course.id)
future_date = datetime.datetime.now() + datetime.timedelta(days=30)
with self.store.bulk_operations(course.id):
chapter = ItemFactory.create(
category='chapter',
parent_location=course.location,
display_name='First Chapter',
)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
chapter = ItemFactory.create(
category='chapter',
parent_location=course.location,
display_name='Future Chapter',
due=future_date,
)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
# Verify that a staff user sees a chapter with a due date in the future
self.client.login(username=staff_user.username, password='test')
url = course_home_url(course)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Future Chapter')
# Verify that staff masquerading as a learner does not see the future chapter.
self.update_masquerade(course, role='student')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'Future Chapter')
...@@ -4,7 +4,7 @@ Defines URLs for the course experience. ...@@ -4,7 +4,7 @@ Defines URLs for the course experience.
from django.conf.urls import url from django.conf.urls import url
from views.course_home import CourseHomeView from views.course_home import CourseHomeView, CourseHomeFragmentView
from views.course_outline import CourseOutlineFragmentView from views.course_outline import CourseOutlineFragmentView
urlpatterns = [ urlpatterns = [
...@@ -14,6 +14,11 @@ urlpatterns = [ ...@@ -14,6 +14,11 @@ urlpatterns = [
name='edx.course_experience.course_home', name='edx.course_experience.course_home',
), ),
url( url(
r'^home_fragment$',
CourseHomeFragmentView.as_view(),
name='edx.course_experience.course_home_fragment_view',
),
url(
r'^outline_fragment$', r'^outline_fragment$',
CourseOutlineFragmentView.as_view(), CourseOutlineFragmentView.as_view(),
name='edx.course_experience.course_outline_fragment_view', name='edx.course_experience.course_outline_fragment_view',
......
...@@ -4,20 +4,22 @@ Views for the course home page. ...@@ -4,20 +4,22 @@ Views for the course home page.
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.shortcuts import render_to_response from django.template.loader import render_to_string
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from lms.djangoapps.courseware.views.views import CourseTabView
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key
from web_fragments.fragment import Fragment
from course_outline import CourseOutlineFragmentView from course_outline import CourseOutlineFragmentView
class CourseHomeView(View): class CourseHomeView(CourseTabView):
""" """
The home page for a course. The home page for a course.
""" """
...@@ -25,21 +27,33 @@ class CourseHomeView(View): ...@@ -25,21 +27,33 @@ class CourseHomeView(View):
@method_decorator(ensure_csrf_cookie) @method_decorator(ensure_csrf_cookie)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key) @method_decorator(ensure_valid_course_key)
def get(self, request, course_id): def get(self, request, course_id, **kwargs):
""" """
Displays the home page for the specified course. Displays the home page for the specified course.
"""
return super(CourseHomeView, self).get(request, course_id, 'courseware', **kwargs)
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
course_id = unicode(course.id)
home_fragment_view = CourseHomeFragmentView()
return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
Arguments: class CourseHomeFragmentView(EdxFragmentView):
request: HTTP request """
course_id (unicode): course id A fragment to render the home page for a course.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the course's home page as a fragment.
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
# Render the outline as a fragment # Render the outline as a fragment
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id) outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
# Render the entire unified course view # Render the course home fragment
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
...@@ -47,4 +61,5 @@ class CourseHomeView(View): ...@@ -47,4 +61,5 @@ class CourseHomeView(View):
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True,
} }
return render_to_response('course_experience/course-home.html', context) html = render_to_string('course_experience/course-home-fragment.html', context)
return Fragment(html)
...@@ -9,12 +9,12 @@ from courseware.courses import get_course_with_access ...@@ -9,12 +9,12 @@ from courseware.courses import get_course_with_access
from lms.djangoapps.courseware.views.views import get_last_accessed_courseware from lms.djangoapps.courseware.views.views import get_last_accessed_courseware
from lms.djangoapps.course_api.blocks.api import get_blocks from lms.djangoapps.course_api.blocks.api import get_blocks
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
from web_fragments.views import FragmentView
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
class CourseOutlineFragmentView(FragmentView): class CourseOutlineFragmentView(EdxFragmentView):
""" """
Course outline fragment to be shown in the unified course view. Course outline fragment to be shown in the unified course view.
""" """
...@@ -35,7 +35,7 @@ class CourseOutlineFragmentView(FragmentView): ...@@ -35,7 +35,7 @@ class CourseOutlineFragmentView(FragmentView):
return block return block
def render_to_fragment(self, request, course_id=None, **kwargs): def render_to_fragment(self, request, course_id=None, page_context=None, **kwargs):
""" """
Renders the course outline as a fragment. Renders the course outline as a fragment.
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment