Commit e86b4a12 by Nimisha Asthagiri

Changes for viewing built-in tabs in studio

Changed "Status Page" -> "Page".

UX:
    support for displaying built-in tabs
    restored drag and drop on Studio Pages
    additional styling for fixed state on Studio Pages
    add a new page action added to bottom of Studio Pages

Dev
    changes for viewing tabs in studio,
    refactored the tab code,
    decoupled the code from django layer.
    is_hideable flag on tabs
    get_discussion method is needed to continue to support
external_discussion links for now since used by 6.00x course.
    override the __eq__ operator to support comparing with
dict-type tabs.

Test
    moved test code to common,
    added acceptance test for built-in pages
    added additional unit tests for tabs.
    changed test_split_modulestore test to support serializing objects
that are fields in a Course.

Env:
    updated environment configuration settings so they are
    consistent for both cms and lms.
parent 91c0b8ee
......@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Studio: Support for viewing built-in tabs on the Pages page. STUD-1193
Blades: Fixed bug when image mapped input's Show Answer multiplies rectangles on
many inputtypes. BLD-810.
......
@shard_2
Feature: CMS.Static Pages
As a course author, I want to be able to add static pages
Feature: CMS.Pages
As a course author, I want to be able to add pages
Scenario: Users can add static pages
Given I have opened a new course in Studio
And I go to the static pages page
Given I have opened the pages page in a new course
Then I should not see any static pages
When I add a new page
When I add a new static page
Then I should see a static page named "Empty"
Scenario: Users can delete static pages
......@@ -16,6 +15,10 @@ Feature: CMS.Static Pages
When I confirm the prompt
Then I should not see any static pages
Scenario: Users can see built-in pages
Given I have opened the pages page in a new course
Then I should see the default built-in pages
# Safari won't update the name properly
@skip_safari
Scenario: Users can edit static pages
......@@ -28,7 +31,7 @@ Feature: CMS.Static Pages
@skip_safari
Scenario: Users can reorder static pages
Given I have created two different static pages
When I reorder the tabs
Then the tabs are in the reverse order
When I reorder the static tabs
Then the static tabs are in the reverse order
And I reload the page
Then the tabs are in the reverse order
Then the static tabs are in the reverse order
# pylint: disable=C0111
# pylint: disable=W0621
# pylint: disable=W0613
from lettuce import world, step
from nose.tools import assert_equal # pylint: disable=E0611
@step(u'I go to the static pages page$')
@step(u'I go to the pages page$')
def go_to_static(step):
menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages a'
......@@ -13,7 +14,7 @@ def go_to_static(step):
world.css_click(static_css)
@step(u'I add a new page$')
@step(u'I add a new static page$')
def add_page(step):
button_css = 'a.new-button'
world.css_click(button_css)
......@@ -32,6 +33,15 @@ def not_see_any_static_pages(step):
assert (world.is_css_not_present(pages_css, wait_time=30))
@step(u'I should see the default built-in pages')
def see_default_built_in_pages(step):
expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']
pages = world.css_find("div.course-nav-tab-header h3.title")
assert_equal(len(expected_pages), len(pages))
for i, page_name in enumerate(expected_pages):
assert_equal(pages[i].text, page_name)
@step(u'I "(edit|delete)" the static page$')
def click_edit_or_delete(step, edit_or_delete):
button_css = 'ul.component-actions a.%s-button' % edit_or_delete
......@@ -50,22 +60,27 @@ def change_name(step, new_name):
world.css_click(save_button)
@step(u'I reorder the tabs')
@step(u'I reorder the static tabs')
def reorder_tabs(_step):
# For some reason, the drag_and_drop method did not work in this case.
draggables = world.css_find('.drag-handle')
draggables = world.css_find('.component .drag-handle')
source = draggables.first
target = draggables.last
source.action_chains.click_and_hold(source._element).perform()
source.action_chains.move_to_element_with_offset(target._element, 0, 50).perform()
source.action_chains.click_and_hold(source._element).perform() # pylint: disable=protected-access
source.action_chains.move_to_element_with_offset(target._element, 0, 50).perform() # pylint: disable=protected-access
source.action_chains.release().perform()
@step(u'I have created a static page')
def create_static_page(step):
step.given('I have opened the pages page in a new course')
step.given('I add a new static page')
@step(u'I have opened the pages page in a new course')
def open_pages_page_in_new_course(step):
step.given('I have opened a new course in Studio')
step.given('I go to the static pages page')
step.given('I add a new page')
step.given('I go to the pages page')
@step(u'I have created two different static pages')
......@@ -73,12 +88,12 @@ def create_two_pages(step):
step.given('I have created a static page')
step.given('I "edit" the static page')
step.given('I change the name to "First"')
step.given('I add a new page')
step.given('I add a new static page')
# Verify order of tabs
_verify_tab_names('First', 'Empty')
@step(u'the tabs are in the reverse order')
@step(u'the static tabs are in the reverse order')
def tabs_in_reverse_order(step):
_verify_tab_names('Empty', 'First')
......
......@@ -22,6 +22,7 @@ from edxmako.shortcuts import render_to_response
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs
from xmodule.modulestore.exceptions import (
ItemNotFoundError, InvalidLocationError)
......@@ -39,7 +40,6 @@ from util.json_request import expect_json
from util.string_utils import _has_non_ascii_characters
from .access import has_course_access
from .tabs import initialize_course_tabs
from .component import (
OPEN_ENDED_COMPONENT_TYPES, NOTE_COMPONENT_TYPES,
ADVANCED_COMPONENT_POLICY_KEY)
......@@ -411,8 +411,6 @@ def create_new_course(request):
definition_data=overview_template.get('data')
)
initialize_course_tabs(new_course, request.user)
new_location = loc_mapper().translate_location(new_course.location.course_id, new_course.location, False, True)
# can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course
# however, we can assume that b/c this user had authority to create the course, the user can add themselves
......@@ -657,8 +655,7 @@ def _config_course_advanced_components(request, course_module):
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES,
}
# Check to see if the user instantiated any notes or open ended
# components
# Check to see if the user instantiated any notes or open ended components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
......@@ -841,8 +838,8 @@ def textbooks_list_handler(request, tag=None, package_id=None, branch=None, vers
textbook["id"] = tid
tids.add(tid)
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
course.tabs.append({"type": "pdf_textbooks"})
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
course.tabs.append(PDFTextbookTabs())
course.pdf_textbooks = textbooks
store.update_item(course, request.user.id)
return JsonResponse(course.pdf_textbooks)
......@@ -858,10 +855,8 @@ def textbooks_list_handler(request, tag=None, package_id=None, branch=None, vers
existing = course.pdf_textbooks
existing.append(textbook)
course.pdf_textbooks = existing
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
tabs = course.tabs
tabs.append({"type": "pdf_textbooks"})
course.tabs = tabs
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
course.tabs.append(PDFTextbookTabs())
store.update_item(course, request.user.id)
resp = JsonResponse(textbook, status=201)
resp["Location"] = locator.url_reverse('textbooks', textbook["id"])
......
......@@ -5,6 +5,7 @@ from access import has_course_access
from util.json_request import expect_json, JsonResponse
from django.http import HttpResponseNotFound
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
......@@ -13,6 +14,7 @@ from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.tabs import CourseTabList, StaticTab, CourseTab
from ..utils import get_modulestore
......@@ -20,33 +22,6 @@ from django.utils.translation import ugettext as _
__all__ = ['tabs_handler']
def initialize_course_tabs(course, user):
"""
set up the default tabs
I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
at least a list populated with the minimal times
@TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
"""
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [
# Translators: "Courseware" is the title of the page where you access a course's videos and problems.
{"type": "courseware", "name": _("Courseware")},
# Translators: "Course Info" is the name of the course's information and updates page
{"type": "course_info", "name": _("Course Info")},
# Translators: "Discussion" is the title of the course forum page
{"type": "discussion", "name": _("Discussion")},
# Translators: "Wiki" is the title of the course's wiki page
{"type": "wiki", "name": _("Wiki")},
# Translators: "Progress" is the title of the student's grade information page
{"type": "progress", "name": _("Progress")},
]
modulestore('direct').update_item(course, user.id)
@expect_json
@login_required
@ensure_csrf_cookie
......@@ -108,12 +83,12 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
reordered_tabs = []
static_tab_idx = 0
for tab in course_item.tabs:
if tab['type'] == 'static_tab':
if isinstance(tab, StaticTab):
reordered_tabs.append(
{'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name,
}
StaticTab(
name=tab_items[static_tab_idx].display_name,
url_slug=tab_items[static_tab_idx].location.name,
)
)
static_tab_idx += 1
else:
......@@ -126,19 +101,19 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
else:
raise NotImplementedError('Creating or changing tab content is not supported.')
elif request.method == 'GET': # assume html
# see tabs have been uninitialized (e.g. supporting courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item, request.user)
# first get all static tabs from the tabs list
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = old_location.replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
built_in_tabs = []
for tab in CourseTabList.iterate_displayable(course_item, settings, include_instructor_tab=False):
if isinstance(tab, StaticTab):
static_tab_loc = old_location.replace(category='static_tab', name=tab.url_slug)
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
else:
built_in_tabs.append(tab)
# create a list of components for each static tab
components = [
loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True
......@@ -149,6 +124,7 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'built_in_tabs': built_in_tabs,
'components': components,
'course_locator': locator
})
......@@ -183,7 +159,7 @@ def primitive_delete(course, num):
def primitive_insert(course, num, tab_type, name):
"Inserts a new tab at the given number (0 based)."
validate_args(num, tab_type)
new_tab = {u'type': unicode(tab_type), u'name': unicode(name)}
new_tab = CourseTab.from_json({u'type': unicode(tab_type), u'name': unicode(name)})
tabs = course.tabs
tabs.insert(num, new_tab)
modulestore('direct').update_item(course, '**replace_user**')
......
......@@ -20,22 +20,22 @@ class PrimitiveTabEdit(TestCase):
tabs.primitive_delete(course, 6)
tabs.primitive_delete(course, 2)
self.assertFalse({u'type': u'textbooks'} in course.tabs)
# Check that discussion has shifted down
# Check that discussion has shifted up
self.assertEquals(course.tabs[2], {'type': 'discussion', 'name': 'Discussion'})
def test_insert(self):
"""Test primitive tab insertion."""
course = CourseFactory.create(org='edX', course='999')
tabs.primitive_insert(course, 2, 'atype', 'aname')
self.assertEquals(course.tabs[2], {'type': 'atype', 'name': 'aname'})
tabs.primitive_insert(course, 2, 'notes', 'aname')
self.assertEquals(course.tabs[2], {'type': 'notes', 'name': 'aname'})
with self.assertRaises(ValueError):
tabs.primitive_insert(course, 0, 'atype', 'aname')
tabs.primitive_insert(course, 0, 'notes', 'aname')
with self.assertRaises(ValueError):
tabs.primitive_insert(course, 3, 'static_tab', 'aname')
def test_save(self):
"""Test course saving."""
course = CourseFactory.create(org='edX', course='999')
tabs.primitive_insert(course, 3, 'atype', 'aname')
tabs.primitive_insert(course, 3, 'notes', 'aname')
course2 = get_course_by_id(course.id)
self.assertEquals(course2.tabs[3], {'type': 'atype', 'name': 'aname'})
self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'})
......@@ -89,6 +89,10 @@ STATICFILES_FINDERS += ('pipeline.finders.PipelineFinder', )
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/acceptance.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
......
......@@ -168,6 +168,8 @@ ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {}))
for feature, value in ENV_FEATURES.items():
FEATURES[feature] = value
WIKI_ENABLED = ENV_TOKENS.get('WIKI_ENABLED', WIKI_ENABLED)
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
......
......@@ -28,7 +28,7 @@ import imp
import sys
import lms.envs.common
from lms.envs.common import (
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, ALL_LANGUAGES
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, ALL_LANGUAGES, WIKI_ENABLED
)
from path import path
......@@ -43,7 +43,9 @@ FEATURES = {
'GITHUB_PUSH': False,
'ENABLE_DISCUSSION_SERVICE': False,
# for consistency in user-experience, keep the value of this setting in sync with the
# one in lms/envs/common.py
'ENABLE_DISCUSSION_SERVICE': True,
'AUTH_USE_CERTIFICATES': False,
......
......@@ -8,6 +8,9 @@ This config file runs the simplest dev environment"""
from .common import *
from logsettings import get_logger_config
# import settings from LMS for consistent behavior with CMS
from lms.envs.dev import (WIKI_ENABLED)
DEBUG = True
TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log",
......
......@@ -17,6 +17,9 @@ import os
from path import path
from warnings import filterwarnings
# import settings from LMS for consistent behavior with CMS
from lms.envs.test import (WIKI_ENABLED)
# Nose Test Runner
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
......@@ -222,3 +225,7 @@ MICROSITE_CONFIGURATION = {
}
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
FEATURES['USE_MICROSITES'] = True
# For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/test.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
......@@ -17,6 +17,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
)
@options.mast.find('.new-tab').on('click', @addNewTab)
$('.add-pages .new-tab').on('click', @addNewTab)
@$('.components').sortable(
handle: '.drag-handle'
update: @tabMoved
......@@ -34,7 +35,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
tabs.push($(element).data('locator'))
)
analytics.track "Reordered Static Pages",
analytics.track "Reordered Pages",
course: course_location_analytics
saving = new NotificationView.Mini({title: gettext("Saving…")})
......@@ -68,7 +69,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
{category: 'static_tab'}
)
analytics.track "Added Static Page",
analytics.track "Added Page",
course: course_location_analytics
deleteTab: (event) =>
......@@ -82,7 +83,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
view.hide()
$component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
analytics.track "Deleted Page",
course: course_location_analytics
id: $component.data('locator')
deleting = new NotificationView.Mini
......
// studio - views - course static pages
// studio - views - course pages
// ====================
.view-static-pages {
.new-static-page-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
// page structure
.content-primary,
.content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
.no-pages-content {
.add-pages {
@extend %ui-well;
padding: ($baseline*2);
margin: ($baseline*1.5) 0;
background-color: $gray-l4;
padding: ($baseline*2);
text-align: center;
color: $gray;
......@@ -30,90 +31,96 @@
}
}
}
}
.actions-list-wrap {
top: 6px;
.content-supplementary {
width: flex-grid(3, 12);
}
.actions-list {
.wrapper-actions-list {
top: 6px;
.action-item {
position: relative;
.actions-list {
.action-item {
position: relative;
display: inline-block;
min-width: ($baseline*1.5);
margin: 0;
text-align: center;
.action-button,
.toggle-actions-view {
@include transition(all $tmg-f2 ease-in-out 0s);
display: inline-block;
margin: 0;
text-align: center;
border: 0;
background: none;
color: $gray-l3;
.action-button,
.toggle-actions-view {
@include transition(all $tmg-f2 ease-in-out 0s);
display: inline-block;
border: 0;
background: none;
color: $gray-l3;
&:hover {
background-color: $blue;
color: $gray-l6;
}
&:hover {
background-color: $blue;
color: $gray-l6;
}
}
&.action-visible {
position: relative;
}
&.action-visible {
position: relative;
}
&.action-visible label {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
&.action-visible label {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
&:hover {
background-color: $blue;
}
&:hover {
background-color: $blue;
}
}
&.action-visible .toggle-checkbox {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
opacity: 0;
}
&.action-visible .toggle-checkbox {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
opacity: 0;
}
&.action-visible .toggle-checkbox:hover ~ .action-button {
background-color: $blue;
color: $gray-l6;
}
&.action-visible .toggle-checkbox:hover ~ .action-button,
&.action-visible .toggle-checkbox:checked:hover ~ .action-button {
background-color: $blue;
color: $gray-l6;
}
&.action-visible .toggle-checkbox ~ .action-button {
.icon-eye-open {
display: inline-block;
}
&.action-visible .toggle-checkbox ~ .action-button {
.icon-eye-open {
display: inline-block;
}
.icon-eye-close {
display: none;
}
.icon-eye-close {
display: none;
}
}
&.action-visible .toggle-checkbox:checked ~ .action-button {
background-color: $gray;
color: $white;
&.action-visible .toggle-checkbox:checked ~ .action-button {
background-color: $gray;
color: $white;
.icon-eye-open {
display: none;
}
.icon-eye-open {
display: none;
}
.icon-eye-close {
display: inline-block;
}
.icon-eye-close {
display: inline-block;
}
}
}
}
}
.unit-body {
padding: 0;
......@@ -209,6 +216,12 @@
&:hover {
background: url(../img/drag-handles.png) center no-repeat #fff;
}
&.is-fixed {
cursor: default;
width: ($baseline*1.5);
background: $gray-l4 none;
}
}
// uses similar styling as assets.scss, unit.scss
......@@ -229,14 +242,14 @@
.course-nav-tab-actions {
display: inline-block;
float: right;
margin-right: $baseline*2;
margin-right: ($baseline*2);
padding: 8px 0px;
vertical-align: middle;
text-align: center;
.action-item {
display: inline-block;
margin: ($baseline/4) 0 ($baseline/4) ($baseline/4);
margin: ($baseline/4) 0 ($baseline/4) ($baseline/2);
.action-button {
@include transition(all $tmg-f2 ease-in-out 0s);
......@@ -275,27 +288,33 @@
}
}
// basic course nav items
// basic course nav items - overrides from above
.course-nav-tab {
padding: ($baseline*.75) $baseline;
padding: ($baseline*.75) ($baseline/4) ($baseline*.75) $baseline;
&.fixed {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: .7;
&.locked {
background-color: $gray-l6;
&:hover {
opacity: 1;
}
}
.course-nav-tab-header {
display: inline-block;
max-width: 80%;
width:80%;
.title {
@extend %t-title4;
font-weight: 300;
color: $gray;
}
}
.course-nav-tab-actions {
display: inline-block;
padding: ($baseline/10);
margin-right: ($baseline*1.5);
}
}
......
......@@ -487,7 +487,7 @@ body.course.unit,.view-unit {
margin-bottom: 0px;
}
// Module Actions, also used for Static Pages
// Module Actions, also used for Pages
.module-actions {
box-shadow: inset 0 1px 2px $shadow;
border-top: 1px solid $gray-l1;
......
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%inherit file="base.html" />
<%block name="title">Pages</%block>
<%block name="title">${_("Pages")}</%block>
<%block name="bodyclass">is-signedin course view-static-pages</%block>
<%block name="jsextra">
......@@ -31,6 +31,7 @@
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Content")}</small>
## Translators: Pages refer to the tabs that appear in the top navigation of each course.
<span class="sr">&gt; </span>${_("Pages")}
</h1>
......@@ -53,27 +54,34 @@
<article class="unit-body">
<div class="tab-list">
<ol class="course-nav-tab-list">
<!-- for testing -->
<li class="course-nav-tab locked">
<div class="course-nav-tab-header">
<h3 class="title">Wiki</h3>
</div>
<div class="course-nav-tab-actions actions-list-wrap">
<ul class="actions-list">
<li class="action-item action-visible">
<label for="[id]"><span class="sr">${_("Show this page")}</span></label>
<input type="checkbox" id="[id]" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" />
<div class="action-button"><i class="icon-eye-open"></i><i class="icon-eye-close"></i></div>
</li>
</ul>
</div>
</li>
<!-- end for testing -->
<ol class="course-nav-tab-list components">
% for tab in built_in_tabs:
<li class="course-nav-tab fixed">
<div class="course-nav-tab-header">
<h3 class="title">${_(tab.name)}</h3>
</div>
<div class="course-nav-tab-actions wrapper-actions-list">
<ul class="actions-list">
% if tab.is_hideable:
<li class="action-item action-visible">
<label for="[id]"><span class="sr">${_("Show this page")}</span></label>
<input type="checkbox" id="[id]" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" />
<div class="action-button"><i class="icon-eye-open"></i><i class="icon-eye-close"></i></div>
</li>
%endif
</ul>
</div>
<div class="drag-handle is-fixed" data-tooltip="${_('Cannot be reordered')}">
<span class="sr">${_("Fixed page")}</span>
</div>
</li>
% endfor
% for locator in components:
<li class="component" data-locator="${locator}"/>
<li class="component" data-locator="${locator}"></li>
% endfor
<li class="new-component-item">
......@@ -81,6 +89,10 @@
</li>
</ol>
</div>
<div class="add-pages">
<p>${_("You can add additional custom pages to your course.")} <a href="#" class="button new-button new-tab"><i class="icon-plus"></i>${_("Add a New Page")}</a></p>
</div>
</article>
</div>
</article>
......@@ -88,7 +100,7 @@
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">${_("What are Pages?")}</h3>
<p>${_("Pages are the items that appear in your course navigation. Some are required (Courseware, Course info, Discussion, Progress), some are optional (Wiki), and you can create your own static pages to hold additional content you want to provide to your students, like a syllabus, calendar, or handouts.")}</p>
<p>${_("Pages are the items that appear in your course navigation. Some are required and cannot be moved or edited (Courseware, Course info, Discussion, Progress, Wiki), and you can add your own custom pages to hold additional content you want to provide to your students, like a syllabus, calendar, or handouts.")}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("How do Pages look to students in my course?")}</h3>
......@@ -100,10 +112,10 @@
</div>
<div class="content-modal" id="preview-lms-staticpages">
<h3 class="title">${_("Static Pages in Your Course")}</h3>
<h3 class="title">${_("Pages in Your Course")}</h3>
<figure>
<img src="${static.url("img/preview-lms-staticpages.png")}" alt="${_('Preview of Static Pages in your course')}" />
<figcaption class="description">${_("The names of your Static Pages appear in your course's main navigation bar, along with Courseware, Course Info, Discussion, Wiki, and Progress.")}</figcaption>
<img src="${static.url("img/preview-lms-staticpages.png")}" alt="${_('Preview of Pages in your course')}" />
<figcaption class="description">${_("The names of your Pages appear in your course's main navigation bar, along with Courseware, Course Info, Discussion, Wiki, and Progress.")}</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
......
......@@ -118,7 +118,7 @@ require(["domReady!", "gettext", "js/views/feedback_prompt"], function(doc, gett
<li class="item-detail">${_("Course Content (all Sections, Sub-sections, and Units)")}</li>
<li class="item-detail">${_("Course Structure")}</li>
<li class="item-detail">${_("Individual Problems")}</li>
<li class="item-detail">${_("Static Pages")}</li>
<li class="item-detail">${_("Pages")}</li>
<li class="item-detail">${_("Course Assets")}</li>
<li class="item-detail">${_("Course Settings")}</li>
</ul>
......
......@@ -53,7 +53,7 @@
<a href="${course_info_url}">${_("Updates")}</a>
</li>
<li class="nav-item nav-course-courseware-pages">
<a href="${tabs_url}">${_("Static Pages")}</a>
<a href="${tabs_url}">${_("Pages")}</a>
</li>
<li class="nav-item nav-course-courseware-uploads">
<a href="${assets_url}">${_("Files &amp; Uploads")}</a>
......
......@@ -12,6 +12,7 @@ from xmodule.modulestore import Location
from xmodule.partitions.partitions import UserPartition
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.graders import grader_from_conf
from xmodule.tabs import CourseTabList
import json
from xblock.fields import Scope, List, String, Dict, Boolean, Integer
......@@ -19,7 +20,6 @@ from .fields import Date
from xmodule.modulestore.locator import CourseLocator
from django.utils.timezone import UTC
log = logging.getLogger(__name__)
......@@ -225,7 +225,7 @@ class CourseFields(object):
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)
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", 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)
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
discussion_topics = Dict(help="Map of topics names to ids", scope=Scope.settings)
......@@ -456,44 +456,15 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self.syllabus_present = False
else:
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self._grading_policy = {}
self._grading_policy = {}
self.set_grading_policy(self.grading_policy)
if self.discussion_topics == {}:
self.discussion_topics = {_('General'): {'id': self.location.html_id()}}
# TODO check that this is still needed here and can't be by defaults.
if not self.tabs:
# When calling the various _tab methods, can omit the 'type':'blah' from the
# first arg, since that's only used for dispatch
tabs = []
tabs.append({'type': 'courseware'})
# Translators: "Course Info" is the name of the course's information and updates page
tabs.append({'type': 'course_info', 'name': _('Course Info')})
if self.syllabus_present:
tabs.append({'type': 'syllabus'})
tabs.append({'type': 'textbooks'})
# # If they have a discussion link specified, use that even if we feature
# # flag discussions off. Disabling that is mostly a server safety feature
# # at this point, and we don't need to worry about external sites.
if self.discussion_link:
tabs.append({'type': 'external_discussion', 'link': self.discussion_link})
else:
# Translators: "Discussion" is the title of the course forum page
tabs.append({'type': 'discussion', 'name': _('Discussion')})
# Translators: "Wiki" is the title of the course's wiki page
tabs.append({'type': 'wiki', 'name': _('Wiki')})
if not self.hide_progress_tab:
# Translators: "Progress" is the title of the student's grade information page
tabs.append({'type': 'progress', 'name': _('Progress')})
self.tabs = tabs
if not getattr(self, "tabs", []):
CourseTabList.initialize_default(self)
def set_grading_policy(self, course_policy):
"""
......
......@@ -264,7 +264,7 @@ class StaticTabFields(object):
)
data = String(
default=textwrap.dedent(u"""\
<p>This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing.</p>
<p>Add the content you want students to see on this page.</p>
"""),
scope=Scope.content,
help="HTML for the additional pages"
......
......@@ -34,6 +34,7 @@ from xmodule.modulestore import ModuleStoreWriteBase, Location, MONGO_MODULESTOR
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import LocationReader
from xmodule.tabs import StaticTab, CourseTabList
from xblock.core import XBlock
log = logging.getLogger(__name__)
......@@ -708,13 +709,12 @@ class MongoModuleStore(ModuleStoreWriteBase):
# TODO move this special casing to app tier (similar to attaching new element to parent)
if location.category == 'static_tab':
course = self._get_course_for_item(location)
existing_tabs = course.tabs or []
existing_tabs.append({
'type': 'static_tab',
'name': new_object.display_name,
'url_slug': new_object.location.name
})
course.tabs = existing_tabs
course.tabs.append(
StaticTab(
name=new_object.display_name,
url_slug=new_object.location.name,
)
)
self.update_item(course)
return new_object
......@@ -797,13 +797,11 @@ class MongoModuleStore(ModuleStoreWriteBase):
if xblock.category == 'static_tab':
course = self._get_course_for_item(xblock.location)
# find the course's reference to this tab and update the name.
for tab in course.tabs:
if tab.get('url_slug') == xblock.location.name:
# only update if changed
if tab['name'] != xblock.display_name:
tab['name'] = xblock.display_name
self.update_item(course, user)
break
static_tab = CourseTabList.get_tab_by_slug(course, xblock.location.name)
# only update if changed
if static_tab and static_tab['name'] != xblock.display_name:
static_tab['name'] = xblock.display_name
self.update_item(course, user)
# recompute (and update) the metadata inheritance tree which is cached
# was conditional on children or metadata having changed before dhm made one update to rule them all
......
......@@ -1393,10 +1393,15 @@ class TestCourseCreation(SplitModuleTest):
original_index = modulestore().get_course_index_info(original_locator)
fields = {}
for field in original.fields.values():
value = getattr(original, field.name)
if not isinstance(value, datetime.datetime):
json_value = field.to_json(value)
else:
json_value = value
if field.scope == Scope.content and field.name != 'location':
fields[field.name] = getattr(original, field.name)
fields[field.name] = json_value
elif field.scope == Scope.settings:
fields[field.name] = getattr(original, field.name)
fields[field.name] = json_value
fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
fields['display_name'] = 'Derivative'
new_draft = modulestore().create_course(
......
......@@ -20,6 +20,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XMLParsingSystem, policy_key
from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS
from xmodule.tabs import CourseTabList
from xblock.fields import ScopeIds
from xblock.field_data import DictFieldData
......@@ -662,9 +663,9 @@ class XMLModuleStore(ModuleStoreReadBase):
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
if category == "static_tab":
for tab in course_descriptor.tabs or []:
if tab.get('url_slug') == slug:
module.display_name = tab['name']
tab = CourseTabList.get_tab_by_slug(course=course_descriptor, url_slug=slug)
if tab:
module.display_name = tab.name
module.data_dir = course_dir
module.save()
......
"""
Static Pages page for a course.
Pages page for a course.
"""
from .course_page import CoursePage
class StaticPagesPage(CoursePage):
class PagesPage(CoursePage):
"""
Static Pages page for a course.
Pages page for a course.
"""
url_path = "tabs"
......
......@@ -21,7 +21,7 @@ class UnitPage(PageObject):
@property
def url(self):
"""URL to the static pages UI in a course."""
"""URL to the pages UI in a course."""
return "{}/unit/{}".format(BASE_URL, self.unit_locator)
def is_browser_on_page(self):
......
......@@ -10,7 +10,7 @@ from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.studio.checklists import ChecklistsPage
from ..pages.studio.course_import import ImportPage
from ..pages.studio.course_info import CourseUpdatesPage
from ..pages.studio.edit_tabs import StaticPagesPage
from ..pages.studio.edit_tabs import PagesPage
from ..pages.studio.export import ExportPage
from ..pages.studio.howitworks import HowitworksPage
from ..pages.studio.index import DashboardPage
......@@ -93,7 +93,7 @@ class CoursePagesTest(UniqueCourseTest):
clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'])
for clz in [
AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage,
StaticPagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage,
PagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage,
AdvancedSettingsPage, GradingPage, TextbooksPage
]
]
......
......@@ -1257,7 +1257,7 @@ def grade_summary(request, course_id):
"""Display the grade summary for a course."""
course = get_course_with_access(request.user, course_id, 'staff')
# For now, just a static page
# For now, just a page
context = {'course': course,
'staff_access': True, }
return render_to_response('courseware/grade_summary.html', context)
......
......@@ -94,6 +94,8 @@ BULK_EMAIL_DEFAULT_FROM_EMAIL = "test@test.org"
# Forums are disabled in test.py to speed up unit tests, but we do not have
# per-test control for acceptance tests
# For consistency in user-experience, keep the value of this setting in sync with
# the one in cms/envs/acceptance.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# Use the auto_auth workflow for creating users and logging them in
......
......@@ -43,9 +43,6 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
# Enable Berkeley forums
FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# IMPORTANT: With this enabled, the server must always be behind a proxy that
# strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise,
# a user can fool our server into thinking it was an https connection.
......
......@@ -77,7 +77,10 @@ FEATURES = {
# set to None to do no university selection
'ENABLE_TEXTBOOK': True,
# for consistency in user-experience, keep the value of this setting in sync with the one in cms/envs/common.py
'ENABLE_DISCUSSION_SERVICE': True,
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
# this should remain off in production until digest notifications are online.
'ENABLE_DISCUSSION_HOME_PANEL': False,
......
......@@ -25,7 +25,8 @@ FEATURES['DISABLE_START_DATES'] = True
# Most tests don't use the discussion service, so we turn it off to speed them up.
# Tests that do can enable this flag, but must use the UrlResetMixin class to force urls.py
# to reload
# to reload. For consistency in user-experience, keep the value of this setting in sync with
# the one in cms/envs/test.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
FEATURES['ENABLE_SERVICE_STATUS'] = True
......
......@@ -12,26 +12,36 @@ def url_class(is_active):
return "active"
return ""
%>
<%! from courseware.tabs import get_course_tabs %>
<%! from xmodule.tabs import CourseTabList %>
<%! from courseware.access import has_access %>
<%! from django.conf import settings %>
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%! from courseware.views import notification_image_for_tab %>
<% import waffle %>
<nav class="${active_page} course-material">
<div class="inner-wrapper">
<ol class="course-tabs">
% for tab in get_course_tabs(user, course, active_page, request):
% if waffle.flag_is_active(request, 'visual_treatment') or waffle.flag_is_active(request, 'merge_course_tabs'):
% for tab in CourseTabList.iterate_displayable(course, settings, user.is_authenticated(), has_access(user, course, 'staff'), include_instructor_tab=True):
<%
tab_is_active = (tab.tab_id == active_page)
tab_image = notification_image_for_tab(tab, user, course)
%>
% if waffle.flag_is_active(request, 'visual_treatment'):
<li class="${"prominent" if tab.name in ("Courseware", "Course Content") else ""}">
% else:
<li>
% endif
<a href="${tab.link | h}" class="${url_class(tab.is_active)}">
<a href="${tab.link_func(course, reverse) | h}" class="${url_class(tab_is_active)}">
${_(tab.name) | h}
% if tab.is_active == True:
% if tab_is_active:
<span class="sr">, current location</span>
%endif
% if tab.has_img == True:
<img src="${tab.img}"/>
% if tab_image:
## Translators: 'needs attention' is an alternative string for the
## notification image that indicates the tab "needs attention".
<img src="${tab_image}" alt="${_('needs attention')}" />
%endif
</a>
</li>
......
......@@ -11,31 +11,3 @@ import waffle
section_name=prev_section.display_name_with_default,
)
)}</p>
% if waffle.flag_is_active(request, 'merge_course_tabs'):
<%! from courseware.courses import get_course_info_section %>
<section class="container">
<div class="info-wrapper">
% if user.is_authenticated():
<section class="updates">
<h1>${_("Course Updates &amp; News")}</h1>
${get_course_info_section(request, course, 'updates')}
</section>
<section aria-label="${_('Handout Navigation')}" class="handouts">
<h1>${course.info_sidebar_name}</h1>
${get_course_info_section(request, course, 'handouts')}
</section>
% else:
<section class="updates">
<h1>${_("Course Updates &amp; News")}</h1>
${get_course_info_section(request, course, 'guest_updates')}
</section>
<section aria-label="${_('Handout Navigation')}" class="handouts">
<h1>${_("Course Handouts")}</h1>
${get_course_info_section(request, course, 'guest_handouts')}
</section>
% endif
</div>
</section>
% endif
......@@ -22,10 +22,7 @@
<li class="course-item">
<article class="course ${enrollment.mode}">
<%
if waffle.flag_is_active(request, 'merge_course_tabs'):
course_target = reverse('courseware', args=[course.id])
else:
course_target = reverse('info', args=[course.id])
course_target = reverse('info', args=[course.id])
%>
% if show_courseware_link:
......
......@@ -4,7 +4,8 @@
<%! from datetime import datetime %>
<%! import pytz %>
<%! from django.conf import settings %>
<%! from courseware.tabs import get_discussion_link %>
<%! from django.core.urlresolvers import reverse %>
<%! from xmodule.tabs import CourseTabList %>
<%! from microsite_configuration import microsite %>
<%! platform_name = microsite.get_value("platform_name", settings.PLATFORM_NAME) %>
......@@ -31,7 +32,8 @@
</header>
<%
discussion_link = get_discussion_link(course) if course else None
discussion_tab = CourseTabList.get_discussion(course) if course else None
discussion_link = discussion_tab.link_func(course, reverse) if (discussion_tab and discussion_tab.can_display(course, settings, True, True)) else None
%>
% if discussion_link:
......
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