Merge branch 'master' into talbs/studio-authorship
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* -text
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ node_modules
|
||||
.prereqs_cache
|
||||
autodeploy.properties
|
||||
.ws_migrations_complete
|
||||
.vagrant/
|
||||
|
||||
3
AUTHORS
3
AUTHORS
@@ -79,4 +79,5 @@ Bethany LaPenta <lapentab@mit.edu>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
Felix Sun <felixsun@mit.edu>
|
||||
Adam Palay <adam@edx.org>
|
||||
Ian Hoover <ihoover@edx.org>
|
||||
Ian Hoover <ihoover@edx.org>
|
||||
Mukul Goyal <miki@edx.org>
|
||||
|
||||
@@ -48,6 +48,8 @@ moved to be edited as metadata.
|
||||
|
||||
XModule: Only write out assets files if the contents have changed.
|
||||
|
||||
Studio: Course settings are now saved explicitly.
|
||||
|
||||
XModule: Don't delete generated xmodule asset files when compiling (for
|
||||
instance, when XModule provides a coffeescript file, don't delete
|
||||
the associated javascript)
|
||||
|
||||
132
README.md
132
README.md
@@ -2,8 +2,136 @@ This is the main edX platform which consists of LMS and Studio.
|
||||
|
||||
See [code.edx.org](http://code.edx.org/) for other parts of the edX code base.
|
||||
|
||||
Installation
|
||||
============
|
||||
Installation - The first time
|
||||
=============================
|
||||
|
||||
The following instructions will help you to download and setup a virtual machine
|
||||
with a minimal amount of steps, using Vagrant. It is recommended for a first
|
||||
installation, as it will save you from many of the common pitfalls of the
|
||||
installation process.
|
||||
|
||||
1. Make sure you have plenty of available disk space, >5GB
|
||||
2. Install Git: http://git-scm.com/downloads
|
||||
3. Install VirtualBox: https://www.virtualbox.org/wiki/Download_Old_Builds_4_2
|
||||
(you need version 4.2.12, as later/earlier versions might not work well with
|
||||
Vagrant)
|
||||
4. Install Vagrant: http://www.vagrantup.com/ (Vagrant 1.2.2 or later)
|
||||
5. Open a terminal
|
||||
6. Download the project: `git clone git://github.com/edx/edx-platform.git`
|
||||
7. Enter the project directory: `cd edx-platform/`
|
||||
8. (Windows only) Run the commands to
|
||||
[deal with line endings and symlinks under Windows](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#dealing-with-line-endings-and-symlinks-under-windows)
|
||||
9. Start: `vagrant up`
|
||||
|
||||
The last step might require your host machine's administrator password to setup NFS.
|
||||
|
||||
Afterwards, it will download an image, install all the dependencies and configure
|
||||
the VM. It will take a while, go grab a coffee.
|
||||
|
||||
Once completed, hopefully you should see a "Success!" message indicating that the
|
||||
installation went fine. (If not, refer to the
|
||||
[troubleshooting section](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#troubleshooting).)
|
||||
|
||||
Note: by default, the VM will get the IP `192.168.20.40`. If you need to use a
|
||||
different IP, you can edit the file `Vagrantfile`. If you have already started the
|
||||
VM with `vagrant up`, see "Stopping and restarting the VM" below to take the change
|
||||
into account.
|
||||
|
||||
Accessing the VM
|
||||
----------------
|
||||
|
||||
Once the installation is finished, to log into the virtual machine:
|
||||
|
||||
```
|
||||
$ vagrant ssh
|
||||
```
|
||||
|
||||
Note: This won't work from Windows, install install PuTTY from
|
||||
http://www.chiark.greenend.org.uk/%7Esgtatham/putty/download.html instead. Then
|
||||
connect to 127.0.0.1, port 2222, using vagrant/vagrant as a user/password.
|
||||
|
||||
Using edX
|
||||
---------
|
||||
|
||||
Once inside the VM, you can start Studio and LMS with the following commands
|
||||
(from the `/opt/edx/edx-platform` folder):
|
||||
|
||||
Learning management system (LMS):
|
||||
|
||||
```
|
||||
$ rake lms[cms.dev,0.0.0.0:8000]
|
||||
```
|
||||
|
||||
Studio:
|
||||
|
||||
```
|
||||
$ rake cms[dev,0.0.0.0:8001]
|
||||
```
|
||||
|
||||
Once started, open the following URLs in your browser:
|
||||
|
||||
* Learning management system (LMS): http://192.168.20.40:8000/
|
||||
* Studio (CMS): http://192.168.20.40:8001/
|
||||
|
||||
You can develop by editing the files directly in the `edx-platform/` directory you
|
||||
downloaded before, you don't need to connect to the VM to edit them (the VM uses
|
||||
those files to run edX, mirroring the folder in `/opt/edx/edx-platform`).
|
||||
|
||||
You may also want to create a super-user with:
|
||||
|
||||
```
|
||||
$ rake django-admin["createsuperuser"]
|
||||
```
|
||||
|
||||
Also note that if you register a new user through the web interface,
|
||||
the activiation email will be posted to your VM's terminal window (search for
|
||||
lines similar to):
|
||||
|
||||
```
|
||||
Subject: Your account for edX Studio
|
||||
From: registration@edx.org
|
||||
```
|
||||
|
||||
and find the activation URL for the account you've created.
|
||||
|
||||
See the [Frequently Asked Questions](https://github.com/edx/edx-platform/wiki/Frequently-Asked-Questions)
|
||||
for more usage tips.
|
||||
|
||||
Stopping & starting
|
||||
-------------------
|
||||
|
||||
To stop the VM (from your `edx-platform/` directory):
|
||||
|
||||
```
|
||||
$ vagrant halt
|
||||
```
|
||||
|
||||
To restart:
|
||||
|
||||
```
|
||||
$ vagrant up
|
||||
```
|
||||
|
||||
or, to start without attempting to update the dependencies:
|
||||
|
||||
```
|
||||
$ vagrant up --no-provision
|
||||
```
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If anything doesn't work as expected, see the
|
||||
[troubleshooting section](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#troubleshooting).
|
||||
|
||||
Installation - Advanced
|
||||
=======================
|
||||
|
||||
Note: The following installation instructions are for advanced users & developers
|
||||
who are familiar with setting up Python, Ruby & node.js virtual environments.
|
||||
Even if you know what you are doing, edX has a large code base with multiple
|
||||
dependencies, so you might still want to use the method described above the
|
||||
first time, as Vagrant helps avoiding issues due to the different environments.
|
||||
|
||||
There is a `scripts/create-dev-env.sh` that will attempt to set up a development
|
||||
environment.
|
||||
|
||||
33
Vagrantfile
vendored
Normal file
33
Vagrantfile
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "precise32"
|
||||
config.vm.box_url = "http://files.vagrantup.com/precise32.box"
|
||||
|
||||
config.vm.network :forwarded_port, guest: 8000, host: 9000
|
||||
config.vm.network :forwarded_port, guest: 8001, host: 9001
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
config.vm.network :private_network, ip: "192.168.20.40"
|
||||
|
||||
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
|
||||
config.vm.synced_folder ".", "/opt/edx/edx-platform", id: "vagrant-root", :nfs => nfs_setting
|
||||
|
||||
# Make it so that network access from the vagrant guest is able to
|
||||
# use SSH private keys that are present on the host without copying
|
||||
# them into the VM.
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
# Use VBoxManage to customize the VM. For example to change memory:
|
||||
vb.customize ["modifyvm", :id, "--memory", "1024"]
|
||||
|
||||
# This setting makes it so that network access from inside the vagrant guest
|
||||
# is able to resolve DNS using the hosts VPN connection.
|
||||
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
end
|
||||
|
||||
config.vm.provision :shell, :path => "scripts/vagrant-provisioning.sh"
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
Feature: Problem Editor
|
||||
As a course author, I want to be able to create problems and edit their settings.
|
||||
|
||||
@skip
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then I see five alphabetized settings and their expected values
|
||||
And Edit High Level Source is not visible
|
||||
|
||||
@skip
|
||||
Scenario: User can modify String values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
@skip
|
||||
Scenario: User can specify special characters in String values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then I can specify special characters in the display name
|
||||
And my special characters and persisted on save
|
||||
|
||||
@skip
|
||||
Scenario: User can revert display name to unset
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then I can revert the display name to unset
|
||||
And my display name is unset on save
|
||||
|
||||
@skip
|
||||
Scenario: User can select values in a Select
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -37,7 +32,6 @@ Feature: Problem Editor
|
||||
And my change to randomization is persisted
|
||||
And I can revert to the default value for randomization
|
||||
|
||||
@skip
|
||||
Scenario: User can modify float input values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -45,25 +39,21 @@ Feature: Problem Editor
|
||||
And my change to weight is persisted
|
||||
And I can revert to the default value of unset for weight
|
||||
|
||||
@skip
|
||||
Scenario: User cannot type letters in float number field
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then if I set the weight to "abc", it remains unset
|
||||
|
||||
@skip
|
||||
Scenario: User cannot type decimal values integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
|
||||
|
||||
@skip
|
||||
Scenario: User cannot type out of range values in an integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
|
||||
|
||||
@skip
|
||||
Scenario: Settings changes are not saved on Cancel
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -71,13 +61,11 @@ Feature: Problem Editor
|
||||
And I can modify the display name
|
||||
Then If I press Cancel my changes are not persisted
|
||||
|
||||
@skip
|
||||
Scenario: Edit High Level source is available for LaTeX problem
|
||||
Given I have created a LaTeX Problem
|
||||
When I edit and select Settings
|
||||
Then Edit High Level Source is visible
|
||||
|
||||
@skip
|
||||
Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
|
||||
Given I have created a LaTeX Problem
|
||||
When I edit and compile the High Level Source
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -164,9 +164,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_edit_unit_toy(self):
|
||||
self.check_edit_unit('toy')
|
||||
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def _get_draft_counts(self, item):
|
||||
cnt = 1 if getattr(item, 'is_draft', False) else 0
|
||||
for child in item.get_children():
|
||||
@@ -301,41 +298,49 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
def test_import_textbook_as_content_element(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
|
||||
|
||||
self.assertGreater(len(course.textbooks), 0)
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
ItemFactory.create(parent_location=course_location,
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name="Static_1")
|
||||
ItemFactory.create(parent_location=course_location,
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name="Static_2")
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
|
||||
|
||||
# reverse the ordering
|
||||
reverse_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
reverse_tabs.insert(0, 'i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
course_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
course_tabs.append('i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
def test_import_polls(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
|
||||
items = module_store.get_items(['i4x', 'edX', 'toy', 'poll_question', None, None])
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
@@ -343,38 +348,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertGreater(len(items[0].question), 0)
|
||||
|
||||
def test_xlint_fails(self):
|
||||
err_cnt = perform_xlint('common/test/data', ['full'])
|
||||
err_cnt = perform_xlint('common/test/data', ['toy'])
|
||||
self.assertGreater(err_cnt, 0)
|
||||
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/toy/.*'])
|
||||
def test_module_preview_in_whitelist(self):
|
||||
'''
|
||||
Tests the ajax callback to render an XModule
|
||||
'''
|
||||
direct_store = modulestore('direct')
|
||||
import_from_xml(direct_store, 'common/test/data/', ['full'])
|
||||
|
||||
html_module_location = Location(['i4x', 'edX', 'full', 'html', 'html_90', None])
|
||||
|
||||
url = reverse('preview_component', kwargs={'location': html_module_location.url()})
|
||||
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('Inline content', resp.content)
|
||||
import_from_xml(direct_store, 'common/test/data/', ['toy'])
|
||||
|
||||
# also try a custom response which will trigger the 'is this course in whitelist' logic
|
||||
problem_module_location = Location(['i4x', 'edX', 'full', 'problem', 'H1P1_Energy', None])
|
||||
problem_module_location = Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None])
|
||||
url = reverse('preview_component', kwargs={'location': problem_module_location.url()})
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_delete(self):
|
||||
direct_store = modulestore('direct')
|
||||
import_from_xml(direct_store, 'common/test/data/', ['full'])
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
|
||||
|
||||
sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
chapterloc = ItemFactory.create(parent_location=course_location, display_name="Chapter").location
|
||||
ItemFactory.create(parent_location=chapterloc, template='i4x://edx/templates/sequential/Empty', display_name="Sequential")
|
||||
|
||||
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
|
||||
sequential = direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
|
||||
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
|
||||
|
||||
# make sure the parent points to the child object which is to be deleted
|
||||
self.assertTrue(sequential.location.url() in chapter.children)
|
||||
@@ -387,14 +387,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
found = False
|
||||
try:
|
||||
direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
|
||||
found = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
|
||||
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertFalse(sequential.location.url() in chapter.children)
|
||||
@@ -405,20 +405,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
while there is a base definition in /about/effort.html
|
||||
'''
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.data, '6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'end_date', None]))
|
||||
self.assertEqual(effort.data, 'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = module_store.get_item(source_location)
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
|
||||
course = module_store.get_item(course_location)
|
||||
self.assertFalse(course.hide_progress_tab)
|
||||
|
||||
def test_asset_import(self):
|
||||
@@ -428,9 +427,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
self.assertIsNotNone(course)
|
||||
@@ -451,7 +450,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
content = None
|
||||
try:
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = content_store.find(location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
@@ -479,10 +478,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
trash_store = contentstore('trashcan')
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
# look up original (and thumbnail) in content store, should be there after import
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = content_store.find(location, throw_on_not_found=False)
|
||||
thumbnail_location = content.thumbnail_location
|
||||
self.assertIsNotNone(content)
|
||||
@@ -495,11 +494,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# go through the website to do the delete, since the soft-delete logic is in the view
|
||||
|
||||
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'full', 'name': '6.002_Spring_2012'})
|
||||
resp = self.client.post(url, {'location': '/c4x/edX/full/asset/circuits_duality.gif'})
|
||||
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
|
||||
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
asset_location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
|
||||
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
|
||||
# now try to find it in store, but they should not be there any longer
|
||||
content = content_store.find(asset_location, throw_on_not_found=False)
|
||||
@@ -518,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertIsNotNone(thumbnail)
|
||||
|
||||
# let's restore the asset
|
||||
restore_asset_from_trashcan('/c4x/edX/full/asset/circuits_duality.gif')
|
||||
restore_asset_from_trashcan('/c4x/edX/toy/asset/sample_static.txt')
|
||||
|
||||
# now try to find it in courseware store, and they should be back after restore
|
||||
content = content_store.find(asset_location, throw_on_not_found=False)
|
||||
@@ -536,18 +535,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
trash_store = contentstore('trashcan')
|
||||
module_store = modulestore('direct')
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012')
|
||||
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/full/asset/circuits_duality.gif')
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = content_store.find(location, throw_on_not_found=False)
|
||||
self.assertIsNotNone(content)
|
||||
|
||||
# go through the website to do the delete, since the soft-delete logic is in the view
|
||||
|
||||
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'full', 'name': '6.002_Spring_2012'})
|
||||
resp = self.client.post(url, {'location': '/c4x/edX/full/asset/circuits_duality.gif'})
|
||||
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
|
||||
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# make sure there's something in the trashcan
|
||||
@@ -582,7 +581,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
}
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -591,16 +590,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
content_store = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
|
||||
|
||||
clone_course(module_store, content_store, source_location, dest_location)
|
||||
|
||||
# now loop through all the units in the course and verify that the clone can render them, which
|
||||
# means the objects are at least present
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'poll_question', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'poll_question', None]))
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
for descriptor in items:
|
||||
new_loc = descriptor.location.replace(org='MITx', course='999')
|
||||
@@ -641,20 +640,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft content is also deleted
|
||||
"""
|
||||
module_store = modulestore('direct')
|
||||
|
||||
content_store = contentstore()
|
||||
draft_store = modulestore('draft')
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
# verify that we actually have assets
|
||||
assets = content_store.get_all_content_for_course(location)
|
||||
self.assertNotEquals(len(assets), 0)
|
||||
location = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course').location
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'vertical_test', None]), depth=1)
|
||||
|
||||
draft_store.clone_item(vertical.location, vertical.location)
|
||||
for child in vertical.get_children():
|
||||
@@ -665,7 +661,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# assert that there's absolutely no non-draft modules in the course
|
||||
# this should also include all draft items
|
||||
items = draft_store.get_items(Location(['i4x', 'edX', 'full', None, None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', '999', 'course', None]))
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
# assert that all content in the asset library is also deleted
|
||||
@@ -688,17 +684,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store = modulestore('draft')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'vertical_test', None]), depth=1)
|
||||
|
||||
draft_store.clone_item(vertical.location, vertical.location)
|
||||
|
||||
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
|
||||
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full',
|
||||
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'no_references', 'draft']))
|
||||
|
||||
for child in vertical.get_children():
|
||||
@@ -708,18 +704,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# now create a private vertical
|
||||
private_vertical = draft_store.clone_item(vertical.location,
|
||||
Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
|
||||
Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
|
||||
|
||||
# add private to list of children
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'sequential', 'vertical_sequential', None]))
|
||||
private_location_no_draft = private_vertical.location.replace(revision=None)
|
||||
module_store.update_children(sequential.location, sequential.children +
|
||||
[private_location_no_draft.url()])
|
||||
|
||||
# read back the sequential, to make sure we have a pointer to
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'sequential', 'vertical_sequential', None]))
|
||||
|
||||
self.assertIn(private_location_no_draft.url(), sequential.children)
|
||||
|
||||
@@ -731,17 +727,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# check for static tabs
|
||||
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
|
||||
# check for about content
|
||||
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
|
||||
|
||||
# check for graiding_policy.json
|
||||
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall')
|
||||
self.assertTrue(filesystem.exists('grading_policy.json'))
|
||||
|
||||
course = module_store.get_item(location)
|
||||
@@ -756,8 +746,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# compare what's on disk to what we have in the course module
|
||||
with filesystem.open('policy.json', 'r') as course_policy:
|
||||
on_disk = loads(course_policy.read())
|
||||
self.assertIn('course/6.002_Spring_2012', on_disk)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
|
||||
self.assertIn('course/2012_Fall', on_disk)
|
||||
self.assertEqual(on_disk['course/2012_Fall'], own_metadata(course))
|
||||
|
||||
# remove old course
|
||||
delete_course(module_store, content_store, location)
|
||||
@@ -765,7 +755,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# reimport
|
||||
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
|
||||
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
# don't try to look at private verticals. Right now we're running
|
||||
@@ -776,27 +766,27 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# verify that we have the content in the draft store as well
|
||||
vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
vertical = draft_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'vertical_test', None]), depth=1)
|
||||
|
||||
self.assertTrue(getattr(vertical, 'is_draft', False))
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'sequential', 'vertical_sequential', None]))
|
||||
|
||||
self.assertFalse(getattr(sequential, 'is_draft', False))
|
||||
|
||||
# verify that we have the private vertical
|
||||
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]))
|
||||
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'a_private_vertical', None]))
|
||||
|
||||
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
|
||||
|
||||
# make sure the textbook survived the export/import
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
|
||||
|
||||
self.assertGreater(len(course.textbooks), 0)
|
||||
|
||||
@@ -806,24 +796,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
# import a test course
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
||||
handout_location = Location(['i4x', 'edX', 'toy', 'course_info', 'handouts'])
|
||||
|
||||
# get module info
|
||||
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
|
||||
|
||||
# make sure we got a successful response
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check that /static/ has been converted to the full path
|
||||
# note, we know the link it should be because that's what in the 'full' course in the test data
|
||||
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
|
||||
# note, we know the link it should be because that's what in the 'toy' course in the test data
|
||||
self.assertContains(resp, '/c4x/edX/toy/asset/handouts_sample_handout.txt')
|
||||
|
||||
def test_prefetch_children(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
|
||||
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
|
||||
module_store.collection.find = wrapper.find
|
||||
@@ -835,19 +824,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(wrapper.counter, 4)
|
||||
|
||||
# make sure we pre-fetched a known sequential which should be at depth=2
|
||||
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
|
||||
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
|
||||
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
|
||||
'vertical_sequential', None]) in course.system.module_data)
|
||||
|
||||
# make sure we don't have a specific vertical which should be at depth=3
|
||||
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
|
||||
self.assertFalse(Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None])
|
||||
in course.system.module_data)
|
||||
|
||||
def test_export_course_with_unknown_metadata(self):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
@@ -1202,9 +1191,9 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_forum_id_generation(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
|
||||
new_component_location = Location('i4x', 'edX', '999', 'discussion', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
@@ -1216,7 +1205,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_update_modulestore_signal_did_fire(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
|
||||
try:
|
||||
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
|
||||
@@ -1228,7 +1217,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
module_store.modulestore_update_signal.connect(_signal_hander)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
new_component_location = Location('i4x', 'edX', '999', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module
|
||||
@@ -1241,11 +1230,11 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
|
||||
|
||||
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
|
||||
verticals = module_store.get_items(['i4x', 'edX', 'toy', 'vertical', None, None])
|
||||
|
||||
# let's assert on the metadata_inheritance on an existing vertical
|
||||
for vertical in verticals:
|
||||
@@ -1253,7 +1242,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
new_component_location = Location('i4x', 'edX', 'toy', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
|
||||
@@ -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'
|
||||
|
||||
77
cms/envs/acceptance_static.py
Normal file
77
cms/envs/acceptance_static.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
This config file extends the test environment configuration
|
||||
so that we can run the lettuce acceptance tests.
|
||||
This is used in the django-admin call as acceptance.py
|
||||
contains random seeding, causing django-admin to create a random collection
|
||||
"""
|
||||
|
||||
# We intentionally define lots of variables that aren't used, and
|
||||
# want to import all variables from base settings files
|
||||
# pylint: disable=W0401, W0614
|
||||
|
||||
from .test import *
|
||||
|
||||
# You need to start the server in debug mode,
|
||||
# otherwise the browser will not render the pages correctly
|
||||
DEBUG = True
|
||||
|
||||
# Disable warnings for acceptance tests, to make the logs readable
|
||||
import logging
|
||||
logging.disable(logging.ERROR)
|
||||
import os
|
||||
import random
|
||||
|
||||
MODULESTORE_OPTIONS = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'acceptance_xmodule',
|
||||
'collection': 'acceptance_modulestore',
|
||||
'fs_root': TEST_ROOT / "data",
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
},
|
||||
'draft': {
|
||||
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
}
|
||||
}
|
||||
|
||||
CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'OPTIONS': {
|
||||
'host': 'localhost',
|
||||
'db': 'acceptance_xcontent',
|
||||
},
|
||||
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
|
||||
'ADDITIONAL_OPTIONS': {
|
||||
'trashcan': {
|
||||
'bucket': 'trash_fs'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set this up so that rake lms[acceptance] and running the
|
||||
# harvest command both use the same (test) database
|
||||
# which they can flush without messing up your dev db
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': TEST_ROOT / "db" / "test_mitx.db",
|
||||
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
|
||||
}
|
||||
}
|
||||
|
||||
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
@@ -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)
|
||||
|
||||
63
cms/static/coffee/spec/views/overview_spec.coffee
Normal file
63
cms/static/coffee/spec/views/overview_spec.coffee
Normal file
@@ -0,0 +1,63 @@
|
||||
describe "Course Overview", ->
|
||||
|
||||
beforeEach ->
|
||||
appendSetFixtures """
|
||||
<script src="/static/js/vendor/date.js"></script>
|
||||
"""
|
||||
|
||||
appendSetFixtures """
|
||||
<script type="text/javascript" src="/jsi18n/"></script>
|
||||
"""
|
||||
|
||||
appendSetFixtures """
|
||||
<div class="section-published-date">
|
||||
<span class="published-status">
|
||||
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
|
||||
</span>
|
||||
<a href="#" class="edit-button" "="" data-date="06/12/2013" data-time="04:00" data-id="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
|
||||
</div>
|
||||
"""#"
|
||||
|
||||
appendSetFixtures """
|
||||
<div class="edit-subsection-publish-settings">
|
||||
<div class="settings">
|
||||
<h3>Section Release Date</h3>
|
||||
<div class="picker datepair">
|
||||
<div class="field field-start-date">
|
||||
<label for="">Release Day</label>
|
||||
<input class="start-date date" type="text" name="start_date" value="04/08/1990" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
</div>
|
||||
<div class="field field-start-time">
|
||||
<label for="">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
|
||||
<input class="start-time time" type="text" name="start_time" value="12:00" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
"""#"
|
||||
|
||||
spyOn(window, 'saveSetSectionScheduleDate').andCallThrough()
|
||||
# Have to do this here, as it normally gets bound in document.ready()
|
||||
$('a.save-button').click(saveSetSectionScheduleDate)
|
||||
@notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough()
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
sinon.useFakeXMLHttpRequest()
|
||||
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
it "should save model when save is clicked", ->
|
||||
$('a.edit-button').click()
|
||||
$('a.save-button').click()
|
||||
expect(saveSetSectionScheduleDate).toHaveBeenCalled()
|
||||
|
||||
it "should show a confirmation on save", ->
|
||||
$('a.edit-button').click()
|
||||
$('a.save-button').click()
|
||||
expect(@notificationSpy).toHaveBeenCalled()
|
||||
@@ -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") + "…",
|
||||
});
|
||||
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…")
|
||||
this.msg = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Saving") + "…"
|
||||
});
|
||||
}
|
||||
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,10 +3,11 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"input input" : "updateModel",
|
||||
"input textarea" : "updateModel",
|
||||
// Leaving change in as fallback for older browsers
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'focus #course-overview' : "codeMirrorize",
|
||||
'mouseover #timezone' : "updateTime",
|
||||
@@ -15,6 +16,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
'blur :input' : "inputUnfocus"
|
||||
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>');
|
||||
// fill in fields
|
||||
@@ -27,6 +29,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
|
||||
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.listenTo(this.model, 'change', this.showNotificationBar);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
@@ -36,25 +39,13 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
this.setupDatePicker('enrollment_start');
|
||||
this.setupDatePicker('enrollment_end');
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
|
||||
this.$el.find('.remove-course-syllabus').hide();
|
||||
}
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video') || '');
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
|
||||
}
|
||||
else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
@@ -67,7 +58,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
'end_date' : 'course-end',
|
||||
'enrollment_start' : 'enrollment-start',
|
||||
'enrollment_end' : 'enrollment-end',
|
||||
'syllabus' : '.current-course-syllabus .doc-filename',
|
||||
'overview' : 'course-overview',
|
||||
'intro_video' : 'course-introduction-video',
|
||||
'effort' : "course-effort"
|
||||
@@ -87,8 +77,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
var datefield = $(div).find("input:.date");
|
||||
var timefield = $(div).find("input:.time");
|
||||
var cachethis = this;
|
||||
var savefield = function () {
|
||||
cachethis.clearValidationErrors();
|
||||
var setfield = function () {
|
||||
var date = datefield.datepicker('getDate');
|
||||
if (date) {
|
||||
var time = timefield.timepicker("getSecondsFromMidnight");
|
||||
@@ -97,14 +86,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
|
||||
cacheModel.save(fieldName, newVal);
|
||||
cachethis.clearValidationErrors();
|
||||
cachethis.setAndValidate(fieldName, newVal);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Clear date (note that this clears the time as well, as date and time are linked).
|
||||
// Note also that the validation logic prevents us from clearing the start date
|
||||
// (start date is required by the back end).
|
||||
cacheModel.save(fieldName, null);
|
||||
cachethis.clearValidationErrors();
|
||||
cachethis.setAndValidate(fieldName, null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,59 +103,51 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
timefield.timepicker({'timeFormat' : 'H:i'});
|
||||
datefield.datepicker();
|
||||
|
||||
// Using the change event causes savefield to be triggered twice, but it is necessary
|
||||
// Using the change event causes setfield to be triggered twice, but it is necessary
|
||||
// to pick up when the date is typed directly in the field.
|
||||
datefield.change(savefield);
|
||||
timefield.on('changeTime', savefield);
|
||||
datefield.change(setfield);
|
||||
timefield.on('changeTime', setfield);
|
||||
|
||||
datefield.datepicker('setDate', this.model.get(fieldName));
|
||||
if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName));
|
||||
// timepicker doesn't let us set null, so check that we have a time
|
||||
if (this.model.has(fieldName)) {
|
||||
timefield.timepicker('setTime', this.model.get(fieldName));
|
||||
} // but reset the field either way
|
||||
else {
|
||||
timefield.val('');
|
||||
}
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date': // handled via onSelect method
|
||||
case 'course-end-date':
|
||||
case 'course-enrollment-start-date':
|
||||
case 'course-enrollment-end-date':
|
||||
break;
|
||||
|
||||
case 'course-overview':
|
||||
// handled via code mirror
|
||||
break;
|
||||
|
||||
case 'course-effort':
|
||||
this.saveIfChanged(event);
|
||||
this.setField(event);
|
||||
break;
|
||||
// Don't make the user reload the page to check the Youtube ID.
|
||||
// Wait for a second to load the video, avoiding egregious AJAX calls.
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.save_videosource($(event.currentTarget).val());
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
var previewsource = this.model.set_videosource($(event.currentTarget).val());
|
||||
clearTimeout(this.videoTimer);
|
||||
this.videoTimer = setTimeout(_.bind(function() {
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
}, this), 1000);
|
||||
break;
|
||||
|
||||
default:
|
||||
default: // Everything else is handled by datepickers and CodeMirror.
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
// TODO implement
|
||||
},
|
||||
|
||||
removeVideo: function() {
|
||||
removeVideo: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.save_videosource(null);
|
||||
this.model.set_videosource(null);
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
@@ -185,15 +168,53 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
var field = this.selectorToField[thisTarget.id];
|
||||
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
|
||||
mode: "text/html", lineNumbers: true, lineWrapping: true,
|
||||
onBlur: function (mirror) {
|
||||
onChange: function (mirror) {
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
|
||||
if (cachethis.model.get(field) != newVal) {
|
||||
cachethis.setAndValidate(field, newVal);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
revertView: function() {
|
||||
// Make sure that the CodeMirror instance has the correct
|
||||
// data from its corresponding textarea
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
success: function() {
|
||||
self.render();
|
||||
_.each(self.codeMirrors,
|
||||
function(mirror) {
|
||||
var ele = mirror.getTextArea();
|
||||
var field = self.selectorToField[ele.id];
|
||||
mirror.setValue(self.model.get(field));
|
||||
});
|
||||
},
|
||||
reset: true,
|
||||
silent: true});
|
||||
},
|
||||
setAndValidate: function(attr, value) {
|
||||
// If we call model.set() with {validate: true}, model fields
|
||||
// will not be set if validation fails. This puts the UI and
|
||||
// the model in an inconsistent state, and causes us to not
|
||||
// see the right validation errors the next time validate() is
|
||||
// called on the model. So we set *without* validating, then
|
||||
// call validate ourselves.
|
||||
this.model.set(attr, value);
|
||||
this.model.isValid();
|
||||
},
|
||||
|
||||
showNotificationBar: function() {
|
||||
// We always call showNotificationBar with the same args, just
|
||||
// delegate to superclass
|
||||
CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
|
||||
this.save_message,
|
||||
_.bind(this.saveView, this),
|
||||
_.bind(this.revertView, this));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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…")
|
||||
var delmsg = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Deleting") + "…"
|
||||
}).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…")
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Saving") + "…"
|
||||
}).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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="wrapper wrapper-<%= type %> wrapper-<%= type %>-<%= intent %>
|
||||
<% if(obj.shown) { %>is-shown<% } else { %>is-hiding<% } %>
|
||||
<% if(_.contains(['help', 'saving'], intent)) { %>wrapper-<%= type %>-status<% } %>"
|
||||
<% if(_.contains(['help', 'mini'], intent)) { %>wrapper-<%= type %>-status<% } %>"
|
||||
id="<%= type %>-<%= intent %>"
|
||||
aria-hidden="<% if(obj.shown) { %>false<% } else { %>true<% } %>"
|
||||
aria-labelledby="<%= type %>-<%= intent %>-title"
|
||||
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<div class="<%= type %> <%= intent %> <% if(obj.actions) { %>has-actions<% } %>">
|
||||
<% if(obj.icon) { %>
|
||||
<% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "saving": "cog"} %>
|
||||
<% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "mini": "cog"} %>
|
||||
<i class="icon-<%= iconClass[intent] %>"></i>
|
||||
<% } %>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This is a realistic course, with many different module types and a lot of structure. It is based on 6.002x.
|
||||
@@ -1,3 +0,0 @@
|
||||
6.002x (Circuits and Electronics) is designed to serve as a first course in an undergraduate electrical engineering (EE), or electrical engineering and computer science (EECS) curriculum. At MIT, 6.002 is in the core of department subjects required for all undergraduates in EECS.
|
||||
|
||||
The course introduces engineering in the context of the lumped circuit abstraction. Topics covered include: resistive elements and networks; independent and dependent sources; switches and MOS transistors; digital abstraction; amplifiers; energy storage elements; dynamics of first- and second-order networks; design in the time and frequency domains; and analog and digital circuits and applications. Design and lab exercises are also significant components of the course. You should expect to spend approximately 10 hours per week on the course.
|
||||
@@ -1 +0,0 @@
|
||||
12 hours
|
||||
@@ -1,14 +0,0 @@
|
||||
<ul>
|
||||
<li>What is the format of the class?
|
||||
<p>The course will consist of 24 lectures, each lasting 50 minutes. There will be regular assignments consisting of map tests and short essays.</p>
|
||||
</li>
|
||||
<li>Are there any prerequisites?
|
||||
<p>No - anyone and everyone is welcome to take this course.</p>
|
||||
</li>
|
||||
<li>What textbook should I buy?
|
||||
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) — 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>
|
||||
@@ -1,9 +0,0 @@
|
||||
<section class="who-should-take">
|
||||
<h3>Who should take this?</h3>
|
||||
<p>If you're one of the many who have a unquenched interest in the worlds history, you'll love this course.</p>
|
||||
</section>
|
||||
|
||||
<section class="who-shouldnt-take">
|
||||
<h3>Who shouldn't take this?</h3>
|
||||
<p>No one. Anyone and everyone is welcome to take this course.</p>
|
||||
</section>
|
||||
@@ -1,2 +0,0 @@
|
||||
<p>In order to succeed in this course, you must have taken an AP level physics course in electricity and magnetism. You must know basic calculus and linear algebra and have some background in differential equations. Since more advanced mathematics will not show up until the second half of the course, the first half of the course will include an optional remedial differential equations component for those who need it.</p>
|
||||
<p>The course web site was developed and tested primarily with Google Chrome. We support current versions of Mozilla Firefox as well. The video player is designed to work with Flash. While we provide a partial non-Flash fallback for the video, as well as partial support for Internet Explorer, other browsers, and tablets, portions of the functionality will be unavailable.</p>
|
||||
@@ -1,17 +0,0 @@
|
||||
<ul>
|
||||
<li><strong>Week 1:</strong> What is World History?</li>
|
||||
<li><strong>Week 2:</strong> Peoples, Plagues and Plunders</li>
|
||||
<li><strong>Week 3:</strong> Warfare and Motion</li>
|
||||
<li><strong>Week 4:</strong> Conquests</li>
|
||||
<li><strong>Week 5:</strong> The Beginnings of Globalization in the Atlantic Worlds</li>
|
||||
<li><strong>Week 6:</strong> The Beginnings of Globalization in the Indian Ocean Worlds</li>
|
||||
<li><strong>Week 7:</strong> The Worlds that Merchants Made</li>
|
||||
<li><strong>Week 8:</strong> The Seventeenth-Century Crisis</li>
|
||||
<li><strong>Week 9:</strong> Empire and Enlightenment</li>
|
||||
<li><strong>Week 10:</strong> The Wealth of Nations</li>
|
||||
<li><strong>Week 11:</strong> The World in Revolution</li>
|
||||
<li><strong>Week 12:</strong> States and Nations</li>
|
||||
<li><strong>Week 13:</strong> Global Frontiers</li>
|
||||
<li><strong>Week 14:</strong> Empires and Nations</li>
|
||||
<li><strong>Week 15:</strong> Back to the Future</li>
|
||||
</ul>
|
||||
@@ -1 +0,0 @@
|
||||
<p>The course uses the textbook Foundations of Analog and Digital Electronic Circuits, by Anant Agarwal and Jeffrey H. Lang. Morgan Kaufmann Publishers, Elsevier, July 2005. While recommended, the book is not required: relevant sections will be provided electronically as part of the online course for personal use in connection with this course only. The copyright for the book is owned by Elsevier. The book can be purchased on <a href="http://www.amazon.com/exec/obidos/ASIN/1558607358/ref=nosim/mitopencourse-20">Amazon</a>.</p>
|
||||
@@ -1 +0,0 @@
|
||||
<iframe width="560" height="315" src="http://www.youtube.com/embed/p2Q6BrNhdh8" frameborder="0" allowfullscreen></iframe>
|
||||
@@ -1,10 +0,0 @@
|
||||
<sequential>
|
||||
<video url_name="welcome"/>
|
||||
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
|
||||
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
|
||||
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab… </html>
|
||||
<html slug="html_5555" filename="html_5555"/>
|
||||
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
|
||||
</vertical>
|
||||
<problem filename="Circuit_Sandbox" slug="Circuit_Sandbox" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Circuit Sandbox"/>
|
||||
</sequential>
|
||||
@@ -1,10 +0,0 @@
|
||||
<sequential>
|
||||
<sequential filename="Administrivia_and_Circuit_Elements" slug="Administrivia_and_Circuit_Elements" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Administrivia and Circuit Elements"/>
|
||||
<vertical slug="Basic_Circuit_Analysis" format="Homework" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="Basic Circuit Analysis">
|
||||
<problem filename="H1P1_Energy" slug="H1P1_Energy" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P1: Energy"/>
|
||||
<problem filename="H1P2_Duality" slug="H1P2_Duality" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P2: Duality"/>
|
||||
<problem filename="H1P3_Poor_Workmanship" slug="H1P3_Poor_Workmanship" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="closed" rerandomize="per_student" due="March 18" graded="true" name="H1P3: Poor Workmanship"/>
|
||||
</vertical>
|
||||
<problem filename="Resistor_Divider" slug="Resistor_Divider" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" due="March 18" graded="true" name="Resistor Divider"/>
|
||||
<html filename="Week_1_Tutorials" slug="Week_1_Tutorials" format="Tutorial Index" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1 Tutorials"/>
|
||||
</sequential>
|
||||
@@ -1 +0,0 @@
|
||||
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" show_timezone="true" advanced_modules="["videoalpha"]"/>
|
||||
@@ -1,13 +0,0 @@
|
||||
<course>
|
||||
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
|
||||
<chapter filename="Overview" slug="Overview" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Overview"/>
|
||||
<chapter filename="Week_1" slug="Week_1" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1"/>
|
||||
<chapter slug="Midterm_Exam" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Midterm Exam">
|
||||
<sequential slug="Midterm_Exam_1121" format="Midterm" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam">
|
||||
<vertical slug="vertical_1122" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true">
|
||||
<html filename="Midterm_Exam_1123" slug="Midterm_Exam_1123" graceperiod="0 day 0 hours 5 minutes 0 seconds" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam"/>
|
||||
</vertical>
|
||||
<vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
|
||||
</sequential>
|
||||
</chapter>
|
||||
</course>
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"GRADER" : [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"category" : "Labs",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm",
|
||||
"name" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"weight" : 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final",
|
||||
"name" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"weight" : 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"A" : 0.87,
|
||||
"B" : 0.7,
|
||||
"C" : 0.6
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
More information given in… <a href="/book/${page}">the text</a>.
|
||||
@@ -1 +0,0 @@
|
||||
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag}… </a>
|
||||
@@ -1 +0,0 @@
|
||||
<img src="http://pmitros.csail.mit.edu/tutorial/images/${file}" />
|
||||
@@ -1 +0,0 @@
|
||||
Lecture Slides Handout [<a href="">Clean… </a>][<a href="">Annotated…</a>]
|
||||
@@ -1,3 +0,0 @@
|
||||
<video width="560" height="340" controls>
|
||||
<source src="http://pmitros.csail.mit.edu/tutorial/raw_links/${name}.webm">
|
||||
</video>
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"GRADER" : [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 3,
|
||||
"drop_count" : 1,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.5
|
||||
},
|
||||
{
|
||||
"type" : "Final",
|
||||
"name" : "Final Question",
|
||||
"short_label" : "Final",
|
||||
"weight" : 0.5
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"A" : 0.8,
|
||||
"B" : 0.7,
|
||||
"C" : 0.44
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<center>
|
||||
Final Exam
|
||||
</center>
|
||||
<p>This exam covers weeks 1-13. You may use your calculator, your notes, your book, the internet, or any other auxiliary materials that you think can help you. However, you are not allowed to communicate with any person on any topic associated with this course while you are taking this exam.</p>
|
||||
<p>
|
||||
Once you click on the next tab, you will have 24 hours to complete the examination. For each problem you will be allowed exactly three submissions. Your
|
||||
answers to that problem will be checked after each submission, so you
|
||||
can fix mistakes you may have made, within the three-submission limit.
|
||||
Your most recently checked answer is the answer that will contribute to your grade on the exam.
|
||||
</p>
|
||||
<p>
|
||||
Each answer box on the exam contributes equally to your grade on the exam, regardless of how they are grouped as problems.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
Hint…
|
||||
<br/><br/>
|
||||
Remember that the time evolution of any variable \(x(t)\) governed by
|
||||
a first-order system with a time-constant \(\tau\) for a time \(t) between an initial
|
||||
value \(x(t_0)\) and a final value \(x(\infty)\) is the following:
|
||||
<br/><br/>
|
||||
\(x(t) = x(\infty) + (x(t_0) - x(\infty)) e^(-(t-t_0)/\tau)\)
|
||||
<br/><br/>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$("#r1_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R1", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r2_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R2", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r3_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R3", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r4_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R4", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#slider").slider(); });
|
||||
</script>
|
||||
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<br><br><i>Note: This part of the lab is just to develop your intuition about
|
||||
superposition. There are no responses that need to be checked.</i>
|
||||
|
||||
<br/><br/>Circuits with multiple sources can be hard to analyze as-is. For example, what is the voltage
|
||||
between the two terminals on the right of Figure 1?
|
||||
|
||||
<center>
|
||||
<input width="425" type="hidden" height="150" id="schematic1" parts="" analyses="" class="schematic ctrls" name="test2" value="[["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["i",[112,64,6],{"name":"","value":"6A"},["0","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["v",[16,16,0],{"name":"","value":"8V"},["3","0"]],["view",-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="[["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["v",[16,16,0],{"name":"","value":"8V"},["3","0"]],["view",-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="[["w",[16,16,16,64]],["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["i",[112,64,6],{"name":"","value":"6A"},["0","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["view",-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>
|
||||
@@ -1,35 +0,0 @@
|
||||
<center>
|
||||
Midterm Exam
|
||||
</center>
|
||||
|
||||
<p>
|
||||
This is a twenty four hour examination. You can start the exam when it is
|
||||
convenient for you, but you must complete this examination by 12:00pm (noon)
|
||||
GMT on April 30th. Please look up what time this is in your local time zone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When you open the next page, you will have started the examination.
|
||||
You do not need to start now: you will not be timed until you open the
|
||||
next page. Once you have opened the next page page you must complete
|
||||
the exam and make your final submission within twenty four hours of starting
|
||||
the exam.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You may use any notes, computational, or auxiliary materials that you
|
||||
think can help you. However, you may not communicate with any person
|
||||
about this examination while working on it. Furthermore, you may not
|
||||
communicate about the exam until the exam has been closed for everyone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For each problem you will be allowed exactly three submissions. Your
|
||||
answers to that problem will be checked after each submission, so you
|
||||
can fix mistakes you may have made, within the three-submission limit.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you want to go back and study some more before starting this
|
||||
exam you can do so. Good Luck!
|
||||
</p>
|
||||
@@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<center>
|
||||
Midterm Exam
|
||||
</center>
|
||||
<p>
|
||||
This is a twenty four hour examination. You can start the exam when it is
|
||||
convenient for you, but you must complete this examination by 12:00pm (noon)
|
||||
GMT on April 30th. Please look up what time this is in your local time zone.
|
||||
</p>
|
||||
<p>
|
||||
When you open the next page, you will have started the examination.
|
||||
You do not need to start now: you will not be timed until you open the
|
||||
next page. Once you have opened the next page page you must complete
|
||||
the exam and make your final submission within twenty four hours of starting
|
||||
the exam.
|
||||
</p>
|
||||
<p>
|
||||
You may use any notes, computational, or auxiliary materials that you
|
||||
think can help you. However, you may not communicate with any person
|
||||
about this examination while working on it. Furthermore, you may not
|
||||
communicate about the exam until the exam has been closed for everyone.
|
||||
</p>
|
||||
<p>
|
||||
For each problem you will be allowed exactly three submissions. Your
|
||||
answers to that problem will be checked after each submission, so you
|
||||
can fix mistakes you may have made, within the three-submission limit.
|
||||
</p>
|
||||
<p>
|
||||
If you want to go back and study some more before starting this
|
||||
exam you can do so. Good Luck!
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<center>
|
||||
Midterm Exam
|
||||
</center>
|
||||
<p>
|
||||
This is a twenty four hour examination. You can start the exam when it is
|
||||
convenient for you, but you must complete this examination by 12:00pm (noon)
|
||||
GMT on April 30th. Please look up what time this is in your local time zone.
|
||||
</p>
|
||||
<p>
|
||||
When you open the next page, you will have started the examination.
|
||||
You do not need to start now: you will not be timed until you open the
|
||||
next page. Once you have opened the next page page you must complete
|
||||
the exam and make your final submission within twenty four hours of starting
|
||||
the exam.
|
||||
</p>
|
||||
<p>
|
||||
You may use any notes, computational, or auxiliary materials that you
|
||||
think can help you. However, you may not communicate with any person
|
||||
about this examination while working on it. Furthermore, you may not
|
||||
communicate about the exam until the exam has been closed for everyone.
|
||||
</p>
|
||||
<p>
|
||||
For each problem you will be allowed exactly three submissions. Your
|
||||
answers to that problem will be checked after each submission, so you
|
||||
can fix mistakes you may have made, within the three-submission limit.
|
||||
</p>
|
||||
<p>
|
||||
If you want to go back and study some more before starting this
|
||||
exam you can do so. Good Luck!
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>Week 13 Tutorials</h1>
|
||||
<section class="tutorials">
|
||||
<h2> Basic Tutorials </h2>
|
||||
<ul>
|
||||
<li><a href="/section/wk13_solder">Soldering</a> — 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> — We explain several techniques for understanding
|
||||
and approximating Bode plots. </li>
|
||||
</ul>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>Week 1 Tutorials</h1>
|
||||
<section class="tutorials">
|
||||
<h2> Introduction </h2>
|
||||
<ul>
|
||||
<li><a href="/section/intro"> Welcome! </a> - We introduce
|
||||
ourselves and explain the tutorial format. </li>
|
||||
</ul>
|
||||
<h2>Basic Tutorials </h2>
|
||||
<ul>
|
||||
<li><a href="/section/circuit_abstraction">The Circuit
|
||||
Abstraction</a> - We look at a lightbulb, and see that abstracting
|
||||
away the circuit from the geometry of the wiring has no visible
|
||||
effect. </li>
|
||||
<li><a href="/section/a_real_circuit/">Lightbulb Circuit</a> - We
|
||||
look at the voltage across a lightbulb with a multimeter, and
|
||||
confirm the device is symmetric. In the process, we see reference
|
||||
directions for currents, voltages, and polarity for powers. </li>
|
||||
<li><a href="/section/parallel_resistors">Parallel Resistors</a> -
|
||||
An explanation of equivalent circuits in the context of parallel
|
||||
resistors.</li>
|
||||
<li><a href="/section/series_and_parallel">Combination of Series
|
||||
and Parallel</a> - Slightly more complex networks of resistors
|
||||
simplified with equivalent networks.</li>
|
||||
<li><a href="/section/3r_nodal">Nodal Analysis</a> - A simple
|
||||
example of nodal analysis.</li>
|
||||
<li><a href="/section/floating_voltage_3r">Floating Voltage</a> -
|
||||
Nodal analysis with a floating source.</li>
|
||||
<li><a href="/section/combination_rules">Combination Rules</a> -
|
||||
We show how to apply combination rules to solving simple
|
||||
circuits.</li>
|
||||
</ul>
|
||||
<h2>Worked Problems </h2>
|
||||
<p> For the first week, we work through <a href="http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-002-circuits-and-electronics-spring-2007/assignments/hw1.pdf"> the first problem set from 2007</a></p>
|
||||
<ul>
|
||||
<li><a href="/section/exercise_1_1">OCW Exercise 1-1</a>: Find voltages in a simple series and parallel circuit, and show power is conserved</li>
|
||||
<li><a href="/section/ex_1_2">OCW Exercise 1-2</a> - Synthesize a network of 3/5k and 5/3k from at most four 1k resistors</li>
|
||||
<li><a href="/section/problem_1-1">OCW Problem 1-1/Textbook 2.7</a> - Find the current through a resistor in a network of resistors</li>
|
||||
<li><a href="/section/problem_1-2_part1">OCW Problem 1-2</a> - Analyze long chains of resistors (similar to transmission lines or R1R2 ladders) </li>
|
||||
<li><a href="/section/problem_1_3">OCW Problem 1-3 </a> - Reverse engineer a black-box resistor network</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. Gratuitous ≥ entity.</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab. </html>
|
||||
@@ -1,39 +0,0 @@
|
||||
Clarification of the term "Linear"
|
||||
<p>
|
||||
The term "linear" is very clear when applied to a
|
||||
mathematical function. A function F is linear if and only
|
||||
if it obeys homogeneity and superposition:
|
||||
</p><p>
|
||||
Homogeneity: F(cx) = cF(x)
|
||||
<br/>
|
||||
Superposition: F(x+y) = F(x) + F(y)
|
||||
</p><p>
|
||||
In the context of what we have seen so far, the only
|
||||
elements that are linear as mathematical functions are
|
||||
resistors. An independent voltage source or an independent
|
||||
current source is not a linear element. (There are also
|
||||
linear dependent sources, linear capacitors and linear
|
||||
inductors, but we have not yet introduced them in our class.
|
||||
You will see them later.)
|
||||
</p><p>
|
||||
Formally, a circuit composed of only linear elements is a
|
||||
linear circuit. When we add independent sources to a linear
|
||||
circuit as inputs, we get a circuit that is not linear
|
||||
because it has an offset: its v-i characteristic at a pair
|
||||
of exposed terminals may not pass through the origin.
|
||||
However, we can make a Thevenin or Norton equivalent model
|
||||
of such a circuit: the Thevenin resistance summarizes the
|
||||
effect of the linear elements and the Thevenin voltage
|
||||
summarizes the effect of the independent sources.
|
||||
</p><p>
|
||||
However the term "linear," when applied to an electrical
|
||||
circuit often takes on an informal meaning. We often say
|
||||
that a circuit containing only linear elements and
|
||||
independent sources is a "linear circuit." So, in the
|
||||
informal sense, a linear circuit is one where we can apply
|
||||
the Thevenin or Norton theorems to summarize the behavior at
|
||||
a pair of exposed terminals.
|
||||
</p><p>
|
||||
Sorry for the confusion of words — natural language is like
|
||||
that!
|
||||
</p>
|
||||
@@ -1,39 +0,0 @@
|
||||
Clarification of the term "Linear"
|
||||
<p>
|
||||
The term "linear" is very clear when applied to a
|
||||
mathematical function. A function F is linear if and only
|
||||
if it obeys homogeneity and superposition:
|
||||
</p><p>
|
||||
Homogeneity: F(cx) = cF(x)
|
||||
<br/>
|
||||
Superposition: F(x+y) = F(x) + F(y)
|
||||
</p><p>
|
||||
In the context of what we have seen so far, the only
|
||||
elements that are linear as mathematical functions are
|
||||
resistors. An independent voltage source or an independent
|
||||
current source is not a linear element. (There are also
|
||||
linear dependent sources, linear
|
||||
inductors, and other linear elements, but we have not yet introduced them in our class.
|
||||
You will see them later.)
|
||||
</p><p>
|
||||
Formally, a circuit composed of only linear elements is a
|
||||
linear circuit. When we add independent sources to a linear
|
||||
circuit as inputs, we get a circuit that is not linear
|
||||
because it has an offset: its v-i characteristic at a pair
|
||||
of exposed terminals may not pass through the origin.
|
||||
However, we can make a Thevenin or Norton equivalent model
|
||||
of such a circuit: the Thevenin resistance summarizes the
|
||||
effect of the linear elements and the Thevenin voltage
|
||||
summarizes the effect of the independent sources.
|
||||
</p><p>
|
||||
However the term "linear," when applied to an electrical
|
||||
circuit often takes on an informal meaning. We often say
|
||||
that a circuit containing only linear elements and
|
||||
independent sources is a "linear circuit." So, in the
|
||||
informal sense, a linear circuit is one where we can apply
|
||||
the Thevenin or Norton theorems to summarize the behavior at
|
||||
a pair of exposed terminals.
|
||||
</p><p>
|
||||
Sorry for the confusion of words — natural language is like
|
||||
that!
|
||||
</p>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user