Merge branch 'master' into talbs/studio-authorship

This commit is contained in:
cahrens
2013-07-12 13:35:52 -04:00
205 changed files with 1632 additions and 10922 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* -text

1
.gitignore vendored
View File

@@ -44,3 +44,4 @@ node_modules
.prereqs_cache
autodeploy.properties
.ws_migrations_complete
.vagrant/

View File

@@ -79,4 +79,5 @@ Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com>
Felix Sun <felixsun@mit.edu>
Adam Palay <adam@edx.org>
Ian Hoover <ihoover@edx.org>
Ian Hoover <ihoover@edx.org>
Mukul Goyal <miki@edx.org>

View File

@@ -48,6 +48,8 @@ moved to be edited as metadata.
XModule: Only write out assets files if the contents have changed.
Studio: Course settings are now saved explicitly.
XModule: Don't delete generated xmodule asset files when compiling (for
instance, when XModule provides a coffeescript file, don't delete
the associated javascript)

132
README.md
View File

@@ -2,8 +2,136 @@ This is the main edX platform which consists of LMS and Studio.
See [code.edx.org](http://code.edx.org/) for other parts of the edX code base.
Installation
============
Installation - The first time
=============================
The following instructions will help you to download and setup a virtual machine
with a minimal amount of steps, using Vagrant. It is recommended for a first
installation, as it will save you from many of the common pitfalls of the
installation process.
1. Make sure you have plenty of available disk space, >5GB
2. Install Git: http://git-scm.com/downloads
3. Install VirtualBox: https://www.virtualbox.org/wiki/Download_Old_Builds_4_2
(you need version 4.2.12, as later/earlier versions might not work well with
Vagrant)
4. Install Vagrant: http://www.vagrantup.com/ (Vagrant 1.2.2 or later)
5. Open a terminal
6. Download the project: `git clone git://github.com/edx/edx-platform.git`
7. Enter the project directory: `cd edx-platform/`
8. (Windows only) Run the commands to
[deal with line endings and symlinks under Windows](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#dealing-with-line-endings-and-symlinks-under-windows)
9. Start: `vagrant up`
The last step might require your host machine's administrator password to setup NFS.
Afterwards, it will download an image, install all the dependencies and configure
the VM. It will take a while, go grab a coffee.
Once completed, hopefully you should see a "Success!" message indicating that the
installation went fine. (If not, refer to the
[troubleshooting section](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#troubleshooting).)
Note: by default, the VM will get the IP `192.168.20.40`. If you need to use a
different IP, you can edit the file `Vagrantfile`. If you have already started the
VM with `vagrant up`, see "Stopping and restarting the VM" below to take the change
into account.
Accessing the VM
----------------
Once the installation is finished, to log into the virtual machine:
```
$ vagrant ssh
```
Note: This won't work from Windows, install install PuTTY from
http://www.chiark.greenend.org.uk/%7Esgtatham/putty/download.html instead. Then
connect to 127.0.0.1, port 2222, using vagrant/vagrant as a user/password.
Using edX
---------
Once inside the VM, you can start Studio and LMS with the following commands
(from the `/opt/edx/edx-platform` folder):
Learning management system (LMS):
```
$ rake lms[cms.dev,0.0.0.0:8000]
```
Studio:
```
$ rake cms[dev,0.0.0.0:8001]
```
Once started, open the following URLs in your browser:
* Learning management system (LMS): http://192.168.20.40:8000/
* Studio (CMS): http://192.168.20.40:8001/
You can develop by editing the files directly in the `edx-platform/` directory you
downloaded before, you don't need to connect to the VM to edit them (the VM uses
those files to run edX, mirroring the folder in `/opt/edx/edx-platform`).
You may also want to create a super-user with:
```
$ rake django-admin["createsuperuser"]
```
Also note that if you register a new user through the web interface,
the activiation email will be posted to your VM's terminal window (search for
lines similar to):
```
Subject: Your account for edX Studio
From: registration@edx.org
```
and find the activation URL for the account you've created.
See the [Frequently Asked Questions](https://github.com/edx/edx-platform/wiki/Frequently-Asked-Questions)
for more usage tips.
Stopping & starting
-------------------
To stop the VM (from your `edx-platform/` directory):
```
$ vagrant halt
```
To restart:
```
$ vagrant up
```
or, to start without attempting to update the dependencies:
```
$ vagrant up --no-provision
```
Troubleshooting
---------------
If anything doesn't work as expected, see the
[troubleshooting section](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#troubleshooting).
Installation - Advanced
=======================
Note: The following installation instructions are for advanced users & developers
who are familiar with setting up Python, Ruby & node.js virtual environments.
Even if you know what you are doing, edX has a large code base with multiple
dependencies, so you might still want to use the method described above the
first time, as Vagrant helps avoiding issues due to the different environments.
There is a `scripts/create-dev-env.sh` that will attempt to set up a development
environment.

33
Vagrantfile vendored Normal file
View File

@@ -0,0 +1,33 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "precise32"
config.vm.box_url = "http://files.vagrantup.com/precise32.box"
config.vm.network :forwarded_port, guest: 8000, host: 9000
config.vm.network :forwarded_port, guest: 8001, host: 9001
# Create a private network, which allows host-only access to the machine
# using a specific IP.
config.vm.network :private_network, ip: "192.168.20.40"
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
config.vm.synced_folder ".", "/opt/edx/edx-platform", id: "vagrant-root", :nfs => nfs_setting
# Make it so that network access from the vagrant guest is able to
# use SSH private keys that are present on the host without copying
# them into the VM.
config.ssh.forward_agent = true
config.vm.provider :virtualbox do |vb|
# Use VBoxManage to customize the VM. For example to change memory:
vb.customize ["modifyvm", :id, "--memory", "1024"]
# This setting makes it so that network access from inside the vagrant guest
# is able to resolve DNS using the hosts VPN connection.
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
config.vm.provision :shell, :path => "scripts/vagrant-provisioning.sh"
end

View File

@@ -46,3 +46,9 @@ Feature: Advanced (manual) course policy
Then it is displayed as a string
And I reload the page
Then it is displayed as a string
Scenario: Confirmation is shown on save
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved

View File

@@ -2,8 +2,8 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches
from common import type_in_codemirror
from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
from common import type_in_codemirror, press_the_notification_button
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
@@ -25,20 +25,6 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
css = 'a.action-%s' % name.lower()
# Save was clicked if either the save notification bar is gone, or we have a error notification
# overlaying it (expected in the case of typing Object into display_name).
def save_clicked():
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing
world.css_click(css, success_condition=save_clicked)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
type_in_codemirror(get_index_of(DISPLAY_NAME_KEY), 'X')
@@ -113,7 +99,7 @@ def assert_policy_entries(expected_keys, expected_values):
def get_index_of(expected_key):
for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = world.css_find(KEY_CSS)[counter].value
key = world.css_value(KEY_CSS, index=counter)
if key == expected_key:
return counter
@@ -122,7 +108,7 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return world.css_find(VALUE_CSS)[index].value
return world.css_value(VALUE_CSS, index=index)
def change_display_name_value(step, new_value):

View File

@@ -61,7 +61,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_in('Course Outline', world.css_find('.outline .page-header')[0].text)
assert_in('Course Outline', world.css_text('.outline .page-header'))
assert_equal(1, len(world.browser.windows))

View File

@@ -12,9 +12,7 @@ import time
from logging import getLogger
logger = getLogger(__name__)
_COURSE_NAME = 'Robot Super Course'
_COURSE_NUM = '999'
_COURSE_ORG = 'MITx'
from terrain.browser import reset_data
########### STEP HELPERS ##############
@@ -55,6 +53,48 @@ def i_have_opened_a_new_course(_step):
open_new_course()
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower()
# The button was clicked if either the notification bar is gone,
# or we see an error overlaying it (expected for invalid inputs).
def button_clicked():
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing
world.css_click(css, success_condition=button_clicked), '%s button not clicked after 5 attempts.' % name
@step('I change the "(.*)" field to "(.*)"$')
def i_change_field_to_value(_step, field, value):
field_css = '#%s' % '-'.join([s.lower() for s in field.split()])
ele = world.css_find(field_css).first
ele.fill(value)
ele._element.send_keys(Keys.ENTER)
@step('I reset the database')
def reset_the_db(_step):
"""
When running Lettuce tests using examples (i.e. "Confirmation is
shown on save" in course-settings.feature), the normal hooks
aren't called between examples. reset_data should run before each
scenario to flush the test database. When this doesn't happen we
get errors due to trying to insert a non-unique entry. So instead,
we delete the database manually. This has the effect of removing
any users and courses that have been created during the test run.
"""
reset_data(None)
@step('I see a confirmation that my changes have been saved')
def i_see_a_confirmation(step):
confirmation_css = '#alert-confirmation'
assert world.is_css_present(confirmation_css)
####### HELPER FUNCTIONS ##############
def open_new_course():
world.clear_courses()
@@ -80,9 +120,9 @@ def create_studio_user(
def fill_in_course_info(
name=_COURSE_NAME,
org=_COURSE_ORG,
num=_COURSE_NUM):
name='Robot Super Course',
org='MITx',
num='999'):
world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num)
@@ -100,21 +140,28 @@ def log_into_studio(
world.is_css_present(signin_css)
world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
def fill_login_form():
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
world.retry_on_exception(fill_login_form)
assert_true(world.is_css_present('.new-course-button'))
world.scenario_dict['USER'] = get_user_by_email(email)
def create_a_course():
world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME)
world.scenario_dict['COURSE'] = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course
# so they will have the permissions to see it in studio
course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_")))
user = get_user_by_email('robot+studio@edx.org')
course = world.GroupFactory.create(name='instructor_MITx/{}/{}'.format(world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(" ", "_")))
if world.scenario_dict.get('USER') is None:
user = world.scenario_dict['USER']
else:
user = get_user_by_email('robot+studio@edx.org')
user.groups.add(course)
user.save()
world.browser.reload()
@@ -179,11 +226,18 @@ def shows_captions(step, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_find('.video')[0].has_class('closed')
assert world.css_has_class('.video', 'closed')
else:
assert world.is_css_not_present('.video.closed')
@step('the save button is disabled$')
def save_button_disabled(step):
button_css = '.action-save'
disabled = 'is-disabled'
assert world.css_has_class(button_css, disabled)
def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index)
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")

View File

@@ -5,15 +5,18 @@ Feature: Course Settings
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
And I press the "Save" notification button
Then I see the set dates on refresh
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
And I press the "Save" notification button
Then I see cleared dates on refresh
Scenario: User cannot clear the course start date
Given I have set course dates
And I press the "Save" notification button
And I clear the course start date
Then I receive a warning about course start date
And The previously set start date is shown on refresh
@@ -21,5 +24,50 @@ Feature: Course Settings
Scenario: User can correct the course start date warning
Given I have tried to clear the course start
And I have entered a new course start date
And I press the "Save" notification button
Then The warning about course start date goes away
And My new course start date is shown on refresh
Scenario: Settings are only persisted when saved
Given I have set course dates
And I press the "Save" notification button
When I change fields
Then I do not see the new changes persisted on refresh
Scenario: Settings are reset on cancel
Given I have set course dates
And I press the "Save" notification button
When I change fields
And I press the "Cancel" notification button
Then I do not see the changes
Scenario: Confirmation is shown on save
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the "<field>" field to "<value>"
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
# Lettuce hooks don't get called between each example, so we need
# to run the before.each_scenario hook manually to avoid database
# errors.
And I reset the database
Examples:
| field | value |
| Course Start Time | 11:00 |
| Course Introduction Video | 4r7wHMg5Yjg |
| Course Effort | 200:00 |
# Special case because we have to type in code mirror
Scenario: Changes in Course Overview show a confirmation
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the course overview
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
Scenario: User cannot save invalid settings
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the "Course Start Date" field to ""
Then the save button is disabled

View File

@@ -4,7 +4,7 @@
from lettuce import world, step
from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys
import time
from common import type_in_codemirror
from nose.tools import assert_true, assert_false, assert_equal
@@ -47,22 +47,11 @@ def test_and_i_set_course_dates(step):
set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
pause()
@step('Then I see the set dates on refresh$')
def test_then_i_see_the_set_dates_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
# Unset times get set to 12 AM once the corresponding date has been set.
verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
i_see_the_set_dates()
@step('And I clear all the dates except start$')
@@ -71,8 +60,6 @@ def test_and_i_clear_all_the_dates_except_start(step):
set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
pause()
@step('Then I see cleared dates on refresh$')
def test_then_i_see_cleared_dates_on_refresh(step):
@@ -119,7 +106,6 @@ def test_i_have_tried_to_clear_the_course_start(step):
@step('I have entered a new course start date$')
def test_i_have_entered_a_new_course_start_date(step):
set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
pause()
@step('The warning about course start date goes away$')
@@ -137,6 +123,30 @@ def test_my_new_course_start_date_is_shown_on_refresh(step):
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
@step('I change fields$')
def test_i_change_fields(step):
set_date_or_time(COURSE_START_DATE_CSS, '7/7/7777')
set_date_or_time(COURSE_END_DATE_CSS, '7/7/7777')
set_date_or_time(ENROLLMENT_START_DATE_CSS, '7/7/7777')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '7/7/7777')
@step('I do not see the new changes persisted on refresh$')
def test_changes_not_shown_on_refresh(step):
step.then('Then I see the set dates on refresh')
@step('I do not see the changes')
def test_i_do_not_see_changes(_step):
i_see_the_set_dates()
@step('I change the course overview')
def test_change_course_overview(_step):
type_in_codemirror(0, "<h1>Overview</h1>")
############### HELPER METHODS ####################
def set_date_or_time(css, date_or_time):
"""
@@ -152,12 +162,20 @@ def verify_date_or_time(css, date_or_time):
"""
Verifies date or time field.
"""
assert_equal(date_or_time, world.css_find(css).first.value)
assert_equal(date_or_time, world.css_value(css))
def pause():
def i_see_the_set_dates():
"""
Must sleep briefly to allow last time save to finish,
else refresh of browser will fail.
Ensure that each field has the value set in `test_and_i_set_course_dates`.
"""
time.sleep(float(1))
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
# Unset times get set to 12 AM once the corresponding date has been set.
verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)

View File

@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from common import create_studio_user, log_into_studio, _COURSE_NAME
from common import create_studio_user, log_into_studio
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
@@ -47,12 +47,12 @@ def other_user_login(_step, name):
@step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, doesnt_see_course, gender):
class_css = 'span.class-name'
all_courses = world.css_find(class_css)
all_courses = world.css_find(class_css, wait_time=1)
all_names = [item.html for item in all_courses]
if doesnt_see_course:
assert not _COURSE_NAME in all_names
assert not world.scenario_dict['COURSE'].display_name in all_names
else:
assert _COURSE_NAME in all_names
assert world.scenario_dict['COURSE'].display_name in all_names
@step(u's?he cannot delete users')

View File

@@ -24,7 +24,7 @@ def add_update(_step, text):
@step(u'I should( not)? see the update "([^"]*)"$')
def check_update(_step, doesnt_see_update, text):
update_css = 'div.update-contents'
update = world.css_find(update_css)
update = world.css_find(update_css, wait_time=1)
if doesnt_see_update:
assert len(update) == 0 or not text in update.html
else:

View File

@@ -45,7 +45,7 @@ def courseware_page_has_loaded_in_studio(step):
@step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name'
assert world.css_has_text(course_css, 'Robot Super Course')
assert world.css_has_text(course_css, world.scenario_dict['COURSE'].display_name)
@step('I am on the "([^"]*)" tab$')

View File

@@ -32,6 +32,7 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Save" notification button
And I go back to the main course page
Then I do see the assignment name "New Type"
And I do not see the assignment name "Homework"
@@ -41,6 +42,7 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I delete the assignment type "Homework"
And I press the "Save" notification button
And I go back to the main course page
Then I do not see the assignment name "Homework"
@@ -49,5 +51,36 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I add a new assignment type "New Type"
And I press the "Save" notification button
And I go back to the main course page
Then I do see the assignment name "New Type"
Scenario: Settings are only persisted when saved
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
Then I do not see the changes persisted on refresh
Scenario: Settings are reset on cancel
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Cancel" notification button
Then I see the assignment type "Homework"
Scenario: Confirmation is shown on save
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
Scenario: User cannot save invalid settings
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to ""
Then the save button is disabled

View File

@@ -3,6 +3,7 @@
from lettuce import world, step
from common import *
from terrain.steps import reload_the_page
@step(u'I am viewing the grading settings')
@@ -63,7 +64,9 @@ def change_assignment_name(step, old_name, new_name):
@step(u'I go back to the main course page')
def main_course_page(step):
main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]'
main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),)
world.css_click(main_page_link_css)
@@ -89,8 +92,8 @@ def add_assignment_type(step, new_name):
add_button_css = '.add-grading-data'
world.css_click(add_button_css)
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)[4]
f._element.send_keys(new_name)
new_assignment = world.css_find(name_id)[-1]
new_assignment._element.send_keys(new_name)
@step(u'I have populated the course')
@@ -99,10 +102,25 @@ def populate_course(step):
step.given('I have added a new subsection')
@step(u'I do not see the changes persisted on refresh$')
def changes_not_persisted(step):
reload_the_page(step)
name_id = '#course-grading-assignment-name'
assert(world.css_value(name_id) == 'Homework')
@step(u'I see the assignment type "(.*)"$')
def i_see_the_assignment_type(_step, name):
assignment_css = '#course-grading-assignment-name'
assignments = world.css_find(assignment_css)
types = [ele['value'] for ele in assignments]
assert name in types
def get_type_index(name):
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)
for i in range(len(f)):
if f[i].value == name:
return i
all_types = world.css_find(name_id)
for index in range(len(all_types)):
if world.css_value(name_id, index=index) == name:
return index
return -1

View File

@@ -1,35 +1,30 @@
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
@skip
Scenario: User can view metadata
Given I have created a Blank Common Problem
When I edit and select Settings
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
@skip
Scenario: User can modify String values
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
@skip
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
@skip
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can revert the display name to unset
And my display name is unset on save
@skip
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
When I edit and select Settings
@@ -37,7 +32,6 @@ Feature: Problem Editor
And my change to randomization is persisted
And I can revert to the default value for randomization
@skip
Scenario: User can modify float input values
Given I have created a Blank Common Problem
When I edit and select Settings
@@ -45,25 +39,21 @@ Feature: Problem Editor
And my change to weight is persisted
And I can revert to the default value of unset for weight
@skip
Scenario: User cannot type letters in float number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the weight to "abc", it remains unset
@skip
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
@skip
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
@skip
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
When I edit and select Settings
@@ -71,13 +61,11 @@ Feature: Problem Editor
And I can modify the display name
Then If I press Cancel my changes are not persisted
@skip
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
When I edit and select Settings
Then Edit High Level Source is visible
@skip
Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
Given I have created a LaTeX Problem
When I edit and compile the High Level Source

View File

@@ -169,7 +169,7 @@ def edit_latex_source(step):
@step('my change to the High Level Source is persisted')
def high_level_source_persisted(step):
def verify_text(driver):
return world.css_find('.problem').text == 'hi'
return world.css_text('.problem') == 'hi'
world.wait_for(verify_text)
@@ -177,7 +177,7 @@ def high_level_source_persisted(step):
@step('I view the High Level Source I see my changes')
def high_level_source_in_editor(step):
open_high_level_source()
assert_equal('hi', world.css_find('.source-edit-box').value)
assert_equal('hi', world.css_value('.source-edit-box'))
def verify_high_level_source_links(step, visible):

View File

@@ -26,6 +26,7 @@ Feature: Create Section
When I click the Edit link for the release date
And I save a new section release date
Then the section release date is updated
And I see a "saving" notification
Scenario: Delete section
Given I have opened a new course in Studio

View File

@@ -42,6 +42,12 @@ def i_save_a_new_section_release_date(_step):
world.browser.click_link_by_text('Save')
@step('I see a "saving" notification')
def i_see_a_saving_notification(step):
saving_css = '.wrapper-notification-mini'
assert world.is_css_present(saving_css)
############ ASSERTIONS ###################
@@ -64,7 +70,7 @@ def i_click_to_edit_section_name(_step):
def i_see_complete_section_name_with_quote_in_editor(_step):
css = '.section-name-edit input[type=text]'
assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
assert_equal(world.css_value(css), 'Section with "Quote"')
@step('the section does not exist$')
@@ -79,7 +85,7 @@ def i_see_a_release_date_for_my_section(_step):
css = 'span.published-status'
assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text
status_text = world.css_text(css)
# e.g. 11/06/2012 at 16:25
msg = 'Will Release:'

View File

@@ -6,12 +6,14 @@ from lettuce import world, step
@step('I fill in the registration form$')
def i_fill_in_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form')
register_form.find_by_name('email').fill('robot+studio@edx.org')
register_form.find_by_name('password').fill('test')
register_form.find_by_name('username').fill('robot-studio')
register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check()
def fill_in_reg_form():
register_form = world.css_find('form#register_form')
register_form.find_by_name('email').fill('robot+studio@edx.org')
register_form.find_by_name('password').fill('test')
register_form.find_by_name('username').fill('robot-studio')
register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check()
world.retry_on_exception(fill_in_reg_form)
@step('I press the Create My Account button on the registration form$')

View File

@@ -92,7 +92,7 @@ def i_expand_a_section(step):
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.is_css_present(span_locator))
assert_equal(world.css_find(span_locator).value, text)
assert_equal(world.css_value(span_locator), text)
assert_true(world.css_visible(span_locator))
@@ -108,13 +108,13 @@ def i_do_not_see_the_span_with_text(step, text):
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_true(s.visible)
for index in range(len(subsections)):
assert_true(world.css_visible(subsection_locator, index=index))
@step(u'all sections are collapsed$')
def all_sections_are_collapsed(step):
subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_false(s.visible)
for index in range(len(subsections)):
assert_false(world.css_visible(subsection_locator, index=index))

View File

@@ -32,7 +32,6 @@ Feature: Create Subsection
And I reload the page
Then I see the correct dates
@skip
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection

View File

@@ -50,7 +50,7 @@ def i_click_to_edit_subsection_name(step):
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
assert world.is_css_present(css)
assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
assert_equal(world.css_value(css), 'Subsection With "Quote"')
@step('I have set a release date and due date in different years$')
@@ -69,7 +69,7 @@ def i_mark_it_as_homework(step):
@step('I see it marked as Homework$')
def i_see_it_marked__as_homework(step):
assert_equal(world.css_find(".status-label").value, 'Homework')
assert_equal(world.css_value(".status-label"), 'Homework')
############ ASSERTIONS ###################

View File

@@ -9,7 +9,7 @@ import random
import os
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
HTTP_PREFIX = "http://localhost:8001"
HTTP_PREFIX = "http://localhost:%s" % settings.LETTUCE_SERVER_PORT
@step(u'I go to the files and uploads page')

View File

@@ -18,7 +18,6 @@ Feature: Video Component
Given I have created a Video component
Then when I view the video it does show the captions
@skip
Scenario: Captions are toggled correctly
Given I have created a Video component
And I have toggled captions

View File

@@ -8,7 +8,7 @@ from lettuce import world, step
@step('when I view the video it does not have autoplay enabled')
def does_not_autoplay(_step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play')
assert world.css_has_class('.video_control', 'play')
@step('creating a video takes a single click')

View File

@@ -164,9 +164,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_toy(self):
self.check_edit_unit('toy')
def test_edit_unit_full(self):
self.check_edit_unit('full')
def _get_draft_counts(self, item):
cnt = 1 if getattr(item, 'is_draft', False) else 0
for child in item.get_children():
@@ -301,41 +298,49 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_import_textbook_as_content_element(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['toy'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
ItemFactory.create(parent_location=course_location,
template="i4x://edx/templates/static_tab/Empty",
display_name="Static_1")
ItemFactory.create(parent_location=course_location,
template="i4x://edx/templates/static_tab/Empty",
display_name="Static_2")
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
# reverse the ordering
reverse_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
reverse_tabs.insert(0, 'i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
course_tabs.append('i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
self.assertEqual(reverse_tabs, course_tabs)
def test_import_polls(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['toy'])
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
items = module_store.get_items(['i4x', 'edX', 'toy', 'poll_question', None, None])
found = len(items) > 0
self.assertTrue(found)
@@ -343,38 +348,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertGreater(len(items[0].question), 0)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['full'])
err_cnt = perform_xlint('common/test/data', ['toy'])
self.assertGreater(err_cnt, 0)
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/toy/.*'])
def test_module_preview_in_whitelist(self):
'''
Tests the ajax callback to render an XModule
'''
direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full'])
html_module_location = Location(['i4x', 'edX', 'full', 'html', 'html_90', None])
url = reverse('preview_component', kwargs={'location': html_module_location.url()})
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn('Inline content', resp.content)
import_from_xml(direct_store, 'common/test/data/', ['toy'])
# also try a custom response which will trigger the 'is this course in whitelist' logic
problem_module_location = Location(['i4x', 'edX', 'full', 'problem', 'H1P1_Energy', None])
problem_module_location = Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None])
url = reverse('preview_component', kwargs={'location': problem_module_location.url()})
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
def test_delete(self):
direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full'])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapterloc = ItemFactory.create(parent_location=course_location, display_name="Chapter").location
ItemFactory.create(parent_location=chapterloc, template='i4x://edx/templates/sequential/Empty', display_name="Sequential")
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
sequential = direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
# make sure the parent points to the child object which is to be deleted
self.assertTrue(sequential.location.url() in chapter.children)
@@ -387,14 +387,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
found = False
try:
direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
found = True
except ItemNotFoundError:
pass
self.assertFalse(found)
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
@@ -405,20 +405,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
while there is a base definition in /about/effort.html
'''
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
import_from_xml(module_store, 'common/test/data/', ['toy'])
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'end_date', None]))
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
course = module_store.get_item(course_location)
self.assertFalse(course.hide_progress_tab)
def test_asset_import(self):
@@ -428,9 +427,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore()
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
course_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
course = module_store.get_item(course_location)
self.assertIsNotNone(course)
@@ -451,7 +450,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content = None
try:
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
content = content_store.find(location)
except NotFoundError:
pass
@@ -479,10 +478,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore()
trash_store = contentstore('trashcan')
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
# look up original (and thumbnail) in content store, should be there after import
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
content = content_store.find(location, throw_on_not_found=False)
thumbnail_location = content.thumbnail_location
self.assertIsNotNone(content)
@@ -495,11 +494,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'full', 'name': '6.002_Spring_2012'})
resp = self.client.post(url, {'location': '/c4x/edX/full/asset/circuits_duality.gif'})
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
asset_location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
# now try to find it in store, but they should not be there any longer
content = content_store.find(asset_location, throw_on_not_found=False)
@@ -518,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertIsNotNone(thumbnail)
# let's restore the asset
restore_asset_from_trashcan('/c4x/edX/full/asset/circuits_duality.gif')
restore_asset_from_trashcan('/c4x/edX/toy/asset/sample_static.txt')
# now try to find it in courseware store, and they should be back after restore
content = content_store.find(asset_location, throw_on_not_found=False)
@@ -536,18 +535,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
trash_store = contentstore('trashcan')
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
course_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012')
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
content = content_store.find(location, throw_on_not_found=False)
self.assertIsNotNone(content)
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'full', 'name': '6.002_Spring_2012'})
resp = self.client.post(url, {'location': '/c4x/edX/full/asset/circuits_duality.gif'})
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
# make sure there's something in the trashcan
@@ -582,7 +581,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
}
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['toy'])
resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200)
@@ -591,16 +590,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(module_store, content_store, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'poll_question', None]))
self.assertGreater(len(items), 0)
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'poll_question', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location.replace(org='MITx', course='999')
@@ -641,20 +640,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft content is also deleted
"""
module_store = modulestore('direct')
content_store = contentstore()
draft_store = modulestore('draft')
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
# verify that we actually have assets
assets = content_store.get_all_content_for_course(location)
self.assertNotEquals(len(assets), 0)
location = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course').location
# get a vertical (and components in it) to put into 'draft'
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]), depth=1)
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
'vertical', 'vertical_test', None]), depth=1)
draft_store.clone_item(vertical.location, vertical.location)
for child in vertical.get_children():
@@ -665,7 +661,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# assert that there's absolutely no non-draft modules in the course
# this should also include all draft items
items = draft_store.get_items(Location(['i4x', 'edX', 'full', None, None]))
items = module_store.get_items(Location(['i4x', 'edX', '999', 'course', None]))
self.assertEqual(len(items), 0)
# assert that all content in the asset library is also deleted
@@ -688,17 +684,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# get a vertical (and components in it) to put into 'draft'
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]), depth=1)
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
'vertical', 'vertical_test', None]), depth=1)
draft_store.clone_item(vertical.location, vertical.location)
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full',
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'toy',
'vertical', 'no_references', 'draft']))
for child in vertical.get_children():
@@ -708,18 +704,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# now create a private vertical
private_vertical = draft_store.clone_item(vertical.location,
Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
# add private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
'sequential', 'vertical_sequential', None]))
private_location_no_draft = private_vertical.location.replace(revision=None)
module_store.update_children(sequential.location, sequential.children +
[private_location_no_draft.url()])
# read back the sequential, to make sure we have a pointer to
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
'sequential', 'vertical_sequential', None]))
self.assertIn(private_location_no_draft.url(), sequential.children)
@@ -731,17 +727,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for static tabs
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags
self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for about content
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
# check for graiding_policy.json
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall')
self.assertTrue(filesystem.exists('grading_policy.json'))
course = module_store.get_item(location)
@@ -756,8 +746,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk to what we have in the course module
with filesystem.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
self.assertIn('course/2012_Fall', on_disk)
self.assertEqual(on_disk['course/2012_Fall'], own_metadata(course))
# remove old course
delete_course(module_store, content_store, location)
@@ -765,7 +755,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# reimport
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
# don't try to look at private verticals. Right now we're running
@@ -776,27 +766,27 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
# verify that we have the content in the draft store as well
vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]), depth=1)
vertical = draft_store.get_item(Location(['i4x', 'edX', 'toy',
'vertical', 'vertical_test', None]), depth=1)
self.assertTrue(getattr(vertical, 'is_draft', False))
for child in vertical.get_children():
self.assertTrue(getattr(child, 'is_draft', False))
# make sure that we don't have a sequential that is in draft mode
sequential = draft_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy',
'sequential', 'vertical_sequential', None]))
self.assertFalse(getattr(sequential, 'is_draft', False))
# verify that we have the private vertical
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]))
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'toy',
'vertical', 'a_private_vertical', None]))
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
# make sure the textbook survived the export/import
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
self.assertGreater(len(course.textbooks), 0)
@@ -806,24 +796,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module_store = modulestore('direct')
# import a test course
import_from_xml(module_store, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['toy'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
handout_location = Location(['i4x', 'edX', 'toy', 'course_info', 'handouts'])
# get module info
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
# make sure we got a successful response
self.assertEqual(resp.status_code, 200)
# check that /static/ has been converted to the full path
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
# note, we know the link it should be because that's what in the 'toy' course in the test data
self.assertContains(resp, '/c4x/edX/toy/asset/handouts_sample_handout.txt')
def test_prefetch_children(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
module_store.collection.find = wrapper.find
@@ -835,19 +824,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(wrapper.counter, 4)
# make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
'vertical_sequential', None]) in course.system.module_data)
# make sure we don't have a specific vertical which should be at depth=3
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
self.assertFalse(Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None])
in course.system.module_data)
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
root_dir = path(mkdtemp_clean())
@@ -1202,9 +1191,9 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_forum_id_generation(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
new_component_location = Location('i4x', 'edX', '999', 'discussion', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
# crate a new module and add it as a child to a vertical
@@ -1216,7 +1205,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_update_modulestore_signal_did_fire(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
try:
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
@@ -1228,7 +1217,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.modulestore_update_signal.connect(_signal_hander)
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
new_component_location = Location('i4x', 'edX', '999', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module
@@ -1241,11 +1230,11 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_metadata_inheritance(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['toy'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
verticals = module_store.get_items(['i4x', 'edX', 'toy', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
@@ -1253,7 +1242,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertGreater(len(verticals), 0)
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
new_component_location = Location('i4x', 'edX', 'toy', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module and add it as a child to a vertical

View File

@@ -14,6 +14,7 @@ from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
@@ -297,9 +298,8 @@ class CourseMetadataEditingTest(CourseTestCase):
"""
def setUp(self):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(get_modulestore(self.course.location), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
self.fullcourse_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course.location)
@@ -309,7 +309,7 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.fetch(self.fullcourse_location)
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ')
@@ -349,7 +349,7 @@ class CourseMetadataEditingTest(CourseTestCase):
# ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')

View File

@@ -16,12 +16,18 @@ DEBUG = True
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
import os
import random
def seed():
return os.getppid()
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'acceptance_modulestore',
'db': 'acceptance_xmodule',
'collection': 'acceptance_modulestore_%s' % seed(),
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
@@ -45,7 +51,7 @@ CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db': 'acceptance_xcontent',
'db': 'acceptance_xcontent_%s' % seed(),
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS': {
@@ -61,13 +67,13 @@ CONTENTSTORE = {
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
'NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
'TEST_NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
}
}
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = 8001
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
LETTUCE_BROWSER = 'chrome'

View File

@@ -0,0 +1,77 @@
"""
This config file extends the test environment configuration
so that we can run the lettuce acceptance tests.
This is used in the django-admin call as acceptance.py
contains random seeding, causing django-admin to create a random collection
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import *
# You need to start the server in debug mode,
# otherwise the browser will not render the pages correctly
DEBUG = True
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
import os
import random
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'acceptance_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db': 'acceptance_xcontent',
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS': {
'trashcan': {
'bucket': 'trash_fs'
}
}
}
# Set this up so that rake lms[acceptance] and running the
# harvest command both use the same (test) database
# which they can flush without messing up your dev db
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
}
}
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
LETTUCE_BROWSER = 'chrome'

View File

@@ -80,6 +80,8 @@ CELERY_QUEUES = {
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None)
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
# Note that MITX_FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.

View File

@@ -368,3 +368,5 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
COURSES_WITH_UNSAFE_CODE = []

View File

@@ -37,6 +37,10 @@ describe "CMS.Views.SystemFeedback", ->
@renderSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'render').andCallThrough()
@showSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'show').andCallThrough()
@hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough()
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "requires a type and an intent", ->
neither = =>
@@ -80,8 +84,8 @@ describe "CMS.Views.SystemFeedback", ->
it "close button sends a .hide() message", ->
view = new CMS.Views.Alert.Confirmation(@options).show()
view.$(".action-close").click()
expect(@hideSpy).toHaveBeenCalled()
@clock.tick(900)
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", ->
@@ -98,9 +102,9 @@ describe "CMS.Views.Prompt", ->
view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown")
describe "CMS.Views.Notification.Saving", ->
describe "CMS.Views.Notification.Mini", ->
beforeEach ->
@view = new CMS.Views.Notification.Saving()
@view = new CMS.Views.Notification.Mini()
it "should have minShown set to 1250 by default", ->
expect(@view.options.minShown).toEqual(1250)

View File

@@ -0,0 +1,63 @@
describe "Course Overview", ->
beforeEach ->
appendSetFixtures """
<script src="/static/js/vendor/date.js"></script>
"""
appendSetFixtures """
<script type="text/javascript" src="/jsi18n/"></script>
"""
appendSetFixtures """
<div class="section-published-date">
<span class="published-status">
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
</span>
<a href="#" class="edit-button" "="" data-date="06/12/2013" data-time="04:00" data-id="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
</div>
"""#"
appendSetFixtures """
<div class="edit-subsection-publish-settings">
<div class="settings">
<h3>Section Release Date</h3>
<div class="picker datepair">
<div class="field field-start-date">
<label for="">Release Day</label>
<input class="start-date date" type="text" name="start_date" value="04/08/1990" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input class="start-time time" type="text" name="start_time" value="12:00" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<div class="description">
<p>On the date set above, this section <strong class="section-name"></strong> will be released to students. Any units marked private will only be visible to admins.</p>
</div>
</div>
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
</div>
</div>
"""#"
spyOn(window, 'saveSetSectionScheduleDate').andCallThrough()
# Have to do this here, as it normally gets bound in document.ready()
$('a.save-button').click(saveSetSectionScheduleDate)
@notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
sinon.useFakeXMLHttpRequest()
afterEach ->
delete window.analytics
delete window.course_location_analytics
it "should save model when save is clicked", ->
$('a.edit-button').click()
$('a.save-button').click()
expect(saveSetSectionScheduleDate).toHaveBeenCalled()
it "should show a confirmation on save", ->
$('a.edit-button').click()
$('a.save-button').click()
expect(@notificationSpy).toHaveBeenCalled()

View File

@@ -73,7 +73,7 @@ describe "CMS.Views.ShowTextbook", ->
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Saving",
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)

View File

@@ -712,6 +712,10 @@ function saveSetSectionScheduleDate(e) {
'start': start
});
var saving = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;",
});
saving.show();
// call into server to commit the new order
$.ajax({
url: "/save_item",
@@ -738,16 +742,7 @@ function saveSetSectionScheduleDate(e) {
'" data-date="' + input_date +
'" data-time="' + input_time +
'" data-id="' + id + '">' + gettext('Edit') + '</a>');
$thisSection.find('.section-published-date').animate({
'background-color': 'rgb(182,37,104)'
}, 300).animate({
'background-color': '#edf1f5'
}, 300).animate({
'background-color': 'rgb(182,37,104)'
}, 300).animate({
'background-color': '#edf1f5'
}, 300);
hideModal();
saving.hide();
});
}

View File

@@ -22,8 +22,8 @@ CMS.Models.Section = Backbone.Model.extend({
},
showNotification: function() {
if(!this.msg) {
this.msg = new CMS.Views.Notification.Saving({
title: gettext("Saving&hellip;")
this.msg = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;"
});
}
this.msg.show();

View File

@@ -5,8 +5,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values.
@@ -18,32 +16,8 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
// add saveSuccess to the success
var success = options.success;
options.success = function(model, resp, options) {
model.afterSave(model);
if (success) success(model, resp, options);
};
Backbone.Model.prototype.save.call(this, attrs, options);
},
afterSave : function(self) {
// remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) {
// remove the to be deleted keys from the returned model
_.each(self.deleteKeys, function(key) { self.unset(key); });
// not able to do via backbone since we're not destroying the model
$.ajax({
url : self.url,
// json to and fro
contentType : "application/json",
dataType : "json",
// delete
type : 'DELETE',
// data
data : JSON.stringify({ deleteKeys : self.deleteKeys})
})
.done(function(data, status, error) {
// clear deleteKeys on success
self.deleteKeys = [];
});
}
}
});

View File

@@ -63,13 +63,13 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
},
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
save_videosource: function(newsource) {
set_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.set({'intro_video': null}, {validate: true});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, {validate: true});
}
return this.videosourceSample();

View File

@@ -71,24 +71,25 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
},
validate : function(attrs) {
var errors = {};
if (attrs['type']) {
if (_.has(attrs, 'type')) {
if (_.isEmpty(attrs['type'])) {
errors.type = "The assignment type must have a name.";
}
else {
// FIXME somehow this.collection is unbound sometimes. I can't track down when
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this);
if (existing) {
errors.type = "There's already another assignment type with this name.";
}
}
}
if (attrs['weight']) {
if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
if (_.has(attrs, 'weight')) {
var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int
if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) {
errors.weight = "Please enter an integer between 0 and 100.";
}
else {
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
attrs.weight = intWeight;
if (this.collection && attrs.weight > 0) {
// FIXME b/c saves don't update the models if validation fails, we should
// either revert the field value to the one in the model and make them make room
@@ -97,19 +98,19 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
// errors.weight = "The weights cannot add to more than 100.";
}
}}
if (attrs['min_count']) {
if (_.has(attrs, 'min_count')) {
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
errors.min_count = "Please enter an integer.";
}
else attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (_.has(attrs, 'drop_count')) {
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
errors.drop_count = "Please enter an integer.";
}
else attrs.drop_count = parseInt(attrs.drop_count);
}
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) {
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
}
if (!_.isEmpty(errors)) return errors;

View File

@@ -140,7 +140,21 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
CMS.Views.Alert = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
type: "alert"
})
}),
slide_speed: 900,
show: function() {
CMS.Views.SystemFeedback.prototype.show.apply(this, arguments);
this.$el.hide();
this.$el.slideDown(this.slide_speed);
return this;
},
hide: function () {
this.$el.slideUp({
duration: this.slide_speed
});
setTimeout(_.bind(CMS.Views.SystemFeedback.prototype.hide, this, arguments),
this.slideSpeed);
}
});
CMS.Views.Notification = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
@@ -171,7 +185,7 @@ CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
var capitalCamel, types, intents;
capitalCamel = _.compose(_.str.capitalize, _.str.camelize);
types = ["alert", "notification", "prompt"];
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "saving"];
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
_.each(types, function(type) {
_.each(intents, function(intent) {
// "class" is a reserved word in Javascript, so use "klass" instead
@@ -187,8 +201,7 @@ _.each(types, function(type) {
});
});
// set more sensible defaults for Notification-Saving views
var savingOptions = CMS.Views.Notification.Saving.prototype.options;
savingOptions.minShown = 1250;
savingOptions.closeIcon = false;
// set more sensible defaults for Notification-Mini views
var miniOptions = CMS.Views.Notification.Mini.prototype.options;
miniOptions.minShown = 1250;
miniOptions.closeIcon = false;

View File

@@ -56,9 +56,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
CodeMirror.fromTextArea(textarea, {
mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) {
instance.save()
// this event's being called even when there's no change :-(
if (instance.getValue() !== oldValue && !self.notificationBarShowing) {
self.showNotificationBar();
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.");
self.showNotificationBar(message,
_.bind(self.saveView, self),
_.bind(self.revertView, self));
}
},
onFocus : function(mirror) {
@@ -91,44 +95,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
self.model.set(key, JSONValue, {validate: true});
self.model.set(key, JSONValue);
}
}
});
},
showNotificationBar: function() {
var self = this;
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.")
var confirm = new CMS.Views.Notification.Warning({
title: gettext("You've Made Some Changes"),
message: message,
actions: {
primary: {
"text": gettext("Save Changes"),
"class": "action-save",
"click": function() {
self.saveView();
confirm.hide();
self.notificationBarShowing = false;
}
},
secondary: [{
"text": gettext("Cancel"),
"class": "action-cancel",
"click": function() {
self.revertView();
confirm.hide();
self.notificationBarShowing = false;
}
}]
}});
this.notificationBarShowing = true;
confirm.show();
if(this.saved) {
this.saved.hide();
}
},
saveView : function() {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
@@ -138,25 +109,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
{
success : function() {
self.render();
var title = gettext("Your policy changes have been saved.");
var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.");
self.saved = new CMS.Views.Alert.Confirmation({
title: gettext("Your policy changes have been saved."),
message: message,
closeIcon: false
});
self.saved.show();
self.showSavedBar(title, message);
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
}
},
silent: true
});
},
revertView : function() {
revertView: function() {
var self = this;
this.model.deleteKeys = [];
this.model.clear({silent : true});
this.model.fetch({
success : function() { self.render(); },
success: function() { self.render(); },
reset: true
});
},

View File

@@ -3,10 +3,11 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {};
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseDetails
events : {
"input input" : "updateModel",
"input textarea" : "updateModel",
// Leaving change in as fallback for older browsers
"change input" : "updateModel",
"change textarea" : "updateModel",
'click .remove-course-syllabus' : "removeSyllabus",
'click .new-course-syllabus' : 'assetSyllabus',
'click .remove-course-introduction-video' : "removeVideo",
'focus #course-overview' : "codeMirrorize",
'mouseover #timezone' : "updateTime",
@@ -15,6 +16,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
'blur :input' : "inputUnfocus"
},
initialize : function() {
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>');
// fill in fields
@@ -27,6 +29,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.listenTo(this.model, 'change', this.showNotificationBar);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},
@@ -36,25 +39,13 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.setupDatePicker('enrollment_start');
this.setupDatePicker('enrollment_end');
if (this.model.has('syllabus')) {
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
this.fileAnchorTemplate({
fullpath : this.model.get('syllabus'),
filename: 'syllabus'}));
this.$el.find('.remove-course-syllabus').show();
}
else {
this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
this.$el.find('.remove-course-syllabus').hide();
}
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
this.codeMirrorize(null, $('#course-overview')[0]);
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video') || '');
if (this.model.has('intro_video')) {
this.$el.find('.remove-course-introduction-video').show();
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
}
else this.$el.find('.remove-course-introduction-video').hide();
@@ -67,7 +58,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
'end_date' : 'course-end',
'enrollment_start' : 'enrollment-start',
'enrollment_end' : 'enrollment-end',
'syllabus' : '.current-course-syllabus .doc-filename',
'overview' : 'course-overview',
'intro_video' : 'course-introduction-video',
'effort' : "course-effort"
@@ -87,8 +77,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var datefield = $(div).find("input:.date");
var timefield = $(div).find("input:.time");
var cachethis = this;
var savefield = function () {
cachethis.clearValidationErrors();
var setfield = function () {
var date = datefield.datepicker('getDate');
if (date) {
var time = timefield.timepicker("getSecondsFromMidnight");
@@ -97,14 +86,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
}
var newVal = new Date(date.getTime() + time * 1000);
if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
cacheModel.save(fieldName, newVal);
cachethis.clearValidationErrors();
cachethis.setAndValidate(fieldName, newVal);
}
}
else {
// Clear date (note that this clears the time as well, as date and time are linked).
// Note also that the validation logic prevents us from clearing the start date
// (start date is required by the back end).
cacheModel.save(fieldName, null);
cachethis.clearValidationErrors();
cachethis.setAndValidate(fieldName, null);
}
};
@@ -112,59 +103,51 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
timefield.timepicker({'timeFormat' : 'H:i'});
datefield.datepicker();
// Using the change event causes savefield to be triggered twice, but it is necessary
// Using the change event causes setfield to be triggered twice, but it is necessary
// to pick up when the date is typed directly in the field.
datefield.change(savefield);
timefield.on('changeTime', savefield);
datefield.change(setfield);
timefield.on('changeTime', setfield);
datefield.datepicker('setDate', this.model.get(fieldName));
if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName));
// timepicker doesn't let us set null, so check that we have a time
if (this.model.has(fieldName)) {
timefield.timepicker('setTime', this.model.get(fieldName));
} // but reset the field either way
else {
timefield.val('');
}
},
updateModel: function(event) {
switch (event.currentTarget.id) {
case 'course-start-date': // handled via onSelect method
case 'course-end-date':
case 'course-enrollment-start-date':
case 'course-enrollment-end-date':
break;
case 'course-overview':
// handled via code mirror
break;
case 'course-effort':
this.saveIfChanged(event);
this.setField(event);
break;
// Don't make the user reload the page to check the Youtube ID.
// Wait for a second to load the video, avoiding egregious AJAX calls.
case 'course-introduction-video':
this.clearValidationErrors();
var previewsource = this.model.save_videosource($(event.currentTarget).val());
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
if (this.model.has('intro_video')) {
this.$el.find('.remove-course-introduction-video').show();
}
else {
this.$el.find('.remove-course-introduction-video').hide();
}
var previewsource = this.model.set_videosource($(event.currentTarget).val());
clearTimeout(this.videoTimer);
this.videoTimer = setTimeout(_.bind(function() {
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
if (this.model.has('intro_video')) {
this.$el.find('.remove-course-introduction-video').show();
}
else {
this.$el.find('.remove-course-introduction-video').hide();
}
}, this), 1000);
break;
default:
default: // Everything else is handled by datepickers and CodeMirror.
break;
}
},
removeSyllabus: function() {
if (this.model.has('syllabus')) this.model.save({'syllabus': null});
},
assetSyllabus : function() {
// TODO implement
},
removeVideo: function() {
removeVideo: function(event) {
event.preventDefault();
if (this.model.has('intro_video')) {
this.model.save_videosource(null);
this.model.set_videosource(null);
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
this.$el.find('.remove-course-introduction-video').hide();
@@ -185,15 +168,53 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var field = this.selectorToField[thisTarget.id];
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
mode: "text/html", lineNumbers: true, lineWrapping: true,
onBlur: function (mirror) {
onChange: function (mirror) {
mirror.save();
cachethis.clearValidationErrors();
var newVal = mirror.getValue();
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
if (cachethis.model.get(field) != newVal) {
cachethis.setAndValidate(field, newVal);
}
}
});
}
}
},
revertView: function() {
// Make sure that the CodeMirror instance has the correct
// data from its corresponding textarea
var self = this;
this.model.fetch({
success: function() {
self.render();
_.each(self.codeMirrors,
function(mirror) {
var ele = mirror.getTextArea();
var field = self.selectorToField[ele.id];
mirror.setValue(self.model.get(field));
});
},
reset: true,
silent: true});
},
setAndValidate: function(attr, value) {
// If we call model.set() with {validate: true}, model fields
// will not be set if validation fails. This puts the UI and
// the model in an inconsistent state, and causes us to not
// see the right validation errors the next time validate() is
// called on the model. So we set *without* validating, then
// call validate ourselves.
this.model.set(attr, value);
this.model.isValid();
},
showNotificationBar: function() {
// We always call showNotificationBar with the same args, just
// delegate to superclass
CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
this.save_message,
_.bind(this.saveView, this),
_.bind(this.revertView, this));
}
});

View File

@@ -3,6 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy
events : {
"input input" : "updateModel",
"input textarea" : "updateModel",
// Leaving change in as fallback for older browsers
"change input" : "updateModel",
"change textarea" : "updateModel",
"change span[contenteditable=true]" : "updateDesignation",
@@ -23,14 +26,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
'</li>');
// Instrument grading scale
// convert cutoffs to inversely ordered list
var modelCutoffs = this.model.get('grade_cutoffs');
for (var cutoff in modelCutoffs) {
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
}
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
function (gradeEle) { return -gradeEle['cutoff']; });
this.setupCutoffs();
// Instrument grace period
this.$el.find('#course-grading-graceperiod').timepicker();
@@ -45,7 +41,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
}
);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.model.get('graders').on('remove', this.render, this);
this.listenTo(this.model, 'change', this.showNotificationBar);
this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this);
this.selectorToField = _.invert(this.fieldToSelectorMap);
@@ -61,11 +57,31 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Undo the double invocation error. At some point, fix the double invocation
$(gradelist).empty();
var gradeCollection = this.model.get('graders');
// We need to bind these events here (rather than in
// initialize), or else we can only press the delete button
// once due to the graders collection changing when we cancel
// our changes.
_.each(['change', 'remove', 'add'],
function (event) {
gradeCollection.on(event, function() {
this.showNotificationBar();
// Since the change event gets fired every time
// we type in an input field, we don't need to
// (and really shouldn't) rerender the whole view.
if(event !== 'change') {
this.render();
}
}, this);
},
this);
gradeCollection.each(function(gradeModel) {
$(gradelist).append(self.template({model : gradeModel }));
var newEle = gradelist.children().last();
var newView = new CMS.Views.Settings.GraderView({el: newEle,
model : gradeModel, collection : gradeCollection });
// Listen in order to rerender when the 'cancel' button is
// pressed
self.listenTo(newView, 'revert', _.bind(self.render, self));
});
// render the grade cutoffs
@@ -88,9 +104,10 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
'grace_period' : 'course-grading-graceperiod'
},
setGracePeriod : function(event) {
event.data.clearValidationErrors();
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
var self = event.data;
self.clearValidationErrors();
var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
self.model.set('grace_period', newVal, {validate: true});
},
updateModel : function(event) {
if (!this.selectorToField[event.currentTarget.id]) return;
@@ -100,8 +117,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
break;
default:
this.saveIfChanged(event);
break;
this.setField(event);
break;
}
},
@@ -220,13 +237,14 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
},
saveCutoffs: function() {
this.model.save('grade_cutoffs',
this.model.set('grade_cutoffs',
_.reduce(this.descendingCutoffs,
function(object, cutoff) {
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
return object;
},
{}));
{}),
{validate: true});
},
addNewGrade: function(e) {
@@ -301,13 +319,45 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
},
setTopGradeLabel: function() {
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
},
setupCutoffs: function() {
// Instrument grading scale
// convert cutoffs to inversely ordered list
var modelCutoffs = this.model.get('grade_cutoffs');
for (var cutoff in modelCutoffs) {
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
}
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
function (gradeEle) { return -gradeEle['cutoff']; });
},
revertView: function() {
var self = this;
this.model.fetch({
success: function() {
self.descendingCutoffs = [];
self.setupCutoffs();
self.render();
self.renderCutoffBar();
},
reset: true,
silent: true});
},
showNotificationBar: function() {
// We always call showNotificationBar with the same args, just
// delegate to superclass
CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
this.save_message,
_.bind(this.saveView, this),
_.bind(this.revertView, this));
}
});
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGrader
events : {
"input input" : "updateModel",
"input textarea" : "updateModel",
// Leaving change in as fallback for older browsers
"change input" : "updateModel",
"change textarea" : "updateModel",
"click .remove-grading-data" : "deleteModel",
@@ -331,7 +381,7 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'drop_count' : 'course-grading-assignment-droppable',
'weight' : 'course-grading-assignment-gradeweight'
},
updateModel : function(event) {
updateModel: function(event) {
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
// give 2 assignments the same name.]
@@ -342,26 +392,27 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
switch (event.currentTarget.id) {
case 'course-grading-assignment-totalassignments':
this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
this.saveIfChanged(event);
this.setField(event);
break;
case 'course-grading-assignment-name':
var oldName = this.model.get('type');
if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
// Keep the original name, until we save
this.oldName = this.oldName === undefined ? this.model.get('type') : this.oldName;
// If the name has changed, alert the user to change all subsection names.
if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) {
// overload the error display logic
this._cacheValidationErrors.push(event.currentTarget);
$(event.currentTarget).parent().append(
this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName +
'" subsections to "' + this.model.get('type') + '".'}));
}
break;
default:
this.saveIfChanged(event);
this.setField(event);
break;
}
},
deleteModel : function(e) {
this.model.destroy();
e.preventDefault();
this.collection.remove(this.model);
}
});

View File

@@ -34,8 +34,8 @@ CMS.Views.ShowTextbook = Backbone.View.extend({
text: gettext("Delete"),
click: function(view) {
view.hide();
var delmsg = new CMS.Views.Notification.Saving({
title: gettext("Deleting&hellip;")
var delmsg = new CMS.Views.Notification.Mini({
title: gettext("Deleting") + "&hellip;"
}).show();
textbook.destroy({
complete: function() {
@@ -121,8 +121,8 @@ CMS.Views.EditTextbook = Backbone.View.extend({
if(e && e.preventDefault) { e.preventDefault(); }
this.setValues();
if(!this.model.isValid()) { return; }
var saving = new CMS.Views.Notification.Saving({
title: gettext("Saving&hellip;")
var saving = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;"
}).show();
var that = this;
this.model.save({}, {

View File

@@ -9,6 +9,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
save_title: gettext("You've made some changes"),
save_message: gettext("Your changes will not take effect until you save your progress."),
error_title: gettext("You've made some changes, but there are some errors"),
error_message: gettext("Please address the errors on this page first, and then save your progress."),
events : {
"change input" : "clearValidationErrors",
"change textarea" : "clearValidationErrors"
@@ -20,6 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
_cacheValidationErrors : [],
handleValidationError : function(model, error) {
this.clearValidationErrors();
// error is object w/ fields and error strings
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
@@ -27,6 +33,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
this.getInputElements(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
$('.wrapper-notification-warning').addClass('wrapper-notification-warning-w-errors');
$('.action-save').addClass('is-disabled');
// TODO: (pfogg) should this text fade in/out on change?
$('#notification-warning-title').text(this.error_title);
$('#notification-warning-description').text(this.error_message);
},
clearValidationErrors : function() {
@@ -36,19 +47,20 @@ CMS.Views.ValidatingView = Backbone.View.extend({
this.getInputElements(ele).removeClass('error');
$(ele).nextAll('.message-error').remove();
}
$('.wrapper-notification-warning').removeClass('wrapper-notification-warning-w-errors');
$('.action-save').removeClass('is-disabled');
$('#notification-warning-title').text(this.save_title);
$('#notification-warning-description').text(this.save_message);
},
saveIfChanged : function(event) {
// returns true if the value changed and was thus sent to server
setField : function(event) {
// Set model field and return the new value.
this.clearValidationErrors();
var field = this.selectorToField[event.currentTarget.id];
var currentVal = this.model.get(field);
var newVal = $(event.currentTarget).val();
this.clearValidationErrors(); // curr = new if user reverts manually
if (currentVal != newVal) {
this.model.save(field, newVal);
return true;
}
else return false;
this.model.set(field, newVal);
this.model.isValid();
return newVal;
},
// these should perhaps go into a superclass but lack of event hash inheritance demotivates me
inputFocus : function(event) {
@@ -67,5 +79,79 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// put error on the contained inputs
return $(ele).find(inputElements);
}
},
showNotificationBar: function(message, primaryClick, secondaryClick) {
// Show a notification with message. primaryClick is called on
// pressing the save button, and secondaryClick (if it's
// passed, which it may not be) will be called on
// cancel. Takes care of hiding the notification bar at the
// appropriate times.
if(this.notificationBarShowing) {
return;
}
// If we've already saved something, hide the alert.
if(this.saved) {
this.saved.hide();
}
var self = this;
this.confirmation = new CMS.Views.Notification.Warning({
title: this.save_title,
message: message,
actions: {
primary: {
"text": gettext("Save Changes"),
"class": "action-save",
"click": function() {
primaryClick();
self.confirmation.hide();
self.notificationBarShowing = false;
}
},
secondary: [{
"text": gettext("Cancel"),
"class": "action-cancel",
"click": function() {
if(secondaryClick) {
secondaryClick();
}
self.model.clear({silent : true});
self.confirmation.hide();
self.notificationBarShowing = false;
}
}]
}});
this.notificationBarShowing = true;
this.confirmation.show();
// Make sure the bar is in the right state
this.model.isValid();
},
showSavedBar: function(title, message) {
var defaultTitle = gettext('Your changes have been saved.');
this.saved = new CMS.Views.Alert.Confirmation({
title: title || defaultTitle,
message: message,
closeIcon: false
});
this.saved.show();
$.smoothScroll({
offset: 0,
easing: 'swing',
speed: 1000
});
},
saveView: function() {
var self = this;
this.model.save(
{},
{
success: function() {
self.showSavedBar();
},
silent: true
}
);
}
});

View File

@@ -274,7 +274,7 @@
}
}
&.wrapper-notification-saving {
&.wrapper-notification-mini {
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $pink;
}
@@ -434,7 +434,7 @@
}
}
&.saving {
&.mini {
[class^="icon"] {
@include animation(rotateCW $tmg-s3 linear infinite);
@@ -665,14 +665,8 @@
}
}
// alert showing/hiding
.wrapper-alert {
display: none;
&.is-shown {
display: block;
}
}
// alert showing/hiding done by jQuery
.wrapper-alert { }
// notification showing/hiding
.wrapper-notification {

View File

@@ -1,6 +1,6 @@
<div class="wrapper wrapper-<%= type %> wrapper-<%= type %>-<%= intent %>
<% if(obj.shown) { %>is-shown<% } else { %>is-hiding<% } %>
<% if(_.contains(['help', 'saving'], intent)) { %>wrapper-<%= type %>-status<% } %>"
<% if(_.contains(['help', 'mini'], intent)) { %>wrapper-<%= type %>-status<% } %>"
id="<%= type %>-<%= intent %>"
aria-hidden="<% if(obj.shown) { %>false<% } else { %>true<% } %>"
aria-labelledby="<%= type %>-<%= intent %>-title"
@@ -9,7 +9,7 @@
>
<div class="<%= type %> <%= intent %> <% if(obj.actions) { %>has-actions<% } %>">
<% if(obj.icon) { %>
<% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "saving": "cog"} %>
<% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "mini": "cog"} %>
<i class="icon-<%= iconClass[intent] %>"></i>
<% } %>

View File

@@ -67,6 +67,8 @@
<%block name="jsextra">
<script type="text/javascript">
var $newUserForm;
var addUserPostbackUrl = "${add_user_postback_url}";
var removeUserPostbackUrl = "${remove_user_postback_url}";
function showNewUserForm(e) {
e.preventDefault();
@@ -91,16 +93,19 @@
e.preventDefault();
$.ajax({
url: '${add_user_postback_url}',
url: addUserPostbackUrl,
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data:JSON.stringify({ 'email': $('#email').val()}),
}).done(function(data) {
if (data.ErrMsg != undefined)
$('#result').show().empty().append(data.ErrMsg);
else
data: JSON.stringify({ 'email': $('#email').val()}),
success: function(data) {
location.reload();
},
notifyOnError: false,
error: function(jqXHR, textStatus, errorThrown) {
data = JSON.parse(jqXHR.responseText);
$('#result').show().empty().append(data.ErrMsg);
}
});
}
@@ -115,7 +120,7 @@
$('.remove-user').click(function() {
$.ajax({
url: '${remove_user_postback_url}',
url: removeUserPostbackUrl,
type: 'POST',
dataType: 'json',
contentType: 'application/json',

View File

@@ -98,7 +98,8 @@ class ShibSPTest(ModuleStoreTestCase):
def test_shib_login(self):
"""
Tests that:
* shib credentials that match an existing ExternalAuthMap with a linked user logs the user in
* shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in
* shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page
* shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
of an existing user without an existing ExternalAuthMap links the two and log the user in
* shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
@@ -117,8 +118,19 @@ class ShibSPTest(ModuleStoreTestCase):
user_wo_map.save()
extauth.save()
inactive_user = UserFactory.create(email='inactive@stanford.edu')
inactive_user.is_active = False
inactive_extauth = ExternalAuthMap(external_id='inactive@stanford.edu',
external_email='',
external_domain='shib:https://idp.stanford.edu/',
external_credentials="",
user=inactive_user)
inactive_user.save()
inactive_extauth.save()
idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/']
remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', 'testuser2@someother_idp.com']
remote_users = ['withmap@stanford.edu', 'womap@stanford.edu',
'testuser2@someother_idp.com', 'inactive@stanford.edu']
for idp in idps:
for remote_user in remote_users:
@@ -133,13 +145,16 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(request.user, user_w_map)
self.assertEqual(response['Location'], '/')
elif idp == "https://idp.stanford.edu/" and remote_user == 'inactive@stanford.edu':
self.assertEqual(response.status_code, 403)
self.assertIn("Account not yet activated: please look for link in your email", response.content)
elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu':
self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map))
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(request.user, user_wo_map)
self.assertEqual(response['Location'], '/')
elif idp == "https://someother.idp.com/" and remote_user in \
['withmap@stanford.edu', 'womap@stanford.edu']:
['withmap@stanford.edu', 'womap@stanford.edu', 'inactive@stanford.edu']:
self.assertEqual(response.status_code, 403)
self.assertIn("You have already created an account using an external login", response.content)
else:

View File

@@ -176,6 +176,7 @@ def external_login_or_signup(request,
# We trust shib's authentication, so no need to authenticate using the password again
if settings.MITX_FEATURES.get('AUTH_USE_SHIB'):
uname = internal_user.username
user = internal_user
# Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe
if settings.AUTHENTICATION_BACKENDS:

View File

@@ -17,6 +17,9 @@ from selenium.common.exceptions import WebDriverException
# These names aren't used, but do important work on import.
from lms import one_time_startup # pylint: disable=W0611
from cms import one_time_startup # pylint: disable=W0611
from pymongo import MongoClient
import xmodule.modulestore.django
from xmodule.contentstore.django import _CONTENTSTORE
# There is an import issue when using django-staticfiles with lettuce
# Lettuce assumes that we are using django.contrib.staticfiles,
@@ -86,6 +89,29 @@ def reset_data(scenario):
"""
LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False)
world.absorb({}, 'scenario_dict')
@after.each_scenario
def clear_data(scenario):
world.spew('scenario_dict')
@after.each_scenario
def reset_databases(scenario):
'''
After each scenario, all databases are cleared/dropped. Contentstore data are stored in unique databases
whereas modulestore data is in unique collection names. This data is created implicitly during the scenarios.
If no data is created during the test, these lines equivilently do nothing.
'''
mongo = MongoClient()
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear()
# Uncomment below to trigger a screenshot on error
# @after.each_scenario

View File

@@ -17,14 +17,14 @@ from urllib import quote_plus
@world.absorb
def create_user(uname):
def create_user(uname, password):
# If the user already exists, don't try to create it again
if len(User.objects.filter(username=uname)) > 0:
return
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test')
portal_user.set_password(password)
portal_user.save()
registration = world.RegistrationFactory(user=portal_user)
@@ -43,8 +43,8 @@ def log_in(username, password):
"""
# Authenticate the user
user = authenticate(username=username, password=password)
assert(user is not None and user.is_active)
world.scenario_dict['USER'] = authenticate(username=username, password=password)
assert(world.scenario_dict['USER'] is not None and world.scenario_dict['USER'].is_active)
# Send a fake HttpRequest to log the user in
# We need to process the request using
@@ -53,7 +53,7 @@ def log_in(username, password):
request = HttpRequest()
SessionMiddleware().process_request(request)
AuthenticationMiddleware().process_request(request)
login(request, user)
login(request, world.scenario_dict['USER'])
# Save the session
request.session.save()

View File

@@ -93,7 +93,7 @@ def i_log_in(step):
@step('I am a logged in user$')
def i_am_logged_in_user(step):
world.create_user('robot')
world.create_user('robot', 'test')
world.log_in('robot', 'test')
@@ -139,7 +139,7 @@ def should_see_in_the_page(step, doesnt_appear, text):
@step('I am logged in$')
def i_am_logged_in(step):
world.create_user('robot')
world.create_user('robot', 'test')
world.log_in('robot', 'test')
world.browser.visit(django_url('/'))
# You should not see the login link
@@ -148,12 +148,12 @@ def i_am_logged_in(step):
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
world.create_user('robot', 'test')
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
world.create_user(uname, 'test')
@step(u'All dialogs should be closed$')

View File

@@ -10,6 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
from nose.tools import assert_true
@world.absorb
@@ -142,27 +143,32 @@ def id_click(elem_id):
@world.absorb
def css_fill(css_selector, text):
assert is_css_present(css_selector), "{} is not present".format(css_selector)
world.browser.find_by_css(css_selector).first.fill(text)
def css_fill(css_selector, text, index=0, max_attempts=5):
assert is_css_present(css_selector)
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].fill(text), max_attempts=max_attempts)
@world.absorb
def click_link(partial_text):
world.browser.find_link_by_partial_text(partial_text).first.click()
def click_link(partial_text, index=0, max_attempts=5):
return world.retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click(), max_attempts=max_attempts)
@world.absorb
def css_text(css_selector, index=0):
def css_text(css_selector, index=0, max_attempts=5):
# Wait for the css selector to appear
if world.is_css_present(css_selector):
try:
return world.browser.find_by_css(css_selector)[index].text
except StaleElementReferenceException:
# The DOM was still redrawing. Wait a second and try again.
world.wait(1)
return world.browser.find_by_css(css_selector)[index].text
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].text, max_attempts=max_attempts)
else:
return ""
@world.absorb
def css_value(css_selector, index=0, max_attempts=5):
# Wait for the css selector to appear
if world.is_css_present(css_selector):
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].value, max_attempts=max_attempts)
else:
return ""
@@ -173,19 +179,18 @@ def css_html(css_selector, index=0, max_attempts=5):
Returns the HTML of a css_selector and will retry if there is a StaleElementReferenceException
"""
assert is_css_present(css_selector)
attempt = 0
while attempt < max_attempts:
try:
return world.browser.find_by_css(css_selector)[index].html
except:
attempt += 1
return ''
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].html, max_attempts=max_attempts)
@world.absorb
def css_visible(css_selector):
assert is_css_present(css_selector), "{} is not present".format(css_selector)
return world.browser.find_by_css(css_selector).visible
def css_has_class(css_selector, class_name, index=0, max_attempts=5):
return world.retry_on_exception(lambda: world.css_find(css_selector)[index].has_class(class_name), max_attempts=max_attempts)
@world.absorb
def css_visible(css_selector, index=0, max_attempts=5):
assert is_css_present(css_selector)
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].visible, max_attempts=max_attempts)
@world.absorb
@@ -232,3 +237,18 @@ def click_tools():
@world.absorb
def is_mac():
return platform.mac_ver()[0] is not ''
@world.absorb
def retry_on_exception(func, max_attempts=5):
attempt = 0
while attempt < max_attempts:
try:
return func()
break
except WebDriverException:
world.wait(1)
attempt += 1
except:
attempt += 1
assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func))

View File

@@ -14,7 +14,9 @@ def can_execute_unsafe_code(course_id):
"""
# To decide if we can run unsafe code, we check the course id against
# a list of regexes configured on the server.
for regex in settings.COURSES_WITH_UNSAFE_CODE:
# If this is not defined in the environment variables then default to the most restrictive, which
# is 'no unsafe courses'
for regex in getattr(settings, 'COURSES_WITH_UNSAFE_CODE', []):
if re.match(regex, course_id):
return True
return False

View File

@@ -25,3 +25,10 @@ class SandboxingTest(TestCase):
"""
self.assertTrue(can_execute_unsafe_code('edX/full/2012_Fall'))
self.assertTrue(can_execute_unsafe_code('edX/full/2013_Spring'))
def test_courses_with_unsafe_code_default(self):
"""
Test that the default setting for COURSES_WITH_UNSAFE_CODE is an empty setting, e.g. we don't use @override_settings in these tests
"""
self.assertFalse(can_execute_unsafe_code('edX/full/2012_Fall'))
self.assertFalse(can_execute_unsafe_code('edX/full/2013_Spring'))

View File

@@ -24,16 +24,16 @@ class TestXMLModuleStore(object):
# uniquification of names, would raise a UnicodeError. It no longer does.
# Ensure that there really is a non-ASCII character in the course.
with open(os.path.join(DATA_DIR, "full/sequential/Administrivia_and_Circuit_Elements.xml")) as xmlf:
with open(os.path.join(DATA_DIR, "toy/sequential/vertical_sequential.xml")) as xmlf:
xml = xmlf.read()
with assert_raises(UnicodeDecodeError):
xml.decode('ascii')
# Load the course, but don't make error modules. This will succeed,
# but will record the errors.
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['full'], load_error_modules=False)
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'], load_error_modules=False)
# Look up the errors during load. There should be none.
location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012")
location = CourseDescriptor.id_to_location("edX/toy/2012_Fall")
errors = modulestore.get_item_errors(location)
assert errors == []

View File

@@ -116,9 +116,6 @@ class RoundTripTestCase(unittest.TestCase):
def test_simple_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "simple")
def test_full_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "full")
def test_conditional_and_poll_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "conditional_and_poll")

View File

@@ -358,7 +358,7 @@ class ImportTestCase(BaseCourseTestCase):
print(err)
chapters = course.get_children()
self.assertEquals(len(chapters), 2)
self.assertEquals(len(chapters), 5)
ch2 = chapters[1]
self.assertEquals(ch2.url_name, "secret:magic")

View File

@@ -109,7 +109,7 @@ if Backbone?
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'_delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
@@ -168,7 +168,7 @@ if Backbone?
'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
'update': -> DiscussionUtil.urlFor('update_comment', @id)
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
'_delete': -> DiscussionUtil.urlFor('delete_comment', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)

View File

@@ -7,7 +7,7 @@ if Backbone?
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
"click .action-delete": "delete"
"click .action-delete": "_delete"
"click .action-openclose": "toggleClosed"
$: (selector) ->
@@ -125,8 +125,8 @@ if Backbone?
edit: (event) ->
@trigger "thread:edit", event
delete: (event) ->
@trigger "thread:delete", event
_delete: (event) ->
@trigger "thread:_delete", event
togglePin: (event) ->
event.preventDefault()

View File

@@ -185,7 +185,7 @@ if Backbone?
@editView = null
@showView = new DiscussionThreadShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:_delete", @_delete
@showView.bind "thread:edit", @edit
renderShowView: () ->
@@ -196,9 +196,11 @@ if Backbone?
@createShowView()
@renderShowView()
delete: (event) =>
url = @model.urlFor('delete')
# If you use "delete" here, it will compile down into JS that includes the
# use of DiscussionThreadView.prototype.delete, and that will break IE8
# because "delete" is a keyword. So, using an underscore to prevent that.
_delete: (event) =>
url = @model.urlFor('_delete')
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"

View File

@@ -48,7 +48,7 @@ if Backbone?
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadInlineShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:_delete", @_delete
@showView.bind "thread:edit", @edit
renderResponses: ->

View File

@@ -3,7 +3,7 @@ if Backbone?
events:
"click .vote-btn": "toggleVote"
"click .action-endorse": "toggleEndorse"
"click .action-delete": "delete"
"click .action-delete": "_delete"
"click .action-edit": "edit"
"click .discussion-flag-abuse": "toggleFlagAbuse"
@@ -77,8 +77,8 @@ if Backbone?
edit: (event) ->
@trigger "response:edit", event
delete: (event) ->
@trigger "response:delete", event
_delete: (event) ->
@trigger "response:_delete", event
toggleEndorse: (event) ->
event.preventDefault()

View File

@@ -93,13 +93,13 @@ if Backbone?
comment.set(response.content)
view.render() # This is just to update the id for the most part, but might be useful in general
delete: (event) =>
_delete: (event) =>
event.preventDefault()
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete this response? "
return
url = @model.urlFor('delete')
url = @model.urlFor('_delete')
@model.remove()
@$el.remove()
$elem = $(event.target)
@@ -141,7 +141,7 @@ if Backbone?
@editView = null
@showView = new ThreadResponseShowView(model: @model)
@showView.bind "response:delete", @delete
@showView.bind "response:_delete", @_delete
@showView.bind "response:edit", @edit
renderShowView: () ->

View File

@@ -126,7 +126,7 @@
padding: ($baseline/5) $baseline ($baseline/4);
font-weight: 700;
&.disabled {
&.disabled, &.is-disabled {
border: 1px solid $gray-l1 !important;
border-radius: 3px !important;
background: $gray-l1 !important;
@@ -157,7 +157,7 @@
color: $white;
}
&.disabled {
&.disabled, &.is-disabled {
border: 1px solid $green-l3 !important;
background: $green-l3 !important;
color: $white !important;
@@ -178,7 +178,7 @@
color: $white;
}
&.disabled {
&.disabled, &.is-disabled {
box-shadow: none;
border: 1px solid $blue-l3 !important;
background: $blue-l3 !important;
@@ -199,7 +199,7 @@
color: $white;
}
&.disabled {
&.disabled, &.is-disabled {
box-shadow: none;
border: 1px solid $red-l3 !important;
background: $red-l3 !important;
@@ -220,7 +220,7 @@
color: $white;
}
&.disabled {
&.disabled, &.is-disabled {
box-shadow: none;
border: 1px solid $pink-l3 !important;
background: $pink-l3 !important;
@@ -242,7 +242,7 @@
color: $gray-d2;
}
&.disabled {
&.disabled, &.is-disabled {
border: 1px solid $orange-l3 !important;
background: $orange-l2 !important;
color: $gray-l1 !important;

View File

@@ -1 +0,0 @@
This is a realistic course, with many different module types and a lot of structure. It is based on 6.002x.

View File

@@ -1,3 +0,0 @@
6.002x (Circuits and Electronics) is designed to serve as a first course in an undergraduate electrical engineering (EE), or electrical engineering and computer science (EECS) curriculum. At MIT, 6.002 is in the core of department subjects required for all undergraduates in EECS.
The course introduces engineering in the context of the lumped circuit abstraction. Topics covered include: resistive elements and networks; independent and dependent sources; switches and MOS transistors; digital abstraction; amplifiers; energy storage elements; dynamics of first- and second-order networks; design in the time and frequency domains; and analog and digital circuits and applications. Design and lab exercises are also significant components of the course. You should expect to spend approximately 10 hours per week on the course.

View File

@@ -1 +0,0 @@
12 hours

View File

@@ -1,14 +0,0 @@
<ul>
<li>What is the format of the class?
<p>The course will consist of 24 lectures, each lasting 50 minutes. There will be regular assignments consisting of map tests and short essays.</p>
</li>
<li>Are there any prerequisites?
<p>No - anyone and everyone is welcome to take this course.</p>
</li>
<li>What textbook should I buy?
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) &mdash; Volume II, which was written specifically for this course.</p>
</li>
<li>Does Harvard award credentials or reports regarding my work in this course?
<p>Princeton does not award credentials or issue reports for student work in this course. However, Coursera could maintain a record of your score on the assessments and, with your permission, verify that score for authorized parties.</p>
</li>
</ul>

View File

@@ -1,9 +0,0 @@
<section class="who-should-take">
<h3>Who should take this?</h3>
<p>If you're one of the many who have a unquenched interest in the worlds history, you'll love this course.</p>
</section>
<section class="who-shouldnt-take">
<h3>Who shouldn't take this?</h3>
<p>No one. Anyone and everyone is welcome to take this course.</p>
</section>

View File

@@ -1,2 +0,0 @@
<p>In order to succeed in this course, you must have taken an AP level physics course in electricity and magnetism. You must know basic calculus and linear algebra and have some background in differential equations. Since more advanced mathematics will not show up until the second half of the course, the first half of the course will include an optional remedial differential equations component for those who need it.</p>
<p>The course web site was developed and tested primarily with Google Chrome. We support current versions of Mozilla Firefox as well. The video player is designed to work with Flash. While we provide a partial non-Flash fallback for the video, as well as partial support for Internet Explorer, other browsers, and tablets, portions of the functionality will be unavailable.</p>

View File

@@ -1,17 +0,0 @@
<ul>
<li><strong>Week 1:</strong> What is World History?</li>
<li><strong>Week 2:</strong> Peoples, Plagues and Plunders</li>
<li><strong>Week 3:</strong> Warfare and Motion</li>
<li><strong>Week 4:</strong> Conquests</li>
<li><strong>Week 5:</strong> The Beginnings of Globalization in the Atlantic Worlds</li>
<li><strong>Week 6:</strong> The Beginnings of Globalization in the Indian Ocean Worlds</li>
<li><strong>Week 7:</strong> The Worlds that Merchants Made</li>
<li><strong>Week 8:</strong> The Seventeenth-Century Crisis</li>
<li><strong>Week 9:</strong> Empire and Enlightenment</li>
<li><strong>Week 10:</strong> The Wealth of Nations</li>
<li><strong>Week 11:</strong> The World in Revolution</li>
<li><strong>Week 12:</strong> States and Nations</li>
<li><strong>Week 13:</strong> Global Frontiers</li>
<li><strong>Week 14:</strong> Empires and Nations</li>
<li><strong>Week 15:</strong> Back to the Future</li>
</ul>

View File

@@ -1 +0,0 @@
<p>The course uses the textbook Foundations of Analog and Digital Electronic Circuits, by Anant Agarwal and Jeffrey H. Lang. Morgan Kaufmann Publishers, Elsevier, July 2005. While recommended, the book is not required: relevant sections will be provided electronically as part of the online course for personal use in connection with this course only. The copyright for the book is owned by Elsevier. The book can be purchased on <a href="http://www.amazon.com/exec/obidos/ASIN/1558607358/ref=nosim/mitopencourse-20">Amazon</a>.</p>

View File

@@ -1 +0,0 @@
<iframe width="560" height="315" src="http://www.youtube.com/embed/p2Q6BrNhdh8" frameborder="0" allowfullscreen></iframe>

View File

@@ -1,10 +0,0 @@
<sequential>
<video url_name="welcome"/>
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab… </html>
<html slug="html_5555" filename="html_5555"/>
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
</vertical>
<problem filename="Circuit_Sandbox" slug="Circuit_Sandbox" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Circuit Sandbox"/>
</sequential>

View File

@@ -1,10 +0,0 @@
<sequential>
<sequential filename="Administrivia_and_Circuit_Elements" slug="Administrivia_and_Circuit_Elements" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Administrivia and Circuit Elements"/>
<vertical slug="Basic_Circuit_Analysis" format="Homework" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="Basic Circuit Analysis">
<problem filename="H1P1_Energy" slug="H1P1_Energy" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P1: Energy"/>
<problem filename="H1P2_Duality" slug="H1P2_Duality" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P2: Duality"/>
<problem filename="H1P3_Poor_Workmanship" slug="H1P3_Poor_Workmanship" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P3: Poor Workmanship"/>
</vertical>
<problem filename="Resistor_Divider" slug="Resistor_Divider" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" due="March 18" graded="true" name="Resistor Divider"/>
<html filename="Week_1_Tutorials" slug="Week_1_Tutorials" format="Tutorial Index" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1 Tutorials"/>
</sequential>

View File

@@ -1 +0,0 @@
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" show_timezone="true" advanced_modules="[&quot;videoalpha&quot;]"/>

View File

@@ -1,13 +0,0 @@
<course>
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
<chapter filename="Overview" slug="Overview" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Overview"/>
<chapter filename="Week_1" slug="Week_1" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1"/>
<chapter slug="Midterm_Exam" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Midterm Exam">
<sequential slug="Midterm_Exam_1121" format="Midterm" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam">
<vertical slug="vertical_1122" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true">
<html filename="Midterm_Exam_1123" slug="Midterm_Exam_1123" graceperiod="0 day 0 hours 5 minutes 0 seconds" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam"/>
</vertical>
<vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
</sequential>
</chapter>
</course>

View File

@@ -1,35 +0,0 @@
{
"GRADER" : [
{
"type" : "Homework",
"min_count" : 12,
"drop_count" : 2,
"short_label" : "HW",
"weight" : 0.15
},
{
"type" : "Lab",
"min_count" : 12,
"drop_count" : 2,
"category" : "Labs",
"weight" : 0.15
},
{
"type" : "Midterm",
"name" : "Midterm Exam",
"short_label" : "Midterm",
"weight" : 0.3
},
{
"type" : "Final",
"name" : "Final Exam",
"short_label" : "Final",
"weight" : 0.4
}
],
"GRADE_CUTOFFS" : {
"A" : 0.87,
"B" : 0.7,
"C" : 0.6
}
}

View File

@@ -1 +0,0 @@
More information given in… <a href="/book/${page}">the text</a>.

View File

@@ -1 +0,0 @@
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag}… </a>

View File

@@ -1 +0,0 @@
<img src="http://pmitros.csail.mit.edu/tutorial/images/${file}" />

View File

@@ -1 +0,0 @@
Lecture Slides Handout [<a href="">Clean… </a>][<a href="">Annotated…</a>]

View File

@@ -1,3 +0,0 @@
<video width="560" height="340" controls>
<source src="http://pmitros.csail.mit.edu/tutorial/raw_links/${name}.webm">
</video>

View File

@@ -1,22 +0,0 @@
{
"GRADER" : [
{
"type" : "Homework",
"min_count" : 3,
"drop_count" : 1,
"short_label" : "HW",
"weight" : 0.5
},
{
"type" : "Final",
"name" : "Final Question",
"short_label" : "Final",
"weight" : 0.5
}
],
"GRADE_CUTOFFS" : {
"A" : 0.8,
"B" : 0.7,
"C" : 0.44
}
}

View File

@@ -1,17 +0,0 @@
<html>
<body>
<center>
Final Exam
</center>
<p>This exam covers weeks 1-13. You may use your calculator, your notes, your book, the internet, or any other auxiliary materials that you think can help you. However, you are not allowed to communicate with any person on any topic associated with this course while you are taking this exam.</p>
<p>
Once you click on the next tab, you will have 24 hours to complete the examination. For each problem you will be allowed exactly three submissions. Your
answers to that problem will be checked after each submission, so you
can fix mistakes you may have made, within the three-submission limit.
Your most recently checked answer is the answer that will contribute to your grade on the exam.
</p>
<p>
Each answer box on the exam contributes equally to your grade on the exam, regardless of how they are grouped as problems.
</p>
</body>
</html>

View File

@@ -1,8 +0,0 @@
Hint…
<br/><br/>
Remember that the time evolution of any variable \(x(t)\) governed by
a first-order system with a time-constant \(\tau\) for a time \(t) between an initial
value \(x(t_0)\) and a final value \(x(\infty)\) is the following:
<br/><br/>
\(x(t) = x(\infty) + (x(t_0) - x(\infty)) e^(-(t-t_0)/\tau)\)
<br/><br/>

View File

@@ -1,105 +0,0 @@
<script type="text/javascript">
$(document).ready(function() {
$("#r1_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R1", property: "r", analysis: "dc",
})
$("#r2_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R2", property: "r", analysis: "dc",
})
$("#r3_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R3", property: "r", analysis: "dc",
})
$("#r4_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R4", property: "r", analysis: "dc",
})
$("#slider").slider(); });
</script>
<b>Lab 2A: Superposition Experiment</b>
<br><br><i>Note: This part of the lab is just to develop your intuition about
superposition. There are no responses that need to be checked.</i>
<br/><br/>Circuits with multiple sources can be hard to analyze as-is. For example, what is the voltage
between the two terminals on the right of Figure 1?
<center>
<input width="425" type="hidden" height="150" id="schematic1" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;i&quot;,[112,64,6],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;6A&quot;},[&quot;0&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;v&quot;,[16,16,0],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;8V&quot;},[&quot;3&quot;,&quot;0&quot;]],[&quot;view&quot;,-24,0,2]]"/>
Figure 1. Example multi-source circuit
</center>
<br/><br/>We can use superposition to make the analysis much easier.
The circuit in Figure 1 can be decomposed into two separate
subcircuits: one involving only the voltage source and one involving only the
current source. We'll analyze each circuit separately and combine the
results using superposition. Recall that to decompose a circuit for
analysis, we'll pick each source in turn and set all the other sources
to zero (i.e., voltage sources become short circuits and current
sources become open circuits). The circuit above has two sources, so
the decomposition produces two subcircuits, as shown in Figure 2.
<center>
<table><tr><td>
<input style="display:inline;" width="425" type="hidden" height="150" id="schematic2" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;v&quot;,[16,16,0],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;8V&quot;},[&quot;3&quot;,&quot;0&quot;]],[&quot;view&quot;,-24,0,2]]"/>
(a) Subcircuit for analyzing contribution of voltage source
</td><td>
<input width="425" type="hidden" height="150" id="schematic3" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[16,16,16,64]],[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;i&quot;,[112,64,6],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;6A&quot;},[&quot;0&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;view&quot;,-24,0,2]]"/>
(b) Subcircuit for analyzing contribution of current source
</td></tr></table>
<br>Figure 2. Decomposition of Figure 1 into subcircuits
</center>
<br/>Let's use the DC analysis capability of the schematic tool to see superposition
in action. The sliders below control the resistances of R1, R2, R3 and R4 in all
the diagrams. As you move the sliders, the schematic tool will adjust the appropriate
resistance, perform a DC analysis and display the node voltages on the diagrams. Here's
what you want to observe as you play with the sliders:
<ul style="margin-left:2em;margin-top:1em;margin-right:2em;margin-bottom:1em;">
<i>The voltage for a node in Figure 1 is the sum of the voltages for
that node in Figures 2(a) and 2(b), just as predicted by
superposition. (Note that due to round-off in the display of the
voltages, the sum of the displayed voltages in Figure 2 may only be within
.01 of the voltages displayed in Figure 1.)</i>
</ul>
<br>
<center>
<table><tr valign="top">
<td>
<table>
<tr valign="top">
<td>R1</td>
<td>
<div id="r1_slider" style="width:200px; height:10px; margin-left:15px"></div>
</td>
</tr>
<tr valign="top">
<td>R2</td>
<td>
<div id="r2_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
<tr valign="top">
<td>R3</td>
<td>
<div id="r3_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
<tr valign="top">
<td>R4</td>
<td>
<div id="r4_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
</table>
</td></tr></table>
</center>

View File

@@ -1,35 +0,0 @@
<center>
Midterm Exam
</center>
<p>
This is a twenty four hour examination. You can start the exam when it is
convenient for you, but you must complete this examination by 12:00pm (noon)
GMT on April 30th. Please look up what time this is in your local time zone.
</p>
<p>
When you open the next page, you will have started the examination.
You do not need to start now: you will not be timed until you open the
next page. Once you have opened the next page page you must complete
the exam and make your final submission within twenty four hours of starting
the exam.
</p>
<p>
You may use any notes, computational, or auxiliary materials that you
think can help you. However, you may not communicate with any person
about this examination while working on it. Furthermore, you may not
communicate about the exam until the exam has been closed for everyone.
</p>
<p>
For each problem you will be allowed exactly three submissions. Your
answers to that problem will be checked after each submission, so you
can fix mistakes you may have made, within the three-submission limit.
</p>
<p>
If you want to go back and study some more before starting this
exam you can do so. Good Luck!
</p>

View File

@@ -1,34 +0,0 @@
<html>
<body>
<center>
Midterm Exam
</center>
<p>
This is a twenty four hour examination. You can start the exam when it is
convenient for you, but you must complete this examination by 12:00pm (noon)
GMT on April 30th. Please look up what time this is in your local time zone.
</p>
<p>
When you open the next page, you will have started the examination.
You do not need to start now: you will not be timed until you open the
next page. Once you have opened the next page page you must complete
the exam and make your final submission within twenty four hours of starting
the exam.
</p>
<p>
You may use any notes, computational, or auxiliary materials that you
think can help you. However, you may not communicate with any person
about this examination while working on it. Furthermore, you may not
communicate about the exam until the exam has been closed for everyone.
</p>
<p>
For each problem you will be allowed exactly three submissions. Your
answers to that problem will be checked after each submission, so you
can fix mistakes you may have made, within the three-submission limit.
</p>
<p>
If you want to go back and study some more before starting this
exam you can do so. Good Luck!
</p>
</body>
</html>

View File

@@ -1,34 +0,0 @@
<html>
<body>
<center>
Midterm Exam
</center>
<p>
This is a twenty four hour examination. You can start the exam when it is
convenient for you, but you must complete this examination by 12:00pm (noon)
GMT on April 30th. Please look up what time this is in your local time zone.
</p>
<p>
When you open the next page, you will have started the examination.
You do not need to start now: you will not be timed until you open the
next page. Once you have opened the next page page you must complete
the exam and make your final submission within twenty four hours of starting
the exam.
</p>
<p>
You may use any notes, computational, or auxiliary materials that you
think can help you. However, you may not communicate with any person
about this examination while working on it. Furthermore, you may not
communicate about the exam until the exam has been closed for everyone.
</p>
<p>
For each problem you will be allowed exactly three submissions. Your
answers to that problem will be checked after each submission, so you
can fix mistakes you may have made, within the three-submission limit.
</p>
<p>
If you want to go back and study some more before starting this
exam you can do so. Good Luck!
</p>
</body>
</html>

View File

@@ -1,19 +0,0 @@
<html>
<body>
<h1>Week 13 Tutorials</h1>
<section class="tutorials">
<h2> Basic Tutorials </h2>
<ul>
<li><a href="/section/wk13_solder">Soldering</a> &mdash; Steve
Finberg, one of the pioneers in from Draper Lab, talks about
soldering. </li>
</ul>
<h2> Bonus Tutorials </h2>
<ul>
<li><a href="/section/wk13_FreqResp">Frequency Response
Curves</a> &mdash; We explain several techniques for understanding
and approximating Bode plots. </li>
</ul>
</section>
</body>
</html>

View File

@@ -1,47 +0,0 @@
<html>
<body>
<h1>Week 1 Tutorials</h1>
<section class="tutorials">
<h2> Introduction </h2>
<ul>
<li><a href="/section/intro"> Welcome! </a> - We introduce
ourselves and explain the tutorial format. </li>
</ul>
<h2>Basic Tutorials </h2>
<ul>
<li><a href="/section/circuit_abstraction">The Circuit
Abstraction</a> - We look at a lightbulb, and see that abstracting
away the circuit from the geometry of the wiring has no visible
effect. </li>
<li><a href="/section/a_real_circuit/">Lightbulb Circuit</a> - We
look at the voltage across a lightbulb with a multimeter, and
confirm the device is symmetric. In the process, we see reference
directions for currents, voltages, and polarity for powers. </li>
<li><a href="/section/parallel_resistors">Parallel Resistors</a> -
An explanation of equivalent circuits in the context of parallel
resistors.</li>
<li><a href="/section/series_and_parallel">Combination of Series
and Parallel</a> - Slightly more complex networks of resistors
simplified with equivalent networks.</li>
<li><a href="/section/3r_nodal">Nodal Analysis</a> - A simple
example of nodal analysis.</li>
<li><a href="/section/floating_voltage_3r">Floating Voltage</a> -
Nodal analysis with a floating source.</li>
<li><a href="/section/combination_rules">Combination Rules</a> -
We show how to apply combination rules to solving simple
circuits.</li>
</ul>
<h2>Worked Problems </h2>
<p> For the first week, we work through <a href="http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-002-circuits-and-electronics-spring-2007/assignments/hw1.pdf"> the first problem set from 2007</a></p>
<ul>
<li><a href="/section/exercise_1_1">OCW Exercise 1-1</a>: Find voltages in a simple series and parallel circuit, and show power is conserved</li>
<li><a href="/section/ex_1_2">OCW Exercise 1-2</a> - Synthesize a network of 3/5k and 5/3k from at most four 1k resistors</li>
<li><a href="/section/problem_1-1">OCW Problem 1-1/Textbook 2.7</a> - Find the current through a resistor in a network of resistors</li>
<li><a href="/section/problem_1-2_part1">OCW Problem 1-2</a> - Analyze long chains of resistors (similar to transmission lines or R1R2 ladders) </li>
<li><a href="/section/problem_1_3">OCW Problem 1-3 </a> - Reverse engineer a black-box resistor network</li>
</ul>
<hr/>
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. Gratuitous &ge; entity.</p>
</section>
</body>
</html>

View File

@@ -1 +0,0 @@
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab. </html>

View File

@@ -1,39 +0,0 @@
Clarification of the term "Linear"
<p>
The term "linear" is very clear when applied to a
mathematical function. A function F is linear if and only
if it obeys homogeneity and superposition:
</p><p>
Homogeneity: F(cx) = cF(x)
<br/>
Superposition: F(x+y) = F(x) + F(y)
</p><p>
In the context of what we have seen so far, the only
elements that are linear as mathematical functions are
resistors. An independent voltage source or an independent
current source is not a linear element. (There are also
linear dependent sources, linear capacitors and linear
inductors, but we have not yet introduced them in our class.
You will see them later.)
</p><p>
Formally, a circuit composed of only linear elements is a
linear circuit. When we add independent sources to a linear
circuit as inputs, we get a circuit that is not linear
because it has an offset: its v-i characteristic at a pair
of exposed terminals may not pass through the origin.
However, we can make a Thevenin or Norton equivalent model
of such a circuit: the Thevenin resistance summarizes the
effect of the linear elements and the Thevenin voltage
summarizes the effect of the independent sources.
</p><p>
However the term "linear," when applied to an electrical
circuit often takes on an informal meaning. We often say
that a circuit containing only linear elements and
independent sources is a "linear circuit." So, in the
informal sense, a linear circuit is one where we can apply
the Thevenin or Norton theorems to summarize the behavior at
a pair of exposed terminals.
</p><p>
Sorry for the confusion of words &mdash; natural language is like
that!
</p>

View File

@@ -1,39 +0,0 @@
Clarification of the term "Linear"
<p>
The term "linear" is very clear when applied to a
mathematical function. A function F is linear if and only
if it obeys homogeneity and superposition:
</p><p>
Homogeneity: F(cx) = cF(x)
<br/>
Superposition: F(x+y) = F(x) + F(y)
</p><p>
In the context of what we have seen so far, the only
elements that are linear as mathematical functions are
resistors. An independent voltage source or an independent
current source is not a linear element. (There are also
linear dependent sources, linear
inductors, and other linear elements, but we have not yet introduced them in our class.
You will see them later.)
</p><p>
Formally, a circuit composed of only linear elements is a
linear circuit. When we add independent sources to a linear
circuit as inputs, we get a circuit that is not linear
because it has an offset: its v-i characteristic at a pair
of exposed terminals may not pass through the origin.
However, we can make a Thevenin or Norton equivalent model
of such a circuit: the Thevenin resistance summarizes the
effect of the linear elements and the Thevenin voltage
summarizes the effect of the independent sources.
</p><p>
However the term "linear," when applied to an electrical
circuit often takes on an informal meaning. We often say
that a circuit containing only linear elements and
independent sources is a "linear circuit." So, in the
informal sense, a linear circuit is one where we can apply
the Thevenin or Norton theorems to summarize the behavior at
a pair of exposed terminals.
</p><p>
Sorry for the confusion of words &mdash; natural language is like
that!
</p>

Some files were not shown because too many files have changed in this diff Show More