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