Commit 9f4b7261 by Julian Arni

Merge branch 'master' into jkarni/docs-merge

parents 5451bb64 e501a176
......@@ -9,6 +9,9 @@ Studio: Send e-mails to new Studio users (on edge only) when their course creato
status has changed. This will not be in use until the course creator table
is enabled.
Studio: Added improvements to Course Creation: richer error messaging, tip
text, and fourth field for course run.
LMS: Added user preferences (arbitrary user/key/value tuples, for which
which user/key is unique) and a REST API for reading users and
preferences. Access to the REST API is restricted by use of the
......@@ -18,6 +21,9 @@ the setting is not present, the API is disabled).
LMS: Added endpoints for AJAX requests to enable/disable notifications
(which are not yet implemented) and a one-click unsubscribe page.
Studio: Allow instructors of a course to designate other staff as instructors;
this allows instructors to hand off management of a course to someone else.
Common: Add a manage.py that knows about edx-platform specific settings and projects
Common: Added *experimental* support for jsinput type.
......
......@@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name):
user.save()
def is_user_in_course_group_role(user, location, role):
def is_user_in_course_group_role(user, location, role, check_staff=True):
if user.is_active and user.is_authenticated:
# all "is_staff" flagged accounts belong to all groups
return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
if check_staff and user.is_staff:
return True
return user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
return False
......
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from lxml import html
from lxml import html, etree
import re
from django.http import HttpResponseBadRequest
import logging
......@@ -74,34 +74,44 @@ def update_course_updates(location, update, passed_id=None):
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
# if there's no ol, create it
if course_html_parsed.tag != 'ol':
# surround whatever's there w/ an ol
if course_html_parsed.tag != 'li':
# but first wrap in an li
li = etree.Element('li')
li.append(course_html_parsed)
course_html_parsed = li
ol = etree.Element('ol')
ol.append(course_html_parsed)
course_html_parsed = ol
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
if passed_id is not None:
idx = get_idx(passed_id)
# idx is count from end of list
course_html_parsed[-idx] = new_html_parsed
else:
course_html_parsed.insert(0, new_html_parsed)
# ??? Should this use the id in the json or in the url or does it matter?
if passed_id is not None:
idx = get_idx(passed_id)
# idx is count from end of list
course_html_parsed[-idx] = new_html_parsed
else:
course_html_parsed.insert(0, new_html_parsed)
idx = len(course_html_parsed)
passed_id = course_updates.location.url() + "/" + str(idx)
idx = len(course_html_parsed)
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
course_updates.data = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
# update db record
course_updates.data = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
if (len(new_html_parsed) == 1):
content = new_html_parsed[0].tail
else:
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
if (len(new_html_parsed) == 1):
content = new_html_parsed[0].tail
else:
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
return {"id": passed_id,
"date": update['date'],
"content": content}
return {"id": passed_id,
"date": update['date'],
"content": content}
def delete_course_update(location, update, passed_id):
......
......@@ -53,6 +53,14 @@ def i_have_opened_a_new_course(_step):
open_new_course()
@step('(I select|s?he selects) the new course')
def select_new_course(_step, whom):
course_link_xpath = '//div[contains(@class, "courses")]//a[contains(@class, "class-link")]//span[contains(., "{name}")]/..'.format(
name="Robot Super Course")
element = world.browser.find_by_xpath(course_link_xpath)
element.click()
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower()
......@@ -118,14 +126,18 @@ def create_studio_user(
registration.register(studio_user)
registration.activate()
return studio_user
def fill_in_course_info(
name='Robot Super Course',
org='MITx',
num='999'):
num='101',
run='2013_Spring'):
world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num)
world.css_fill('.new-course-run', run)
def log_into_studio(
......@@ -242,7 +254,7 @@ def save_button_disabled(step):
@step('I confirm the prompt')
def confirm_the_prompt(step):
prompt_css = 'a.button.action-primary'
world.css_click(prompt_css)
world.css_click(prompt_css, success_condition=lambda: not world.css_visible(prompt_css))
@step(u'I am shown a (.*)$')
......@@ -252,6 +264,7 @@ def i_am_shown_a_notification(step, notification_type):
def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index)
world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')")
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
if world.is_mac():
g._element.send_keys(Keys.COMMAND + 'a')
......
......@@ -12,11 +12,20 @@ def create_component_instance(step, component_button_css, category,
has_multiple_templates=True):
click_new_component_button(step, component_button_css)
if category in ('problem', 'html'):
def animation_done(_driver):
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
world.wait_for(animation_done)
if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css)
assert_equal(1, len(world.css_find(expected_css)))
assert_equal(
1,
len(world.css_find(expected_css)),
"Component instance with css {css} was not created successfully".format(css=expected_css))
@world.absorb
def click_new_component_button(step, component_button_css):
......@@ -39,11 +48,13 @@ def click_component_from_menu(category, boilerplate, expected_css):
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
assert_equal(len(elements), 1)
world.css_click(elem_css)
world.wait_for(lambda _driver: world.css_visible(elem_css))
world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
world.css_click('a.edit-button')
world.css_click('#settings-mode')
......
......@@ -63,3 +63,10 @@ Feature: Course Overview
When I navigate to the course overview page
And I change an assignment's grading status
Then I am shown a notification
Scenario: Notification is shown on subsection reorder
Given I have opened a new course section in Studio
And I have added a new subsection
And I have added a new subsection
When I reorder subsections
Then I am shown a notification
......@@ -124,3 +124,14 @@ def all_sections_are_collapsed(step):
def change_grading_status(step):
world.css_find('a.menu-toggle').click()
world.css_find('.menu li').first.click()
@step(u'I reorder subsections')
def reorder_subsections(_step):
draggable_css = 'a.drag-handle'
ele = world.css_find(draggable_css).first
ele.action_chains.drag_and_drop_by_offset(
ele._element,
30,
0
).perform()
Feature: Course Team
As a course author, I want to be able to add others to my team
Scenario: Users can add other users
Scenario: Admins can add other users
Given I have opened a new course in Studio
And the user "alice" exists
And I am viewing the course team settings
......@@ -9,7 +9,7 @@ Feature: Course Team
And "alice" logs in
Then she does see the course on her page
Scenario: Added users cannot delete or add other users
Scenario: Added admins cannot delete or add other users
Given I have opened a new course in Studio
And the user "bob" exists
And I am viewing the course team settings
......@@ -18,7 +18,7 @@ Feature: Course Team
Then he cannot delete users
And he cannot add users
Scenario: Users can delete other users
Scenario: Admins can delete other users
Given I have opened a new course in Studio
And the user "carol" exists
And I am viewing the course team settings
......@@ -27,8 +27,33 @@ Feature: Course Team
And "carol" logs in
Then she does not see the course on her page
Scenario: Users cannot add users that do not exist
Scenario: Admins cannot add users that do not exist
Given I have opened a new course in Studio
And I am viewing the course team settings
When I add "dennis" to the course team
Then I should see "Could not find user by email address" somewhere on the page
Scenario: Admins should be able to make other people into admins
Given I have opened a new course in Studio
And the user "emily" exists
And I am viewing the course team settings
And I add "emily" to the course team
When I make "emily" a course team admin
And "emily" logs in
And she selects the new course
And she views the course team settings
Then "emily" should be marked as an admin
And she can add users
And she can delete users
Scenario: Admins should be able to remove other admins
Given I have opened a new course in Studio
And the user "frank" exists as a course admin
And I am viewing the course team settings
When I remove admin rights from "frank"
And "frank" logs in
And he selects the new course
And he views the course team settings
Then "frank" should not be marked as an admin
And he cannot add users
And he cannot delete users
......@@ -3,65 +3,105 @@
from lettuce import world, step
from common import create_studio_user, log_into_studio
from django.contrib.auth.models import Group
from auth.authz import get_course_groupname_for_role
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
@step(u'I am viewing the course team settings')
def view_grading_settings(_step):
@step(u'(I am viewing|s?he views) the course team settings')
def view_grading_settings(_step, whom):
world.click_course_settings()
link_css = 'li.nav-course-settings-team a'
world.css_click(link_css)
@step(u'the user "([^"]*)" exists$')
def create_other_user(_step, name):
create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
@step(u'the user "([^"]*)" exists( as a course admin)?$')
def create_other_user(_step, name, course_admin):
user = create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
if course_admin:
location = world.scenario_dict["COURSE"].location
for role in ("staff", "instructor"):
group, __ = Group.objects.get_or_create(name=get_course_groupname_for_role(location, role))
user.groups.add(group)
user.save()
@step(u'I add "([^"]*)" to the course team')
def add_other_user(_step, name):
new_user_css = 'a.new-user-button'
new_user_css = 'a.create-user-button'
world.css_click(new_user_css)
world.wait(0.5)
email_css = 'input.email-input'
email_css = 'input#user-email-input'
f = world.css_find(email_css)
f._element.send_keys(name, EMAIL_EXTENSION)
confirm_css = '#add_user'
confirm_css = 'form.create-user button.action-primary'
world.css_click(confirm_css)
@step(u'I delete "([^"]*)" from the course team')
def delete_other_user(_step, name):
to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION)
to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
email="{0}{1}".format(name, EMAIL_EXTENSION))
world.css_click(to_delete_css)
@step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
email=name+EMAIL_EXTENSION)
world.css_click(admin_btn_css)
@step(u'I remove admin rights from "([^"]*)"')
def remove_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
email=name+EMAIL_EXTENSION)
world.css_click(admin_btn_css)
@step(u'"([^"]*)" logs in$')
def other_user_login(_step, name):
log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION)
@step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, doesnt_see_course, gender):
def see_course(_step, inverted, gender):
class_css = 'span.class-name'
all_courses = world.css_find(class_css, wait_time=1)
all_names = [item.html for item in all_courses]
if doesnt_see_course:
if inverted:
assert not world.scenario_dict['COURSE'].display_name in all_names
else:
assert world.scenario_dict['COURSE'].display_name in all_names
@step(u's?he cannot delete users')
def cannot_delete(_step):
@step(u'"([^"]*)" should( not)? be marked as an admin')
def marked_as_admin(_step, name, inverted):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+EMAIL_EXTENSION)
if inverted:
assert world.is_css_not_present(flag_css)
else:
assert world.is_css_present(flag_css)
@step(u's?he can(not)? delete users')
def can_delete_users(_step, inverted):
to_delete_css = 'a.remove-user'
assert world.is_css_not_present(to_delete_css)
if inverted:
assert world.is_css_not_present(to_delete_css)
else:
assert world.is_css_present(to_delete_css)
@step(u's?he cannot add users')
def cannot_add(_step):
add_css = 'a.new-user'
assert world.is_css_not_present(add_css)
@step(u's?he can(not)? add users')
def can_add_users(_step, inverted):
add_css = 'a.create-user-button'
if inverted:
assert world.is_css_not_present(add_css)
else:
assert world.is_css_present(add_css)
......@@ -155,6 +155,10 @@ def cancel_does_not_save_changes(step):
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
def animation_done(_driver):
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
world.wait_for(animation_done)
# Go to advanced tab.
world.css_click('#ui-id-2')
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
......
......@@ -8,8 +8,7 @@ Feature: Sign in
When I click the link with the text "Sign Up"
And I fill in the registration form
And I press the Create My Account button on the registration form
Then I should see be on the studio home page
And I should see the message "complete your sign up we need you to verify your email address"
Then I should see an email verification prompt
Scenario: Login with a valid redirect
Given I have opened a new course in Studio
......
......@@ -22,14 +22,10 @@ def i_press_the_button_on_the_registration_form(step):
world.css_click(submit_css)
@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):
step.given('I should see the message "My Courses"')
@step(u'I should see the message "([^"]*)"$')
def i_should_see_the_message(step, msg):
assert world.browser.is_text_present(msg, 5)
@step('I should see an email verification prompt')
def i_should_see_an_email_verification_prompt(step):
world.css_has_text('h1.page-header', u'My Courses')
world.css_has_text('div.msg h3.title', u'We need to verify your email address')
@step(u'I fill in and submit the signin form$')
......
......@@ -58,7 +58,7 @@ def delete_file(_step, file_name):
world.css_click(delete_css, index=index)
prompt_confirm_css = 'li.nav-item > a.action-primary'
world.css_click(prompt_confirm_css)
world.css_click(prompt_confirm_css, success_condition=lambda: not world.css_visible(prompt_confirm_css))
@step(u'I should see only one "([^"]*)"$')
......
......@@ -19,5 +19,6 @@ def i_see_the_correct_settings_and_values(step):
@step('I have set "show captions" to (.*)')
def set_show_captions(step, setting):
world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.browser.select('Show Captions', setting)
world.css_click('a.save-button')
###
### Script for cloning a course
###
"""
Script for cloning a course
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore
......@@ -15,23 +15,25 @@ from auth.authz import _copy_course_group
class Command(BaseCommand):
"""Clone a MongoDB-backed course to another location"""
help = 'Clone a MongoDB backed course to another location'
def handle(self, *args, **options):
"Execute the command"
if len(args) != 2:
raise CommandError("clone requires two arguments: <source-location> <dest-location>")
source_location_str = args[0]
dest_location_str = args[1]
ms = modulestore('direct')
cs = contentstore()
mstore = modulestore('direct')
cstore = contentstore()
print "Cloning course {0} to {1}".format(source_location_str, dest_location_str)
print("Cloning course {0} to {1}".format(source_location_str, dest_location_str))
source_location = CourseDescriptor.id_to_location(source_location_str)
dest_location = CourseDescriptor.id_to_location(dest_location_str)
if clone_course(ms, cs, source_location, dest_location):
print "copying User permissions..."
if clone_course(mstore, cstore, source_location, dest_location):
print("copying User permissions...")
_copy_course_group(source_location, dest_location)
"""
Script for dumping course dumping the course structure
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
......@@ -9,10 +12,14 @@ filter_list = ['xml_attributes', 'checklists']
class Command(BaseCommand):
"""
The Django command for dumping course structure
"""
help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized
in a JSON format. This can be used for analytics.'''
def handle(self, *args, **options):
"Execute the command"
if len(args) < 2 or len(args) > 3:
raise CommandError("dump_course_structure requires two or more arguments: <location> <outfile> |<db>|")
......@@ -32,7 +39,7 @@ class Command(BaseCommand):
try:
course = store.get_item(loc, depth=4)
except:
print 'Could not find course at {0}'.format(course_id)
print('Could not find course at {0}'.format(course_id))
return
info = {}
......
###
### Script for exporting courseware from Mongo to a tar.gz file
###
"""
Script for exporting courseware from Mongo to a tar.gz file
"""
import os
from django.core.management.base import BaseCommand, CommandError
......@@ -10,20 +10,21 @@ from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
unnamed_modules = 0
class Command(BaseCommand):
"""
Export the specified data directory into the default ModuleStore
"""
help = 'Export the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
"Execute the command"
if len(args) != 2:
raise CommandError("export requires two arguments: <course location> <output path>")
course_id = args[0]
output_path = args[1]
print "Exporting course id = {0} to {1}".format(course_id, output_path)
print("Exporting course id = {0} to {1}".format(course_id, output_path))
location = CourseDescriptor.id_to_location(course_id)
......
###
### Script for exporting all courseware from Mongo to a directory
###
import os
"""
Script for exporting all courseware from Mongo to a directory
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
......@@ -10,13 +8,12 @@ from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
unnamed_modules = 0
class Command(BaseCommand):
"""Export all courses from mongo to the specified data directory"""
help = 'Export all courses from mongo to the specified data directory'
def handle(self, *args, **options):
"Execute the command"
if len(args) != 1:
raise CommandError("export requires one argument: <output path>")
......@@ -27,14 +24,14 @@ class Command(BaseCommand):
root_dir = output_path
courses = ms.get_courses()
print "%d courses to export:" % len(courses)
print("%d courses to export:" % len(courses))
cids = [x.id for x in courses]
print cids
print(cids)
for course_id in cids:
print "-"*77
print "Exporting course id = {0} to {1}".format(course_id, output_path)
print("-"*77)
print("Exporting course id = {0} to {1}".format(course_id, output_path))
if 1:
try:
......@@ -42,6 +39,6 @@ class Command(BaseCommand):
course_dir = course_id.replace('/', '...')
export_to_xml(ms, cs, location, root_dir, course_dir, modulestore())
except Exception as err:
print "="*30 + "> Oops, failed to export %s" % course_id
print "Error:"
print err
print("="*30 + "> Oops, failed to export %s" % course_id)
print("Error:")
print(err)
###
### Script for importing courseware from XML format
###
"""
Script for importing courseware from XML format
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -8,13 +8,14 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
unnamed_modules = 0
class Command(BaseCommand):
"""
Import the specified data directory into the default ModuleStore
"""
help = 'Import the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
"Execute the command"
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
......@@ -23,8 +24,8 @@ class Command(BaseCommand):
course_dirs = args[1:]
else:
course_dirs = None
print "Importing. Data_dir={data}, course_dirs={courses}".format(
print("Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir,
courses=course_dirs)
courses=course_dirs))
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True)
"""
Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import perform_xlint
unnamed_modules = 0
class Command(BaseCommand):
help = \
'''
Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
'''
"""Verify the structure of courseware as to it's suitability for import"""
help = "Verify the structure of courseware as to it's suitability for import"
def handle(self, *args, **options):
"Execute the command"
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
......@@ -21,7 +20,7 @@ class Command(BaseCommand):
course_dirs = args[1:]
else:
course_dirs = None
print "Importing. Data_dir={data}, course_dirs={courses}".format(
print("Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir,
courses=course_dirs)
courses=course_dirs))
perform_xlint(data_dir, course_dirs, load_error_modules=False)
......@@ -50,9 +50,9 @@ class UploadTestCase(CourseTestCase):
@skip("CorruptGridFile error on continuous integration server")
def test_happy_path(self):
file = BytesIO("sample content")
file.name = "sample.txt"
resp = self.client.post(self.url, {"name": "my-name", "file": file})
f = BytesIO("sample content")
f.name = "sample.txt"
resp = self.client.post(self.url, {"name": "my-name", "file": f})
self.assert2XX(resp.status_code)
def test_no_file(self):
......
""" Unit tests for checklist methods in views.py. """
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.utils import get_modulestore
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
......@@ -27,6 +27,7 @@ class ChecklistTestCase(CourseTestCase):
"""
self.assertEqual(persisted['short_description'], request['short_description'])
compare_urls = (persisted.get('action_urls_expanded') == request.get('action_urls_expanded'))
pers, req = None, None
for pers, req in zip(persisted['items'], request['items']):
self.assertEqual(pers['short_description'], req['short_description'])
self.assertEqual(pers['long_description'], req['long_description'])
......@@ -38,7 +39,11 @@ class ChecklistTestCase(CourseTestCase):
def test_get_checklists(self):
""" Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course)
checklists_url = reverse("checklists", kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
response = self.client.get(checklists_url)
self.assertContains(response, "Getting Started With Studio")
payload = response.content
......
......@@ -95,8 +95,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
def tearDown(self):
mongo = MongoClient()
mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
def check_components_on_page(self, component_types, expected_types):
......@@ -604,6 +603,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
'run': '2013_Spring'
}
module_store = modulestore('direct')
......@@ -612,12 +612,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
self.assertEqual(data['id'], 'i4x://MITx/999/course/2013_Spring')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
dest_location = CourseDescriptor.id_to_location('MITx/999/2013_Spring')
clone_course(module_store, content_store, source_location, dest_location)
......@@ -855,6 +855,68 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir)
def test_export_course_with_metadata_only_word_cloud(self):
"""
Similar to `test_export_course_with_metadata_only_video`.
"""
module_store = modulestore('direct')
draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['word_cloud'])
location = CourseDescriptor.id_to_location('HarvardX/ER22x/2013_Spring')
verticals = module_store.get_items(['i4x', 'HarvardX', 'ER22x', 'vertical', None, None])
self.assertGreater(len(verticals), 0)
parent = verticals[0]
ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled")
root_dir = path(mkdtemp_clean())
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
shutil.rmtree(root_dir)
def test_empty_data_roundtrip(self):
"""
Test that an empty `data` field is preserved through
export/import.
"""
module_store = modulestore('direct')
draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
verticals = module_store.get_items(['i4x', 'edX', 'toy', 'vertical', None, None])
self.assertGreater(len(verticals), 0)
parent = verticals[0]
# Create a module, and ensure that its `data` field is empty
word_cloud = ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled")
del word_cloud.data
self.assertEquals(word_cloud.data, '')
# Export the course
root_dir = path(mkdtemp_clean())
export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip', draft_modulestore=draft_store)
# Reimport and get the video back
import_from_xml(module_store, root_dir)
imported_word_cloud = module_store.get_item(Location(['i4x', 'edX', 'toy', 'word_cloud', 'untitled', None]))
# It should now contain empty data
self.assertEquals(imported_word_cloud.data, '')
def test_course_handouts_rewrites(self):
module_store = modulestore('direct')
......@@ -954,6 +1016,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
'run': '2013_Spring'
}
def tearDown(self):
......@@ -965,24 +1028,30 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test new course creation - happy path"""
self.assert_created_course()
def assert_created_course(self):
def assert_created_course(self, number_suffix=None):
"""
Checks that the course was created properly.
"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
test_course_data = {}
test_course_data.update(self.course_data)
if number_suffix:
test_course_data['number'] = '{0}_{1}'.format(test_course_data['number'], number_suffix)
resp = self.client.post(reverse('create_new_course'), test_course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
self.assertNotIn('ErrMsg', data)
self.assertEqual(data['id'], 'i4x://MITx/{0}/course/2013_Spring'.format(test_course_data['number']))
return test_course_data
def test_create_course_check_forum_seeding(self):
"""Test new course creation and verify forum seeding """
self.assert_created_course()
self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course'))
test_course_data = self.assert_created_course(number_suffix=uuid4().hex)
self.assertTrue(are_permissions_roles_seeded('MITx/{0}/2013_Spring'.format(test_course_data['number'])))
def test_create_course_duplicate_course(self):
"""Test new course creation - error path"""
self.client.post(reverse('create_new_course'), self.course_data)
self.assert_course_creation_failed('There is already a course defined with this name.')
self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
def assert_course_creation_failed(self, error_message):
"""
......@@ -997,8 +1066,9 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test new course creation - error path"""
self.client.post(reverse('create_new_course'), self.course_data)
self.course_data['display_name'] = 'Robot Super Course Two'
self.course_data['run'] = '2013_Summer'
self.assert_course_creation_failed('There is already a course defined with the same organization and course number.')
self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.')
def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name"""
......@@ -1167,7 +1237,9 @@ class ContentStoreTest(ModuleStoreTestCase):
# manage users
resp = self.client.get(reverse('manage_users',
kwargs={'location': loc.url()}))
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# course info
......
......@@ -18,8 +18,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
from .utils import CourseTestCase
......@@ -167,8 +165,8 @@ class CourseDetailsViewTest(CourseTestCase):
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(dt):
return Date().to_json(dt)
def convert_datetime_to_iso(datetime_obj):
return Date().to_json(datetime_obj)
def test_update_and_fetch(self):
loc = self.course.location
......
......@@ -2,6 +2,7 @@
from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json
from xmodule.modulestore.django import modulestore
class CourseUpdateTest(CourseTestCase):
......@@ -145,3 +146,36 @@ class CourseUpdateTest(CourseTestCase):
resp = self.client.delete(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1)
def test_no_ol_course_update(self):
'''Test trying to add to a saved course_update which is not an ol.'''
# get the updates and set to something wrong
location = self.course.location.replace(category='course_info', name='updates')
modulestore('direct').create_and_save_xmodule(location)
course_updates = modulestore('direct').get_item(location)
course_updates.data = 'bad news'
modulestore('direct').update_item(location, course_updates.data)
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
content = init_content + '</iframe>'
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': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
self.assertHTMLEqual(payload['content'], content)
# now confirm that the bad news and the iframe make up 2 updates
url = reverse('course_info_json',
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)
......@@ -127,7 +127,7 @@ class TemplateTests(unittest.TestCase):
persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location)
id_locator = CourseLocator(course_id=test_course.location.course_id, revision='draft')
id_locator = CourseLocator(course_id=test_course.location.course_id, branch='draft')
guid_locator = CourseLocator(version_guid=test_course.location.version_guid)
# verify it can be retireved by id
self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor)
......
......@@ -85,9 +85,11 @@ class InternationalizationTest(ModuleStoreTestCase):
HTTP_ACCEPT_LANGUAGE='fr'
)
TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>'
TEST_STRING = (
u'<h1 class="title-1">'
u'My \xc7\xf6\xfcrs\xe9s L#'
u'</h1>'
)
self.assertContains(resp,
TEST_STRING,
......
......@@ -14,19 +14,26 @@ class DeleteItem(CourseTestCase):
super(DeleteItem, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course')
def testDeleteStaticPage(self):
def test_delete_static_page(self):
# Add static tab
data = json.dumps({
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'category': 'static_tab'
})
resp = self.client.post(reverse('create_item'), data,
content_type="application/json")
resp = self.client.post(
reverse('create_item'),
data,
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
resp = self.client.post(
reverse('delete_item'),
resp.content,
"application/json"
)
self.assertEqual(resp.status_code, 200)
......@@ -122,6 +129,7 @@ class TestCreateItem(CourseTestCase):
)
self.assertEqual(resp.status_code, 200)
class TestEditItem(CourseTestCase):
"""
Test contentstore.views.item.save_item
......@@ -151,10 +159,10 @@ class TestEditItem(CourseTestCase):
chap_location = self.response_id(resp)
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': chap_location,
'category': 'sequential'
}),
json.dumps({
'parent_location': chap_location,
'category': 'sequential',
}),
content_type="application/json"
)
self.seq_location = self.response_id(resp)
......@@ -162,9 +170,10 @@ class TestEditItem(CourseTestCase):
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({'parent_location': self.seq_location,
'category': 'problem',
'boilerplate': template_id
json.dumps({
'parent_location': self.seq_location,
'category': 'problem',
'boilerplate': template_id,
}),
content_type="application/json"
)
......@@ -195,7 +204,6 @@ class TestEditItem(CourseTestCase):
problem = modulestore('draft').get_item(self.problems[0])
self.assertEqual(problem.rerandomize, 'never')
def test_null_field(self):
"""
Sending null in for a field 'deletes' it
......@@ -240,4 +248,3 @@ class TestEditItem(CourseTestCase):
sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.assertEqual(sequential.lms.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
......@@ -72,50 +72,6 @@ class LMSLinksTestCase(TestCase):
)
class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """
def test_course_page_names(self):
""" Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('ManageUsers', course)
)
self.assertEquals(
'/mitX/666/settings-details/URL_Reverse_Course',
utils.get_url_reverse('SettingsDetails', course)
)
self.assertEquals(
'/mitX/666/settings-grading/URL_Reverse_Course',
utils.get_url_reverse('SettingsGrading', course)
)
self.assertEquals(
'/mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('CourseOutline', course)
)
self.assertEquals(
'/mitX/666/checklists/URL_Reverse_Course',
utils.get_url_reverse('Checklists', course)
)
def test_unknown_passes_through(self):
""" Test that unknown values pass through. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'foobar',
utils.get_url_reverse('foobar', course)
)
self.assertEquals(
'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. """
......
......@@ -15,14 +15,16 @@ class ContentStoreTestCase(ModuleStoreTestCase):
Login. View should always return 200. The success/fail is in the
returned json
"""
resp = self.client.post(reverse('login_post'),
{'email': email, 'password': password})
resp = self.client.post(
reverse('login_post'),
{'email': email, 'password': password}
)
self.assertEqual(resp.status_code, 200)
return resp
def login(self, email, pw):
def login(self, email, password):
"""Login, check that it worked."""
resp = self._login(email, pw)
resp = self._login(email, password)
data = parse_json(resp)
self.assertTrue(data['success'])
return resp
......@@ -178,11 +180,15 @@ class ForumTestCase(CourseTestCase):
def test_blackouts(self):
now = datetime.datetime.now(UTC)
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
[(now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
times1 = [
(now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))
]
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times1]
self.assertTrue(self.course.forum_posts_allowed)
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
[(now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
times2 = [
(now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))
]
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times2]
self.assertFalse(self.course.forum_posts_allowed)
......@@ -188,38 +188,6 @@ def update_item(location, value):
get_modulestore(location).update_item(location, value)
def get_url_reverse(course_page_name, course_module):
"""
Returns the course URL link to the specified location. This value is suitable to use as an href link.
course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
course_page_names so that it can also be used for absolute (known) URLs.
course_module is used to obtain the location, org, course, and name properties for a course, if
course_page_name corresponds to an attribute in CoursePageNames.
"""
url_name = getattr(CoursePageNames, course_page_name, None)
ctx_loc = course_module.location
if CoursePageNames.ManageUsers == url_name:
return reverse(url_name, kwargs={"location": ctx_loc})
elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
else:
return course_page_name
class CoursePageNames:
""" Constants for pages that are recognized by get_url_reverse method. """
ManageUsers = "manage_users"
SettingsDetails = "settings_details"
SettingsGrading = "settings_grading"
CourseOutline = "course_index"
Checklists = "checklists"
def add_extra_panel_tab(tab_type, course):
"""
Used to add the panel tab to a course if it does not exist.
......
......@@ -29,7 +29,6 @@ from xmodule.util.date_utils import get_default_time_display
from xmodule.modulestore import InvalidLocationError
from xmodule.exceptions import NotFoundError
from ..utils import get_url_reverse
from .access import get_location_and_verify_access
from util.json_request import JsonResponse
......@@ -284,7 +283,7 @@ def import_course(request, org, course, name):
tar_file.extractall(course_dir + '/')
# find the 'course.xml' file
dirpath = None
for dirpath, _dirnames, filenames in os.walk(course_dir):
for filename in filenames:
if filename == 'course.xml':
......@@ -320,7 +319,11 @@ def import_course(request, org, course, name):
return render_to_response('import.html', {
'context_course': course_module,
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
'successful_import_redirect_url': reverse('course_index', kwargs={
'org': location.org,
'course': location.course,
'name': location.name,
})
})
......
......@@ -4,12 +4,13 @@ from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.core.urlresolvers import reverse
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from ..utils import get_modulestore
from .access import get_location_and_verify_access
from xmodule.course_module import CourseDescriptor
......@@ -96,10 +97,25 @@ def expand_checklist_action_urls(course_module):
"""
checklists = course_module.checklists
modified = False
urlconf_map = {
"ManageUsers": "manage_users",
"SettingsDetails": "settings_details",
"SettingsGrading": "settings_grading",
"CourseOutline": "course_index",
"Checklists": "checklists",
}
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)
action_url = item.get('action_url')
if action_url not in urlconf_map:
continue
urlconf_name = urlconf_map[action_url]
item['action_url'] = reverse(urlconf_name, kwargs={
'org': course_module.location.org,
'course': course_module.location.course,
'name': course_module.location.name,
})
checklist['action_urls_expanded'] = True
modified = True
......
......@@ -46,13 +46,19 @@ COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_TYPES = [
'annotatable',
'word_cloud',
'videoalpha',
'graphical_slider_tool'
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required
def edit_subsection(request, location):
"Edit the subsection of a course"
# check that we have permissions to edit this item
try:
course = get_course_for_item(location)
......@@ -264,6 +270,7 @@ def assignment_type_update(request, org, course, category, name):
@login_required
@expect_json
def create_draft(request):
"Create a draft"
location = request.POST['id']
# check permissions for this user within this course
......@@ -280,6 +287,7 @@ def create_draft(request):
@login_required
@expect_json
def publish_draft(request):
"Publish a draft"
location = request.POST['id']
# check permissions for this user within this course
......@@ -295,6 +303,7 @@ def publish_draft(request):
@login_required
@expect_json
def unpublish_unit(request):
"Unpublish a unit"
location = request.POST['id']
# check permissions for this user within this course
......@@ -312,6 +321,7 @@ def unpublish_unit(request):
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
"Get or set information for a module in the modulestore"
location = Location(module_location)
# check that logged in user has permissions to this item
......
......@@ -3,6 +3,7 @@ Views related to operations on course objects
"""
import json
import random
from django.utils.translation import ugettext as _
import string # pylint: disable=W0402
from django.contrib.auth.decorators import login_required
......@@ -101,12 +102,13 @@ def create_new_course(request):
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
run = request.POST.get('run')
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
dest_location = Location('i4x', org, number, 'course', run)
except InvalidLocationError as error:
return JsonResponse({
"ErrMsg": "Unable to create course '{name}'.\n\n{err}".format(
"ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(
name=display_name, err=error.message)})
# see if the course already exists
......@@ -116,12 +118,24 @@ def create_new_course(request):
except ItemNotFoundError:
pass
if existing_course is not None:
return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'})
return JsonResponse(
{
'ErrMsg': _('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.'),
'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'),
}
)
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'})
return JsonResponse(
{
'ErrMsg': _('There is already a course defined with the same organization and course number. Please change at least one field to be unique.'),
'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'),
}
)
# instantiate the CourseDescriptor and then persist it
# note: no system to pass
......
......@@ -68,6 +68,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
@login_required
def preview_component(request, location):
"Return the HTML preview of a component"
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
return HttpResponseForbidden()
......@@ -91,6 +92,7 @@ def preview_module_system(request, preview_id, descriptor):
"""
def preview_model_data(descriptor):
"Helper method to create a DbModel from a descriptor"
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
......@@ -105,7 +107,7 @@ def preview_module_system(request, preview_id, descriptor):
# 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),
get_module=partial(load_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),
......@@ -115,28 +117,13 @@ def preview_module_system(request, preview_id, descriptor):
)
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
Return a preview XModule instantiated from the supplied descriptor.
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:
......
"""
Public views
"""
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
......@@ -10,10 +13,6 @@ from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks']
"""
Public views
"""
@ensure_csrf_cookie
def signup(request):
......@@ -45,6 +44,7 @@ def login_page(request):
def howitworks(request):
"Proxy view"
if request.user.is_authenticated():
return index(request)
else:
......
from django.http import HttpResponse
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
......@@ -11,7 +12,7 @@ def landing(request, org, course, coursename):
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
return redirect('/')
def event(request):
......
"""
Views related to course tabs
"""
from access import has_access
from util.json_request import expect_json
......@@ -39,6 +42,7 @@ def initialize_course_tabs(course):
@login_required
@expect_json
def reorder_static_tabs(request):
"Order the static tabs in the requested order"
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
......@@ -86,6 +90,7 @@ def reorder_static_tabs(request):
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
"Edit tabs"
location = ['i4x', org, course, 'course', coursename]
store = get_modulestore(location)
course_item = store.get_item(location)
......@@ -122,6 +127,7 @@ def edit_tabs(request, org, course, coursename):
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
"Static pages view"
location = get_location_and_verify_access(request, org, course, coursename)
......
"""
A Django settings file for use on AWS while running
database migrations, since we don't want to normally run the
LMS with enough privileges to modify the database schema.
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# Import everything from .aws so that our settings are based on those.
from .aws import *
import os
from django.core.exceptions import ImproperlyConfigured
USER = os.environ.get('DB_MIGRATION_USER', 'root')
PASSWORD = os.environ.get('DB_MIGRATION_PASS', None)
if not PASSWORD:
raise ImproperlyConfigured("No database password was provided for running "
"migrations. This is fatal.")
DATABASES['default']['USER'] = USER
DATABASES['default']['PASSWORD'] = PASSWORD
......@@ -597,11 +597,9 @@ function cancelNewSection(e) {
function addNewCourse(e) {
e.preventDefault();
$('.new-course-button').addClass('disabled');
$(e.target).addClass('disabled');
var $newCourse = $($('#new-course-template').html());
$('.new-course-button').addClass('is-disabled');
var $newCourse = $('.wrapper-create-course').addClass('is-shown');
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.courses').prepend($newCourse);
$newCourse.find('.new-course-name').focus().select();
$newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
......@@ -613,41 +611,97 @@ function addNewCourse(e) {
function saveNewCourse(e) {
e.preventDefault();
var $newCourse = $(this).closest('.new-course');
var org = $newCourse.find('.new-course-org').val();
var number = $newCourse.find('.new-course-number').val();
var display_name = $newCourse.find('.new-course-name').val();
var $newCourseForm = $(this).closest('#create-course-form');
var display_name = $newCourseForm.find('.new-course-name').val();
var org = $newCourseForm.find('.new-course-org').val();
var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
if (org == '' || number == '' || display_name == '') {
alert(gettext('You must specify all fields in order to create a new course.'));
return;
var required_field_text = gettext('Required field');
var display_name_errMsg = (display_name === '') ? required_field_text : null;
var org_errMsg = (org === '') ? required_field_text : null;
var number_errMsg = (number === '') ? required_field_text : null;
var run_errMsg = (run === '') ? required_field_text : null;
var bInErr = (display_name_errMsg || org_errMsg || number_errMsg || run_errMsg);
// check for suitable encoding
if (!bInErr) {
var encoding_errMsg = gettext('Please do not use any spaces or special characters in this field.');
if (encodeURIComponent(org) != org)
org_errMsg = encoding_errMsg;
if (encodeURIComponent(number) != number)
number_errMsg = encoding_errMsg;
if (encodeURIComponent(run) != run)
run_errMsg = encoding_errMsg;
bInErr = (org_errMsg || number_errMsg || run_errMsg);
}
var header_err_msg = (bInErr) ? gettext('Please correct the fields below.') : null;
var setNewCourseErrMsgs = function(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg) {
if (header_err_msg) {
$('.wrapper-create-course').addClass('has-errors');
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + header_err_msg + '</p>');
} else {
$('.wrap-error').removeClass('is-shown');
$('#course_creation_error').html('');
}
var setNewCourseFieldInErr = function(el, msg) {
el.children('.tip-error').remove();
if (msg !== null && msg !== '') {
el.addClass('error');
el.append('<span class="tip tip-error">' + msg + '</span>');
} else {
el.removeClass('error');
}
};
setNewCourseFieldInErr($('#field-course-name'), display_name_errMsg);
setNewCourseFieldInErr($('#field-organization'), org_errMsg);
setNewCourseFieldInErr($('#field-course-number'), number_errMsg);
setNewCourseFieldInErr($('#field-course-run'), run_errMsg);
};
setNewCourseErrMsgs(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg);
if (bInErr)
return;
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name
'display_name': display_name,
'run': run
});
$.post('/create_new_course', {
'org': org,
'number': number,
'display_name': display_name
},
function(data) {
if (data.id != undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg != undefined) {
alert(data.ErrMsg);
'org': org,
'number': number,
'display_name': display_name,
'run': run
},
function(data) {
if (data.id !== undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg !== undefined) {
var orgErrMsg = (data.OrgErrMsg !== undefined) ? data.OrgErrMsg : null;
var courseErrMsg = (data.CourseErrMsg !== undefined) ? data.CourseErrMsg : null;
setNewCourseErrMsgs(data.ErrMsg, null, orgErrMsg, courseErrMsg, null);
}
}
});
);
}
function cancelNewCourse(e) {
e.preventDefault();
$('.new-course-button').removeClass('disabled');
$(this).parents('section.new-course').remove();
$('.new-course-button').removeClass('is-disabled');
$('.wrapper-create-course').removeClass('is-shown');
}
function addNewSubsection(e) {
......
......@@ -34,16 +34,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
},
initialize: function() {
var self = this;
// instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("course_info_update",
// TODO Where should the template reside? how to use the static.url to create the path?
"/static/client_templates/course_info_update.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
this.template = _.template($("#course_info_update-tpl").text());
this.render();
// when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render);
},
......@@ -241,16 +233,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
},
initialize: function() {
this.template = _.template($("#course_info_handouts-tpl").text());
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();
}
);
self.render();
},
reset: true
});
......
......@@ -225,12 +225,19 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
children.push(ui.draggable.data('id'));
}
var saving = new CMS.Views.Notification.Mini({
title: gettext('Saving') + '&hellip;'
});
saving.show();
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
data:JSON.stringify({ 'id' : subsection_id, 'children' : children}),
success: function() {
saving.hide();
}
});
}
......
......@@ -11,16 +11,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
},
initialize : function() {
var self = this;
// instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("advanced_entry",
"/static/client_templates/advanced_entry.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
this.template = _.template($("#advanced_entry-tpl").text());
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.render();
},
render: function() {
// catch potential outside call before template loaded
......@@ -56,7 +49,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
CodeMirror.fromTextArea(textarea, {
mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) {
instance.save()
instance.save();
// this event's being called even when there's no change :-(
if (instance.getValue() !== oldValue) {
var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.");
......@@ -105,8 +98,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// call validateKey on each to ensure proper format
// check for dupes
var self = this;
this.model.save({},
{
this.model.save({}, {
success : function() {
self.render();
var title = gettext("Your policy changes have been saved.");
......
......@@ -23,6 +23,13 @@ body.dashboard {
}
// yes we have no boldness today - need to fix the resets
body strong,
body b {
font-weight: 700;
}
// known things to do (paint the fence, sand the floor, wax on/off)
// ====================
......
......@@ -93,6 +93,227 @@ form {
}
}
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ELEM: form
// form styling for creating a new content item (course, user, textbook)
form[class^="create-"] {
@extend .ui-window;
.title {
@extend .t-title4;
font-weight: 600;
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
}
fieldset {
padding: $baseline ($baseline*1.5);
}
.list-input {
@extend .cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
}
&.required {
label {
font-weight: 600;
}
label:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
}
label {
@extend .t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
&.is-focused {
color: $blue;
}
}
input, textarea {
@extend .t-copy-base;
@include transition(all $tmg-f2 ease-in-out 0s);
height: 100%;
width: 100%;
padding: ($baseline/2);
&.long {
width: 100%;
}
&.short {
width: 25%;
}
/*@include placeholder {
color: $gray-l3;
}*/
&:focus {
+ .tip {
color: $gray;
}
}
}
textarea.long {
height: ($baseline*5);
}
input[type="checkbox"] {
display: inline-block;
margin-right: ($baseline/4);
width: auto;
height: auto;
& + label {
display: inline-block;
}
}
.tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.tip-error {
display: none;
float: none;
}
&.error {
label {
color: $red;
}
.tip-error {
@extend .anim-fadeIn;
display: block;
color: $red;
}
input {
border-color: $red;
}
}
}
.field-inline {
input, textarea, select {
width: 62%;
display: inline-block;
}
.tip-stacked {
display: inline-block;
float: right;
width: 35%;
margin-top: 0;
}
&.error {
.tip-error {
}
}
}
.field-group {
@include clearfix();
margin: 0 0 ($baseline/2) 0;
.field {
display: block;
width: 47%;
border-bottom: none;
margin: 0 ($baseline*0.75) 0 0;
padding: ($baseline/4) 0 0 0;
float: left;
position: relative;
&:nth-child(odd) {
float: left;
}
&:nth-child(even) {
float: right;
margin-right: 0;
}
input, textarea {
width: 100%;
}
}
}
}
.actions {
box-shadow: inset 0 1px 2px $shadow;
margin-top: ($baseline*0.75);
border-top: 1px solid $gray-l1;
padding: ($baseline*0.75) ($baseline*1.5);
background: $gray-l6;
.action {
@include transition(all $tmg-f2 linear 0s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
.action-primary {
@include blue-button;
@extend .t-action2;
}
.action-secondary {
@include grey-button;
@extend .t-action2;
}
}
}
// ====================
// forms - grandfathered
......
// studio - elements - icons
// studio - elements - icons & badges
// ====================
.icon {
......@@ -14,3 +14,45 @@
vertical-align: middle;
margin-right: ($baseline/4);
}
// ui - badges
.wrapper-ui-badge {
position: absolute;
top: -1px;
left: ($baseline*1.5);
width: 100%;
}
.ui-badge {
@extend .t-title9;
position: relative;
border-bottom-right-radius: ($baseline/10);
border-bottom-left-radius: ($baseline/10);
padding: ($baseline/4) ($baseline/2) ($baseline/4) ($baseline/2);
font-weight: 600;
text-transform: uppercase;
* [class^="icon-"] {
margin-right: ($baseline/5);
}
// OPTION: add this class for a visual hanging display
&.is-hanging {
@include box-sizing(border-box);
@extend .ui-depth2;
top: -($baseline/4);
&:after {
position: absolute;
top: 0;
right: -($baseline/4);
display: block;
height: 0;
width: 0;
border-bottom: ($baseline/4) solid $black-t3;
border-right: ($baseline/4) solid transparent;
content: "";
opacity: 0.5;
}
}
}
......@@ -64,12 +64,14 @@ nav {
opacity: 0.0;
pointer-events: none;
width: ($baseline*8);
overflow: hidden;
// dropped down state
&.is-shown {
opacity: 1.0;
pointer-events: auto;
overflow: visible;
}
}
......
......@@ -55,8 +55,8 @@
margin-bottom: $baseline;
.title {
@extend .t-title7;
margin-bottom: ($baseline/4);
@extend .t-title6;
margin-bottom: ($baseline/2);
font-weight: 700;
}
......@@ -167,6 +167,34 @@
}
}
// particular notice - create
.notice-create {
background-color: $gray-l4;
.title {
color: $gray-d2;
}
.copy {
color: $gray-d2;
}
&.has-actions {
.list-actions {
.action-item {
}
.action-primary {
@extend .btn-primary-green;
@extend .t-action3;
}
}
}
}
// particular notice - confirmation
.notice-confirmation {
background-color: $green-l5;
......
......@@ -358,22 +358,30 @@ body.dashboard {
}
}
.new-course {
@include clearfix();
padding: ($baseline*0.75) ($baseline*1.25);
margin-top: $baseline;
border-radius: 3px;
border: 1px solid $gray;
background: $white;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
.title {
@extend .t-title4;
font-weight: 600;
margin-bottom: ($baseline/2);
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline/2);
}
// ELEM: new user form
.wrapper-create-course {
// CASE: when form is animating
&.animate {
// STATE: shown
&.is-shown {
height: ($baseline*26);
// STATE: errors
&.has-errors {
height: ($baseline*33);
}
}
}
}
// ====================
// course listings
.create-course {
.row {
@include clearfix();
......@@ -389,10 +397,6 @@ body.dashboard {
margin-right: 4%;
}
.course-info {
width: 600px;
}
label {
@extend .t-title7;
display: block;
......@@ -401,7 +405,8 @@ body.dashboard {
.new-course-org,
.new-course-number,
.new-course-name {
.new-course-name,
.new-course-run {
width: 100%;
}
......@@ -421,5 +426,25 @@ body.dashboard {
.item-details {
padding-bottom: 0;
}
.wrap-error {
@include transition(all $tmg-f2 ease 0s);
height: 0;
overflow: hidden;
opacity: 0;
}
.wrap-error.is-shown {
height: 65px;
opacity: 1;
}
.message-status {
display: block;
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
font-weight: bold;
}
}
}
......@@ -30,7 +30,7 @@ body.course.textbooks {
}
.textbook {
@extend .window;
@extend .ui-window;
position: relative;
.view-textbook {
......
......@@ -3,80 +3,227 @@
body.course.users {
.new-user-form {
display: none;
padding: 15px 20px;
background-color: $lightBluishGrey2;
#result {
display: none;
float: left;
margin-bottom: 15px;
padding: 3px 15px;
border-radius: 3px;
background: $error-red;
font-size: 14px;
color: #fff;
}
// LAYOUT: page
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.form-elements {
clear: both;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
label {
display: inline-block;
margin-right: 10px;
}
.content-supplementary {
width: flex-grid(3, 12);
}
.email-input {
width: 350px;
padding: 8px 8px 10px;
border-color: $darkGrey;
// ELEM: content
.content {
.introduction {
@extend .t-copy-sub1;
margin: 0 0 ($baseline*2) 0;
}
}
// ELEM: no users notice
.content .notice-create {
width: flexgrid(9, 9);
margin-top: $baseline;
// CASE: notice has actions {
&.has-actions {
.msg, .list-actions {
display: inline-block;
vertical-align: middle;
}
.msg {
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
.list-actions {
width: flex-grid(3, 9);
text-align: right;
margin-top: 0;
.action-item {
}
.add-button {
@include blue-button;
padding: 5px 20px 9px;
.action-primary {
@include green-button(); // overwriting for the sake of syncing older green button styles for now
@extend .t-action3;
padding: ($baseline/2) $baseline;
}
}
}
}
.cancel-button {
@include white-button;
padding: 5px 20px 9px;
// ELEM: new user form
.wrapper-create-user {
&.is-shown {
height: ($baseline*15);
}
}
// ELEM: listing of users
.user-list, .user-item, .item-metadata, .item-actions {
@include box-sizing(border-box);
}
.user-list {
border: 1px solid $mediumGrey;
background: #fff;
li {
.user-item {
@extend .ui-window;
@include clearfix();
position: relative;
padding: 20px;
border-bottom: 1px solid $mediumGrey;
width: flex-grid(9, 9);
margin: 0 0 ($baseline/2) 0;
padding: ($baseline*1.25) ($baseline*1.5) $baseline ($baseline*1.5);
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
span {
.item-metadata, .item-actions {
display: inline-block;
vertical-align: middle;
}
.user-name {
margin-right: 10px;
font-size: 24px;
font-weight: 300;
// ELEM: item - flag
.flag-role {
@extend .ui-badge;
color: $white;
.msg-you {
margin-left: ($baseline/5);
text-transform: none;
font-weight: 500;
color: $pink-l3;
}
&:after {
border-bottom-color: $pink-d4;
}
&.flag-role-staff {
background: $pink-u3;
}
&.flag-role-admin {
background: $pink;
}
}
.user-email {
font-size: 14px;
font-style: italic;
color: $mediumGrey;
// ELEM: item - metadata
.item-metadata {
width: flex-grid(5, 9);
margin-right: flex-gutter();
.user-username, .user-email {
display: inline-block;
vertical-align: middle;
}
.user-username {
@extend .t-title4;
@include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 ($baseline/2) ($baseline/10) 0;
color: $gray-d4;
font-weight: 600;
}
.user-email {
@extend .t-title6;
}
}
// ELEM: item - actions
.item-actions {
top: 24px;
width: flex-grid(4, 9);
position: static; // nasty reset needed due to base.scss
text-align: right;
.action {
display: inline-block;
vertical-align: middle;
}
.action-role {
width: flex-grid(3, 4);
margin-right: flex-gutter();
}
.action-delete {
width: flex-grid(1, 4);
// STATE: disabled
&.is-disabled {
opacity: 0.0;
visibility: hidden;
pointer-events: none;
}
}
.delete {
@extend .ui-btn-non;
}
// HACK: nasty reset needed due to base.scss
.delete-button {
margin-right: 0;
float: none;
color: inherit;
}
// ELEM: admin role controls
.toggle-admin-role {
&.add-admin-role {
@include blue-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
}
&.remove-admin-role {
@include grey-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
}
}
.notoggleforyou {
@extend .t-copy-sub1;
color: $gray-l2;
}
}
// STATE: hover
&:hover {
.user-username {
}
.user-email {
}
.item-actions {
}
}
}
}
}
\ No newline at end of file
}
......@@ -6,9 +6,15 @@
<%block name="title">${_("Course Updates")}</%block>
<%block name="bodyclass">is-signedin course course-info updates</%block>
<%block name="header_extras">
% for template_name in ["course_info_update", "course_info_handouts"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
......
......@@ -36,36 +36,6 @@
</script>
</%block>
<%block name="header_extras">
<script type="text/template" id="new-course-template">
<section class="new-course">
<h3 class="title">${_("Create a New Course:")}</h3>
<div class="item-details">
<form class="course-info">
<div class="row">
<label>${_("Course Name")}</label>
<input type="text" class="new-course-name" />
</div>
<div class="row">
<div class="column">
<label>${_("Organization")}</label>
<input type="text" class="new-course-org" />
</div>
<div class="column">
<label>${_("Course Number")}</label>
<input type="text" class="new-course-number" />
</div>
</div>
<div class="row">
<input type="submit" value="${_('Save')}" class="new-course-save"/>
<input type="button" value="${_('Cancel')}" class="new-course-cancel" />
</div>
</form>
</div>
</section>
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions">
......@@ -109,6 +79,57 @@
%endif
</div>
% if course_creator_status=='granted':
<div class="wrapper-create-element wrapper-create-course">
<form class="create-course course-info" id="create-course-form" name="create-course-form">
<div class="wrap-error">
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert">
<p>${_("Please correct the highlighted fields below.")}</p>
</div>
</div>
<div class="wrapper-form">
<h3 class="title">${_("Create a New Course")}</h3>
<fieldset>
<legend class="sr">${_("Required Information to Create a New Course")}</legend>
<ol class="list-input">
<li class="field field-inline text required" id="field-course-name">
<label for="new-course-name">${_("Course Name")}</label>
<input class="new-course-name" id="new-course-name" type="text" name="new-course-name" aria-required="true" placeholder="${_('e.g. Introduction to Computer Science')}" />
<span class="tip tip-stacked">${_("The public display name for your course.")}</span>
</li>
<li class="field field-inline text required" id="field-organization">
<label for="new-course-org">${_("Organization")}</label>
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" aria-required="true" placeholder="${_('e.g. MITX or IMF')}" />
<span class="tip tip-stacked">${_("The name of the organization sponsoring the course")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
<li class="field field-inline text required" id="field-course-number">
<label for="new-course-number">${_("Course Number")}</label>
<input class="new-course-number" id="new-course-number" type="text" name="new-course-number" aria-required="true" placeholder="${_('e.g. CS101')}" />
<span class="tip tip-stacked">${_("The unique number that identifies your course within your organization")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
<li class="field field-inline text required" id="field-course-run">
<label for="new-course-run">${_("Course Run")}</label>
<input class="new-course-run" id="new-course-run" type="text" name="new-course-run" aria-required="true"placeholder="${_('e.g. 2013_Spring')}" />
<span class="tip tip-stacked">${_("The term in which your course will run")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<input type="submit" value="${_('Save')}" class="action action-primary new-course-save" />
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-course-cancel" />
</div>
</form>
</div>
% endif
%if len(courses) > 0:
<div class="courses">
<ul class="list-courses">
......
<li class="field-group course-advanced-policy-list-item">
<div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
</li>
......@@ -262,7 +262,7 @@ from contentstore import utils
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul>
</nav>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%! from contentstore import utils %>
<%block name="title">${_("Advanced Settings")}</%block>
<%block name="bodyclass">is-signedin course advanced settings</%block>
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
%>
<%block name="jsextra">
% for template_name in ["advanced_entry"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
......@@ -96,7 +98,7 @@ editor.render();
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Course Team")}</a></li>
</ul>
</nav>
% endif
......
......@@ -140,7 +140,7 @@ from contentstore import utils
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul>
</nav>
......
......@@ -60,7 +60,7 @@
<a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a>
</li>
<li class="nav-item nav-course-settings-team">
<a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a>
<a href="${reverse('manage_users', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Course Team")}</a>
</li>
<li class="nav-item nav-course-settings-advanced">
<a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a>
......
......@@ -40,14 +40,12 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
'contentstore.views.upload_asset', name='upload_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/team/(?P<name>[^/]+)$',
'contentstore.views.manage_users', name='manage_users'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/team/(?P<name>[^/]+)/(?P<email>[^/]+)$',
'contentstore.views.course_team_user', name='course_team_user'),
url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
url(r'^add_user/(?P<location>.*?)$',
'contentstore.views.add_user', name='add_user'),
url(r'^remove_user/(?P<location>.*?)$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$',
'contentstore.views.course_info', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$',
......
......@@ -92,9 +92,10 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
return template.render_unicode(**context_dictionary)
def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs):
def render_to_response(template_name, dictionary=None, context_instance=None, namespace='main', **kwargs):
"""
Returns a HttpResponse whose content is filled with the result of calling
lookup.get_template(args[0]).render with the passed arguments.
"""
dictionary = dictionary or {}
return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace), **kwargs)
......@@ -90,10 +90,7 @@ def index(request, extra_context={}, user=None):
courses = get_courses(None, domain=domain)
courses = sort_by_announcement(courses)
# Get the 3 most recent news
top_news = _get_news(top=3)
context = {'courses': courses, 'news': top_news}
context = {'courses': courses}
context.update(extra_context)
return render_to_response('index.html', context)
......@@ -285,9 +282,6 @@ def dashboard(request):
exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
# Get the 3 most recent news
top_news = _get_news(top=3) if not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False) else None
# get info w.r.t ExternalAuthMap
external_auth_map = None
try:
......@@ -302,7 +296,6 @@ def dashboard(request):
'errored_courses': errored_courses,
'show_courseware_links_for': show_courseware_links_for,
'cert_statuses': cert_statuses,
'news': top_news,
'exam_registrations': exam_registrations,
}
......@@ -1242,28 +1235,3 @@ def accept_name_change(request):
raise Http404
return accept_name_change_by_id(int(request.POST['id']))
def _get_news(top=None):
"Return the n top news items on settings.RSS_URL"
# Don't return anything if we're in a themed site
if settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
return None
feed_data = cache.get("students_index_rss_feed_data")
if feed_data is None:
if hasattr(settings, 'RSS_URL'):
feed_data = urllib.urlopen(settings.RSS_URL).read()
else:
feed_data = render_to_string("feed.rss", None)
cache.set("students_index_rss_feed_data", feed_data, settings.RSS_TIMEOUT)
feed = feedparser.parse(feed_data)
entries = feed['entries'][0:top] # all entries if top is None
for entry in entries:
soup = BeautifulSoup(entry.description)
entry.image = soup.img['src'] if soup.img else None
entry.summary = soup.getText()
return entries
// In the LMS sliders use built-in styles from jquery-ui-1.8.22.custom.css.
// CMS uses its own sliders styles.
// These styles we use only to sure, that slider in GST module
// will be render correctly (just like a duplication some from jquery-ui-1.8.22.custom.css).
// Cause, for example, CMS overwrites many jquery-ui-1.8.22.custom.css styles,
// and we must overwrite them again.
.ui-widget-content {
border: 1px solid #dddddd;
color: #333333;
}
.ui-widget {
font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif;
font-size: 1.1em;
}
.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl {
-moz-border-radius-topleft: 4px;
-webkit-border-top-left-radius: 4px;
-khtml-border-top-left-radius: 4px;
border-top-left-radius: 4px;
}
.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr {
-moz-border-radius-topright: 4px;
-webkit-border-top-right-radius: 4px;
-khtml-border-top-right-radius: 4px;
border-top-right-radius: 4px;
}
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl {
-moz-border-radius-bottomleft: 4px;
-webkit-border-bottom-left-radius: 4px;
-khtml-border-bottom-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br {
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-right-radius: 4px;
-khtml-border-bottom-right-radius: 4px;
border-bottom-right-radius: 4px;
}
\ No newline at end of file
......@@ -20,7 +20,7 @@ def parse_url(string):
with key 'version_guid' and the value,
If it can be parsed as a course_id, returns a dict
with keys 'id' and 'revision' (value of 'revision' may be None),
with keys 'id' and 'branch' (value of 'branch' may be None),
"""
match = URL_RE.match(string)
......@@ -69,14 +69,14 @@ def parse_guid(string):
return None
COURSE_ID_RE = re.compile(r'^(?P<id>(\w+)(\.\w+\w*)*)(;(?P<revision>\w+))?(#(?P<block>\w+))?$', re.IGNORECASE)
COURSE_ID_RE = re.compile(r'^(?P<id>(\w+)(\.\w+\w*)*)(;(?P<branch>\w+))?(#(?P<block>\w+))?$', re.IGNORECASE)
def parse_course_id(string):
r"""
A course_id has a main id component.
There may also be an optional revision (;published or ;draft).
There may also be an optional branch (;published or ;draft).
There may also be an optional block (#HW3 or #Quiz2).
Examples of valid course_ids:
......@@ -89,11 +89,11 @@ def parse_course_id(string):
Syntax:
course_id = main_id [; revision] [# block]
course_id = main_id [; branch] [# block]
main_id = name [. name]*
revision = name
branch = name
block = name
......@@ -104,8 +104,8 @@ def parse_course_id(string):
and the underscore. (see definition of \w in python regular expressions,
at http://docs.python.org/dev/library/re.html)
If string is a course_id, returns a dict with keys 'id', 'revision', and 'block'.
Revision is optional: if missing returned_dict['revision'] is None.
If string is a course_id, returns a dict with keys 'id', 'branch', and 'block'.
Revision is optional: if missing returned_dict['branch'] is None.
Block is optional: if missing returned_dict['block'] is None.
Else returns None.
"""
......
......@@ -81,7 +81,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
version_guid=course_entry_override['_id'],
usage_id=usage_id,
course_id=course_entry_override.get('course_id'),
revision=course_entry_override.get('revision')
branch=course_entry_override.get('branch')
)
kvs = SplitMongoKVS(
......
......@@ -186,8 +186,8 @@ class SplitMongoModuleStore(ModuleStoreBase):
return the CourseDescriptor! It returns the actual db json from
structures.
Semantics: if course_id and revision given, then it will get that revision. If
also give a version_guid, it will see if the current head of that revision == that guid. If not
Semantics: if course_id and branch given, then it will get that branch. If
also give a version_guid, it will see if the current head of that branch == that guid. If not
it raises VersionConflictError (the version now differs from what it was when you got your
reference)
......@@ -198,19 +198,19 @@ class SplitMongoModuleStore(ModuleStoreBase):
if not course_locator.is_fully_specified():
raise InsufficientSpecificationError('Not fully specified: %s' % course_locator)
if course_locator.course_id is not None and course_locator.revision is not None:
if course_locator.course_id is not None and course_locator.branch is not None:
# use the course_id
index = self.course_index.find_one({'_id': course_locator.course_id})
if index is None:
raise ItemNotFoundError(course_locator)
if course_locator.revision not in index['versions']:
if course_locator.branch not in index['versions']:
raise ItemNotFoundError(course_locator)
version_guid = index['versions'][course_locator.revision]
version_guid = index['versions'][course_locator.branch]
if course_locator.version_guid is not None and version_guid != course_locator.version_guid:
# This may be a bit too touchy but it's hard to infer intent
raise VersionConflictError(course_locator, CourseLocator(course_locator, version_guid=version_guid))
else:
# TODO should this raise an exception if revision was provided?
# TODO should this raise an exception if branch was provided?
version_guid = course_locator.version_guid
# cast string to ObjectId if necessary
......@@ -223,29 +223,29 @@ class SplitMongoModuleStore(ModuleStoreBase):
if course_locator.course_id:
entry['course_id'] = course_locator.course_id
entry['revision'] = course_locator.revision
entry['branch'] = course_locator.branch
return entry
def get_courses(self, revision, qualifiers=None):
def get_courses(self, branch, qualifiers=None):
'''
Returns a list of course descriptors matching any given qualifiers.
qualifiers should be a dict of keywords matching the db fields or any
legal query for mongo to use against the active_versions collection.
Note, this is to find the current head of the named revision type
Note, this is to find the current head of the named branch type
(e.g., 'draft'). To get specific versions via guid use get_course.
'''
if qualifiers is None:
qualifiers = {}
qualifiers.update({"versions.{}".format(revision): {"$exists": True}})
qualifiers.update({"versions.{}".format(branch): {"$exists": True}})
matching = self.course_index.find(qualifiers)
# collect ids and then query for those
version_guids = []
id_version_map = {}
for course_entry in matching:
version_guid = course_entry['versions'][revision]
version_guid = course_entry['versions'][branch]
version_guids.append(version_guid)
id_version_map[version_guid] = course_entry['_id']
......@@ -667,7 +667,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
# update the index entry if appropriate
if index_entry is not None:
self._update_head(index_entry, course_or_parent_locator.revision, new_id)
self._update_head(index_entry, course_or_parent_locator.branch, new_id)
course_parent = course_or_parent_locator.as_course_locator()
else:
course_parent = None
......@@ -786,7 +786,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
'edited_on': datetime.datetime.utcnow(),
'versions': versions_dict}
new_id = self.course_index.insert(index_entry)
return self.get_course(CourseLocator(course_id=new_id, revision=master_version))
return self.get_course(CourseLocator(course_id=new_id, branch=master_version))
def update_item(self, descriptor, user_id, force=False):
"""
......@@ -835,7 +835,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
# update the index entry if appropriate
if index_entry is not None:
self._update_head(index_entry, descriptor.location.revision, new_id)
self._update_head(index_entry, descriptor.location.branch, new_id)
# fetch and return the new item--fetching is unnecessary but a good qc step
return self.get_item(BlockUsageLocator(descriptor.location, version_guid=new_id))
......@@ -876,7 +876,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
# update the index entry if appropriate
if index_entry is not None:
self._update_head(index_entry, xblock.location.revision, new_id)
self._update_head(index_entry, xblock.location.branch, new_id)
# fetch and return the new item--fetching is unnecessary but a good qc step
return self.get_item(BlockUsageLocator(xblock.location, version_guid=new_id))
......@@ -1028,9 +1028,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
# update the index entry if appropriate
if index_entry is not None:
self._update_head(index_entry, usage_locator.revision, new_id)
self._update_head(index_entry, usage_locator.branch, new_id)
result.course_id = usage_locator.course_id
result.revision = usage_locator.revision
result.branch = usage_locator.branch
return result
......@@ -1186,19 +1186,19 @@ class SplitMongoModuleStore(ModuleStoreBase):
:param locator:
"""
if locator.course_id is None or locator.revision is None:
if locator.course_id is None or locator.branch is None:
return None
else:
index_entry = self.course_index.find_one({'_id': locator.course_id})
if (locator.version_guid is not None
and index_entry['versions'][locator.revision] != locator.version_guid
and index_entry['versions'][locator.branch] != locator.version_guid
and not force):
raise VersionConflictError(
locator,
CourseLocator(
course_id=index_entry['_id'],
version_guid=index_entry['versions'][locator.revision],
revision=locator.revision))
version_guid=index_entry['versions'][locator.branch],
branch=locator.branch))
else:
return index_entry
......@@ -1227,9 +1227,9 @@ class SplitMongoModuleStore(ModuleStoreBase):
return False
def _update_head(self, index_entry, revision, new_id):
def _update_head(self, index_entry, branch, new_id):
"""
Update the active index for the given course's revision to point to new_id
Update the active index for the given course's branch to point to new_id
:param index_entry:
:param course_locator:
......@@ -1237,4 +1237,4 @@ class SplitMongoModuleStore(ModuleStoreBase):
"""
self.course_index.update(
{"_id": index_entry["_id"]},
{"$set": {"versions.{}".format(revision): new_id}})
{"$set": {"versions.{}".format(branch): new_id}})
......@@ -32,3 +32,22 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
context=lines[line - 1][offset - 40:offset + 40],
loc=self.location))
raise Exception, msg, sys.exc_info()[2]
class EmptyDataRawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
Version of RawDescriptor for modules which may have no XML data,
but use XMLEditingDescriptor for import/export handling.
"""
data = String(default='', scope=Scope.content)
@classmethod
def definition_from_xml(cls, xml_object, system):
if len(xml_object) == 0 and len(xml_object.items()) == 0:
return {'data': ''}, []
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, []
def definition_to_xml(self, resource_fs):
if self.data:
return etree.fromstring(self.data)
return etree.Element(self.category)
......@@ -445,7 +445,7 @@ class ImportTestCase(BaseCourseTestCase):
render_string_from_sample_gst_xml = """
<slider var="a" style="width:400px;float:left;"/>\
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml)
self.assertIn(render_string_from_sample_gst_xml, gst_sample.data)
def test_word_cloud_import(self):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud'])
......
......@@ -66,7 +66,7 @@ class TestXBlockWrapper(object):
@property
def leaf_module_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs))
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
runtime.anonymous_student_id = 'dummy_anonymous_student_id'
runtime.open_ended_grading_interface = {}
runtime.seed = 5
......@@ -78,7 +78,7 @@ class TestXBlockWrapper(object):
@property
def leaf_descriptor_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs))
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime
def leaf_descriptor(self, descriptor_cls):
......@@ -102,7 +102,7 @@ class TestXBlockWrapper(object):
@property
def container_descriptor_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs))
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime
def container_descriptor(self, descriptor_cls):
......
......@@ -12,7 +12,7 @@ import time
from django.http import Http404
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import Integer, Scope, String, Float, Boolean
......@@ -97,7 +97,7 @@ class VideoModule(VideoFields, XModule):
class VideoDescriptor(VideoFields,
MetadataOnlyEditingDescriptor,
RawDescriptor):
EmptyDataRawDescriptor):
module_class = VideoModule
def __init__(self, *args, **kwargs):
......@@ -130,19 +130,15 @@ class VideoDescriptor(VideoFields,
_parse_video_xml(video, video.data)
return video
def definition_to_xml(self, resource_fs):
"""
Override the base implementation. We don't actually have anything in the 'data' field
(it's an empty string), so we just return a simple XML element
"""
return etree.Element('video')
def _parse_video_xml(video, xml_data):
"""
Parse video fields out of xml_data. The fields are set if they are
present in the XML.
"""
if not xml_data:
return
xml = etree.fromstring(xml_data)
display_name = xml.get('display_name')
......
......@@ -10,7 +10,7 @@ import json
import logging
from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule
......@@ -240,7 +240,7 @@ class WordCloudModule(WordCloudFields, XModule):
return self.content
class WordCloudDescriptor(WordCloudFields, MetadataOnlyEditingDescriptor, RawDescriptor):
class WordCloudDescriptor(WordCloudFields, MetadataOnlyEditingDescriptor, EmptyDataRawDescriptor):
"""Descriptor for WordCloud Xmodule."""
module_class = WordCloudModule
template_dir_name = 'word_cloud'
......@@ -12,6 +12,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecif
from xblock.core import XBlock, Scope, String, Integer, Float, ModelType
from xblock.fragment import Fragment
from xblock.runtime import Runtime
from xmodule.modulestore.locator import BlockUsageLocator
log = logging.getLogger(__name__)
......@@ -870,7 +871,7 @@ class XMLParsingSystem(DescriptorSystem):
self.policy = policy
class ModuleSystem(object):
class ModuleSystem(Runtime):
'''
This is an abstraction such that x_modules can function independent
of the courseware (e.g. import into other types of courseware, LMS,
......
......@@ -205,7 +205,7 @@
// extends - UI archetypes - well
.ui-well {
box-shadow: inset 0 1px 2px 1px $shadow;
padding: ($baseline*0.75);
padding: ($baseline*0.75) $baseline;
}
// ====================
......
......@@ -8,6 +8,7 @@
<script type="text/javascript" src="${static.url('js/vendor/jquery.cookie.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.scrollTo-1.4.2-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
## codemirror
<link rel="stylesheet" href="${static.url('js/vendor/CodeMirror/codemirror.css')}" type="text/css" media="all" />
......
......@@ -17,3 +17,5 @@ input_encoding = utf-8
input_encoding = utf-8
[mako: common/templates/**.html]
input_encoding = utf-8
[mako: cms/templates/emails/**.txt]
input_encoding = utf-8
{
"locales" : ["en", "es"],
"locales" : ["en", "zh_CN"],
"dummy-locale" : "fr"
}
......@@ -186,8 +186,8 @@ uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser.
**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
installed to run the tests in Chrome. The tests are confirmed to run
with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver
version r195636.
with Chrome (not Chromium) version 28.0.1500.71 with ChromeDriver
version 2.1.210398.
To run all the acceptance tests:
......
......@@ -24,13 +24,13 @@ def index(request):
from external_auth.views import ssl_login
return ssl_login(request)
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE'):
return redirect(settings.MKTG_URLS.get('ROOT'))
return redirect(settings.MKTG_URLS.get('ROOT'))
university = branding.get_university(request.META.get('HTTP_HOST'))
if university is None:
return student.views.index(request, user=request.user)
return courseware.views.university_profile(request, university)
return redirect('/')
@ensure_csrf_cookie
......@@ -48,4 +48,4 @@ def courses(request):
if university is None:
return courseware.views.courses(request)
return courseware.views.university_profile(request, university)
return redirect('/')
......@@ -90,8 +90,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
"""
Ensure that the response has the course navigator.
"""
self.assertTrue("course info" in resp.content.lower())
self.assertTrue("courseware" in resp.content.lower())
self.assertContains(resp, "Course Info")
self.assertContains(resp, "courseware")
def test_course_navigator(self):
""""
......
......@@ -163,7 +163,7 @@ def get_course_about_section(course, section_key):
html = ''
if about_module is not None:
html = about_module.get_html()
html = about_module.runtime.render(about_module, None, 'student_view').content
return html
......@@ -211,7 +211,7 @@ def get_course_info_section(request, course, section_key):
html = ''
if info_module is not None:
html = info_module.get_html()
html = info_module.runtime.render(info_module, None, 'student_view').content
return html
......
......@@ -8,7 +8,7 @@ from nose.tools import assert_in, assert_equals
@step(u'I should see the following Partners in the Partners section')
def i_should_see_partner(step):
partners = world.browser.find_by_css(".partner .name span")
names = set(span.text for span in partners)
names = set(span.html for span in partners)
for partner in step.hashes:
assert_in(partner['Partner'], names)
......
......@@ -91,6 +91,13 @@ def _discussion(tab, user, course, active_page):
return []
def _external_discussion(tab, user, course, active_page):
"""
This returns a tab that links to an external discussion service
"""
return [CourseTab('Discussion', tab['link'], active_page == 'discussion')]
def _external_link(tab, user, course, active_page):
# external links are never active
return [CourseTab(tab['name'], tab['link'], False)]
......@@ -150,6 +157,12 @@ def _staff_grading(tab, user, course, active_page):
return []
def _syllabus(tab, user, course, active_page):
"""Display the syllabus tab"""
link = reverse('syllabus', args=[course.id])
return [CourseTab('Syllabus', link, active_page == 'syllabus')]
def _peer_grading(tab, user, course, active_page):
if user.is_authenticated():
......@@ -216,6 +229,7 @@ VALID_TAB_TYPES = {
'course_info': TabImpl(need_name, _course_info),
'wiki': TabImpl(need_name, _wiki),
'discussion': TabImpl(need_name, _discussion),
'external_discussion': TabImpl(key_checker(['link']), _external_discussion),
'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
'textbooks': TabImpl(null_validator, _textbooks),
'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
......@@ -225,7 +239,8 @@ VALID_TAB_TYPES = {
'peer_grading': TabImpl(null_validator, _peer_grading),
'staff_grading': TabImpl(null_validator, _staff_grading),
'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
'notes': TabImpl(null_validator, _notes_tab)
'notes': TabImpl(null_validator, _notes_tab),
'syllabus': TabImpl(null_validator, _syllabus)
}
......@@ -371,6 +386,6 @@ def get_static_tab_contents(request, course, tab):
html = ''
if tab_module is not None:
html = tab_module.get_html()
html = tab_module.runtime.render(tab_module, None, 'student_view').content
return html
......@@ -77,13 +77,15 @@ class BaseTestXmodule(ModuleStoreTestCase):
data=self.DATA
)
system = get_test_system()
system.render_template = lambda template, context: context
self.runtime = get_test_system()
# Allow us to assert that the template was called in the same way from
# different code paths while maintaining the type returned by render_template
self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items()))
model_data = {'location': self.item_descriptor.location}
model_data.update(self.MODEL_DATA)
self.item_module = self.item_descriptor.module_class(
system, self.item_descriptor, model_data
self.runtime, self.item_descriptor, model_data
)
self.item_url = Location(self.item_module.location).url()
......
"""
Test for lms courseware app, module render unit
"""
from mock import MagicMock, patch
from mock import MagicMock, patch, Mock
import json
from django.http import Http404, HttpResponse
......@@ -12,8 +12,10 @@ from django.test.client import RequestFactory
from django.test.utils import override_settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
import courseware.module_render as render
from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE
from courseware.model_data import ModelDataCache
from modulestore_config import TEST_DATA_XML_MODULESTORE
......@@ -49,8 +51,10 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase):
dispatch=self.dispatch))
def test_get_module(self):
self.assertIsNone(render.get_module('dummyuser', None,
'invalid location', None, None))
self.assertEqual(
None,
render.get_module('dummyuser', None, 'invalid location', None, None)
)
def test_module_render_with_jump_to_id(self):
"""
......@@ -230,7 +234,8 @@ class TestTOC(TestCase):
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
assert reduce(lambda x, y: x and (y in actual), expected, True)
for toc_section in expected:
self.assertIn(toc_section, actual)
def test_toc_toy_from_section(self):
chapter = 'Overview'
......@@ -257,4 +262,109 @@ class TestTOC(TestCase):
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
assert reduce(lambda x, y: x and (y in actual), expected, True)
for toc_section in expected:
self.assertIn(toc_section, actual)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestHtmlModifiers(ModuleStoreTestCase):
"""
Tests to verify that standard modifications to the output of XModule/XBlock
student_view are taking place
"""
def setUp(self):
self.user = UserFactory.create()
self.request = RequestFactory().get('/')
self.request.user = self.user
self.request.session = {}
self.course = CourseFactory.create()
self.content_string = '<p>This is the content<p>'
self.rewrite_link = '<a href="/static/foo/content">Test rewrite</a>'
self.course_link = '<a href="/course/bar/content">Test course rewrite</a>'
self.descriptor = ItemFactory.create(
category='html',
data=self.content_string + self.rewrite_link + self.course_link
)
self.location = self.descriptor.location
self.model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
self.course.id,
self.user,
self.descriptor
)
def test_xmodule_display_wrapper_enabled(self):
module = render.get_module(
self.user,
self.request,
self.location,
self.model_data_cache,
self.course.id,
wrap_xmodule_display=True,
)
result_fragment = module.runtime.render(module, None, 'student_view')
self.assertIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content)
def test_xmodule_display_wrapper_disabled(self):
module = render.get_module(
self.user,
self.request,
self.location,
self.model_data_cache,
self.course.id,
wrap_xmodule_display=False,
)
result_fragment = module.runtime.render(module, None, 'student_view')
self.assertNotIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content)
def test_static_link_rewrite(self):
module = render.get_module(
self.user,
self.request,
self.location,
self.model_data_cache,
self.course.id,
)
result_fragment = module.runtime.render(module, None, 'student_view')
self.assertIn(
'/c4x/{org}/{course}/asset/foo_content'.format(
org=self.course.location.org,
course=self.course.location.course,
),
result_fragment.content
)
def test_course_link_rewrite(self):
module = render.get_module(
self.user,
self.request,
self.location,
self.model_data_cache,
self.course.id,
)
result_fragment = module.runtime.render(module, None, 'student_view')
self.assertIn(
'/courses/{course_id}/bar/content'.format(
course_id=self.course.id
),
result_fragment.content
)
@patch('courseware.module_render.has_access', Mock(return_value=True))
def test_histogram(self):
module = render.get_module(
self.user,
self.request,
self.location,
self.model_data_cache,
self.course.id,
)
result_fragment = module.runtime.render(module, None, 'student_view')
self.assertIn(
'Staff Debug',
result_fragment.content
)
......@@ -34,9 +34,7 @@ class TestVideo(BaseTestXmodule):
def test_videoalpha_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = self.item_module.get_html()
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
......@@ -51,7 +49,7 @@ class TestVideo(BaseTestXmodule):
'youtube_streams': self.item_module.youtube_streams,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertDictEqual(context, expected_context)
self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context))
class TestVideoNonYouTube(TestVideo):
......@@ -78,9 +76,7 @@ class TestVideoNonYouTube(TestVideo):
the template generates an empty string for the YouTube streams.
"""
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = self.item_module.get_html()
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
......@@ -95,4 +91,4 @@ class TestVideoNonYouTube(TestVideo):
'youtube_streams': '',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertDictEqual(context, expected_context)
self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context))
......@@ -104,10 +104,9 @@ class VideoAlphaModuleUnitTest(unittest.TestCase):
def test_videoalpha_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoAlphaFactory.create()
module.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items()))
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = module.get_html()
fragment = module.runtime.render(module, None, 'student_view')
expected_context = {
'caption_asset_path': '/static/subs/',
'sub': module.sub,
......@@ -122,7 +121,7 @@ class VideoAlphaModuleUnitTest(unittest.TestCase):
'track': module.track,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertDictEqual(context, expected_context)
self.assertEqual(fragment.content, module.runtime.render_template('videoalpha.html', expected_context))
self.assertDictEqual(
json.loads(module.get_instance_state()),
......
......@@ -242,9 +242,7 @@ class TestWordCloud(BaseTestXmodule):
def test_word_cloud_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = self.item_module.get_html()
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = {
'ajax_url': self.item_module.system.ajax_url,
......@@ -253,4 +251,4 @@ class TestWordCloud(BaseTestXmodule):
'num_inputs': 5, # default value
'submitted': False # default value
}
self.assertDictEqual(context, expected_context)
self.assertEqual(fragment.content, self.runtime.render_template('word_cloud.html', expected_context))
......@@ -120,9 +120,8 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
self.assertEqual(response.redirect_chain[0][1], 302)
if check_content:
unavailable_msg = "this module is temporarily unavailable"
self.assertEqual(response.content.find(unavailable_msg), -1)
self.assertFalse(isinstance(descriptor, ErrorDescriptor))
self.assertNotContains(response, "this module is temporarily unavailable")
self.assertNotIsInstance(descriptor, ErrorDescriptor)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
......
......@@ -400,7 +400,7 @@ def index(request, course_id, chapter=None, section=None,
# add in the appropriate timer information to the rendering context:
context.update(check_for_active_timelimit_module(request, course_id, course))
context['content'] = section_module.get_html()
context['content'] = section_module.runtime.render(section_module, None, 'student_view').content
else:
# section is none, so display a message
prev_section = get_current_child(chapter_module)
......@@ -632,57 +632,6 @@ def mktg_course_about(request, course_id):
'show_courseware_link': show_courseware_link})
@ensure_csrf_cookie
@cache_if_anonymous
def static_university_profile(request, org_id):
"""
Return the profile for the particular org_id that does not have any courses.
"""
# Redirect to the properly capitalized org_id
last_path = request.path.split('/')[-1]
if last_path != org_id:
return redirect('static_university_profile', org_id=org_id)
# Render template
template_file = "university_profile/{0}.html".format(org_id).lower()
context = dict(courses=[], org_id=org_id)
return render_to_response(template_file, context)
@ensure_csrf_cookie
@cache_if_anonymous
def university_profile(request, org_id):
"""
Return the profile for the particular org_id. 404 if it's not valid.
"""
virtual_orgs_ids = settings.VIRTUAL_UNIVERSITIES
meta_orgs = getattr(settings, 'META_UNIVERSITIES', {})
# Get all the ids associated with this organization
all_courses = modulestore().get_courses()
valid_orgs_ids = set(c.org for c in all_courses)
valid_orgs_ids.update(virtual_orgs_ids + meta_orgs.keys())
if org_id not in valid_orgs_ids:
raise Http404("University Profile not found for {0}".format(org_id))
# Grab all courses for this organization(s)
org_ids = set([org_id] + meta_orgs.get(org_id, []))
org_courses = []
domain = request.META.get('HTTP_HOST')
for key in org_ids:
cs = get_courses_by_university(request.user, domain=domain)[key]
org_courses.extend(cs)
org_courses = sort_by_announcement(org_courses)
context = dict(courses=org_courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower()
return render_to_response(template_file, context)
def render_notifications(request, course, notifications):
context = {
'notifications': notifications,
......@@ -779,12 +728,16 @@ def submission_history(request, course_id, student_username, location):
except StudentModule.DoesNotExist:
return HttpResponse(escape("{0} has never accessed problem {1}".format(student_username, location)))
history_entries = StudentModuleHistory.objects.filter(student_module=student_module).order_by('-id')
history_entries = StudentModuleHistory.objects.filter(
student_module=student_module
).order_by('-id')
# If no history records exist, let's force a save to get history started.
if not history_entries:
student_module.save()
history_entries = StudentModuleHistory.objects.filter(student_module=student_module).order_by('-id')
history_entries = StudentModuleHistory.objects.filter(
student_module=student_module
).order_by('-id')
context = {
'history_entries': history_entries,
......
......@@ -21,6 +21,7 @@ from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse
from django.core.mail import send_mail
from django.utils import timezone
import xmodule.graders as xmgraders
from xmodule.modulestore.django import modulestore
......@@ -93,6 +94,7 @@ def instructor_dashboard(request, course_id):
'title': 'Course Statistics At A Glance',
}
data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]]
data += [['Date', timezone.now().isoformat()]]
data += compute_course_stats(course).items()
if request.user.is_staff:
for field in course.fields:
......
import base64
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
......@@ -31,6 +33,9 @@ class UserApiTestCase(TestCase):
UserPreferenceFactory.create(user=self.users[1], key="key0")
]
def basic_auth(self, username, password):
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
def request_with_auth(self, method, *args, **kwargs):
"""Issue a get request to the given URI with the API key header"""
return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs)
......@@ -127,6 +132,15 @@ class UserViewSetTest(UserApiTestCase):
def test_debug_auth(self):
self.assertHttpOK(self.client.get(self.LIST_URI))
@override_settings(DEBUG=False)
@override_settings(EDX_API_KEY=TEST_API_KEY)
def test_basic_auth(self):
# ensure that having basic auth headers in the mix does not break anything
self.assertHttpOK(
self.request_with_auth("get", self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
self.assertHttpForbidden(
self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
def test_get_list_empty(self):
User.objects.all().delete()
result = self.get_json(self.LIST_URI)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment