Commit 923ba365 by cahrens

Merge branch 'master' into talbs/studio-authorship

parents 08a1055c e7bb85de
......@@ -44,3 +44,4 @@ node_modules
.prereqs_cache
autodeploy.properties
.ws_migrations_complete
.vagrant/
......@@ -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>
\ No newline at end of file
Ian Hoover <ihoover@edx.org>
Mukul Goyal <miki@edx.org>
......@@ -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)
......
......@@ -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.
......
# -*- 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
......@@ -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
......@@ -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):
......
......@@ -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))
......
......@@ -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")
......
......@@ -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
......@@ -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)
......@@ -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')
......
......@@ -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:
......
......@@ -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$')
......
......@@ -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
......@@ -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
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
......
......@@ -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):
......
......@@ -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
......
......@@ -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:'
......
......@@ -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$')
......
......@@ -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))
......@@ -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
......
......@@ -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 ###################
......
......@@ -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')
......
......@@ -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
......
......@@ -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')
......
......@@ -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')
......
......@@ -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'
"""
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'
......@@ -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.
......
......@@ -368,3 +368,5 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
COURSES_WITH_UNSAFE_CODE = []
......@@ -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)
......
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()
......@@ -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)
......
......@@ -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();
});
}
......@@ -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();
......
......@@ -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 = [];
});
}
}
});
......@@ -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();
......
......@@ -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;
......
......@@ -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;
......@@ -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
});
},
......
......@@ -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);
}
});
......@@ -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({}, {
......
......@@ -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
}
);
}
});
......@@ -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 {
......
<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>
<% } %>
......
......@@ -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',
......
......@@ -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:
......
......@@ -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:
......
......@@ -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
......
......@@ -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()
......
......@@ -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$')
......
......@@ -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))
......@@ -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
......@@ -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'))
......@@ -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 == []
......@@ -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")
......
......@@ -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")
......
......@@ -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)
......
......@@ -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()
......
......@@ -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')}\"?"
......
......@@ -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: ->
......
......@@ -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()
......
......@@ -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: () ->
......
......@@ -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;
......
This is a realistic course, with many different module types and a lot of structure. It is based on 6.002x.
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.
\ No newline at end of file
12 hours
\ No newline at end of file
<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>
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
<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>
\ No newline at end of file
<iframe width="560" height="315" src="http://www.youtube.com/embed/p2Q6BrNhdh8" frameborder="0" allowfullscreen></iframe>
\ No newline at end of file
<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>
<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>
<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;]"/>
<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>
{
"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
}
}
More information given in… <a href="/book/${page}">the text</a>.
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag}… </a>
\ No newline at end of file
<img src="http://pmitros.csail.mit.edu/tutorial/images/${file}" />
Lecture Slides Handout [<a href="">Clean… </a>][<a href="">Annotated…</a>]
<video width="560" height="340" controls>
<source src="http://pmitros.csail.mit.edu/tutorial/raw_links/${name}.webm">
</video>
{
"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
}
}
<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>
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/>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
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>
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>
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment