Merge branch 'master' into drupal-new

This commit is contained in:
Diana Huang
2013-05-16 16:27:36 -04:00
237 changed files with 11314 additions and 3995 deletions

View File

@@ -11,7 +11,8 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot click notification "Cancel"
@skip
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
@@ -20,7 +21,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot click notification "Save"
@skip
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
@@ -28,7 +30,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
@@ -36,7 +39,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes

View File

@@ -10,8 +10,6 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
@@ -19,8 +17,6 @@ Feature: Course checklists
And I press the browser back button
Then I am brought back to the course outline in the correct state
@skip-phantom
@skip-firefox
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page

View File

@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_equal
from nose.tools import assert_true, assert_equal, assert_in
from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
@@ -63,7 +63,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_in('Course Outline', world.css_find('.outline .page-header')[0].text)
assert_equal(1, len(world.browser.windows))

View File

@@ -1,20 +1,17 @@
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date
Given I have set course dates
And I clear the course start date

View File

@@ -3,7 +3,6 @@ Feature: Create Section
As a course author
I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course
Given I have opened a new course in Studio
When I click the New Section link
@@ -27,7 +26,8 @@ Feature: Create Section
And I save a new section release date
Then the section release date is updated
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete section
Given I have opened a new course in Studio
And I have added a new section

View File

@@ -18,10 +18,7 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = world.css_find(submit_css)
e.type(' ')
world.css_click(submit_css)
@step('I should see be on the studio home page$')

View File

@@ -14,7 +14,6 @@ Feature: Overview Toggle Section
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
@@ -22,7 +21,8 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page

View File

@@ -3,14 +3,12 @@ Feature: Create Subsection
As a course author
I want to create and edit subsections
@skip-phantom
Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
@skip-phantom
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
@@ -27,7 +25,6 @@ Feature: Create Subsection
And I reload the page
Then I see it marked as Homework
@skip-phantom
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
@@ -35,7 +32,8 @@ Feature: Create Subsection
And I reload the page
Then I see the correct dates
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection

View File

@@ -63,14 +63,6 @@ def test_have_set_dates_in_different_years(step):
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('03:00', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('04:00', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$')
def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle')
@@ -101,8 +93,20 @@ def the_subsection_does_not_exist(step):
assert world.browser.is_element_not_present_by_css(css)
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', get_date('input#start_date'))
assert_equal('03:00', get_date('input#start_time'))
assert_equal('01/02/2012', get_date('input#due_date'))
assert_equal('04:00', get_date('input#due_time'))
############ HELPER METHODS ###################
def get_date(css):
return world.css_find(css).first.value.strip()
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'

View File

@@ -1,4 +1,5 @@
from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand
@@ -6,4 +7,4 @@ class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
update_templates()
update_templates(modulestore('direct'))

View File

@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1)
def test_import_textbook_as_content_element(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -293,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
@@ -490,6 +497,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
# make sure the textbook survived the export/import
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
@@ -634,7 +646,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('index'))
self.assertContains(
resp,
'<h1 class="title-1">My Courses</h1>',
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True
)
@@ -937,7 +949,7 @@ class TemplateTestCase(ModuleStoreTestCase):
self.assertIsNotNone(verify_create)
# now run cleanup
update_templates()
update_templates(modulestore('direct'))
# now try to find dangling template, it should not be in DB any longer
asserted = False

View File

@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase):
'''Go through each interface and ensure it works.'''
# first get the update to force the creation
url = reverse('course_info',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'name': self.course_location.name})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'name': self.course_location.name})
self.client.get(url)
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 8, 2013'}
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(payload['content'], content)
first_update_url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': payload['id']})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>'
payload['content'] = content
resp = self.client.post(first_update_url, json.dumps(payload),
"application/json")
"application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div")
"iframe w/ div")
# now put in an evil update
content = '<ol/>'
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(content, payload['content'], "self closing ol")
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2)
# can't test non-json paylod b/c expect_json throws error
# try json w/o required fields
self.assertContains(
self.client.post(url, json.dumps({'garbage': 1}),
"application/json"),
'Failed to save', status_code=400)
self.assertContains(self.client.post(url, json.dumps({'garbage': 1}),
"application/json"),
'Failed to save', status_code=400)
# now try to update a non-existent update
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '9'})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '9'})
content = 'blah blah'
payload = {'content': content,
'date': 'January 21, 2013'}
@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"),
@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase):
# now try to delete a non-existent update
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '19'})
'course': self.course_location.course,
'provided_id': '19'})
payload = {'content': content,
'date': 'January 21, 2013'}
self.assertContains(self.client.delete(url), "delete", status_code=400)
@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 28, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
this_id = payload['id']
self.assertHTMLEqual(content, payload['content'], "single iframe")
# first count the entries
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
before_delete = len(payload)
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': this_id})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': this_id})
resp = self.client.delete(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1)

View File

@@ -6,6 +6,7 @@ from django.test.client import Client
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class InternationalizationTest(ModuleStoreTestCase):
"""
Tests to validate Internationalization.
@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
}
def test_course_plain_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True)
def test_course_explicit_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='en'
)
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True)
# ****
# NOTE:
@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase):
# This test will break when we replace this fake 'test' language
# with actual French. This test will need to be updated with
# actual French at that time.
# Test temporarily disable since it depends on creation of dummy strings
@skip
def test_course_with_accents (self):
def test_course_with_accents(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='fr'
@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase):
TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>'
self.assertContains(resp,
TEST_STRING,
status_code=200,
html=True)
html=True)

View File

@@ -1,6 +1,8 @@
""" Tests for utils. """
from contentstore import utils
import mock
import collections
import copy
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase):
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """
def get_tab_type_dicts(self, tab_types):
""" Returns an array of tab dictionaries. """
if tab_types:
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
else:
return []
def get_course_with_tabs(self, tabs=[]):
""" Returns a mock course object with a tabs attribute. """
course = collections.namedtuple('MockCourse', ['tabs'])
if isinstance(tabs, basestring):
course.tabs = self.get_tab_type_dicts(tabs)
else:
course.tabs = tabs
return course
def test_add_extra_panel_tab(self):
""" Tests if a tab can be added to a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test adding with changed = True
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
expected_tabs.append(tab)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test adding with changed = False
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
def test_remove_extra_panel_tab(self):
""" Tests if a tab can be removed from a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test removing with changed = True
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test removing with changed = False
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)

View File

@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location):
@@ -192,9 +194,10 @@ class CoursePageNames:
Checklists = "checklists"
def add_open_ended_panel_tab(course):
def add_extra_panel_tab(tab_type, course):
"""
Used to add the open ended panel tab to a course if it does not exist.
Used to add the panel tab to a course if it does not exist.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL)
course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
def remove_extra_panel_tab(tab_type, course):
"""
Used to remove the open ended panel tab from a course if it exists.
Used to remove the panel tab from a course if it exists.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs

View File

@@ -1,1685 +0,0 @@
from util.json_request import expect_json
import json
import logging
import os
import sys
import time
import tarfile
import shutil
from collections import defaultdict
from uuid import uuid4
from path import path
from xmodule.modulestore.xml_exporter import export_to_xml
from tempfile import mkdtemp
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
import static_replace
from external_auth.views import ssl_login_shortcut
from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError, ProcessingError
from functools import partial
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
remove_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \
update_course_updates, delete_course_update
from cache_toolbox.core import del_cached_content
from contentstore.module_info_model import get_module_info, set_module_info
from models.settings.course_details import CourseDetails, \
CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.shortcuts import redirect
from models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
# ==== Public views ==================================================
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})
# static/proof-of-concept views
def ux_alerts(request):
return render_to_response('ux-alerts.html', {})
# ==== Views for any logged-in user ==================================
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
# ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
"""
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course = modulestore().get_item(location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
'active_tab': 'courseware',
'context_course': course,
'lms_link': lms_link,
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
})
@login_required
def edit_subsection(request, location):
# check that we have permissions to edit this item
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest()
parent_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict(
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
for unit in subsection_units:
state = compute_unit_state(unit)
if state == UnitState.public or state == UnitState.draft:
can_view_live = True
break
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
@login_required
def edit_unit(request, location):
"""
Display an editing page for the specified module.
Expects a GET request with the parameter 'id'.
id: A Location URL
"""
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
))
components = [
component.location.url()
for component
in item.get_children()
]
# TODO (cpennington): If we share units between courses,
# this will need to change to check permissions correctly so as
# to pick the correct parent subsection
containing_subsection_locs = modulestore().get_parent_locations(location, None)
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
# so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this
index = 1
for child in containing_subsection.get_children():
if child.location == item.location:
break
index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
unit_state = compute_unit_state(item)
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
'unit': item,
'unit_location': location,
'components': components,
'component_templates': component_templates,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
@expect_json
@login_required
@ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name):
'''
CRUD operations on assignment types for sections and subsections and anything else gradable.
'''
location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location):
raise HttpResponseForbidden()
if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json")
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest()
# construct a location from the passed in path
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
try:
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent
filename = request.FILES['file'].name
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location)
# now store thumbnail location only if we could create it
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
# readback the saved content - we need the database timestamp
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# 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
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 = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
@login_required
@ensure_csrf_cookie
def course_info(request, org, course, name, provided_id=None):
"""
Send models and views as well as html for editing the course info to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
return render_to_response('course_info.html', {
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_info_updates(request, org, course, provided_id=None):
"""
restful CRUD operations on course_info updates.
org, course: Attributes of the Location for the item to edit
provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
"""
# ??? No way to check for access permission afaik
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '':
provided_id = None
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
@expect_json
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
location = Location(module_location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
else:
return HttpResponseBadRequest()
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings.html', {
'context_course': course_module,
'course_location': location,
'details_url': reverse(course_settings_updates,
kwargs={"org": org,
"course": course,
"name": name,
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
"""
Send models and views as well as html for editing the advanced course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section):
"""
restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
# # NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_advanced_updates(request, org, course, name):
"""
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
the payload is either a key or a list of keys to delete.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
request_body = json.loads(request.body)
#Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
#module, and to remove it if they have removed the open ended elements.
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Check to see if the user instantiated any open ended components
found_oe_type = False
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add an open ended tab to the course if needed
changed, new_tabs = add_open_ended_panel_tab(course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the open ended tab removal code below.
found_oe_type = True
break
#If we did not find an open ended module type in the advanced settings,
# we may need to remove the open ended tab from the course.
if not found_oe_type:
#Remove open ended tab to the course if needed
changed, new_tabs = remove_open_ended_panel_tab(course_module)
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
"""
Display an editable asset library
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
asset_display = []
for asset in assets:
id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
@login_required
@expect_json
def create_new_course(request):
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
except InvalidLocationError as e:
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
# see if the course already exists
existing_course = None
try:
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
if existing_course is not None:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
new_course = modulestore('direct').clone_item(template, dest_location)
# clone a default 'about' module as well
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
dest_about_location = dest_location._replace(category='about', name='overview')
modulestore('direct').clone_item(about_template_location, dest_about_location)
if display_name is not None:
new_course.display_name = display_name
# set a default start date to now
new_course.start = time.gmtime()
initialize_course_tabs(new_course)
create_all_course_groups(request.user, new_course.location)
return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course):
# 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 = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tf = tarfile.open(temp_filepath)
tf.extractall(course_dir + '/')
# find the 'course.xml' file
for r, d, f in os.walk(course_dir):
for files in f:
if files == 'course.xml':
break
if files == 'course.xml':
break
if files != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(r))
if r != course_dir:
for fname in os.listdir(r):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
# export out to a tempdir
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz')
tf.add(root_dir / name, arcname=name)
tf.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'active_tab': 'export',
'successful_import_redirect_url': ''
})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-)
'''
return HttpResponse(True)
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method

View File

@@ -0,0 +1,15 @@
# pylint: disable=W0401, W0511
# Disable warnings about import from wildcard
# All files below declare exports with __all__
from .assets import *
from .checklist import *
from .component import *
from .course import *
from .error import *
from .item import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *

View File

@@ -0,0 +1,36 @@
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from django.core.exceptions import PermissionDenied
from ..utils import get_course_location_for_item
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access

View File

@@ -0,0 +1,263 @@
import logging
import json
import os
import tarfile
import shutil
from tempfile import mkdtemp
from path import path
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from auth.authz import create_all_course_groups
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from ..utils import get_url_reverse
from .access import get_location_and_verify_access
__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
"""
Display an editable asset library
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
asset_display = []
for asset in assets:
asset_id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest()
# construct a location from the passed in path
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
try:
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent
filename = request.FILES['file'].name
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location)
# now store thumbnail location only if we could create it
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
# readback the saved content - we need the database timestamp
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tar_file = tarfile.open(temp_filepath)
tar_file.extractall(course_dir + '/')
# find the 'course.xml' file
for dirpath, _dirnames, filenames in os.walk(course_dir):
for files in filenames:
if files == 'course.xml':
break
if files == 'course.xml':
break
if files != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(dirpath))
if dirpath != course_dir:
for fname in os.listdir(dirpath):
shutil.move(dirpath / fname, course_dir)
_module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
# export out to a tempdir
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tar_file = tarfile.open(name=export_file.name, mode='w:gz')
tar_file.add(root_dir / name, arcname=name)
tar_file.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'active_tab': 'export',
'successful_import_redirect_url': ''
})

View File

@@ -0,0 +1,104 @@
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .requests import get_request_method
from .access import get_location_and_verify_access
__all__ = ['get_checklists', 'update_checklist']
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified

View File

@@ -0,0 +1,304 @@
import json
import logging
from collections import defaultdict
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.util.date_utils import get_default_time_display
from xblock.core import Scope
from util.json_request import expect_json
from contentstore.module_info_model import get_module_info, set_module_info
from contentstore.utils import get_modulestore, get_lms_link_for_item, \
compute_unit_state, UnitState, get_course_for_item
from models.settings.course_grading import CourseGradingModel
from .requests import get_request_method, _xmodule_recurse
from .access import has_access
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
'edit_subsection',
'edit_unit',
'assignment_type_update',
'create_draft',
'publish_draft',
'unpublish_unit',
'module_info']
log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required
def edit_subsection(request, location):
# check that we have permissions to edit this item
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest()
parent_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict(
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
for unit in subsection_units:
state = compute_unit_state(unit)
if state == UnitState.public or state == UnitState.draft:
can_view_live = True
break
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
@login_required
def edit_unit(request, location):
"""
Display an editing page for the specified module.
Expects a GET request with the parameter 'id'.
id: A Location URL
"""
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
))
components = [
component.location.url()
for component
in item.get_children()
]
# TODO (cpennington): If we share units between courses,
# this will need to change to check permissions correctly so as
# to pick the correct parent subsection
containing_subsection_locs = modulestore().get_parent_locations(location, None)
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
# so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this
index = 1
for child in containing_subsection.get_children():
if child.location == item.location:
break
index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
unit_state = compute_unit_state(item)
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
'unit': item,
'unit_location': location,
'components': components,
'component_templates': component_templates,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@expect_json
@login_required
@ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name):
'''
CRUD operations on assignment types for sections and subsections and anything else gradable.
'''
location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location):
raise HttpResponseForbidden()
if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json")
@login_required
@expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@expect_json
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
location = Location(module_location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
else:
return HttpResponseBadRequest()

View File

@@ -0,0 +1,405 @@
"""
Views related to operations on course objects
"""
import json
import time
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions \
import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore import Location
from contentstore.course_info_model \
import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils \
import get_lms_link_for_item, add_extra_panel_tab, \
remove_extra_panel_tab
from models.settings.course_details \
import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from auth.authz import create_all_course_groups
from util.json_request import expect_json
from .access import has_access, get_location_and_verify_access
from .requests import get_request_method
from .tabs import initialize_course_tabs
from .component import OPEN_ENDED_COMPONENT_TYPES, \
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
'course_config_advanced_page',
'course_settings_updates',
'course_grader_updates',
'course_advanced_updates']
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
"""
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course = modulestore().get_item(location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
'active_tab': 'courseware',
'context_course': course,
'lms_link': lms_link,
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
})
@login_required
@expect_json
def create_new_course(request):
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
except InvalidLocationError as error:
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" +
display_name + "'.\n\n" + error.message}))
# see if the course already exists
existing_course = None
try:
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
if existing_course is not None:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
new_course = modulestore('direct').clone_item(template, dest_location)
# clone a default 'about' module as well
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
dest_about_location = dest_location._replace(category='about', name='overview')
modulestore('direct').clone_item(about_template_location, dest_about_location)
if display_name is not None:
new_course.display_name = display_name
# set a default start date to now
new_course.start = time.gmtime()
initialize_course_tabs(new_course)
create_all_course_groups(request.user, new_course.location)
return HttpResponse(json.dumps({'id': new_course.location.url()}))
@login_required
@ensure_csrf_cookie
def course_info(request, org, course, name, provided_id=None):
"""
Send models and views as well as html for editing the course info to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
return render_to_response('course_info.html', {
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_info_updates(request, org, course, provided_id=None):
"""
restful CRUD operations on course_info updates.
org, course: Attributes of the Location for the item to edit
provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
"""
# ??? No way to check for access permission afaik
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '':
provided_id = None
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings.html', {
'context_course': course_module,
'course_location': location,
'details_url': reverse(course_settings_updates,
kwargs={"org": org,
"course": course,
"name": name,
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
"""
Send models and views as well as html for editing the advanced course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section):
"""
restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
# # NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_advanced_updates(request, org, course, name):
"""
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
the payload is either a key or a list of keys to delete.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)),
mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location,
json.loads(request.body))),
mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json
# cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
request_body = json.loads(request.body)
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
#Check to see if the user instantiated any advanced components. This is a hack
#that does the following :
# 1) adds/removes the open ended panel tab to a course automatically if the user
# has indicated that they want to edit the combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if the user has
# indicated that they want the notes module enabled in their course
# TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
#Maps tab types to components
tab_component_map = {
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES,
}
#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
for ac_type in component_types:
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the tab removal code below.
found_ac_type = True
break
#If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type:
#Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
#Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body,
filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")

View File

@@ -0,0 +1,20 @@
from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))

View File

@@ -0,0 +1,138 @@
import json
from uuid import uuid4
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
__all__ = ['save_item', 'clone_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()

View File

@@ -0,0 +1,177 @@
import logging
import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .access import has_access
__all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html

View File

@@ -0,0 +1,58 @@
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts']
"""
Public views
"""
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})
def ux_alerts(request):
"""
static/proof-of-concept views
"""
return render_to_response('ux-alerts.html', {})

View File

@@ -0,0 +1,60 @@
import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-)
'''
return HttpResponse(True)
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)

View File

@@ -0,0 +1,28 @@
from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session

View File

@@ -0,0 +1,132 @@
from access import has_access
from util.json_request import expect_json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
def initialize_course_tabs(course):
# 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 = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# 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
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 = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})

View File

@@ -0,0 +1,144 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from .access import has_access
from .requests import create_json_response
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()

View File

@@ -8,27 +8,41 @@ from .test import *
# otherwise the browser will not render the pages correctly
DEBUG = True
# Show the courses that are in the data directory
COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# MODULESTORE = {
# 'default': {
# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
# 'OPTIONS': {
# 'data_dir': DATA_DIR,
# 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
# }
# }
# }
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
# Set this up so that rake lms[acceptance] and running the
# harvest command both use the same (test) database
# which they can flush without messing up your dev db
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "test_mitx.db",
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
}
}

View File

@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value

View File

@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = (
('edX Admins', 'admin@edx.org'),
)

View File

@@ -33,7 +33,7 @@ PIPELINE_JS['spec'] = {
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
# Remove the localization middleware class because it requires the test database
# to be sync'd and migrated in order to run the jasmine tests interactively

View File

@@ -41,8 +41,8 @@ MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'collection': 'test_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}

View File

@@ -22,6 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({
}
);
},
reset: true,
error: CMS.ServerError
}
);

View File

@@ -160,11 +160,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event);
this.modelDom(event).remove();
var cacheThis = this;
targetModel.destroy({success : function (model, response) {
cacheThis.collection.fetch({success : function() {cacheThis.render();},
error : CMS.ServerError});
},
error : CMS.ServerError
targetModel.destroy({
success: function (model, response) {
cacheThis.collection.fetch({
success: function() {
cacheThis.render();
},
reset: true,
error: CMS.ServerError
});
},
error : CMS.ServerError
});
},
@@ -238,20 +244,19 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
initialize: function() {
var self = this;
this.model.fetch(
{
complete: function() {
window.templateLoader.loadRemoteTemplate("course_info_handouts",
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
error : CMS.ServerError
}
);
this.model.fetch({
complete: function() {
window.templateLoader.loadRemoteTemplate("course_info_handouts",
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
reset: true,
error: CMS.ServerError
});
},
render: function () {

View File

@@ -155,6 +155,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
self.model.clear({silent : true});
self.model.fetch({
success : function() { self.render(); },
reset: true,
error : CMS.ServerError
});
},

View File

@@ -8,11 +8,10 @@ html {
}
body {
@include font-size(16);
@extend .t-copy-base;
min-width: $fg-min-width;
background: $gray-l5;
line-height: 1.6;
color: $baseFontColor;
color: $gray-d2;
}
body, input {
@@ -30,7 +29,7 @@ a {
}
h1 {
@include font-size(28);
@extend .t-title4;
font-weight: 300;
}
@@ -51,43 +50,183 @@ h1 {
// ====================
// typography - basic
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 {
.page-header {
@extend .t-title3;
display: block;
font-weight: 600;
color: $gray-d3;
margin: 0;
padding: 0;
.subtitle {
@extend .t-title7;
position: relative;
top: ($baseline/4);
display: block;
color: $gray-l2;
font-weight: 400;
}
}
.section-header {
@extend .t-title4;
font-weight: 600;
.subtitle {
@extend .t-title7;
}
}
.area-header {
@extend .t-title6;
font-weight: 600;
.subtitle {
@extend .t-title8;
}
}
.area-subheader {
@extend .t-title7;
font-weight: 600;
.subtitle {
@extend .t-title9;
}
}
// ====================
// typography - primary content
.content-primary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.area-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.area-subheader {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
}
// typography - primary content
.content-secondary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
}
// ====================
// typography - loose headings (BT: needs to be removed once html is clean)
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 {
font-weight: 600;
}
// typography - primary content
.content-secondary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
}
// ====================
// typography - loose headings (BT: needs to be removed once html is clean)
.title-1 {
@include font-size(32);
@extend .t-title3;
margin-bottom: ($baseline*1.5);
}
.title-2 {
@include font-size(24);
@extend .t-title4;
margin-bottom: $baseline;
}
.title-3 {
@include font-size(18);
@extend .t-title5;
margin-bottom: ($baseline/2);
}
.title-4 {
@include font-size(14);
@extend .t-title7;
margin-bottom: $baseline;
font-weight: 500
}
.title-5 {
@include font-size(14);
@extend .t-title7;
color: $gray-l1;
margin-bottom: $baseline;
font-weight: 500
}
.title-6 {
@include font-size(14);
@extend .t-title7;
color: $gray-l2;
margin-bottom: $baseline;
font-weight: 500
@@ -118,7 +257,6 @@ p, ul, ol, dl {
.mast, .metadata {
@include clearfix();
@include font-size(16);
position: relative;
max-width: $fg-max-width;
min-width: $fg-min-width;
@@ -131,53 +269,8 @@ p, ul, ol, dl {
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
.title-sub {
@include font-size(14);
position: relative;
top: ($baseline/4);
display: block;
margin: 0;
color: $gray-l2;
font-weight: 400;
}
.title, .title-1 {
@include font-size(32);
margin: 0;
padding: 0;
font-weight: 600;
color: $gray-d3;
}
.nav-hierarchy {
@include font-size(14);
display: block;
margin: 0;
color: $gray-l2;
font-weight: 400;
.nav-item {
display: inline;
vertical-align: middle;
margin-right: ($baseline/4);
&:after {
content: ">>";
margin-left: ($baseline/4);
}
&:last-child {
margin-right: 0;
&:after {
content: none;
}
}
}
}
// layout with actions
.title {
.page-header {
width: flex-grid(12);
}
@@ -185,7 +278,7 @@ p, ul, ol, dl {
&.has-actions {
@include clearfix();
.title {
.page-header {
float: left;
width: flex-grid(6,12);
margin-right: flex-gutter();
@@ -210,22 +303,20 @@ p, ul, ol, dl {
// buttons
.button {
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2) !important;
font-weight: 400 !important;
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2);
}
.new-button {
font-weight: 700 !important;
}
.view-button {
font-weight: 700 !important;
}
.upload-button .icon-create {
@include font-size(18);
margin-top: ($baseline/4);
@extend .t-action2;
line-height: 0 !important;
}
}
}
@@ -254,7 +345,7 @@ p, ul, ol, dl {
.content {
@include clearfix();
@include font-size(16);
@extend .t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
@@ -268,14 +359,14 @@ p, ul, ol, dl {
padding-bottom: ($baseline/2);
.title-sub {
@include font-size(14);
@extend .t-copy-sub1;
display: block;
margin: 0;
color: $gray-l2;
}
.title-1 {
@include font-size(32);
@extend .t-title3;
margin: 0;
padding: 0;
font-weight: 600;
@@ -285,7 +376,7 @@ p, ul, ol, dl {
.introduction {
@include box-sizing(border-box);
@include font-size(14);
@extend .t-copy-sub1;
width: flex-grid(12);
margin: 0 0 $baseline 0;
@@ -303,14 +394,14 @@ p, ul, ol, dl {
}
.nav-introduction-supplementary {
@include font-size(13);
@extend .t-copy-sub2;
float: right;
width: flex-grid(4,12);
display: block;
text-align: right;
.icon {
@include font-size(14);
@extend .t-action3;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
@@ -327,21 +418,17 @@ p, ul, ol, dl {
// layout - primary content
.content-primary {
.title-1, .title-2, .title-3, .title-4, .title-5, .title-5 {
color: $gray-d3;
}
.title-1 {
@extend .t-title-1;
@extend .t-title3;
}
.title-2 {
@extend .t-title-2;
@extend .t-title4;
margin: 0 0 ($baseline/2) 0;
}
.title-3 {
@extend .t-title-3;
@extend .t-title6;
margin: 0 0 ($baseline/2) 0;
}
@@ -355,7 +442,7 @@ p, ul, ol, dl {
}
.tip {
@include font-size(13);
@extend .t-copy-sub2;
width: flex-grid(7, 12);
float: right;
margin-top: ($baseline/2);
@@ -373,7 +460,7 @@ p, ul, ol, dl {
}
.bit {
@include font-size(13);
@extend .t-copy-sub1;
margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0;
@@ -386,7 +473,7 @@ p, ul, ol, dl {
}
h3 {
@include font-size(14);
@extend .t-title7;
margin: 0 0 ($baseline/4) 0;
color: $gray-d2;
font-weight: 600;
@@ -494,7 +581,7 @@ p, ul, ol, dl {
// misc
hr.divide {
@include text-sr();
@extend .text-sr;
}
.item-details {
@@ -655,7 +742,7 @@ hr.divide {
.new-button {
@include green-button;
@include font-size(13);
@extend .t-action4;
padding: 8px 20px 10px;
text-align: center;
@@ -674,7 +761,7 @@ hr.divide {
.view-button {
@include blue-button;
@include font-size(13);
@extend .t-copy-base;
text-align: center;
&.big {
@@ -693,7 +780,7 @@ hr.divide {
.edit-button.standard,
.delete-button.standard {
@include font-size(12);
@extend .t-action4;
@include white-button;
float: left;
padding: 3px 10px 4px;
@@ -714,6 +801,7 @@ hr.divide {
}
.tooltip {
@extend .t-copy-sub2;
position: absolute;
top: 0;
left: 0;
@@ -721,7 +809,6 @@ hr.divide {
padding: 0 10px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.85);
font-size: 11px;
font-weight: normal;
line-height: 26px;
color: #fff;
@@ -745,7 +832,7 @@ hr.divide {
// basic utility
.sr {
@include text-sr();
@extend .text-sr;
}
.fake-link {
@@ -798,7 +885,7 @@ body.js {
text-align: center;
.label {
@include text-sr();
@extend .text-sr;
}
.ss-icon {
@@ -821,14 +908,14 @@ body.js {
}
.title {
@include font-size(18);
@extend .t-title5;
margin: 0 0 ($baseline/2) 0;
font-weight: 600;
color: $gray-d3;
}
.description {
@include font-size(13);
@extend .t-copy-sub2;
margin-top: ($baseline/2);
color: $gray-l1;
}

View File

@@ -1,437 +0,0 @@
// studio - utilities - mixins and extends
// ====================
// mixins - utility
@mixin clearfix {
&:after {
content: '';
display: block;
height: 0;
visibility: hidden;
clear: both;
}
}
// mixins - grandfathered
@mixin button {
display: inline-block;
padding: ($baseline/5) $baseline ($baseline/4);
@include font-size(14);
font-weight: 700;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
@include transition(background-color .15s, box-shadow .15s);
&.disabled {
border: 1px solid $gray-l1 !important;
border-radius: 3px !important;
background: $gray-l1 !important;
color: $gray-d1 !important;
pointer-events: none;
cursor: none;
&:hover {
box-shadow: 0 0 0 0 !important;
}
}
&:hover, &.active {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
}
}
@mixin green-button {
@include button;
border: 1px solid $green-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $white;
&:hover {
background-color: $green-s1;
color: $white;
}
&.disabled {
border: 1px solid $green-l3 !important;
background: $green-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin blue-button {
@include button;
border: 1px solid $blue-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $blue;
color: $white;
&:hover, &.active {
background-color: $blue-s2;
color: $white;
}
&.disabled {
border: 1px solid $blue-l3 !important;
background: $blue-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin red-button {
@include button;
border: 1px solid $red-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $red;
color: $white;
&:hover, &.active {
background-color: $red-s1;
color: $white;
}
&.disabled {
border: 1px solid $red-l3 !important;
background: $red-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin pink-button {
@include button;
border: 1px solid $pink-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $pink;
color: $white;
&:hover, &.active {
background-color: $pink-s1;
color: $white;
}
&.disabled {
border: 1px solid $pink-l3 !important;
background: $pink-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin orange-button {
@include button;
border: 1px solid $orange-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
background-color: $orange;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $gray-d2;
&:hover {
background-color: $orange-s2;
color: $gray-d2;
}
&.disabled {
border: 1px solid $orange-l3 !important;
background: $orange-l2 !important;
color: $gray-l1 !important;
@include box-shadow(none);
}
}
@mixin white-button {
@include button;
border: 1px solid $mediumGrey;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: rgb(92, 103, 122);
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
&:hover {
background-color: rgb(222, 236, 247);
color: rgb(92, 103, 122);
}
}
@mixin grey-button {
@include button;
border: 1px solid $gray-d2;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #d1dae3;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #6d788b;
&:hover {
background-color: #d9e3ee;
color: #6d788b;
}
}
@mixin gray-button {
@include button;
border: 1px solid $gray-d1;
border-radius: 3px;
@include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0));
background-color: $gray-d2;
@include box-shadow(0 1px 0 $white-t1 inset);
color: $gray-l3;
&:hover {
background-color: $gray-d3;
color: $white;
}
}
@mixin dark-grey-button {
@include button;
border: 1px solid $gray-d2;
border-radius: 3px;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
color: $white;
&:hover {
background-color: $gray-d4;
color: $white;
}
}
@mixin edit-box {
padding: 15px 20px;
border-radius: 3px;
background-color: $lightBluishGrey2;
color: #3c3c3c;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
label {
color: $baseFontColor;
}
input,
textarea {
border: 1px solid $darkGrey;
}
textarea {
min-height: 80px;
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.row {
margin-bottom: 10px;
padding: 0;
border: none;
}
.save-button {
@include blue-button;
margin-top: 0;
}
.cancel-button {
@include white-button;
margin-top: 0;
}
}
@mixin tree-view {
border: 1px solid $mediumGrey;
background: $lightGrey;
.branch {
margin-bottom: 10px;
&.collapsed {
margin-bottom: 0;
}
}
.branch > .section-item {
border-top: 1px solid #c5cad4;
}
.section-item {
position: relative;
display: block;
padding: 6px 8px 8px 16px;
background: #edf1f5;
font-size: 13px;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
}
}
&.editing {
background: #fffcf1;
}
.draft-item:after,
.public-item:after,
.private-item:after {
margin-left: 3px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
}
.draft-item:after {
content: "- draft";
}
.private-item:after {
content: "- private";
}
.private-item {
color: #a4aab7;
}
.draft-item {
color: #9f7d10;
}
}
a {
color: $baseFontColor;
&.new-unit-item {
color: #6d788b;
}
}
ol {
.section-item {
padding-left: 56px;
}
.new-unit-item {
margin-left: 56px;
}
}
ol ol {
.section-item {
padding-left: 96px;
}
.new-unit-item {
margin-left: 96px;
}
}
}
// ====================
// sunsetted mixins
@mixin active {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: rgba(255, 255, 255, .3);
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
// ====================
// extends - buttons
.btn {
@include box-sizing(border-box);
@include transition(color 0.25s ease-in-out, border-color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out);
display: inline-block;
cursor: pointer;
&:hover, &:active {
}
&.disabled, &[disabled] {
cursor: default;
pointer-events: none;
opacity: 0.5;
}
.icon-inline {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
// pill button
.btn-pill {
@include border-radius($baseline/5);
}
.btn-rounded {
@include border-radius($baseline/2);
}
// primary button
.btn-primary {
@extend .btn;
@extend .btn-pill;
padding:($baseline/2) $baseline;
border-width: 1px;
border-style: solid;
line-height: 1.5em;
text-align: center;
&:hover, &:active {
@include box-shadow(0 2px 1px $shadow-l1);
}
&.current, &.active {
@include box-shadow(inset 1px 1px 2px $shadow-d1);
&:hover, &:active {
@include box-shadow(inset 1px 1px 1px $shadow-d1);
}
}
}
// secondary button
.btn-secondary {
@extend .btn;
@extend .btn-pill;
border-width: 1px;
border-style: solid;
padding:($baseline/2) $baseline;
background: transparent;
line-height: 1.5em;
text-align: center;
&:hover, &:active {
}
&.current, &.active {
}
}
// ====================
// extends - depth levels
.depth0 { z-index: 0; }
.depth1 { z-index: 10; }
.depth2 { z-index: 100; }
.depth3 { z-index: 1000; }
.depth4 { z-index: 10000; }
.depth5 { z-index: 100000; }

View File

@@ -0,0 +1,448 @@
// studio - utilities - INHERITED mixins and extends
// NOTE: these are older/poorly architected mixins that we want to move away from using or refactor in the future.
// They are still referenced when styliing current interface elements and as such need to be preserved for the time being.
// talbs: we need to slowly ween ourselves off of these
// ====================
// inherited - vertical and horizontal centering
@mixin vertically-and-horizontally-centered ($height, $width) {
left: 50%;
margin-left: -$width / 2;
min-height: $height;
min-width: $width;
position: absolute;
top: 150px;
}
// ====================
// inherited - dividers
.faded-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-medium {
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
rgba(240,240,240, 1) 50%,
rgba(240,240,240, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-light {
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.8) 50%,
rgba(255,255,255, 0)));
height: 1px;
width: 100%;
}
.faded-vertical-divider {
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 100%;
width: 1px;
}
.faded-vertical-divider-light {
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.6) 50%,
rgba(255,255,255, 0)));
height: 100%;
width: 1px;
}
.vertical-divider {
@extend .faded-vertical-divider;
position: relative;
&::after {
@extend .faded-vertical-divider-light;
content: "";
display: block;
position: absolute;
left: 1px;
}
}
.horizontal-divider {
border: none;
@extend .faded-hr-divider;
position: relative;
&::after {
@extend .faded-hr-divider-light;
content: "";
display: block;
position: absolute;
top: 1px;
}
}
.fade-right-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1)));
border: none;
}
.fade-left-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
rgba(200,200,200, 0)));
border: none;
}
// ====================
// inherited - ui
.window {
@include clearfix();
@include border-radius(3px);
@include box-shadow(0 1px 1px $shadow-l1);
margin-bottom: $baseline;
border: 1px solid $gray-l2;
background: $white;
}
// ====================
// mixins - grandfathered
@mixin button {
@include font-size(14);
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
@include transition(background-color .15s, box-shadow .15s);
display: inline-block;
padding: ($baseline/5) $baseline ($baseline/4);
font-weight: 700;
&.disabled {
border: 1px solid $gray-l1 !important;
border-radius: 3px !important;
background: $gray-l1 !important;
color: $gray-d1 !important;
pointer-events: none;
cursor: none;
&:hover {
box-shadow: 0 0 0 0 !important;
}
}
&:hover, &.active {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
}
}
@mixin green-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
border: 1px solid $green-d1;
border-radius: 3px;
background-color: $green;
color: $white;
&:hover {
background-color: $green-s1;
color: $white;
}
&.disabled {
border: 1px solid $green-l3 !important;
background: $green-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin blue-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
border: 1px solid $blue-d1;
border-radius: 3px;
background-color: $blue;
color: $white;
&:hover, &.active {
background-color: $blue-s2;
color: $white;
}
&.disabled {
@include box-shadow(none);
border: 1px solid $blue-l3 !important;
background: $blue-l3 !important;
color: $white !important;
}
}
@mixin red-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
border: 1px solid $red-d1;
border-radius: 3px;
background-color: $red;
color: $white;
&:hover, &.active {
background-color: $red-s1;
color: $white;
}
&.disabled {
@include box-shadow(none);
border: 1px solid $red-l3 !important;
background: $red-l3 !important;
color: $white !important;
}
}
@mixin pink-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
border: 1px solid $pink-d1;
border-radius: 3px;
background-color: $pink;
color: $white;
&:hover, &.active {
background-color: $pink-s1;
color: $white;
}
&.disabled {
@include box-shadow(none);
border: 1px solid $pink-l3 !important;
background: $pink-l3 !important;
color: $white !important;
}
}
@mixin orange-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
border: 1px solid $orange-d1;
border-radius: 3px;
background-color: $orange;
color: $gray-d2;
&:hover {
background-color: $orange-s2;
color: $gray-d2;
}
&.disabled {
border: 1px solid $orange-l3 !important;
background: $orange-l2 !important;
color: $gray-l1 !important;
@include box-shadow(none);
}
}
@mixin white-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
border: 1px solid $mediumGrey;
border-radius: 3px;
background-color: #dfe5eb;
color: rgb(92, 103, 122);
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
&:hover {
background-color: rgb(222, 236, 247);
color: rgb(92, 103, 122);
}
}
@mixin grey-button {
@include button;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
border: 1px solid $gray-d2;
border-radius: 3px;
background-color: #d1dae3;
color: #6d788b;
&:hover {
background-color: #d9e3ee;
color: #6d788b;
}
}
@mixin gray-button {
@include button;
@include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0));
@include box-shadow(0 1px 0 $white-t1 inset);
border: 1px solid $gray-d1;
border-radius: 3px;
background-color: $gray-d2;
color: $gray-l3;
&:hover {
background-color: $gray-d3;
color: $white;
}
}
@mixin dark-grey-button {
@include button;
border: 1px solid $gray-d2;
border-radius: 3px;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
color: $white;
&:hover {
background-color: $gray-d4;
color: $white;
}
}
@mixin edit-box {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
padding: 15px 20px;
border-radius: 3px;
background-color: $lightBluishGrey2;
color: #3c3c3c;
label {
color: $baseFontColor;
}
input,
textarea {
border: 1px solid $darkGrey;
}
textarea {
min-height: 80px;
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.row {
margin-bottom: 10px;
padding: 0;
border: none;
}
.save-button {
@include blue-button;
margin-top: 0;
}
.cancel-button {
@include white-button;
margin-top: 0;
}
}
@mixin tree-view {
border: 1px solid $mediumGrey;
background: $lightGrey;
.branch {
margin-bottom: 10px;
&.collapsed {
margin-bottom: 0;
}
}
.branch > .section-item {
border-top: 1px solid #c5cad4;
}
.section-item {
position: relative;
display: block;
padding: 6px 8px 8px 16px;
background: #edf1f5;
font-size: 13px;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
}
}
&.editing {
background: #fffcf1;
}
.draft-item:after,
.public-item:after,
.private-item:after {
margin-left: 3px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
}
.draft-item:after {
content: "- draft";
}
.private-item:after {
content: "- private";
}
.private-item {
color: #a4aab7;
}
.draft-item {
color: #9f7d10;
}
}
a {
color: $baseFontColor;
&.new-unit-item {
color: #6d788b;
}
}
ol {
.section-item {
padding-left: 56px;
}
.new-unit-item {
margin-left: 56px;
}
}
ol ol {
.section-item {
padding-left: 96px;
}
.new-unit-item {
margin-left: 96px;
}
}
}
// ====================
// sunsetted mixins
@mixin active {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
background-color: rgba(255, 255, 255, .3);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}

View File

@@ -6,6 +6,11 @@
// @include box-sizing(border-box);
// }
// better text rendering/kerning through SVG
* {
text-rendering: optimizeLegibility;
}
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
@@ -15,7 +20,7 @@ b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin: 0;
@@ -31,7 +36,7 @@ html,body {
height: 100%;
}
article, aside, details, figcaption, figure,
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
@@ -110,7 +115,7 @@ abbr[title] {
border: none;
top: 0;
margin: 0;
float: none;
float: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@@ -131,7 +136,7 @@ abbr[title] {
padding: 0;
}
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
.problem-type-tabs {
border:none;
list-style-type: none;
@@ -139,7 +144,7 @@ abbr[title] {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
//background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child {
margin-left: 20px;
}
@@ -156,4 +161,4 @@ abbr[title] {
border: 0px;
}
}
}
}

View File

@@ -0,0 +1,3 @@
// studio - shame
// // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
// ====================

View File

@@ -12,9 +12,15 @@ $fg-max-columns: 12;
$fg-max-width: 1280px;
$fg-min-width: 900px;
// type
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1);
// ====================
// fonts
$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif;
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-decorative: '';
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// ====================
// colors - new for re-org
$black: rgb(0,0,0);
@@ -152,11 +158,14 @@ $shadow-l1: rgba(0,0,0,0.1);
$shadow-l2: rgba(0,0,0,0.05);
$shadow-d1: rgba(0,0,0,0.4);
// ====================
// specific UI
$notification-height: ($baseline*10);
// colors - inherited
// ====================
// inherited
$baseFontColor: $gray-d2;
$offBlack: #3c3c3c;
$green: #108614;
@@ -173,3 +182,8 @@ $darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);

View File

@@ -94,7 +94,7 @@
// notifications slide up then down
@mixin notificationsSlideUpDown {
0%, 100% {
0%, 100% {
@include transform(translateY(0));
}
@@ -199,4 +199,4 @@
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
}

View File

@@ -1,36 +1,8 @@
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
// studio - assets - fonts
// ====================
// import from google fonts - Open Sans
@import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,700,300);
// import from google fonts - Bree
@import url(http://fonts.googleapis.com/css?family=Bree+Serif);

View File

@@ -1,41 +1,48 @@
// studio - css architecture
// ====================
// bourbon libs and resets
@import 'bourbon/bourbon';
@import 'bourbon/addons/button';
@import "variables";
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'bourbon/addons/button'; // lib bourbon - button add-on
// VENDOR + REBASE *referenced/used vendor presentation and reset*
// ====================
@import 'vendor/normalize';
@import 'reset';
// utilities
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'variables';
@import 'mixins';
@import 'cms_mixins';
@import 'mixins-inherited';
// assets
// base - assets
@import 'assets/fonts';
@import 'assets/graphics';
@import 'assets/keyframes';
@import 'assets/graphics'; // sprites, basic img/figure/svg styling
@import 'assets/anims'; // animations
// base
// base - starter
@import 'base';
// elements
// base - elements
@import 'elements/typography';
@import 'elements/icons';
@import 'elements/controls';
@import 'elements/navigation';
@import 'elements/icons'; // references to icons used
@import 'elements/controls'; // buttons, link styles, sliders, etc.
@import 'elements/navigation'; // all archetypes of navigation
@import 'elements/forms';
@import 'elements/header';
@import 'elements/footer';
@import 'elements/sock';
@import 'elements/forms';
@import 'elements/modal';
@import 'elements/alerts';
@import 'elements/vendor';
@import 'elements/tender-widget';
@import 'elements/system-feedback'; // alerts, notifications, states
@import 'elements/system-help'; // help UI
@import 'elements/modal'; // interstitial UI, dialogs, modal windows
@import 'elements/vendor'; // overrides to vendor-provided styling
// specific views
// base - specific views
@import 'views/account';
@import 'views/assets';
@import 'views/updates';
@@ -51,8 +58,12 @@
@import 'views/users';
@import 'views/checklists';
// temp - inherited
@import 'assets/content-types';
// xblock-related
// xmodule
@import 'xmodule/modules/css/module-styles.scss';
@import 'xmodule/descriptors/css/module-styles.scss';
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)

View File

@@ -141,3 +141,9 @@
// calls-to-action
// ====================
// specific buttons - view live
.view-live-button {
@extend .t-action4;
}

View File

@@ -8,8 +8,8 @@
padding: $baseline;
footer.primary {
@extend .t-copy-sub2;
@include clearfix();
@include font-size(13);
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
@@ -72,4 +72,4 @@
}
}
}
}
}

View File

@@ -2,6 +2,7 @@
// ====================
.wrapper-header {
@extend .depth3;
margin: 0;
padding: $baseline;
border-bottom: 1px solid $gray;
@@ -10,7 +11,6 @@
height: 76px;
position: relative;
width: 100%;
z-index: 1000;
a {
color: $baseFontColor;
@@ -67,7 +67,7 @@
padding-right: ($baseline*0.75);
a {
@include text-hide();
@extend .text-hide;
display: block;
width: 164px;
height: 32px;
@@ -146,7 +146,7 @@
.title {
display: block;
padding: 5px;
padding: 0 ($baseline/4);
text-transform: uppercase;
font-weight: 600;
color: $gray-d3;
@@ -516,21 +516,21 @@ body.signup .nav-not-signedin-signin {
a {
background-color: #d9e3ee;
color: #6d788b;
color: #6d788b;
}
}
body.signin .nav-not-signedin-signup {
a {
background-color: #62aaf5;
color: #fff;
color: #fff;
}
}
// ====================
// STATE: js enabled
// STATE: js enabled
.js {
.nav-dropdown {
@@ -559,4 +559,4 @@ body.signin .nav-not-signedin-signup {
pointer-events: auto;
}
}
}
}

View File

@@ -2,6 +2,19 @@
// ====================
// common
nav {
ol, ul {
@extend .no-list;
}
.nav-item {
a {
}
}
}
// ====================
@@ -21,4 +34,4 @@
// ====================
//
//

View File

@@ -28,13 +28,13 @@
.cta-show-sock {
@extend .btn-pill;
@extend .t-action3;
@extend .t-action4;
background: $gray-l5;
padding: ($baseline/2) $baseline;
color: $gray;
.icon {
@include font-size(16);
@include font-size(14);
}
&:hover {
@@ -59,7 +59,7 @@
header {
.title {
@extend .t-title-2;
@extend .t-title4;
}
.ss-icon {
@@ -73,12 +73,13 @@
@include box-sizing(border-box);
.title {
@extend .t-title-3;
@extend .t-title6;
color: $white;
margin-bottom: ($baseline/2);
}
.copy {
@extend .t-copy-sub2;
margin: 0 0 $baseline 0;
}
@@ -94,6 +95,7 @@
}
.action {
@extend .t-action4;
display: block;
.icon {
@@ -149,4 +151,4 @@
color: $white;
}
}
}
}

View File

@@ -1,4 +1,5 @@
// studio alerts, prompts and notifications
// studio - elements - system feedback
// alerts, notifications, prompts, and status communication
// ====================
// shared
@@ -6,7 +7,7 @@
@include box-sizing(border-box);
.copy {
@extend .t-copy-sub2;
@extend .t-copy-sub1;
}
}
@@ -36,6 +37,7 @@
.nav-actions .action-primary {
@include blue-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $blue-d2;
}
@@ -53,6 +55,7 @@
.nav-actions .action-primary {
@include orange-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $orange-d2;
color: $gray-d4;
}
@@ -71,6 +74,7 @@
.nav-actions .action-primary {
@include red-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $red-d2;
}
@@ -88,6 +92,7 @@
.nav-actions .action-primary {
@include blue-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $blue-d2;
}
@@ -105,6 +110,7 @@
.nav-actions .action-primary {
@include green-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $green-d2;
}
@@ -121,8 +127,9 @@
&.step-required {
.nav-actions .action-primary {
border-color: $pink-d2;
@include pink-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $pink-d2;
}
a {
@@ -137,6 +144,7 @@
// prompts
.wrapper-prompt {
@extend .depth4;
@include transition(all 0.05s ease-in-out);
position: fixed;
top: 0;
@@ -144,7 +152,6 @@
width: 100%;
height: 100%;
text-align: center;
z-index: 10000;
&:before {
content: '';
@@ -184,12 +191,12 @@
}
.action-primary {
@extend .t-action3;
@extend .t-action4;
font-weight: 600;
}
.action-secondary {
@extend .t-action3;
@extend .t-action4;
}
}
}
@@ -235,11 +242,11 @@
// notifications
.wrapper-notification {
@extend .depth3;
@include clearfix();
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue);
position: fixed;
bottom: 0;
z-index: 1000;
width: 100%;
padding: $baseline ($baseline*2);
@@ -361,18 +368,19 @@
@include font-size(22);
width: flex-grid(1, 12);
height: ($baseline*1.25);
margin-top: ($baseline/4);
margin-right: flex-gutter();
text-align: right;
color: $white;
}
.copy {
@extend .t-copy-sub2;
@extend .t-copy-sub1;
width: flex-grid(10, 12);
color: $gray-l2;
.title {
@extend .t-title-4;
@extend .t-title7;
margin-bottom: 0;
color: $white;
}
@@ -409,13 +417,13 @@
.action-primary {
@include blue-button();
@extend .t-action3;
border-color: $blue-d2;
font-weight: 600;
}
.action-secondary {
@extend .t-action3;
@extend .t-action4;
}
}
@@ -434,7 +442,7 @@
}
.copy p {
@include text-sr();
@extend .text-sr;
}
}
}
@@ -443,10 +451,10 @@
// alerts
.wrapper-alert {
@extend .depth2;
@include box-sizing(border-box);
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue);
position: relative;
z-index: 100;
overflow: hidden;
width: 100%;
border-top: 1px solid $black;
@@ -504,7 +512,6 @@
// adopted alerts
.alert {
@extend .t-copy-sub2;
@include box-sizing(border-box);
@include clearfix();
margin: 0 auto;
@@ -530,11 +537,11 @@
}
.copy {
@extend .t-copy-sub2;
width: flex-grid(10, 12);
color: $gray-l2;
.title {
@extend .t-title7;
margin-bottom: 0;
color: $white;
}
@@ -568,12 +575,12 @@
}
.action-primary {
@extend .t-action3;
@extend .t-action4;
font-weight: 600;
}
.action-secondary {
@extend .t-action3;
@extend .t-action4;
}
}
}
@@ -590,7 +597,7 @@
text-align: center;
.label {
@include text-sr();
@extend .text-sr;
}
.icon {
@@ -730,7 +737,7 @@ body.uxdesign.alerts {
border-radius: 3px;
background: #fbf6e1;
// background: #edbd3c;
@extend .t-copy-sub1;
font-size: 14px;
@include clearfix;
.alert-message {

View File

@@ -0,0 +1,2 @@
// studio - elements - system help
// ====================

View File

@@ -1,80 +1,132 @@
// studio - elements - typography
// ====================
// Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)
// headings/titles
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5 {
color: $gray-d3;
.t-title {
font-family: $f-sans-serif;
}
.t-title-1 {
.t-title1 {
@extend .t-title;
@include font-size(60);
@include lh(60);
}
.t-title2 {
@extend .t-title;
@include font-size(48);
@include lh(48);
}
.t-title3 {
@include font-size(36);
@include lh(36);
}
.t-title-2 {
.t-title4 {
@extend .t-title;
@include font-size(24);
font-weight: 600;
@include lh(24);
}
.t-title-3 {
.t-title5 {
@extend .t-title;
@include font-size(18);
@include lh(18);
}
.t-title6 {
@extend .t-title;
@include font-size(16);
font-weight: 600;
@include lh(16);
}
.t-title-4 {
.t-title7 {
@extend .t-title;
@include font-size(14);
@include lh(14);
}
.t-title-5 {
.t-title8 {
@extend .t-title;
@include font-size(12);
@include lh(12);
}
.t-title9 {
@extend .t-title;
@include font-size(11);
@include lh(11);
}
// ====================
// copy
.t-copy {
font-family: $f-sans-serif;
}
.t-copy-base {
@extend .t-copy;
@include font-size(16);
@include lh(16);
}
.t-copy-lead1 {
@extend .t-copy;
@include font-size(18);
@include lh(18);
}
.t-copy-lead2 {
@include font-size(20);
@extend .t-copy;
@include font-size(24);
@include lh(24);
}
.t-copy-sub1 {
@extend .t-copy;
@include font-size(14);
@include lh(14);
}
.t-copy-sub2 {
@include font-size(13);
}
.t-copy-sub3 {
@extend .t-copy;
@include font-size(12);
@include lh(12);
}
// ====================
// actions/labels
.t-action1 {
@include font-size(14);
font-weight: 600;
@include font-size(18);
@include lh(18);
}
.t-action2 {
@include font-size(13);
font-weight: 600;
text-transform: uppercase;
@include font-size(16);
@include lh(16);
}
.t-action3 {
@include font-size(13);
@include font-size(14);
@include lh(14);
}
.t-action4 {
@include font-size(12);
@include lh(12);
}
// ====================
// code
.t-code {
font-family: $f-monospace;
}
// ====================

View File

@@ -6,7 +6,7 @@
border-color: $darkGrey;
border-radius: 2px;
background: #fff;
font-family: $sans-serif;
font-family: $f-sans-serif;
font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important;
@@ -62,4 +62,4 @@
// JQUI timepicker
.ui-timepicker-list {
z-index: 100000 !important;
}
}

View File

@@ -12,7 +12,7 @@ body.signup, body.signin {
.content {
@include clearfix();
@include font-size(16);
@extend .t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
@@ -26,14 +26,14 @@ body.signup, body.signin {
padding-bottom: ($baseline/2);
h1 {
@include font-size(32);
@extend .t-title3;
margin: 0;
padding: 0;
font-weight: 600;
}
.action {
@include font-size(13);
@extend .t-action3;
position: absolute;
right: 0;
top: 40%;
@@ -41,7 +41,7 @@ body.signup, body.signin {
}
.introduction {
@include font-size(14);
@extend .t-copy-sub1;
margin: 0 0 $baseline 0;
}
}
@@ -69,8 +69,8 @@ body.signup, body.signin {
.action-primary {
@include blue-button;
@extend .t-action2;
@include transition(all .15s);
@include font-size(15);
display: block;
width: 100%;
padding: ($baseline*0.75) ($baseline/2);
@@ -108,7 +108,7 @@ body.signup, body.signin {
}
label {
@include font-size(14);
@extend .t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
@@ -118,7 +118,7 @@ body.signup, body.signin {
}
input, textarea {
@include font-size(16);
@extend .t-copy-base;
height: 100%;
width: 100%;
padding: ($baseline/2);
@@ -171,8 +171,8 @@ body.signup, body.signin {
}
.tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
@include font-size(13);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
@@ -212,7 +212,7 @@ body.signup, body.signin {
width: flex-grid(4, 12);
.bit {
@include font-size(13);
@extend .t-copy-sub1;
margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0;
@@ -225,7 +225,7 @@ body.signup, body.signin {
}
h3 {
@include font-size(14);
@extend .t-title7;
margin: 0 0 ($baseline/4) 0;
color: $gray-d2;
font-weight: 600;
@@ -245,7 +245,7 @@ body.signup, body.signin {
position: relative;
.action-forgotpassword {
@include font-size(13);
@extend .t-action3;
position: absolute;
top: 0;
right: 0;
@@ -257,7 +257,7 @@ body.signup, body.signin {
// messages
.message {
@include font-size(14);
@extend .t-copy-sub1;
display: block;
}

View File

@@ -23,7 +23,7 @@ body.course.checklists {
// visual status
.viz-checklist-status {
@include text-hide();
@extend .text-hide;
@include size(100%,($baseline/4));
position: relative;
display: block;
@@ -40,7 +40,7 @@ body.course.checklists {
background: $green;
.int {
@include text-sr();
@extend .text-sr;
}
}
}
@@ -83,13 +83,13 @@ body.course.checklists {
}
.checklist-status {
@include font-size(13);
@extend .t-copy-sub1;
width: flex-grid(3, 9);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
.icon-confirm {
@include font-size(20);
@@ -100,7 +100,7 @@ body.course.checklists {
}
.status-count {
@include font-size(16);
@extend .t-copy-base;
margin-left: ($baseline/4);
margin-right: ($baseline/4);
color: $gray-d3;
@@ -108,7 +108,7 @@ body.course.checklists {
}
.status-amount {
@include font-size(16);
@extend .t-copy-base;
margin-left: ($baseline/4);
color: $gray-d3;
font-weight: 600;
@@ -138,8 +138,8 @@ body.course.checklists {
}
.action-secondary {
@include font-size(14);
@include grey-button();
@extend .t-action3;
font-weight: 400;
float: right;
@@ -174,7 +174,7 @@ body.course.checklists {
// state - completed
&.is-completed {
.viz-checklist-status {
.viz-checklist-status-value {
@@ -189,9 +189,9 @@ body.course.checklists {
}
.checklist-status {
.status-count, .status-amount, .icon-confirm {
color: $green;
color: $green;
}
}
}
@@ -244,17 +244,17 @@ body.course.checklists {
vertical-align: baseline;
cursor: pointer;
margin-bottom: 0;
}
}
.task-description {
@extend .t-copy-sub1;
@include transition(color .15s .25s ease-in-out);
@include font-size(14);
color: $gray-l2;
}
.task-support {
@extend .t-copy-sub2;
@include transition(opacity .15s .25s ease-in-out);
@include font-size(12);
opacity: 0;
pointer-events: none;
}
@@ -274,14 +274,14 @@ body.course.checklists {
.action-primary {
@include blue-button;
@extend .t-action4;
@include transition(all .15s);
@include font-size(12);
font-weight: 600;
text-align: center;
}
.action-secondary {
@include font-size(13);
@extend .t-action4;
margin-top: ($baseline/2);
}
}
@@ -322,7 +322,7 @@ body.course.checklists {
.action-primary {
@include grey-button;
@include font-size(12);
@extend .t-action4;
font-weight: 600;
text-align: center;
}
@@ -344,4 +344,4 @@ body.course.checklists {
.content-supplementary {
width: flex-grid(3, 12);
}
}
}

View File

@@ -18,14 +18,14 @@ body.index {
}
.content {
@extend .t-copy-base;
@include clearfix();
@include font-size(16);
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
border: none;
padding-bottom: 0;
@@ -62,7 +62,7 @@ body.index {
color: $white;
h1 {
@include font-size(52);
@extend .t-title2;
float: none;
margin: 0 0 ($baseline/2) 0;
border-bottom: 1px solid $blue-l1;
@@ -72,7 +72,7 @@ body.index {
}
.logo {
@include text-hide();
@extend .text-hide;
position: relative;
top: 3px;
display: inline-block;
@@ -83,7 +83,7 @@ body.index {
}
.tagline {
@include font-size(24);
@extend .t-title4;
margin: 0;
color: $blue-l3;
}
@@ -198,13 +198,13 @@ body.index {
margin-top: -($baseline/4);
h3 {
@extend .t-title4;
margin: 0 0 ($baseline/2) 0;
@include font-size(24);
font-weight: 600;
}
> p {
@include font-size(18);
@extend .t-copy-lead1;
color: $gray-d1;
}
@@ -214,8 +214,8 @@ body.index {
}
.list-proofpoints {
@extend .t-copy-sub1;
@include clearfix();
@include font-size(14);
width: flex-grid(9, 9);
margin: ($baseline*1.5) 0 0 0;
@@ -233,7 +233,7 @@ body.index {
color: $gray-l1;
.title {
@include font-size(16);
@extend .t-copy-base;
margin: 0 0 ($baseline/4) 0;
font-weight: 500;
color: $gray-d3;
@@ -314,7 +314,7 @@ body.index {
margin-top: -($baseline*1.5);
li {
width: flex-grid(6, 12);
width: flex-grid(6, 12);
margin: 0 auto;
}
@@ -322,23 +322,23 @@ body.index {
display: block;
width: 100%;
text-align: center;
}
.action-primary {
@include blue-button;
@include transition(all .15s);
@include font-size(18);
padding: ($baseline*0.75) ($baseline/2);
font-weight: 600;
text-align: center;
text-transform: uppercase;
}
&.action-primary {
@extend .t-action1;
@include blue-button;
@include transition(all .15s);
padding: ($baseline*0.75) ($baseline/2);
font-weight: 600;
text-align: center;
text-transform: uppercase;
}
.action-secondary {
@include font-size(14);
margin-top: ($baseline/2);
&.action-secondary {
@extend .t-action3;
margin-top: ($baseline/2);
}
}
}
}
}
}
}

View File

@@ -29,6 +29,7 @@ body.course.outline {
width: 100px;
.status-label {
@include font-size(12);
position: absolute;
top: 2px;
right: -5px;
@@ -38,7 +39,6 @@ body.course.outline {
@include border-radius(3px);
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
@@ -57,6 +57,10 @@ body.course.outline {
}
.menu {
@include font-size(12);
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
z-index: 1;
display: none;
opacity: 0.0;
@@ -67,10 +71,6 @@ body.course.outline {
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
@@ -167,7 +167,7 @@ body.course.outline {
text-align: right;
.published-status {
font-size: 12px;
@include font-size(12);
margin-right: 15px;
strong {
@@ -185,19 +185,19 @@ body.course.outline {
.schedule-button,
.edit-button {
font-size: 11px;
@include font-size(11);
padding: 3px 15px 5px;
}
}
.datepair .date,
.datepair .time {
@include font-size(13);
@include box-shadow(none);
padding-left: 0;
padding-right: 0;
border: none;
background: none;
@include box-shadow(none);
font-size: 13px;
font-weight: bold;
color: $blue;
cursor: pointer;
@@ -231,10 +231,10 @@ body.course.outline {
@include clearfix();
.section-name {
@include font-size(19);
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
@@ -254,9 +254,9 @@ body.course.outline {
background: $white;
input {
font-size: 16px;
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
@@ -275,7 +275,7 @@ body.course.outline {
background: $lightGrey;
.published-status {
font-size: 12px;
@include font-size(12);
margin-right: 15px;
strong {
@@ -293,9 +293,8 @@ body.course.outline {
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
@include font-size(11);
padding: 0 15px 2px 15px;
}
}
@@ -306,17 +305,17 @@ body.course.outline {
width: 145px;
.status-label {
@include font-size(12);
@include border-radius(3px);
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
@@ -335,6 +334,11 @@ body.course.outline {
}
.menu {
@include font-size(12);
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
z-index: 1;
display: none;
opacity: 0.0;
@@ -345,11 +349,7 @@ body.course.outline {
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li {
@@ -410,7 +410,7 @@ body.course.outline {
}
}
.item-actions {
.item-actions {
margin-top: 21px;
margin-right: 12px;
@@ -422,7 +422,7 @@ body.course.outline {
.expand-collapse-icon {
float: left;
margin: 29px 6px 16px 16px;
margin: 25px 6px 16px 16px;
@include transition(none);
&.expand {
@@ -430,7 +430,7 @@ body.course.outline {
}
&.collapsed {
}
}
@@ -440,7 +440,7 @@ body.course.outline {
}
h3 {
font-size: 19px;
@include font-size(19);
font-weight: 700;
color: $blue;
}
@@ -460,9 +460,9 @@ body.course.outline {
.section-name-edit {
input {
font-size: 16px;
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
@@ -476,7 +476,7 @@ body.course.outline {
}
h4 {
font-size: 12px;
@include font-size(12);
color: #878e9d;
strong {
@@ -502,8 +502,8 @@ body.course.outline {
&.new-section {
header {
height: auto;
@include clearfix();
height: auto;
}
.expand-collapse-icon {
@@ -522,12 +522,11 @@ body.course.outline {
}
.toggle-button-sections {
@include font-size(12);
display: none;
position: relative;
float: right;
margin-top: 10px;
font-size: 13px;
margin-top: ($baseline/4);
color: $darkGrey;
&.is-shown {
@@ -535,13 +534,13 @@ body.course.outline {
}
.ss-icon {
@include font-size(11);
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.label {
@@ -599,7 +598,7 @@ body.course.outline {
}
h3 {
font-size: 34px;
@include font-size(34);
font-weight: 300;
}
@@ -625,16 +624,16 @@ body.course.outline {
}
label {
@include font-size(14);
@extend .t-copy-sub1;
margin-bottom: ($baseline/4);
}
}
}
.description {
@include font-size(14);
float: left;
margin-top: 30px;
font-size: 14px;
line-height: 20px;
width: 100%;
}
@@ -645,7 +644,7 @@ body.course.outline {
.start-date,
.start-time {
font-size: 19px;
@include font-size(19);
}
.save-button {
@@ -659,14 +658,14 @@ body.course.outline {
.save-button,
.cancel-button {
font-size: 16px;
@include font-size(16);
}
}
.collapse-all-button {
@include font-size(13);
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
}
@@ -686,7 +685,7 @@ body.course.outline {
border: 1px solid $darkGrey;
opacity : 0.2;
&:hover {
opacity : 1.0;
opacity : 1.0;
.section-item {
background: $yellow !important;
}
@@ -701,4 +700,4 @@ body.course.outline {
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
}
}
}

View File

@@ -66,7 +66,7 @@ body.course.settings {
}
.tip {
@include font-size(13);
@extend .t-copy-sub2;
width: flex-grid(5, 9);
float: right;
margin-top: ($baseline/2);
@@ -86,20 +86,20 @@ body.course.settings {
// in form -UI hints/tips/messages
.instructions {
@include font-size(14);
@extend .t-copy-sub1;
margin: 0 0 $baseline 0;
}
.tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
@include font-size(13);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.message-error {
@include font-size(13);
@extend .t-copy-sub1;
display: block;
margin-top: ($baseline/4);
margin-bottom: ($baseline/2);
@@ -109,12 +109,13 @@ body.course.settings {
// buttons
.remove-item {
@include white-button;
@include font-size(13);
@extend .t-action-3;
font-weight: 400;
}
.new-button {
@include font-size(13);
// @extend .t-action-3; - bad buttons won't render this properly
@include font-size(14);
}
// form basics
@@ -158,8 +159,8 @@ body.course.settings {
}
input, textarea {
@extend .t-copy-base;
@include placeholder($gray-l4);
@include font-size(16);
@include size(100%,100%);
padding: ($baseline/2);
@@ -324,7 +325,7 @@ body.course.settings {
.action-primary {
@include blue-button();
@include font-size(13);
@extend .t-action-3;
font-weight: 600;
.icon {
@@ -747,7 +748,7 @@ body.course.settings {
// specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
.CodeMirror {
@include font-size(16);
@extend .t-copy-base;
@include box-sizing(border-box);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
@include linear-gradient($lightGrey, tint($lightGrey, 90%));

View File

@@ -2,7 +2,7 @@
// ====================
body.course.static-pages {
.new-static-page-button {
@include grey-button;
display: block;
@@ -44,7 +44,7 @@ body.course.static-pages {
label {
display: inline-block;
margin-right: 10px;
margin-right: 10px;
}
}
@@ -123,7 +123,7 @@ body.course.static-pages {
.component-actions {
position: absolute;
top: 26px;
top: 20px;
right: 44px;
}
}
@@ -150,12 +150,12 @@ body.course.static-pages {
}
.static-page-item {
position: relative;
position: relative;
margin: 10px 0;
padding: 22px 20px;
border: 1px solid $darkGrey;
border-radius: 3px;
background: #fff;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
.page-name {
@@ -204,4 +204,4 @@ body.course.static-pages {
color: #3c3c3c;
outline: 0;
}
}
}

View File

@@ -1,12 +1,17 @@
<%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="title">Studio Server Error</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
<h1>The <em>Studio</em> servers encountered an error</h1>
<p>
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at <a href="mailto:technical@edx.org">technical@edx.org</a>.
</p>
</section>
</div>

View File

@@ -10,7 +10,7 @@
</%block>
<%block name="content">
<script type="text/template" id="new-asset-element">
<tr data-id='{{url}}'>
<td class="thumb-col">
@@ -35,10 +35,10 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Files &amp; Uploads</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<span class="sr">&gt; </span>Files &amp; Uploads
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
@@ -91,7 +91,7 @@
</tbody>
</table>
<nav class="pagination wip-box">
Page:
Page:
<ol class="pages">
<li>1</li>
<li><a href="#">2</a></li>

View File

@@ -30,10 +30,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Checklists</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Tools</small>
<span class="sr">&gt; </span>Course Checklists
</h1>
</header>
</div>

View File

@@ -16,18 +16,18 @@
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function(){
var course_updates = new CMS.Models.CourseUpdateCollection();
course_updates.urlbase = '${url_base}';
course_updates.fetch();
course_updates.fetch({reset: true});
var course_handouts = new CMS.Models.ModuleInfo({
id: '${handouts_location}'
});
course_handouts.urlbase = '${url_base}';
var editor = new CMS.Views.CourseInfoEdit({
el: $('.main-wrapper'),
model : new CMS.Models.CourseInfo({
@@ -44,10 +44,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Course Updates</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<span class="sr">&gt; </span>Course Updates
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
@@ -80,4 +80,4 @@
</div>
</div>
</div>
</%block>
</%block>

View File

@@ -18,10 +18,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Static Pages</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<span class="sr">&gt; </span>Static Pages
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
@@ -52,17 +52,17 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="unit-body">
<div class="tab-list">
<ol class='components'>
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item">
</li>
</ol>
</ol>
</div>
</article>
</div>
@@ -80,4 +80,4 @@
<span class="label">close modal</span>
</a>
</div>
</%block>
</%block>

View File

@@ -85,6 +85,7 @@
</div>
</div>
</div>
</div>
</%block>
<%block name="jsextra">

View File

@@ -8,10 +8,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Export</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Tools</small>
<span class="sr">&gt; </span>Course Export
</h1>
</header>
</div>
@@ -37,9 +37,9 @@
<div class="export-form-wrapper">
<form action="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2>
<p class="error-block"></p>
<a href="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" class="button-export">Download Files</a>
</form>
</div>
@@ -49,11 +49,11 @@
<div class="export-form-wrapper is-downloading">
<form action="${reverse('export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2>
<p class="error-block"></p>
<a href="#" class="button-export disabled">Files Downloading</a>
<p class="message-status">Download not start? <a href="#" class="text-export">Try again</a></p>
<p class="message-status">Download not start? <a href="#" class="text-export">Try again</a></p>
</form>
</div>
</%doc>

View File

@@ -8,10 +8,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Import</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Tools</small>
<span class="sr">&gt; </span>Course Import
</h1>
</header>
</div>
@@ -79,4 +79,4 @@ $('form').ajaxForm({
});
})();
</script>
</%block>
</%block>

View File

@@ -37,9 +37,7 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions">
<div class="title">
<h1 class="title-1">${_("My Courses")}</h1>
</div>
<h1 class="page-header">${_("My Courses")}</h1>
% if user.is_active:
<nav class="nav-actions">

View File

@@ -6,10 +6,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Settings</span>
<h1 class="title-1">Course Team</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Course Settings</small>
<span class="sr">&gt; </span>Course Team
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
@@ -31,7 +31,7 @@
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p>
</div>
<article class="user-overview">
<article class="user-overview">
%if allow_actions:
<form class="new-user-form">
<div id="result"></div>
@@ -106,7 +106,7 @@
$(document).ready(function() {
$newUserForm = $('.new-user-form');
var $cancelButton = $newUserForm.find('.cancel-button');
var $cancelButton = $newUserForm.find('.cancel-button');
$newUserForm.bind('submit', addUser);
$cancelButton.bind('click', hideNewUserForm);
@@ -119,7 +119,7 @@
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data:JSON.stringify({ 'email': $(this).data('id')}),
data:JSON.stringify({ 'email': $(this).data('id')}),
}).done(function() {
location.reload();
})

View File

@@ -106,10 +106,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Course Outline</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<span class="sr">&gt; </span>Course Outline
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>

View File

@@ -32,15 +32,15 @@ from contentstore import utils
});
var model = new CMS.Models.Settings.CourseDetails();
model.urlRoot = '${details_url}';
model.fetch({success :
function(model) {
var editor = new CMS.Views.Settings.Details({
el: $('.settings-details'),
model: model
});
editor.render();
}
model.fetch({
success: function(model) {
var editor = new CMS.Views.Settings.Details({
el: $('.settings-details'),
model: model
});
editor.render();
},
reset: true
});
});
@@ -50,10 +50,10 @@ from contentstore import utils
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Settings</span>
<h1 class="title-1">Schedule &amp; Details</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Settings</small>
<span class="sr">&gt; </span>Schedule &amp; Details
</h1>
</header>
</div>

View File

@@ -44,10 +44,10 @@ editor.render();
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Settings</span>
<h1 class="title-1">Advanced Settings</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Settings</small>
<span class="sr">&gt; </span>Advanced Settings
</h1>
</header>
</div>

View File

@@ -41,10 +41,10 @@ from contentstore import utils
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Settings</span>
<h1 class="title-1">Grading</h1>
</div>
<h1 class="page-header">
<small class="subtitle">Settings</small>
<span class="sr">&gt; </span>Grading
</h1>
</header>
</div>

View File

@@ -82,10 +82,10 @@
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">UX Design</span>
<h1 class="title-1">System Notifications</h1>
</div>
<h1 class="page-header">
<small class="subtitle">UX Design</small>
<span class="sr">&gt; </span>System Notifications
</h1>
</header>
</div>
@@ -133,10 +133,6 @@
<a href="#notification-changesMade" class="show-notification">Show Changes Made (used in Advanced Settings)</a>
<a href="#notification-changesMade" class="hide-notification">Hide Changes Made (used in Advanced Settings)</a>
</li>
<li>
<a href="#notification-change" class="show-notification">Show Change Warning</a>
<a href="#notification-change" class="hide-notification">Hide Change Warning</a>
</li>
<li>
<a href="#notification-version" class="show-notification">Show New Version Warning</a>
<a href="#notification-version" class="hide-notification">Hide New Version Warning</a>
@@ -156,10 +152,6 @@
<a href="#notification-help" class="show-notification">Show Help</a>
<a href="#notification-help" class="hide-notification">Hide Help</a>
</li>
<li>
<a href="#notification-threeActions" class="show-notification">Show Notification with three actions</a>
<a href="#notification-threeActions" class="hide-notification">Hide Notification with three actions</a>
</li>
</ul>
</section>
@@ -402,57 +394,6 @@
</%block>
<%block name="view_notifications">
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-change-title" aria-describedby="notification-change-description" id="notification-change">
<div class="notification change has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i>
<div class="copy">
<h2 class="title title-3" id="notification-change-title">You've Made Some Changes</h2>
<p class="message" id="notification-change-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary">Save Changes</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Don't Save</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: three actions example -->
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-threeActions-title" aria-describedby="notification-threeActions-description" id="notification-threeActions">
<div class="notification change has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i>
<div class="copy">
<h2 class="title title-3" id="notification-threeActions-title">You've Made Some Changes</h2>
<p class="message" id="notification-threeActions-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary">Save Changes</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Don't Save</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Do something else</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description" id="notification-changesMade">
<div class="notification warning has-actions">