diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..fa1385d99a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* -text diff --git a/.gitignore b/.gitignore index 69bc47afdd..b1a36e5f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ node_modules .prereqs_cache autodeploy.properties .ws_migrations_complete +.vagrant/ diff --git a/AUTHORS b/AUTHORS index 9bb4ede121..697f42b36c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -78,3 +78,5 @@ Peter Fogg Bethany LaPenta Renzo Lucioni Felix Sun +Adam Palay +Ian Hoover \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ff900d6161..41c171c52c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,14 +5,36 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Added *experimental* support for jsinput type. + +Common: Added setting to specify Celery Broker vhost + +Studio: Add table for tracking course creator permissions (not yet used). +Update rake django-admin[syncdb] and rake django-admin[migrate] so they +run for both LMS and CMS. + +LMS: Added *experimental* crowdsource hinting manager page. + +XModule: Added *experimental* crowdsource hinting module. + +Studio: Added support for uploading and managing PDF textbooks + +Common: Student information is now passed to the tracking log via POST instead of GET. + Common: Add tests for documentation generation to test suite Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems +LMS: Removed press releases + +Common: Updated Sass and Bourbon libraries, added Neat library + LMS: Users are no longer auto-activated if they click "reset password" This is now done when they click on the link in the reset password email they receive (along with usual path through activation email). +LMS: Fixed a reflected XSS problem in the static textbook views. + LMS: Problem rescoring. Added options on the Grades tab of the Instructor Dashboard to allow a particular student's submission for a particular problem to be rescored. Provides an option to see a diff --git a/Gemfile b/Gemfile index 1ad685c34d..8a6a2c8ccc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,8 @@ source 'https://rubygems.org' gem 'rake', '~> 10.0.3' -gem 'sass', '3.1.15' -gem 'bourbon', '~> 1.3.6' +gem 'sass', '3.2.9' +gem 'bourbon', '~> 3.1.8' +gem 'neat', '~> 1.3.0' gem 'colorize', '~> 0.5.8' gem 'launchy', '~> 2.1.2' gem 'sys-proctable', '~> 0.9.3' diff --git a/LICENSE b/LICENSE index dba13ed2dd..1eb391f388 100644 --- a/LICENSE +++ b/LICENSE @@ -659,3 +659,13 @@ specific requirements. if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . + +EdX Inc. wishes to state, in clarification of the above license terms, that +any public, independently available web service offered over the network and +communicating with edX's copyrighted works by any form of inter-service +communication, including but not limited to Remote Procedure Call (RPC) +interfaces, is not a work based on our copyrighted work within the meaning +of the license. "Corresponding Source" of this work, or works based on this +work, as defined by the terms of this license do not include source code +files for programs used solely to provide those public, independently +available web services. diff --git a/README.md b/README.md index 92a4116354..e533459c8b 100644 --- a/README.md +++ b/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. @@ -152,6 +280,12 @@ otherwise noted. Please see ``LICENSE.txt`` for details. +Documentation +------------ + +High-level documentation of the code is located in the `doc` subdirectory. Start +with `overview.md` to get an introduction to the architecture of the system. + How to Contribute ----------------- diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..0d409cc408 --- /dev/null +++ b/Vagrantfile @@ -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 diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index a544906875..0f2e60dd6e 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -36,7 +36,7 @@ def get_course_groupname_for_role(location, role): def get_users_in_course_group_by_role(location, role): groupname = get_course_groupname_for_role(location, role) - (group, created) = Group.objects.get_or_create(name=groupname) + (group, _created) = Group.objects.get_or_create(name=groupname) return group.user_set.all() @@ -59,6 +59,7 @@ def create_new_course_group(creator, location, role): return + def _delete_course_group(location): """ This is to be called only by either a command line code path or through a app which has already @@ -75,6 +76,7 @@ def _delete_course_group(location): user.groups.remove(staff) user.save() + def _copy_course_group(source, dest): """ This is to be called only by either a command line code path or through an app which has already @@ -205,3 +207,29 @@ def is_user_in_creator_group(user): return user.groups.filter(name=COURSE_CREATOR_GROUP_NAME).count() > 0 return True + + +def get_users_with_instructor_role(): + """ + Returns all users with the role 'instructor' + """ + return _get_users_with_role(INSTRUCTOR_ROLE_NAME) + + +def get_users_with_staff_role(): + """ + Returns all users with the role 'staff' + """ + return _get_users_with_role(STAFF_ROLE_NAME) + + +def _get_users_with_role(role): + """ + Returns all users with the specified role. + """ + users = set() + for group in Group.objects.all(): + if group.name.startswith(role + "_"): + for user in group.user_set.all(): + users.add(user) + return users diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 173155df4c..e04c108250 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -9,7 +9,8 @@ from django.core.exceptions import PermissionDenied from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group,\ create_all_course_groups, add_user_to_course_group, STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME,\ - is_user_in_course_group_role, remove_user_from_course_group + is_user_in_course_group_role, remove_user_from_course_group, get_users_with_staff_role,\ + get_users_with_instructor_role class CreatorGroupTest(TestCase): @@ -174,3 +175,28 @@ class CourseGroupTest(TestCase): create_all_course_groups(self.creator, self.location) with self.assertRaises(PermissionDenied): remove_user_from_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME) + + def test_get_staff(self): + # Do this test with staff in 2 different classes. + create_all_course_groups(self.creator, self.location) + add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME) + + location2 = 'i4x', 'mitX', '103', 'course2', 'test2' + staff2 = User.objects.create_user('teststaff2', 'teststaff2+courses@edx.org', 'foo') + create_all_course_groups(self.creator, location2) + add_user_to_course_group(self.creator, staff2, location2, STAFF_ROLE_NAME) + + self.assertSetEqual({self.staff, staff2, self.creator}, get_users_with_staff_role()) + + def test_get_instructor(self): + # Do this test with creators in 2 different classes. + create_all_course_groups(self.creator, self.location) + add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME) + + location2 = 'i4x', 'mitX', '103', 'course2', 'test2' + creator2 = User.objects.create_user('testcreator2', 'testcreator2+courses@edx.org', 'foo') + staff2 = User.objects.create_user('teststaff2', 'teststaff2+courses@edx.org', 'foo') + create_all_course_groups(creator2, location2) + add_user_to_course_group(creator2, staff2, location2, STAFF_ROLE_NAME) + + self.assertSetEqual({self.creator, creator2}, get_users_with_instructor_role()) diff --git a/cms/djangoapps/contentstore/debug_file_uploader.py b/cms/djangoapps/contentstore/debug_file_uploader.py new file mode 100644 index 0000000000..d783e16192 --- /dev/null +++ b/cms/djangoapps/contentstore/debug_file_uploader.py @@ -0,0 +1,22 @@ +from django.core.files.uploadhandler import FileUploadHandler +import time + + +class DebugFileUploader(FileUploadHandler): + def __init__(self, request=None): + super(DebugFileUploader, self).__init__(request) + self.count = 0 + + def receive_data_chunk(self, raw_data, start): + time.sleep(1) + self.count = self.count + len(raw_data) + fail_at = None + if 'fail_at' in self.request.GET: + fail_at = int(self.request.GET.get('fail_at')) + if fail_at and self.count > fail_at: + raise Exception('Triggered fail') + + return raw_data + + def file_complete(self, file_size): + return None diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 1661e1c391..37d5c12ecc 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -2,7 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step -from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true +from nose.tools import assert_false, assert_equal, assert_regexp_matches from common import type_in_codemirror KEY_CSS = '.key input.policy-key' @@ -36,7 +36,7 @@ def press_the_notification_button(step, name): error_showing = world.is_css_present('.is-shown.wrapper-notification-error') return confirmation_dismissed or error_showing - assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') + world.css_click(css, success_condition=save_clicked) @step(u'I edit the value of a policy key$') diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index 9552d35036..fe20fb9b77 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -115,7 +115,7 @@ def clickActionLink(checklist, task, actionText): # text will be empty initially, wait for it to populate def verify_action_link_text(driver): - return action_link.text == actionText + return world.css_text('#course-checklist' + str(checklist) + ' a', index=task) == actionText world.wait_for(verify_action_link_text) - action_link.click() + world.css_click('#course-checklist' + str(checklist) + ' a', index=task) diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature new file mode 100644 index 0000000000..2291712f2d --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.feature @@ -0,0 +1,69 @@ +Feature: Component Adding + As a course author, I want to be able to add a wide variety of components + + @skip + Scenario: I can add components + Given I have opened a new course in studio + And I am editing a new unit + When I add the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + Then I see the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + + @skip + Scenario: I can delete Components + Given I have opened a new course in studio + And I am editing a new unit + And I add the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + When I will confirm all alerts + And I delete all components + Then I see no components diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py new file mode 100644 index 0000000000..217ad84591 --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.py @@ -0,0 +1,126 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from nose.tools import assert_true + +DATA_LOCATION = 'i4x://edx/templates' + + +@step(u'I am editing a new unit') +def add_unit(step): + css_selectors = ['a.new-courseware-section-button', 'input.new-section-name-save', 'a.new-subsection-item', + 'input.new-subsection-name-save', 'div.section-item a.expand-collapse-icon', 'a.new-unit-item'] + for selector in css_selectors: + world.css_click(selector) + + +@step(u'I add the following components:') +def add_components(step): + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + for css in COMPONENT_DICTIONARY[component]['steps']: + world.css_click(css) + + +@step(u'I see the following components') +def check_components(step): + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + assert_true(COMPONENT_DICTIONARY[component]['found_func'](), "{} couldn't be found".format(component)) + + +@step(u'I delete all components') +def delete_all_components(step): + for _ in range(len(COMPONENT_DICTIONARY)): + world.css_click('a.delete-button') + + +@step(u'I see no components') +def see_no_components(steps): + assert world.is_css_not_present('li.component') + + +def step_selector_list(data_type, path, index=1): + selector_list = ['a[data-type="{}"]'.format(data_type)] + if index != 1: + selector_list.append('a[id="ui-id-{}"]'.format(index)) + if path is not None: + selector_list.append('a[data-location="{}/{}/{}"]'.format(DATA_LOCATION, data_type, path)) + return selector_list + + +def found_text_func(text): + return lambda: world.browser.is_text_present(text) + + +def found_css_func(css): + return lambda: world.is_css_present(css, wait_time=2) + +COMPONENT_DICTIONARY = { + 'Discussion': { + 'steps': step_selector_list('discussion', None), + 'found_func': found_css_func('section.xmodule_DiscussionModule') + }, + 'Blank HTML': { + 'steps': step_selector_list('html', 'Blank_HTML_Page'), + #this one is a blank html so a more refined search is being done + 'found_func': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')] + }, + 'LaTex': { + 'steps': step_selector_list('html', 'E-text_Written_in_LaTeX'), + 'found_func': found_text_func('EXAMPLE: E-TEXT PAGE') + }, + 'Blank Problem': { + 'steps': step_selector_list('problem', 'Blank_Common_Problem'), + 'found_func': found_text_func('BLANK COMMON PROBLEM') + }, + 'Dropdown': { + 'steps': step_selector_list('problem', 'Dropdown'), + 'found_func': found_text_func('DROPDOWN') + }, + 'Multi Choice': { + 'steps': step_selector_list('problem', 'Multiple_Choice'), + 'found_func': found_text_func('MULTIPLE CHOICE') + }, + 'Numerical': { + 'steps': step_selector_list('problem', 'Numerical_Input'), + 'found_func': found_text_func('NUMERICAL INPUT') + }, + 'Text Input': { + 'steps': step_selector_list('problem', 'Text_Input'), + 'found_func': found_text_func('TEXT INPUT') + }, + 'Advanced': { + 'steps': step_selector_list('problem', 'Blank_Advanced_Problem', index=2), + 'found_func': found_text_func('BLANK ADVANCED PROBLEM') + }, + 'Circuit': { + 'steps': step_selector_list('problem', 'Circuit_Schematic_Builder', index=2), + 'found_func': found_text_func('CIRCUIT SCHEMATIC BUILDER') + }, + 'Custom Python': { + 'steps': step_selector_list('problem', 'Custom_Python-Evaluated_Input', index=2), + 'found_func': found_text_func('CUSTOM PYTHON-EVALUATED INPUT') + }, + 'Image Mapped': { + 'steps': step_selector_list('problem', 'Image_Mapped_Input', index=2), + 'found_func': found_text_func('IMAGE MAPPED INPUT') + }, + 'Math Input': { + 'steps': step_selector_list('problem', 'Math_Expression_Input', index=2), + 'found_func': found_text_func('MATH EXPRESSION INPUT') + }, + 'Problem LaTex': { + 'steps': step_selector_list('problem', 'Problem_Written_in_LaTeX', index=2), + 'found_func': found_text_func('PROBLEM WRITTEN IN LATEX') + }, + 'Adaptive Hint': { + 'steps': step_selector_list('problem', 'Problem_with_Adaptive_Hint', index=2), + 'found_func': found_text_func('PROBLEM WITH ADAPTIVE HINT') + }, + 'Video': { + 'steps': step_selector_list('video', None), + 'found_func': found_css_func('section.xmodule_VideoModule') + } +} diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index d838061698..e7fbb2f90c 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -60,8 +60,7 @@ def change_date(_step, new_date): @step(u'I should see the date "([^"]*)"$') def check_date(_step, date): date_css = 'span.date-display' - date_html = world.css_find(date_css) - assert date == date_html.html + assert date == world.css_html(date_css) @step(u'I modify the handout to "([^"]*)"$') @@ -74,8 +73,7 @@ def edit_handouts(_step, text): @step(u'I see the handout "([^"]*)"$') def check_handout(_step, handout): handout_css = 'div.handouts-content' - handouts = world.css_find(handout_css) - assert handout in handouts.html + assert handout in world.css_html(handout_css) def change_text(text): diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 4e59897c1c..dc41cda30f 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -47,7 +47,7 @@ def confirm_change(step): range_css = '.range' all_ranges = world.css_find(range_css) for i in range(len(all_ranges)): - assert all_ranges[i].html != '0-50' + assert world.css_html(range_css, index=i) != '0-50' @step(u'I change assignment type "([^"]*)" to "([^"]*)"$') diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index cc1d766d2e..fe876aa4e4 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -1,30 +1,35 @@ 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 @@ -32,6 +37,7 @@ 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 @@ -39,21 +45,25 @@ 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 @@ -61,11 +71,13 @@ 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 diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 80ccb6cc7a..a91960cd2a 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -3,6 +3,7 @@ Feature: Create Section As a course author I want to create and edit sections + @skip Scenario: Add a new section to a course Given I have opened a new course in Studio When I click the New Section link diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index a16a3246da..3c9226f874 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -9,14 +9,14 @@ from selenium.webdriver.common.keys import Keys def go_to_static(_step): menu_css = 'li.nav-course-courseware' static_css = 'li.nav-course-courseware-pages' - world.css_find(menu_css).click() - world.css_find(static_css).click() + world.css_click(menu_css) + world.css_click(static_css) @step(u'I add a new page') def add_page(_step): button_css = 'a.new-button' - world.css_find(button_css).click() + world.css_click(button_css) @step(u'I should( not)? see a "([^"]*)" static page$') @@ -33,13 +33,13 @@ def click_edit_delete(_step, edit_delete, page): button_css = 'a.%s-button' % edit_delete index = get_index(page) assert index != -1 - world.css_find(button_css)[index].click() + world.css_click(button_css, index=index) @step(u'I change the name to "([^"]*)"$') def change_name(_step, new_name): settings_css = '#settings-mode' - world.css_find(settings_css).click() + world.css_click(settings_css) input_css = 'input.setting-input' name_input = world.css_find(input_css) old_name = name_input.value @@ -47,13 +47,13 @@ def change_name(_step, new_name): name_input._element.send_keys(Keys.END, Keys.BACK_SPACE) name_input._element.send_keys(new_name) save_button = 'a.save-button' - world.css_find(save_button).click() + world.css_click(save_button) def get_index(name): page_name_css = 'section[data-type="HTMLModule"]' all_pages = world.css_find(page_name_css) for i in range(len(all_pages)): - if all_pages[i].html == '\n {name}\n'.format(name=name): + if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name): return i return -1 diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index a11467e3f9..1dfe5d95f5 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -32,6 +32,7 @@ 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 diff --git a/cms/djangoapps/contentstore/features/textbooks.feature b/cms/djangoapps/contentstore/features/textbooks.feature new file mode 100644 index 0000000000..0758a0b57b --- /dev/null +++ b/cms/djangoapps/contentstore/features/textbooks.feature @@ -0,0 +1,47 @@ +Feature: Textbooks + + Scenario: No textbooks + Given I have opened a new course in Studio + When I go to the textbooks page + Then I should see a message telling me to create a new textbook + + Scenario: Create a textbook + Given I have opened a new course in Studio + And I go to the textbooks page + When I click on the New Textbook button + And I name my textbook "Economics" + And I name the first chapter "Chapter 1" + And I click the Upload Asset link for the first chapter + And I upload the textbook "textbook.pdf" + And I wait for "2" seconds + And I save the textbook + Then I should see a textbook named "Economics" with a chapter path containing "/c4x/MITx/999/asset/textbook.pdf" + And I reload the page + Then I should see a textbook named "Economics" with a chapter path containing "/c4x/MITx/999/asset/textbook.pdf" + + Scenario: Create a textbook with multiple chapters + Given I have opened a new course in Studio + And I go to the textbooks page + When I click on the New Textbook button + And I name my textbook "History" + And I name the first chapter "Britain" + And I type in "britain.pdf" for the first chapter asset + And I click Add a Chapter + And I name the second chapter "America" + And I type in "america.pdf" for the second chapter asset + And I save the textbook + Then I should see a textbook named "History" with 2 chapters + And I click the textbook chapters + Then I should see a textbook named "History" with 2 chapters + And the first chapter should be named "Britain" + And the first chapter should have an asset called "britain.pdf" + And the second chapter should be named "America" + And the second chapter should have an asset called "america.pdf" + And I reload the page + Then I should see a textbook named "History" with 2 chapters + And I click the textbook chapters + Then I should see a textbook named "History" with 2 chapters + And the first chapter should be named "Britain" + And the first chapter should have an asset called "britain.pdf" + And the second chapter should be named "America" + And the second chapter should have an asset called "america.pdf" diff --git a/cms/djangoapps/contentstore/features/textbooks.py b/cms/djangoapps/contentstore/features/textbooks.py new file mode 100644 index 0000000000..ca135d9725 --- /dev/null +++ b/cms/djangoapps/contentstore/features/textbooks.py @@ -0,0 +1,121 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from django.conf import settings +import os + +TEST_ROOT = settings.COMMON_TEST_DATA_ROOT + + +@step(u'I go to the textbooks page') +def go_to_uploads(_step): + world.click_course_content() + menu_css = 'li.nav-course-courseware-textbooks' + world.css_find(menu_css).click() + + +@step(u'I should see a message telling me to create a new textbook') +def assert_create_new_textbook_msg(_step): + css = ".wrapper-content .no-textbook-content" + assert world.is_css_present(css) + no_tb = world.css_find(css) + assert "You haven't added any textbooks" in no_tb.text + + +@step(u'I upload the textbook "([^"]*)"$') +def upload_file(_step, file_name): + file_css = '.upload-dialog input[type=file]' + upload = world.css_find(file_css) + # uploading the file itself + path = os.path.join(TEST_ROOT, 'uploads', file_name) + upload._element.send_keys(os.path.abspath(path)) + button_css = ".upload-dialog .action-upload" + world.css_click(button_css) + + +@step(u'I click (on )?the New Textbook button') +def click_new_textbook(_step, on): + button_css = ".nav-actions .new-button" + button = world.css_find(button_css) + button.click() + + +@step(u'I name my textbook "([^"]*)"') +def name_textbook(_step, name): + input_css = ".textbook input[name=textbook-name]" + world.css_fill(input_css, name) + + +@step(u'I name the (first|second|third) chapter "([^"]*)"') +def name_chapter(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1) + world.css_fill(input_css, name) + + +@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset') +def asset_chapter(_step, name, ordinal): + index = ["first", "second", "third"].index(ordinal) + input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1) + world.css_fill(input_css, name) + + +@step(u'I click the Upload Asset link for the (first|second|third) chapter') +def click_upload_asset(_step, ordinal): + index = ["first", "second", "third"].index(ordinal) + button_css = ".textbook .chapter{i} .action-upload".format(i=index+1) + world.css_click(button_css) + + +@step(u'I click Add a Chapter') +def click_add_chapter(_step): + button_css = ".textbook .action-add-chapter" + world.css_click(button_css) + + +@step(u'I save the textbook') +def save_textbook(_step): + submit_css = "form.edit-textbook button[type=submit]" + world.css_click(submit_css) + + +@step(u'I should see a textbook named "([^"]*)" with a chapter path containing "([^"]*)"') +def check_textbook(_step, textbook_name, chapter_name): + title = world.css_find(".textbook h3.textbook-title") + chapter = world.css_find(".textbook .wrap-textbook p") + assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name) + assert chapter.text == chapter_name, "{} != {}".format(chapter.text, chapter_name) + + +@step(u'I should see a textbook named "([^"]*)" with (\d+) chapters') +def check_textbook_chapters(_step, textbook_name, num_chapters_str): + num_chapters = int(num_chapters_str) + title = world.css_find(".textbook .view-textbook h3.textbook-title") + toggle = world.css_find(".textbook .view-textbook .chapter-toggle") + assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name) + assert toggle.text == "{num} PDF Chapters".format(num=num_chapters), \ + "Expected {num} chapters, found {real}".format(num=num_chapters, real=toggle.text) + + +@step(u'I click the textbook chapters') +def click_chapters(_step): + world.css_click(".textbook a.chapter-toggle") + + +@step(u'the (first|second|third) chapter should be named "([^"]*)"') +def check_chapter_name(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + chapter = world.css_find(".textbook .view-textbook ol.chapters li")[index] + element = chapter.find_by_css(".chapter-name") + assert element.text == name, "Expected chapter named {expected}, found chapter named {actual}".format( + expected=name, actual=element.text) + + +@step(u'the (first|second|third) chapter should have an asset called "([^"]*)"') +def check_chapter_asset(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + chapter = world.css_find(".textbook .view-textbook ol.chapters li")[index] + element = chapter.find_by_css(".chapter-asset-path") + assert element.text == name, "Expected chapter with asset {expected}, found chapter with asset {actual}".format( + expected=name, actual=element.text) diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature index b3c1fc2ce3..8d40163685 100644 --- a/cms/djangoapps/contentstore/features/upload.feature +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -21,6 +21,7 @@ Feature: Upload Files When I upload the file "test" And I delete the file "test" Then I should not see the file "test" was uploaded + And I see a confirmation that the file was deleted Scenario: Users can download files Given I have opened a new course in studio diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 258fc5ebcf..0c700956e3 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -9,21 +9,21 @@ 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') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = 'li.nav-course-courseware-uploads' - world.css_find(menu_css).click() - world.css_find(uploads_css).click() + world.css_click(menu_css) + world.css_click(uploads_css) @step(u'I upload the file "([^"]*)"$') def upload_file(_step, file_name): upload_css = 'a.upload-button' - world.css_find(upload_css).click() + world.css_click(upload_css) file_css = 'input.file-input' upload = world.css_find(file_css) @@ -32,7 +32,7 @@ def upload_file(_step, file_name): upload._element.send_keys(os.path.abspath(path)) close_css = 'a.close-button' - world.css_find(close_css).click() + world.css_click(close_css) @step(u'I should( not)? see the file "([^"]*)" was uploaded$') @@ -67,7 +67,7 @@ def no_duplicate(_step, file_name): all_names = world.css_find(names_css) only_one = False for i in range(len(all_names)): - if file_name == all_names[i].html: + if file_name == world.css_html(names_css, index=i): only_one = not only_one assert only_one @@ -90,11 +90,17 @@ def modify_upload(_step, file_name): cur_file.write(new_text) +@step('I see a confirmation that the file was deleted') +def i_see_a_delete_confirmation(_step): + alert_css = '#notification-confirmation' + assert world.is_css_present(alert_css) + + def get_index(file_name): names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) for i in range(len(all_names)): - if file_name == all_names[i].html: + if file_name == world.css_html(names_css, index=i): return i return -1 diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature index e4caa70ef6..548ba12a3d 100644 --- a/cms/djangoapps/contentstore/features/video.feature +++ b/cms/djangoapps/contentstore/features/video.feature @@ -18,6 +18,7 @@ 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 diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py new file mode 100644 index 0000000000..90d8b3c668 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -0,0 +1,48 @@ +""" +Script for granting existing course instructors course creator privileges. + +This script is only intended to be run once on a given environment. +""" +from auth.authz import get_users_with_instructor_role, get_users_with_staff_role +from course_creators.views import add_user_with_status_granted, add_user_with_status_unrequested +from django.core.management.base import BaseCommand + +from django.contrib.auth.models import User +from django.db.utils import IntegrityError + + +class Command(BaseCommand): + """ + Script for granting existing course instructors course creator privileges. + """ + help = 'Grants all users with INSTRUCTOR role permission to create courses' + + def handle(self, *args, **options): + """ + The logic of the command. + """ + username = 'populate_creators_command' + email = 'grant+creator+access@edx.org' + try: + admin = User.objects.create_user(username, email, 'foo') + admin.is_staff = True + admin.save() + except IntegrityError: + # If the script did not complete the last time it was run, + # the admin user will already exist. + admin = User.objects.get(username=username, email=email) + + for user in get_users_with_instructor_role(): + add_user_with_status_granted(admin, user) + + # Some users will be both staff and instructors. Those folks have been + # added with status granted above, and add_user_with_status_unrequested + # will not try to add them again if they already exist in the course creator database. + for user in get_users_with_staff_role(): + add_user_with_status_unrequested(admin, user) + + # There could be users who are not in either staff or instructor (they've + # never actually done anything in Studio). I plan to add those as unrequested + # when they first go to their dashboard. + + admin.delete() diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py new file mode 100644 index 0000000000..58aee3c77d --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -0,0 +1,95 @@ +""" +Unit tests for the asset upload endpoint. +""" + +import json +from datetime import datetime +from io import BytesIO +from pytz import UTC +from unittest import TestCase, skip +from .utils import CourseTestCase +from django.core.urlresolvers import reverse +from contentstore.views import assets + + +class AssetsTestCase(CourseTestCase): + def setUp(self): + super(AssetsTestCase, self).setUp() + self.url = reverse("asset_index", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + }) + + def test_basic(self): + resp = self.client.get(self.url) + self.assertEquals(resp.status_code, 200) + + def test_json(self): + resp = self.client.get( + self.url, + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEquals(resp.status_code, 200) + content = json.loads(resp.content) + self.assertIsInstance(content, list) + + +class UploadTestCase(CourseTestCase): + """ + Unit tests for uploading a file + """ + def setUp(self): + super(UploadTestCase, self).setUp() + self.url = reverse("upload_asset", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'coursename': self.course.location.name, + }) + + @skip("CorruptGridFile error on continuous integration server") + def test_happy_path(self): + file = BytesIO("sample content") + file.name = "sample.txt" + resp = self.client.post(self.url, {"name": "my-name", "file": file}) + self.assert2XX(resp.status_code) + + def test_no_file(self): + resp = self.client.post(self.url, {"name": "file.txt"}) + self.assert4XX(resp.status_code) + + def test_get(self): + resp = self.client.get(self.url) + self.assertEquals(resp.status_code, 405) + + +class AssetsToJsonTestCase(TestCase): + """ + Unit tests for transforming the results of a database call into something + we can send out to the client via JSON. + """ + def test_basic(self): + upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) + asset = { + "displayname": "foo", + "chunkSize": 512, + "filename": "foo.png", + "length": 100, + "uploadDate": upload_date, + "_id": { + "course": "course", + "org": "org", + "revision": 12, + "category": "category", + "name": "name", + "tag": "tag", + } + } + output = assets.assets_to_json_dict([asset]) + self.assertEquals(len(output), 1) + compare = output[0] + self.assertEquals(compare["name"], "foo") + self.assertEquals(compare["path"], "foo.png") + self.assertEquals(compare["uploaded"], upload_date.isoformat()) + self.assertEquals(compare["id"], "/tag/org/course/12/category/name") diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 52e9ba14fe..99ffb8678d 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -1,10 +1,10 @@ """ Unit tests for checklist methods in views.py. """ from contentstore.utils import get_modulestore, get_url_reverse -from contentstore.tests.test_course_settings import CourseTestCase from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse import json +from .utils import CourseTestCase class ChecklistTestCase(CourseTestCase): @@ -117,4 +117,4 @@ class ChecklistTestCase(CourseTestCase): 'name': self.course.location.name, 'checklist_index': 100}) response = self.client.delete(update_url) - self.assertContains(response, 'Unsupported request', status_code=400) + self.assertEqual(response.status_code, 405) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b946aac6bb..8400f35171 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,3 +1,5 @@ +#pylint: disable=E1101 + import json import shutil import mock @@ -344,6 +346,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): err_cnt = perform_xlint('common/test/data', ['full']) self.assertGreater(err_cnt, 0) + @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*']) + 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) + + # 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]) + 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']) @@ -612,18 +636,42 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertEqual(resp.status_code, 400) def test_delete_course(self): + """ + This test will import a course, make a draft item, and delete it. This will also assert that the + draft content is also deleted + """ module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['full']) - content_store = contentstore() + draft_store = modulestore('draft') + + import_from_xml(module_store, 'common/test/data/', ['full'], 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) + + # 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) + + draft_store.clone_item(vertical.location, vertical.location) + for child in vertical.get_children(): + draft_store.clone_item(child.location, child.location) + + # delete the course delete_course(module_store, content_store, location, commit=True) - items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) + # 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])) self.assertEqual(len(items), 0) + # assert that all content in the asset library is also deleted + assets = content_store.get_all_content_for_course(location) + self.assertEqual(len(assets), 0) + def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''): filesystem = OSFS(root_dir / 'test_export') self.assertTrue(filesystem.exists(dirname)) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 5c2a15ac87..972bc22dce 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -6,8 +6,6 @@ import json import copy import mock -from django.contrib.auth.models import User -from django.test.client import Client from django.core.urlresolvers import reverse from django.utils.timezone import UTC from django.test.utils import override_settings @@ -17,45 +15,12 @@ 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.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.modulestore.xml_importer import import_from_xml from xmodule.fields import Date - -class CourseTestCase(ModuleStoreTestCase): - """ - Base class for test classes below. - """ - def setUp(self): - """ - These tests need a user in the DB so that the django Test Client - can log them in. - They inherit from the ModuleStoreTestCase class so that the mongodb collection - will be cleared out before each test case execution and deleted - afterwards. - """ - uname = 'testuser' - email = 'test+courses@edx.org' - password = 'foo' - - # Create the use so we can log them in. - self.user = User.objects.create_user(uname, email, password) - - # Note that we do not actually need to do anything - # for registration if we directly mark them active. - self.user.is_active = True - # Staff has access to view all courses - self.user.is_staff = True - self.user.save() - - self.client = Client() - self.client.login(username=uname, password=password) - - course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course') - self.course_location = course.location +from .utils import CourseTestCase class CourseDetailsTestCase(CourseTestCase): @@ -63,8 +28,8 @@ class CourseDetailsTestCase(CourseTestCase): Tests the first course settings page (course dates, overview, etc.). """ def test_virgin_fetch(self): - details = CourseDetails.fetch(self.course_location) - self.assertEqual(details.course_location, self.course_location, "Location not copied into") + details = CourseDetails.fetch(self.course.location) + self.assertEqual(details.course_location, self.course.location, "Location not copied into") self.assertIsNotNone(details.start_date.tzinfo) self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) @@ -75,10 +40,10 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) def test_encoder(self): - details = CourseDetails.fetch(self.course_location) + details = CourseDetails.fetch(self.course.location) jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) - self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=") + self.assertTupleEqual(Location(jsondetails['course_location']), self.course.location, "Location !=") self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") @@ -91,10 +56,12 @@ class CourseDetailsTestCase(CourseTestCase): """ Test the encoder out of its original constrained purpose to see if it functions for general use """ - details = {'location': Location(['tag', 'org', 'course', 'category', 'name']), - 'number': 1, - 'string': 'string', - 'datetime': datetime.datetime.now(UTC())} + details = { + 'location': Location(['tag', 'org', 'course', 'category', 'name']), + 'number': 1, + 'string': 'string', + 'datetime': datetime.datetime.now(UTC()) + } jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) @@ -105,8 +72,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(jsondetails['string'], 'string') def test_update_and_fetch(self): - # # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions - jsondetails = CourseDetails.fetch(self.course_location) + jsondetails = CourseDetails.fetch(self.course.location) jsondetails.syllabus = "bar" # encode - decode to convert date fields and other data which changes form self.assertEqual( @@ -128,15 +94,20 @@ class CourseDetailsTestCase(CourseTestCase): CourseDetails.update_from_json(jsondetails.__dict__).effort, jsondetails.effort, "After set effort" ) + jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC()) + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).start_date, + jsondetails.start_date + ) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): settings_details_url = reverse( 'settings_details', kwargs={ - 'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course + 'org': self.course.location.org, + 'name': self.course.location.name, + 'course': self.course.location.course } ) @@ -158,9 +129,9 @@ class CourseDetailsTestCase(CourseTestCase): settings_details_url = reverse( 'settings_details', kwargs={ - 'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course + 'org': self.course.location.org, + 'name': self.course.location.name, + 'course': self.course.location.course } ) @@ -200,11 +171,12 @@ class CourseDetailsViewTest(CourseTestCase): return Date().to_json(dt) def test_update_and_fetch(self): - details = CourseDetails.fetch(self.course_location) + loc = self.course.location + details = CourseDetails.fetch(loc) # resp s/b json from here on - url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'name': self.course_location.name, 'section': 'details'}) + url = reverse('course_settings', kwargs={'org': loc.org, 'course': loc.course, + 'name': loc.name, 'section': 'details'}) resp = self.client.get(url) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") @@ -235,8 +207,7 @@ class CourseDetailsViewTest(CourseTestCase): dt1 = date.from_json(encoded[field]) dt2 = details[field] - expected_delta = datetime.timedelta(0) - self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) + self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context)) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: @@ -248,49 +219,49 @@ class CourseGradingTest(CourseTestCase): Tests for the course settings grading page. """ def test_initial_grader(self): - descriptor = get_modulestore(self.course_location).get_item(self.course_location) + descriptor = get_modulestore(self.course.location).get_item(self.course.location) test_grader = CourseGradingModel(descriptor) # ??? How much should this test bake in expectations about defaults and thus fail if defaults change? - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") def test_fetch_grader(self): - test_grader = CourseGradingModel.fetch(self.course_location.url()) - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + test_grader = CourseGradingModel.fetch(self.course.location.url()) + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") - test_grader = CourseGradingModel.fetch(self.course_location) - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + test_grader = CourseGradingModel.fetch(self.course.location) + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") for i, grader in enumerate(test_grader.graders): - subgrader = CourseGradingModel.fetch_grader(self.course_location, i) + subgrader = CourseGradingModel.fetch_grader(self.course.location, i) self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal") - subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0) + subgrader = CourseGradingModel.fetch_grader(self.course.location.list(), 0) self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list") def test_fetch_cutoffs(self): - test_grader = CourseGradingModel.fetch_cutoffs(self.course_location) + test_grader = CourseGradingModel.fetch_cutoffs(self.course.location) # ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think) self.assertIsNotNone(test_grader, "No cutoffs via fetch") - test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url()) + test_grader = CourseGradingModel.fetch_cutoffs(self.course.location.url()) self.assertIsNotNone(test_grader, "No cutoffs via fetch with url") def test_fetch_grace(self): - test_grader = CourseGradingModel.fetch_grace_period(self.course_location) + test_grader = CourseGradingModel.fetch_grace_period(self.course.location) # almost a worthless test self.assertIn('grace_period', test_grader, "No grace via fetch") - test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url()) + test_grader = CourseGradingModel.fetch_grace_period(self.course.location.url()) self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url") def test_update_from_json(self): - test_grader = CourseGradingModel.fetch(self.course_location) + test_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update") @@ -304,11 +275,10 @@ class CourseGradingTest(CourseTestCase): test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0} altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) - print test_grader.grace_period, altered_grader.grace_period self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") def test_update_grader_from_json(self): - test_grader = CourseGradingModel.fetch(self.course_location) + test_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update") @@ -328,11 +298,11 @@ 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']) + 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]) def test_fetch_initial_fields(self): - test_model = CourseMetadata.fetch(self.course_location) + test_model = CourseMetadata.fetch(self.course.location) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") @@ -345,17 +315,17 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('xqa_key', test_model, 'xqa_key field ') def test_update_from_json(self): - test_model = CourseMetadata.update_from_json(self.course_location, { + test_model = CourseMetadata.update_from_json(self.course.location, { "advertised_start": "start A", "testcenter_info": {"c": "test"}, "days_early_for_beta": 2 }) self.update_check(test_model) # try fresh fetch to ensure persistence - test_model = CourseMetadata.fetch(self.course_location) + test_model = CourseMetadata.fetch(self.course.location) self.update_check(test_model) # now change some of the existing metadata - test_model = CourseMetadata.update_from_json(self.course_location, { + test_model = CourseMetadata.update_from_json(self.course.location, { "advertised_start": "start B", "display_name": "jolly roger"} ) @@ -384,3 +354,35 @@ class CourseMetadataEditingTest(CourseTestCase): # check for deletion effectiveness self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in') self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in') + + +class CourseGraderUpdatesTest(CourseTestCase): + def setUp(self): + super(CourseGraderUpdatesTest, self).setUp() + self.url = reverse("course_settings", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'grader_index': 0, + }) + + def test_get(self): + resp = self.client.get(self.url) + self.assert2XX(resp.status_code) + obj = json.loads(resp.content) + + def test_delete(self): + resp = self.client.delete(self.url) + self.assert2XX(resp.status_code) + + def test_post(self): + grader = { + "type": "manual", + "min_count": 5, + "drop_count": 10, + "short_label": "yo momma", + "weight": 17.3, + } + resp = self.client.post(self.url, grader) + self.assert2XX(resp.status_code) + obj = json.loads(resp.content) diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index ae14555b32..4f92806871 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase): '''Go through each interface and ensure it works.''' # first get the update to force the creation url = reverse('course_info', - kwargs={'org': self.course_location.org, - 'course': self.course_location.course, - 'name': self.course_location.name}) + kwargs={'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name}) self.client.get(url) init_content = ' + + +
+ +

+ +

+

+

+ +
+ + +
+ +
+ + + +
+ +

+ +

+

+

+ +
+ +
+ + diff --git a/common/static/js/capa/src/jsinput.js b/common/static/js/capa/src/jsinput.js new file mode 100644 index 0000000000..9d3fde32fc --- /dev/null +++ b/common/static/js/capa/src/jsinput.js @@ -0,0 +1,199 @@ +(function (jsinput, undefined) { + // Initialize js inputs on current page. + // N.B.: No library assumptions about the iframe can be made (including, + // most relevantly, jquery). Keep in mind what happens in which context + // when modifying this file. + + /* Check whether there is anything to be done */ + + // When all the problems are first loaded, we want to make sure the + // constructor only runs once for each iframe; but we also want to make + // sure that if part of the page is reloaded (e.g., a problem is + // submitted), the constructor is called again. + + if (!jsinput) { + jsinput = { + runs : 1, + arr : [], + exists : function(id) { + jsinput.arr.filter(function(e, i, a) { + return e.id = id; + }); + } + }; + } + + jsinput.runs++; + + + /* Utils */ + + + // Take a string and find the nested object that corresponds to it. E.g.: + // deepKey(obj, "an.example") -> obj["an"]["example"] + var _deepKey = function(obj, path){ + for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){ + obj = obj[p[i]]; + } + return obj; + }; + + + /* END Utils */ + + + + + function jsinputConstructor(spec) { + // Define an class that will be instantiated for each jsinput element + // of the DOM + + // 'that' is the object returned by the constructor. It has a single + // public method, "update", which updates the hidden input field. + var that = {}; + + /* Private methods */ + + var sect = $(spec.elem).parent().find('section[class="jsinput"]'); + var sectAttr = function (e) { return $(sect).attr(e); }; + var thisIFrame = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0); + var cWindow = thisIFrame.contentWindow; + + // Get the hidden input field to pass to customresponse + function _inputField() { + var parent = $(spec.elem).parent(); + return parent.find('input[id^="input_"]'); + } + var inputField = _inputField(); + + // Get the grade function name + var getGradeFn = sectAttr("data"); + // Get state getter + var getStateGetter = sectAttr("data-getstate"); + // Get state setter + var getStateSetter = sectAttr("data-setstate"); + // Get stored state + var getStoredState = sectAttr("data-stored"); + + + + // Put the return value of gradeFn in the hidden inputField. + var update = function () { + var ans; + + ans = _deepKey(cWindow, gradeFn)(); + // setstate presumes getstate, so don't getstate unless setstate is + // defined. + if (getStateGetter && getStateSetter) { + var state, store; + state = unescape(_deepKey(cWindow, getStateGetter)()); + store = { + answer: ans, + state: state + }; + inputField.val(JSON.stringify(store)); + } else { + inputField.val(ans); + } + return; + }; + + /* Public methods */ + + that.update = update; + + + + /* Initialization */ + + jsinput.arr.push(that); + + // Put the update function as the value of the inputField's "waitfor" + // attribute so that it is called when the check button is clicked. + function bindCheck() { + inputField.data('waitfor', that.update); + return; + } + + var gradeFn = getGradeFn; + + + bindCheck(); + + // Check whether application takes in state and there is a saved + // state to give it. If getStateSetter is specified but calling it + // fails, wait and try again, since the iframe might still be + // loading. + if (getStateSetter && getStoredState) { + var sval, jsonVal; + + try { + jsonVal = JSON.parse(getStoredState); + } catch (err) { + jsonVal = getStoredState; + } + + if (typeof(jsonVal) === "object") { + sval = jsonVal["state"]; + } else { + sval = jsonVal; + } + + + // Try calling setstate every 200ms while it throws an exception, + // up to five times; give up after that. + // (Functions in the iframe may not be ready when we first try + // calling it, but might just need more time. Give the functions + // more time.) + function whileloop(n) { + if (n < 5){ + try { + _deepKey(cWindow, getStateSetter)(sval); + } catch (err) { + setTimeout(whileloop(n+1), 200); + } + } + else { + console.debug("Error: could not set state"); + } + } + whileloop(0); + + } + + + return that; + } + + + function walkDOM() { + var newid; + + // Find all jsinput elements, and create a jsinput object for each one + var all = $(document).find('section[class="jsinput"]'); + + all.each(function(index, value) { + // Get just the mako variable 'id' from the id attribute + newid = $(value).attr("id").replace(/^inputtype_/, ""); + + + if (!jsinput.exists(newid)){ + var newJsElem = jsinputConstructor({ + id: newid, + elem: value, + }); + } + }); + } + + // This is ugly, but without a timeout pages with multiple/heavy jsinputs + // don't load properly. + if ($.isReady) { + setTimeout(walkDOM, 300); + } else { + $(document).ready(setTimeout(walkDOM, 300)); + } + +})(window.jsinput = window.jsinput || false); diff --git a/common/static/js/test/i18n.js b/common/static/js/test/i18n.js index 3cd6d52ae8..42131211a2 100644 --- a/common/static/js/test/i18n.js +++ b/common/static/js/test/i18n.js @@ -1 +1 @@ -window.gettext = window.ngettext = function(){}; +window.gettext = window.ngettext = function(s){return s;}; diff --git a/common/static/js/vendor/backbone-associations-min.js b/common/static/js/vendor/backbone-associations-min.js new file mode 100644 index 0000000000..77d398d962 --- /dev/null +++ b/common/static/js/vendor/backbone-associations-min.js @@ -0,0 +1,11 @@ +(function(){var v=this,g,h,w,m,r,s,z,o,A,B;"undefined"===typeof window?(g=require("underscore"),h=require("backbone"),"undefined"!==typeof exports&&(exports=module.exports=h)):(g=v._,h=v.Backbone);w=h.Model;m=h.Collection;r=w.prototype;s=m.prototype;A=/[\.\[\]]+/g;z="change add remove reset sort destroy".split(" ");B=["reset","sort"];h.Associations={VERSION:"0.5.0"};h.Associations.Many=h.Many="Many";h.Associations.One=h.One="One";o=h.AssociatedModel=h.Associations.AssociatedModel=w.extend({relations:void 0, +_proxyCalls:void 0,get:function(a){var c=r.get.call(this,a);return c?c:this._getAttr.apply(this,arguments)},set:function(a,c,d){var b;if(g.isObject(a)||a==null){b=a;d=c}else{b={};b[a]=c}a=this._set(b,d);this._processPendingEvents();return a},_set:function(a,c){var d,b,n,f,j=this;if(!a)return this;for(d in a){b||(b={});if(d.match(A)){var k=x(d);f=g.initial(k);k=k[k.length-1];f=this.get(f);if(f instanceof o){f=b[f.cid]||(b[f.cid]={model:f,data:{}});f.data[k]=a[d]}}else{f=b[this.cid]||(b[this.cid]={model:this, +data:{}});f.data[d]=a[d]}}if(b)for(n in b){f=b[n];this._setAttr.call(f.model,f.data,c)||(j=false)}else j=this._setAttr.call(this,a,c);return j},_setAttr:function(a,c){var d;c||(c={});if(c.unset)for(d in a)a[d]=void 0;this.parents=this.parents||[];this.relations&&g.each(this.relations,function(b){var d=b.key,f=b.relatedModel,j=b.collectionType,k=b.map,i=this.attributes[d],y=i&&i.idAttribute,e,q,l,p;f&&g.isString(f)&&(f=t(f));j&&g.isString(j)&&(j=t(j));k&&g.isString(k)&&(k=t(k));q=b.options?g.extend({}, +b.options,c):c;if(a[d]){e=g.result(a,d);e=k?k(e):e;if(b.type===h.Many){if(j&&!j.prototype instanceof m)throw Error("collectionType must inherit from Backbone.Collection");if(e instanceof m)l=e;else if(i){i._deferEvents=true;i.set(e,c);l=i}else{l=j?new j:this._createCollection(f);l.add(e,q)}}else if(b.type===h.One&&f)if(e instanceof o)l=e;else if(i)if(i&&e[y]&&i.get(y)===e[y]){i._deferEvents=true;i._set(e,c);l=i}else l=new f(e,q);else l=new f(e,q);if((p=a[d]=l)&&!p._proxyCallback){p._proxyCallback= +function(){return this._bubbleEvent.call(this,d,p,arguments)};p.on("all",p._proxyCallback,this)}}if(a.hasOwnProperty(d)){b=a[d];f=this.attributes[d];if(b){b.parents=b.parents||[];g.indexOf(b.parents,this)==-1&&b.parents.push(this)}else if(f&&f.parents.length>0)f.parents=g.difference(f.parents,[this])}},this);return r.set.call(this,a,c)},_bubbleEvent:function(a,c,d){var b=d[0].split(":"),n=b[0],f=d[0]=="nested-change",j=d[1],k=d[2],i=-1,h=c._proxyCalls,e,q=g.indexOf(z,n)!==-1;if(!f){g.size(b)>1&&(e= +b[1]);g.indexOf(B,n)!==-1&&(k=j);if(c instanceof m&&q&&j){var l=x(e),p=g.initial(l);(b=c.find(function(a){if(j===a)return true;if(!a)return false;var b=a.get(p);if((b instanceof o||b instanceof m)&&j===b)return true;b=a.get(l);if((b instanceof o||b instanceof m)&&j===b||b instanceof m&&k&&k===b)return true}))&&(i=c.indexOf(b))}e=a+(i!==-1&&(n==="change"||e)?"["+i+"]":"")+(e?"."+e:"");if(/\[\*\]/g.test(e))return this;b=e.replace(/\[\d+\]/g,"[*]");i=[];i.push.apply(i,d);i[0]=n+":"+e;h=c._proxyCalls= +h||{};if(this._isEventAvailable.call(this,h,e))return this;h[e]=true;if("change"===n){this._previousAttributes[a]=c._previousAttributes;this.changed[a]=c}this.trigger.apply(this,i);"change"===n&&this.get(e)!=d[2]&&this.trigger.apply(this,["nested-change",e,d[1]]);h&&e&&delete h[e];if(e!==b){i[0]=n+":"+b;this.trigger.apply(this,i)}return this}},_isEventAvailable:function(a,c){return g.find(a,function(a,b){return c.indexOf(b,c.length-b.length)!==-1})},_createCollection:function(a){var c=a;g.isString(c)&& +(c=t(c));if(c&&c.prototype instanceof o){a=new m;a.model=c}else throw Error("type must inherit from Backbone.AssociatedModel");return a},_processPendingEvents:function(){if(!this.visited){this.visited=true;this._deferEvents=false;g.each(this._pendingEvents,function(a){a.c.trigger.apply(a.c,a.a)});this._pendingEvents=[];g.each(this.relations,function(a){(a=this.attributes[a.key])&&a._processPendingEvents()},this);delete this.visited}},trigger:function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents|| +[];this._pendingEvents.push({c:this,a:arguments})}else r.trigger.apply(this,arguments)},toJSON:function(a){var c,d;if(!this.visited){this.visited=true;c=r.toJSON.apply(this,arguments);this.relations&&g.each(this.relations,function(b){var h=this.attributes[b.key];if(h){d=h.toJSON(a);c[b.key]=g.isArray(d)?g.compact(d):d}},this);delete this.visited}return c},clone:function(){return new this.constructor(this.toJSON())},_getAttr:function(a){var c=this,a=x(a),d,b;if(!(g.size(a)<1)){for(b=0;b= 0) { + if (callThat.call(context || this, this.calls[i]) === true) { + return this.calls[i]; + } + i--; + } + }; + + jasmine.Matchers.ArgThat = (function(_super) { + + __extends(ArgThat, _super); + + function ArgThat(matcher) { + this.matcher = matcher; + } + + ArgThat.prototype.jasmineMatches = function(actual) { + return this.matcher(actual); + }; + + return ArgThat; + + })(jasmine.Matchers.Any); + + jasmine.Matchers.ArgThat.prototype.matches = jasmine.Matchers.ArgThat.prototype.jasmineMatches; + + jasmine.argThat = function(expected) { + return new jasmine.Matchers.ArgThat(expected); + }; + + jasmine.Matchers.Capture = (function(_super) { + + __extends(Capture, _super); + + function Capture(captor) { + this.captor = captor; + } + + Capture.prototype.jasmineMatches = function(actual) { + this.captor.value = actual; + return true; + }; + + return Capture; + + })(jasmine.Matchers.Any); + + jasmine.Matchers.Capture.prototype.matches = jasmine.Matchers.Capture.prototype.jasmineMatches; + + Captor = (function() { + + function Captor() {} + + Captor.prototype.capture = function() { + return new jasmine.Matchers.Capture(this); + }; + + return Captor; + + })(); + + jasmine.captor = function() { + return new Captor(); + }; + +}).call(this); diff --git a/common/static/sass/_mixins-inherited.scss b/common/static/sass/_mixins-inherited.scss new file mode 100644 index 0000000000..82813153fa --- /dev/null +++ b/common/static/sass/_mixins-inherited.scss @@ -0,0 +1,454 @@ +// studio - utilities - INHERITED mixins and extends + +// NOTE: these are older/poorly architected mixins that we want to move away from using or refactor in the future. +// They are still referenced when styliing current interface elements and as such need to be preserved for the time being. +// talbs: we need to slowly ween ourselves off of these +// ==================== + + +// line-height (old way) +@function lh($amount: 1) { + @return $body-line-height * $amount; +} + +// inherited - vertical and horizontal centering +@mixin vertically-and-horizontally-centered ($height, $width) { + left: 50%; + margin-left: -$width / 2; + min-height: $height; + min-width: $width; + position: absolute; + top: 150px; +} + +// ==================== + +// inherited - dividers +.faded-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1) 50%, + rgba(200,200,200, 0))); + height: 1px; + width: 100%; +} + +.faded-hr-divider-medium { + @include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%, + rgba(240,240,240, 1) 50%, + rgba(240,240,240, 0))); + height: 1px; + width: 100%; +} + +.faded-hr-divider-light { + @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%, + rgba(255,255,255, 0.8) 50%, + rgba(255,255,255, 0))); + height: 1px; + width: 100%; +} + +.faded-vertical-divider { + @include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1) 50%, + rgba(200,200,200, 0))); + height: 100%; + width: 1px; +} + +.faded-vertical-divider-light { + @include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%, + rgba(255,255,255, 0.6) 50%, + rgba(255,255,255, 0))); + height: 100%; + width: 1px; +} + +.vertical-divider { + @extend .faded-vertical-divider; + position: relative; + + &::after { + @extend .faded-vertical-divider-light; + content: ""; + display: block; + position: absolute; + left: 1px; + } +} + +.horizontal-divider { + border: none; + @extend .faded-hr-divider; + position: relative; + + &::after { + @extend .faded-hr-divider-light; + content: ""; + display: block; + position: absolute; + top: 1px; + } +} + +.fade-right-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1))); + border: none; +} + +.fade-left-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%, + rgba(200,200,200, 0))); + border: none; +} + +// ==================== + +// inherited - ui +.window { + @include clearfix(); + box-shadow: 0 1px 1px $shadow-l1; + border-radius: 3px; + margin-bottom: $baseline; + border: 1px solid $gray-l2; + background: $white; +} + +// ==================== + +// mixins - grandfathered +@mixin button { + @include font-size(14); + @include transition(background-color 0.15s, box-shadow 0.15s); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0); + display: inline-block; + padding: ($baseline/5) $baseline ($baseline/4); + font-weight: 700; + + &.disabled { + border: 1px solid $gray-l1 !important; + border-radius: 3px !important; + background: $gray-l1 !important; + color: $gray-d1 !important; + pointer-events: none; + cursor: none; + &:hover { + box-shadow: 0 0 0 0 !important; + } + } + + &:hover, &.active { + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15); + } +} + +@mixin green-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + border: 1px solid $green-d1; + border-radius: 3px; + background-color: $green; + color: $white; + + &:hover { + background-color: $green-s1; + color: $white; + } + + &.disabled { + border: 1px solid $green-l3 !important; + background: $green-l3 !important; + color: $white !important; + box-shadow: none; + } +} + +@mixin blue-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + border: 1px solid $blue-d1; + border-radius: 3px; + background-color: $blue; + color: $white; + + &:hover, &.active { + background-color: $blue-s2; + color: $white; + } + + &.disabled { + box-shadow: none; + border: 1px solid $blue-l3 !important; + background: $blue-l3 !important; + color: $white !important; + } +} + +@mixin red-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + border: 1px solid $red-d1; + border-radius: 3px; + background-color: $red; + color: $white; + + &:hover, &.active { + background-color: $red-s1; + color: $white; + } + + &.disabled { + box-shadow: none; + border: 1px solid $red-l3 !important; + background: $red-l3 !important; + color: $white !important; + } +} + +@mixin pink-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + border: 1px solid $pink-d1; + border-radius: 3px; + background-color: $pink; + color: $white; + + &:hover, &.active { + background-color: $pink-s1; + color: $white; + } + + &.disabled { + box-shadow: none; + border: 1px solid $pink-l3 !important; + background: $pink-l3 !important; + color: $white !important; + } +} + +@mixin orange-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + border: 1px solid $orange-d1; + border-radius: 3px; + background-color: $orange; + color: $gray-d2; + + &:hover { + background-color: $orange-s2; + color: $gray-d2; + } + + &.disabled { + border: 1px solid $orange-l3 !important; + background: $orange-l2 !important; + color: $gray-l1 !important; + box-shadow: none; + } +} + +@mixin white-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0)); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + border: 1px solid $mediumGrey; + border-radius: 3px; + background-color: #dfe5eb; + color: rgb(92, 103, 122); + text-shadow: 0 1px 0 rgba(255, 255, 255, .5); + + &:hover { + background-color: rgb(222, 236, 247); + color: rgb(92, 103, 122); + } +} + +@mixin grey-button { + @include button; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + border: 1px solid $gray-d2; + border-radius: 3px; + background-color: #d1dae3; + color: #6d788b; + + &:hover { + background-color: #d9e3ee; + color: #6d788b; + } +} + +@mixin gray-button { + @include button; + @include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0)); + box-shadow: 0 1px 0 $white-t1 inset; + border: 1px solid $gray-d1; + border-radius: 3px; + background-color: $gray-d2; + color: $gray-l3; + + &:hover { + background-color: $gray-d3; + color: $white; + } +} + +@mixin dark-grey-button { + @include button; + border: 1px solid $gray-d2; + border-radius: 3px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1; + box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; + color: $white; + + &:hover { + background-color: $gray-d4; + color: $white; + } +} + +@mixin edit-box { + box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; + padding: 15px 20px; + border-radius: 3px; + background-color: $lightBluishGrey2; + color: #3c3c3c; + + label { + color: $baseFontColor; + } + + input, + textarea { + border: 1px solid $darkGrey; + } + + textarea { + min-height: 80px; + } + + h5 { + margin-bottom: 8px; + color: #fff; + font-weight: 700; + } + + .row { + margin-bottom: 10px; + padding: 0; + border: none; + } + + .save-button { + @include blue-button; + margin-top: 0; + } + + .cancel-button { + @include white-button; + margin-top: 0; + } +} + +@mixin tree-view { + border: 1px solid $mediumGrey; + background: $lightGrey; + + .branch { + margin-bottom: 10px; + + &.collapsed { + margin-bottom: 0; + } + } + + .branch > .section-item { + border-top: 1px solid #c5cad4; + } + + .section-item { + position: relative; + display: block; + padding: 6px 8px 8px 16px; + background: #edf1f5; + font-size: 13px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.editing { + background: #fffcf1; + } + + .draft-item:after, + .public-item:after, + .private-item:after { + margin-left: 3px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + } + + .draft-item:after { + content: "- draft"; + } + + .private-item:after { + content: "- private"; + } + + .private-item { + color: #a4aab7; + } + + .draft-item { + color: #9f7d10; + } + } + + a { + color: $baseFontColor; + + &.new-unit-item { + color: #6d788b; + } + } + + ol { + .section-item { + padding-left: 56px; + } + + .new-unit-item { + margin-left: 56px; + } + } + + ol ol { + .section-item { + padding-left: 96px; + } + + .new-unit-item { + margin-left: 96px; + } + } +} + +// ==================== + +// sunsetted mixins +@mixin active { + @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); + box-shadow: 0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset; + background-color: rgba(255, 255, 255, .3); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index e5548aeaaa..64248734c3 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -117,7 +117,7 @@ // extends - buttons .btn { @include box-sizing(border-box); - @include transition(color 0.25s ease-in-out, border-color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out); + @include transition(color 0.25s ease-in-out 0s, border-color 0.25s ease-in-out 0s, background 0.25s ease-in-out 0s, box-shadow 0.25s ease-in-out 0s); display: inline-block; cursor: pointer; @@ -140,11 +140,11 @@ // pill button .btn-pill { - @include border-radius($baseline/5); + border-radius: ($baseline/5); } .btn-rounded { - @include border-radius($baseline/2); + border-radius: ($baseline/2); } // primary button @@ -158,14 +158,14 @@ text-align: center; &:hover, &:active { - @include box-shadow(0 2px 1px $shadow-l1); + box-shadow: 0 2px 1px $shadow-l1; } &.current, &.active { - @include box-shadow(inset 1px 1px 2px $shadow-d1); + box-shadow: inset 1px 1px 2px $shadow-d1; &:hover, &:active { - @include box-shadow(inset 1px 1px 1px $shadow-d1); + box-shadow: inset 1px 1px 1px $shadow-d1; } } } @@ -190,9 +190,54 @@ } } -// UI archetypes - well -.ui-well { - @include box-shadow(inset 0 1px 2px 1px $shadow); - padding: ($baseline*0.75); +.btn-flat-outline { + @extend .t-action4; + @include transition(all .15s); + font-weight: 600; + text-align: center; + border-radius: ($baseline/4); + border: 1px solid $blue-l2; + padding: 1px ($baseline/2) 2px ($baseline/2); + background-color: $white; + color: $blue-l2; + + &:hover { + border: 1px solid $blue; + background-color: $blue; + color: $white; + } + + &.is-disabled, + &[disabled="disabled"]{ + border: 1px solid $gray-l2; + background-color: $gray-l4; + color: $gray-l2; + pointer-events: none; + } } +// button with no button shell until hover for understated actions +.btn-non { + @include transition(all .15s); + border: none; + border-radius: ($baseline/4); + background: none; + padding: 3px ($baseline/2); + vertical-align: middle; + color: $gray-l1; + + &:hover { + background-color: $gray-l1; + color: $white; + } + + span { + @extend .text-sr; + } +} + +// UI archetypes - well +.ui-well { + box-shadow: inset 0 1px 2px 1px $shadow; + padding: ($baseline*0.75); +} diff --git a/common/static/sass/bourbon/_bourbon-deprecated-upcoming.scss b/common/static/sass/bourbon/_bourbon-deprecated-upcoming.scss new file mode 100644 index 0000000000..5332496d82 --- /dev/null +++ b/common/static/sass/bourbon/_bourbon-deprecated-upcoming.scss @@ -0,0 +1,13 @@ +//************************************************************************// +// These mixins/functions are deprecated +// They will be removed in the next MAJOR version release +//************************************************************************// +@mixin box-shadow ($shadows...) { + @include prefixer(box-shadow, $shadows, spec); + @warn "box-shadow is deprecated and will be removed in the next major version release"; +} + +@mixin background-size ($lengths...) { + @include prefixer(background-size, $lengths, spec); + @warn "background-size is deprecated and will be removed in the next major version release"; +} diff --git a/common/static/sass/bourbon/_bourbon.scss b/common/static/sass/bourbon/_bourbon.scss index 27b056e303..53fbca877f 100644 --- a/common/static/sass/bourbon/_bourbon.scss +++ b/common/static/sass/bourbon/_bourbon.scss @@ -1,35 +1,59 @@ +// Custom Helpers +@import "helpers/deprecated-webkit-gradient"; +@import "helpers/gradient-positions-parser"; +@import "helpers/linear-positions-parser"; +@import "helpers/radial-arg-parser"; +@import "helpers/radial-positions-parser"; +@import "helpers/render-gradients"; +@import "helpers/shape-size-stripper"; + // Custom Functions -@import "functions/deprecated-webkit-gradient"; +@import "functions/compact"; @import "functions/flex-grid"; @import "functions/grid-width"; @import "functions/linear-gradient"; @import "functions/modular-scale"; +@import "functions/px-to-em"; @import "functions/radial-gradient"; -@import "functions/render-gradients"; @import "functions/tint-shade"; +@import "functions/transition-property-name"; // CSS3 Mixins @import "css3/animation"; @import "css3/appearance"; +@import "css3/backface-visibility"; +@import "css3/background"; @import "css3/background-image"; -@import "css3/background-size"; @import "css3/border-image"; @import "css3/border-radius"; -@import "css3/box-shadow"; @import "css3/box-sizing"; @import "css3/columns"; @import "css3/flex-box"; +@import "css3/font-face"; +@import "css3/hidpi-media-query"; +@import "css3/image-rendering"; @import "css3/inline-block"; +@import "css3/keyframes"; @import "css3/linear-gradient"; +@import "css3/perspective"; @import "css3/radial-gradient"; @import "css3/transform"; @import "css3/transition"; @import "css3/user-select"; +@import "css3/placeholder"; // Addons & other mixins @import "addons/button"; @import "addons/clearfix"; @import "addons/font-family"; +@import "addons/hide-text"; @import "addons/html5-input-types"; @import "addons/position"; +@import "addons/prefixer"; +@import "addons/retina-image"; +@import "addons/size"; @import "addons/timing-functions"; +@import "addons/triangle"; + +// Soon to be deprecated Mixins +@import "bourbon-deprecated-upcoming"; diff --git a/common/static/sass/bourbon/addons/_button.scss b/common/static/sass/bourbon/addons/_button.scss index 1d32125140..3ae393c090 100644 --- a/common/static/sass/bourbon/addons/_button.scss +++ b/common/static/sass/bourbon/addons/_button.scss @@ -34,6 +34,11 @@ @include pill($base-color); } } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } } @@ -59,18 +64,19 @@ } border: 1px solid $border; - @include border-radius (3px); - @include box-shadow (inset 0 1px 0 0 $inset-shadow); + border-radius: 3px; + box-shadow: inset 0 1px 0 0 $inset-shadow; color: $color; - display: inline; + display: inline-block; font-size: 11px; font-weight: bold; @include linear-gradient ($base-color, $stop-gradient); - padding: 6px 18px 7px; + padding: 7px 18px; + text-decoration: none; text-shadow: 0 1px 0 $text-shadow; - -webkit-background-clip: padding-box; + background-clip: padding-box; - &:hover { + &:hover:not(:disabled) { $base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%); $inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%); $stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%); @@ -81,12 +87,12 @@ $stop-gradient-hover: grayscale($stop-gradient-hover); } - @include box-shadow (inset 0 1px 0 0 $inset-shadow-hover); + box-shadow: inset 0 1px 0 0 $inset-shadow-hover; cursor: pointer; @include linear-gradient ($base-color-hover, $stop-gradient-hover); } - &:active { + &:active:not(:disabled) { $border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%); $inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%); @@ -96,7 +102,7 @@ } border: 1px solid $border-active; - @include box-shadow (inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee); + box-shadow: inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee; } } @@ -130,19 +136,19 @@ border: 1px solid $border; border-bottom: 1px solid $border-bottom; - @include border-radius(5px); - @include box-shadow(inset 0 1px 0 0 $inset-shadow); + border-radius: 5px; + box-shadow: inset 0 1px 0 0 $inset-shadow; color: $color; - display: inline; + display: inline-block; font-size: 14px; font-weight: bold; @include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%); - padding: 7px 20px 8px; + padding: 8px 20px; text-align: center; text-decoration: none; text-shadow: 0 -1px 1px $text-shadow; - &:hover { + &:hover:not(:disabled) { $first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18); $second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51); $third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66); @@ -162,14 +168,14 @@ $fourth-stop-hover 100%); } - &:active { + &:active:not(:disabled) { $inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122); @if $grayscale == true { $inset-shadow-active: grayscale($inset-shadow-active); } - @include box-shadow(inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff); + box-shadow: inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff; } } @@ -201,20 +207,21 @@ border: 1px solid $border-top; border-color: $border-top $border-sides $border-bottom; - @include border-radius(16px); - @include box-shadow(inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3); + border-radius: 16px; + box-shadow: inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3; color: $color; - display: inline; + display: inline-block; font-size: 11px; font-weight: normal; line-height: 1; @include linear-gradient ($base-color, $stop-gradient); - padding: 3px 16px 5px; + padding: 5px 16px; text-align: center; + text-decoration: none; text-shadow: 0 -1px 1px $text-shadow; - -webkit-background-clip: padding-box; + background-clip: padding-box; - &:hover { + &:hover:not(:disabled) { $base-color-hover: adjust-color($base-color, $lightness: -4.5%); $border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%); $border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%); @@ -235,14 +242,14 @@ border: 1px solid $border-top; border-color: $border-top $border-sides $border-bottom; - @include box-shadow(inset 0 1px 0 0 $inset-shadow-hover); + box-shadow: inset 0 1px 0 0 $inset-shadow-hover; cursor: pointer; @include linear-gradient ($base-color-hover, $stop-gradient-hover); text-shadow: 0 -1px 1px $text-shadow-hover; - -webkit-background-clip: padding-box; + background-clip: padding-box; } - &:active { + &:active:not(:disabled) { $active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%); $border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%); $border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%); @@ -260,8 +267,7 @@ background: $active-color; border: 1px solid $border-active; border-bottom: 1px solid $border-bottom-active; - @include box-shadow(inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff); + box-shadow: inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff; text-shadow: 0 -1px 1px $text-shadow-active; } } - diff --git a/common/static/sass/bourbon/addons/_clearfix.scss b/common/static/sass/bourbon/addons/_clearfix.scss index a9f6a795c5..ca9903cf02 100644 --- a/common/static/sass/bourbon/addons/_clearfix.scss +++ b/common/static/sass/bourbon/addons/_clearfix.scss @@ -12,11 +12,11 @@ // } @mixin clearfix { - zoom: 1; + *zoom: 1; &:before, &:after { - content: ""; + content: " "; display: table; } diff --git a/common/static/sass/bourbon/addons/_hide-text.scss b/common/static/sass/bourbon/addons/_hide-text.scss new file mode 100644 index 0000000000..68d4bf86cb --- /dev/null +++ b/common/static/sass/bourbon/addons/_hide-text.scss @@ -0,0 +1,5 @@ +@mixin hide-text { + color: transparent; + font: 0/0 a; + text-shadow: none; +} diff --git a/common/static/sass/bourbon/addons/_html5-input-types.scss b/common/static/sass/bourbon/addons/_html5-input-types.scss index 9d86fbb4d4..b184382d91 100644 --- a/common/static/sass/bourbon/addons/_html5-input-types.scss +++ b/common/static/sass/bourbon/addons/_html5-input-types.scss @@ -21,15 +21,35 @@ $inputs-list: 'input[type="email"]', 'input[type="week"]'; $unquoted-inputs-list: (); - @each $input-type in $inputs-list { $unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma); } $all-text-inputs: $unquoted-inputs-list; + +// Hover Pseudo-class +//************************************************************************// +$all-text-inputs-hover: (); +@each $input-type in $unquoted-inputs-list { + $input-type-hover: $input-type + ":hover"; + $all-text-inputs-hover: append($all-text-inputs-hover, $input-type-hover, comma); +} + +// Focus Pseudo-class +//************************************************************************// +$all-text-inputs-focus: (); +@each $input-type in $unquoted-inputs-list { + $input-type-focus: $input-type + ":focus"; + $all-text-inputs-focus: append($all-text-inputs-focus, $input-type-focus, comma); +} + // You must use interpolation on the variable: // #{$all-text-inputs} +// #{$all-text-inputs-hover} +// #{$all-text-inputs-focus} + +// Example //************************************************************************// // #{$all-text-inputs}, textarea { // border: 1px solid red; diff --git a/common/static/sass/bourbon/addons/_position.scss b/common/static/sass/bourbon/addons/_position.scss index 6ad330f1df..faad1cae50 100644 --- a/common/static/sass/bourbon/addons/_position.scss +++ b/common/static/sass/bourbon/addons/_position.scss @@ -12,19 +12,31 @@ position: $position; - @if not(unitless($top)) { + @if $top == auto { + top: $top; + } + @else if not(unitless($top)) { top: $top; } - @if not(unitless($right)) { + @if $right == auto { + right: $right; + } + @else if not(unitless($right)) { right: $right; } - @if not(unitless($bottom)) { + @if $bottom == auto { + bottom: $bottom; + } + @else if not(unitless($bottom)) { bottom: $bottom; } - @if not(unitless($left)) { + @if $left == auto { + left: $left; + } + @else if not(unitless($left)) { left: $left; } } diff --git a/common/static/sass/bourbon/addons/_prefixer.scss b/common/static/sass/bourbon/addons/_prefixer.scss new file mode 100644 index 0000000000..6bfd23a1dd --- /dev/null +++ b/common/static/sass/bourbon/addons/_prefixer.scss @@ -0,0 +1,49 @@ +//************************************************************************// +// Example: @include prefixer(border-radius, $radii, webkit ms spec); +//************************************************************************// +$prefix-for-webkit: true !default; +$prefix-for-mozilla: true !default; +$prefix-for-microsoft: true !default; +$prefix-for-opera: true !default; +$prefix-for-spec: true !default; // required for keyframe mixin + +@mixin prefixer ($property, $value, $prefixes) { + @each $prefix in $prefixes { + @if $prefix == webkit { + @if $prefix-for-webkit { + -webkit-#{$property}: $value; + } + } + @else if $prefix == moz { + @if $prefix-for-mozilla { + -moz-#{$property}: $value; + } + } + @else if $prefix == ms { + @if $prefix-for-microsoft { + -ms-#{$property}: $value; + } + } + @else if $prefix == o { + @if $prefix-for-opera { + -o-#{$property}: $value; + } + } + @else if $prefix == spec { + @if $prefix-for-spec { + #{$property}: $value; + } + } + @else { + @warn "Unrecognized prefix: #{$prefix}"; + } + } +} + +@mixin disable-prefix-for-all() { + $prefix-for-webkit: false; + $prefix-for-mozilla: false; + $prefix-for-microsoft: false; + $prefix-for-opera: false; + $prefix-for-spec: false; +} diff --git a/common/static/sass/bourbon/addons/_retina-image.scss b/common/static/sass/bourbon/addons/_retina-image.scss new file mode 100644 index 0000000000..a84b6faebc --- /dev/null +++ b/common/static/sass/bourbon/addons/_retina-image.scss @@ -0,0 +1,32 @@ +@mixin retina-image($filename, $background-size, $extension: png, $retina-filename: null, $asset-pipeline: false) { + @if $asset-pipeline { + background-image: image-url("#{$filename}.#{$extension}"); + } + @else { + background-image: url("#{$filename}.#{$extension}"); + } + + @include hidpi { + + @if $asset-pipeline { + @if $retina-filename { + background-image: image-url("#{$retina-filename}.#{$extension}"); + } + @else { + background-image: image-url("#{$filename}@2x.#{$extension}"); + } + } + + @else { + @if $retina-filename { + background-image: url("#{$retina-filename}.#{$extension}"); + } + @else { + background-image: url("#{$filename}@2x.#{$extension}"); + } + } + + background-size: $background-size; + + } +} diff --git a/common/static/sass/bourbon/addons/_size.scss b/common/static/sass/bourbon/addons/_size.scss new file mode 100644 index 0000000000..342e41b79f --- /dev/null +++ b/common/static/sass/bourbon/addons/_size.scss @@ -0,0 +1,44 @@ +@mixin size($size) { + @if length($size) == 1 { + @if $size == auto { + width: $size; + height: $size; + } + + @else if unitless($size) { + width: $size + px; + height: $size + px; + } + + @else if not(unitless($size)) { + width: $size; + height: $size; + } + } + + // Width x Height + @if length($size) == 2 { + $width: nth($size, 1); + $height: nth($size, 2); + + @if $width == auto { + width: $width; + } + @else if not(unitless($width)) { + width: $width; + } + @else if unitless($width) { + width: $width + px; + } + + @if $height == auto { + height: $height; + } + @else if not(unitless($height)) { + height: $height; + } + @else if unitless($height) { + height: $height + px; + } + } +} diff --git a/common/static/sass/bourbon/addons/_triangle.scss b/common/static/sass/bourbon/addons/_triangle.scss new file mode 100644 index 0000000000..0e02aca2ca --- /dev/null +++ b/common/static/sass/bourbon/addons/_triangle.scss @@ -0,0 +1,45 @@ +@mixin triangle ($size, $color, $direction) { + height: 0; + width: 0; + + @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) { + border-color: transparent; + border-style: solid; + border-width: $size / 2; + + @if $direction == up { + border-bottom-color: $color; + + } @else if $direction == right { + border-left-color: $color; + + } @else if $direction == down { + border-top-color: $color; + + } @else if $direction == left { + border-right-color: $color; + } + } + + @else if ($direction == up-right) or ($direction == up-left) { + border-top: $size solid $color; + + @if $direction == up-right { + border-left: $size solid transparent; + + } @else if $direction == up-left { + border-right: $size solid transparent; + } + } + + @else if ($direction == down-right) or ($direction == down-left) { + border-bottom: $size solid $color; + + @if $direction == down-right { + border-left: $size solid transparent; + + } @else if $direction == down-left { + border-right: $size solid transparent; + } + } +} diff --git a/common/static/sass/bourbon/css3/_animation.scss b/common/static/sass/bourbon/css3/_animation.scss index f99e06eb6f..08c3dbf157 100644 --- a/common/static/sass/bourbon/css3/_animation.scss +++ b/common/static/sass/bourbon/css3/_animation.scss @@ -2,170 +2,51 @@ // Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties. // Official animation shorthand property. -@mixin animation ($animation-1, - $animation-2: false, $animation-3: false, - $animation-4: false, $animation-5: false, - $animation-6: false, $animation-7: false, - $animation-8: false, $animation-9: false) - { - $full: compact($animation-1, $animation-2, $animation-3, $animation-4, - $animation-5, $animation-6, $animation-7, $animation-8, $animation-9); - - -webkit-animation: $full; - -moz-animation: $full; - animation: $full; +@mixin animation ($animations...) { + @include prefixer(animation, $animations, webkit moz spec); } // Individual Animation Properties -@mixin animation-name ($name-1, - $name-2: false, $name-3: false, - $name-4: false, $name-5: false, - $name-6: false, $name-7: false, - $name-8: false, $name-9: false) - { - $full: compact($name-1, $name-2, $name-3, $name-4, - $name-5, $name-6, $name-7, $name-8, $name-9); - - -webkit-animation-name: $full; - -moz-animation-name: $full; - animation-name: $full; +@mixin animation-name ($names...) { + @include prefixer(animation-name, $names, webkit moz spec); } -@mixin animation-duration ($time-1: 0, - $time-2: false, $time-3: false, - $time-4: false, $time-5: false, - $time-6: false, $time-7: false, - $time-8: false, $time-9: false) - { - $full: compact($time-1, $time-2, $time-3, $time-4, - $time-5, $time-6, $time-7, $time-8, $time-9); - - -webkit-animation-duration: $full; - -moz-animation-duration: $full; - animation-duration: $full; +@mixin animation-duration ($times...) { + @include prefixer(animation-duration, $times, webkit moz spec); } -@mixin animation-timing-function ($motion-1: ease, -// ease | linear | ease-in | ease-out | ease-in-out - $motion-2: false, $motion-3: false, - $motion-4: false, $motion-5: false, - $motion-6: false, $motion-7: false, - $motion-8: false, $motion-9: false) - { - $full: compact($motion-1, $motion-2, $motion-3, $motion-4, - $motion-5, $motion-6, $motion-7, $motion-8, $motion-9); - - -webkit-animation-timing-function: $full; - -moz-animation-timing-function: $full; - animation-timing-function: $full; +@mixin animation-timing-function ($motions...) { +// ease | linear | ease-in | ease-out | ease-in-out + @include prefixer(animation-timing-function, $motions, webkit moz spec); } -@mixin animation-iteration-count ($value-1: 1, -// infinite | - $value-2: false, $value-3: false, - $value-4: false, $value-5: false, - $value-6: false, $value-7: false, - $value-8: false, $value-9: false) - { - $full: compact($value-1, $value-2, $value-3, $value-4, - $value-5, $value-6, $value-7, $value-8, $value-9); - - -webkit-animation-iteration-count: $full; - -moz-animation-iteration-count: $full; - animation-iteration-count: $full; +@mixin animation-iteration-count ($values...) { +// infinite | + @include prefixer(animation-iteration-count, $values, webkit moz spec); } -@mixin animation-direction ($direction-1: normal, -// normal | alternate - $direction-2: false, $direction-3: false, - $direction-4: false, $direction-5: false, - $direction-6: false, $direction-7: false, - $direction-8: false, $direction-9: false) - { - $full: compact($direction-1, $direction-2, $direction-3, $direction-4, - $direction-5, $direction-6, $direction-7, $direction-8, $direction-9); - - -webkit-animation-direction: $full; - -moz-animation-direction: $full; - animation-direction: $full; +@mixin animation-direction ($directions...) { +// normal | alternate + @include prefixer(animation-direction, $directions, webkit moz spec); } -@mixin animation-play-state ($state-1: running, -// running | paused - $state-2: false, $state-3: false, - $state-4: false, $state-5: false, - $state-6: false, $state-7: false, - $state-8: false, $state-9: false) - { - $full: compact($state-1, $state-2, $state-3, $state-4, - $state-5, $state-6, $state-7, $state-8, $state-9); - - -webkit-animation-play-state: $full; - -moz-animation-play-state: $full; - animation-play-state: $full; +@mixin animation-play-state ($states...) { +// running | paused + @include prefixer(animation-play-state, $states, webkit moz spec); } -@mixin animation-delay ($time-1: 0, - $time-2: false, $time-3: false, - $time-4: false, $time-5: false, - $time-6: false, $time-7: false, - $time-8: false, $time-9: false) - { - $full: compact($time-1, $time-2, $time-3, $time-4, - $time-5, $time-6, $time-7, $time-8, $time-9); - - -webkit-animation-delay: $full; - -moz-animation-delay: $full; - animation-delay: $full; +@mixin animation-delay ($times...) { + @include prefixer(animation-delay, $times, webkit moz spec); } -@mixin animation-fill-mode ($mode-1: none, -// http://goo.gl/l6ckm -// none | forwards | backwards | both - $mode-2: false, $mode-3: false, - $mode-4: false, $mode-5: false, - $mode-6: false, $mode-7: false, - $mode-8: false, $mode-9: false) - { - $full: compact($mode-1, $mode-2, $mode-3, $mode-4, - $mode-5, $mode-6, $mode-7, $mode-8, $mode-9); - - -webkit-animation-fill-mode: $full; - -moz-animation-fill-mode: $full; - animation-fill-mode: $full; +@mixin animation-fill-mode ($modes...) { +// none | forwards | backwards | both + @include prefixer(animation-fill-mode, $modes, webkit moz spec); } - - -// Deprecated -@mixin animation-basic ($name, $time: 0, $motion: ease) { - $length-of-name: length($name); - $length-of-time: length($time); - $length-of-motion: length($motion); - - @if $length-of-name > 1 { - @include animation-name(zip($name)); - } @else { - @include animation-name( $name); - } - - @if $length-of-time > 1 { - @include animation-duration(zip($time)); - } @else { - @include animation-duration( $time); - } - - @if $length-of-motion > 1 { - @include animation-timing-function(zip($motion)); - } @else { - @include animation-timing-function( $motion); - } - @warn "The animation-basic mixin is deprecated. Use the animation mixin instead."; -} - diff --git a/common/static/sass/bourbon/css3/_appearance.scss b/common/static/sass/bourbon/css3/_appearance.scss index 548767e166..3eb16e45de 100644 --- a/common/static/sass/bourbon/css3/_appearance.scss +++ b/common/static/sass/bourbon/css3/_appearance.scss @@ -1,7 +1,3 @@ @mixin appearance ($value) { - -webkit-appearance: $value; - -moz-appearance: $value; - -ms-appearance: $value; - -o-appearance: $value; - appearance: $value; + @include prefixer(appearance, $value, webkit moz ms o spec); } diff --git a/common/static/sass/bourbon/css3/_backface-visibility.scss b/common/static/sass/bourbon/css3/_backface-visibility.scss new file mode 100644 index 0000000000..1161fe60dd --- /dev/null +++ b/common/static/sass/bourbon/css3/_backface-visibility.scss @@ -0,0 +1,6 @@ +//************************************************************************// +// Backface-visibility mixin +//************************************************************************// +@mixin backface-visibility($visibility) { + @include prefixer(backface-visibility, $visibility, webkit spec); +} diff --git a/common/static/sass/bourbon/css3/_background-image.scss b/common/static/sass/bourbon/css3/_background-image.scss index c23cef7c31..17016b91b9 100644 --- a/common/static/sass/bourbon/css3/_background-image.scss +++ b/common/static/sass/bourbon/css3/_background-image.scss @@ -3,42 +3,35 @@ // gradients, or for stringing multiple gradients together. //************************************************************************// -@mixin background-image( - $image-1 , $image-2: false, - $image-3: false, $image-4: false, - $image-5: false, $image-6: false, - $image-7: false, $image-8: false, - $image-9: false, $image-10: false -) { - $images: compact($image-1, $image-2, - $image-3, $image-4, - $image-5, $image-6, - $image-7, $image-8, - $image-9, $image-10); - - background-image: add-prefix($images, webkit); - background-image: add-prefix($images, moz); - background-image: add-prefix($images, ms); - background-image: add-prefix($images, o); - background-image: add-prefix($images); +@mixin background-image($images...) { + background-image: _add-prefix($images, webkit); + background-image: _add-prefix($images); } - -@function add-prefix($images, $vendor: false) { +@function _add-prefix($images, $vendor: false) { $images-prefixed: (); - + $gradient-positions: false; @for $i from 1 through length($images) { $type: type-of(nth($images, $i)); // Get type of variable - List or String // If variable is a list - Gradient @if $type == list { - $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) - $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) + $gradient-type: nth(nth($images, $i), 1); // linear or radial + $gradient-pos: null; + $gradient-args: null; - $gradient: render-gradients($gradient-args, $gradient-type, $vendor); + @if ($gradient-type == linear) or ($gradient-type == radial) { + $gradient-pos: nth(nth($images, $i), 2); // Get gradient position + $gradient-args: nth(nth($images, $i), 3); // Get actual gradient (red, blue) + } + @else { + $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) + } + + $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); + $gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); $images-prefixed: append($images-prefixed, $gradient, comma); } - // If variable is a string - Image @else if $type == string { $images-prefixed: join($images-prefixed, nth($images, $i), comma); @@ -47,11 +40,9 @@ @return $images-prefixed; } - - //Examples: //@include background-image(linear-gradient(top, orange, red)); //@include background-image(radial-gradient(50% 50%, cover circle, orange, red)); //@include background-image(url("/images/a.png"), linear-gradient(orange, red)); //@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png")); - //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red); + //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red)); diff --git a/common/static/sass/bourbon/css3/_background-size.scss b/common/static/sass/bourbon/css3/_background-size.scss deleted file mode 100644 index 4bba11027d..0000000000 --- a/common/static/sass/bourbon/css3/_background-size.scss +++ /dev/null @@ -1,15 +0,0 @@ -@mixin background-size ($length-1, - $length-2: false, $length-3: false, - $length-4: false, $length-5: false, - $length-6: false, $length-7: false, - $length-8: false, $length-9: false) - { - $full: compact($length-1, $length-2, $length-3, $length-4, - $length-5, $length-6, $length-7, $length-8, $length-9); - - -webkit-background-size: $full; - -moz-background-size: $full; - -ms-background-size: $full; - -o-background-size: $full; - background-size: $full; -} diff --git a/common/static/sass/bourbon/css3/_background.scss b/common/static/sass/bourbon/css3/_background.scss new file mode 100644 index 0000000000..766d5d3224 --- /dev/null +++ b/common/static/sass/bourbon/css3/_background.scss @@ -0,0 +1,103 @@ +//************************************************************************// +// Background property for adding multiple backgrounds using shorthand +// notation. +//************************************************************************// + +@mixin background( + $background-1 , $background-2: false, + $background-3: false, $background-4: false, + $background-5: false, $background-6: false, + $background-7: false, $background-8: false, + $background-9: false, $background-10: false, + $fallback: false +) { + $backgrounds: compact($background-1, $background-2, + $background-3, $background-4, + $background-5, $background-6, + $background-7, $background-8, + $background-9, $background-10); + + $fallback-color: false; + @if (type-of($fallback) == color) or ($fallback == "transparent") { + $fallback-color: $fallback; + } + @else { + $fallback-color: _extract-background-color($backgrounds); + } + + @if $fallback-color { + background-color: $fallback-color; + } + background: _background-add-prefix($backgrounds, webkit); + background: _background-add-prefix($backgrounds); +} + +@function _extract-background-color($backgrounds) { + $final-bg-layer: nth($backgrounds, length($backgrounds)); + @if type-of($final-bg-layer) == list { + @for $i from 1 through length($final-bg-layer) { + $value: nth($final-bg-layer, $i); + @if type-of($value) == color { + @return $value; + } + } + } + @return false; +} + +@function _background-add-prefix($backgrounds, $vendor: false) { + $backgrounds-prefixed: (); + + @for $i from 1 through length($backgrounds) { + $shorthand: nth($backgrounds, $i); // Get member for current index + $type: type-of($shorthand); // Get type of variable - List (gradient) or String (image) + + // If shorthand is a list (gradient) + @if $type == list { + $first-member: nth($shorthand, 1); // Get first member of shorthand + + // Linear Gradient + @if index(linear radial, nth($first-member, 1)) { + $gradient-type: nth($first-member, 1); // linear || radial + $gradient-args: false; + $gradient-positions: false; + $shorthand-start: false; + @if type-of($first-member) == list { // Linear gradient plus additional shorthand values - lg(red,orange)repeat,... + $gradient-positions: nth($first-member, 2); + $gradient-args: nth($first-member, 3); + $shorthand-start: 2; + } + @else { // Linear gradient only - lg(red,orange),... + $gradient-positions: nth($shorthand, 2); + $gradient-args: nth($shorthand, 3); // Get gradient (red, blue) + } + + $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-positions); + $gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); + + // Append any additional shorthand args to gradient + @if $shorthand-start { + @for $j from $shorthand-start through length($shorthand) { + $gradient: join($gradient, nth($shorthand, $j), space); + } + } + $backgrounds-prefixed: append($backgrounds-prefixed, $gradient, comma); + } + // Image with additional properties + @else { + $backgrounds-prefixed: append($backgrounds-prefixed, $shorthand, comma); + } + } + // If shorthand is a simple string (color or image) + @else if $type == string { + $backgrounds-prefixed: join($backgrounds-prefixed, $shorthand, comma); + } + } + @return $backgrounds-prefixed; +} + +//Examples: + //@include background(linear-gradient(top, orange, red)); + //@include background(radial-gradient(circle at 40% 40%, orange, red)); + //@include background(url("/images/a.png") no-repeat, linear-gradient(orange, red)); + //@include background(url("image.png") center center, linear-gradient(orange, red), url("image.png")); diff --git a/common/static/sass/bourbon/css3/_border-image.scss b/common/static/sass/bourbon/css3/_border-image.scss index da4f20ba49..1fff212df8 100644 --- a/common/static/sass/bourbon/css3/_border-image.scss +++ b/common/static/sass/bourbon/css3/_border-image.scss @@ -1,45 +1,43 @@ @mixin border-image($images) { - -webkit-border-image: border-add-prefix($images, webkit); - -moz-border-image: border-add-prefix($images, moz); - -o-border-image: border-add-prefix($images, o); - border-image: border-add-prefix($images); + -webkit-border-image: _border-add-prefix($images, webkit); + -moz-border-image: _border-add-prefix($images, moz); + -o-border-image: _border-add-prefix($images, o); + border-image: _border-add-prefix($images); } -@function border-add-prefix($images, $vendor: false) { - $border-image: (); +@function _border-add-prefix($images, $vendor: false) { + $border-image: null; $images-type: type-of(nth($images, 1)); $first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial) // If input is a gradient @if $images-type == string { @if ($first-var == "linear") or ($first-var == "radial") { - @for $i from 2 through length($images) { - $gradient-type: nth($images, 1); // Get type of gradient (linear || radial) - $gradient-args: nth($images, $i); // Get actual gradient (red, blue) - $border-image: render-gradients($gradient-args, $gradient-type, $vendor); - } + $gradient-type: nth($images, 1); // Get type of gradient (linear || radial) + $gradient-pos: nth($images, 2); // Get gradient position + $gradient-args: nth($images, 3); // Get actual gradient (red, blue) + $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); + $border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); } - // If input is a URL @else { $border-image: $images; } } - // If input is gradient or url + additional args @else if $images-type == list { - @for $i from 1 through length($images) { - $type: type-of(nth($images, $i)); // Get type of variable - List or String + $type: type-of(nth($images, 1)); // Get type of variable - List or String - // If variable is a list - Gradient - @if $type == list { - $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) - $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) - $border-image: render-gradients($gradient-args, $gradient-type, $vendor); - } + // If variable is a list - Gradient + @if $type == list { + $gradient: nth($images, 1); + $gradient-type: nth($gradient, 1); // Get type of gradient (linear || radial) + $gradient-pos: nth($gradient, 2); // Get gradient position + $gradient-args: nth($gradient, 3); // Get actual gradient (red, blue) + $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); + $border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); - // If variable is a string - Image or number - @else if ($type == string) or ($type == number) { + @for $i from 2 through length($images) { $border-image: append($border-image, nth($images, $i)); } } @@ -54,3 +52,4 @@ // @include border-image(linear-gradient(45deg, orange, yellow) stretch); // @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round); // @include border-image(radial-gradient(top, cover, orange, yellow, orange)); + diff --git a/common/static/sass/bourbon/css3/_border-radius.scss b/common/static/sass/bourbon/css3/_border-radius.scss index f24389ebbe..7c17190109 100644 --- a/common/static/sass/bourbon/css3/_border-radius.scss +++ b/common/static/sass/bourbon/css3/_border-radius.scss @@ -1,63 +1,22 @@ -@mixin border-radius ($radii) { - -webkit-border-radius: $radii; - -moz-border-radius: $radii; - -ms-border-radius: $radii; - -o-border-radius: $radii; - border-radius: $radii; -} - -@mixin border-top-left-radius($radii) { - -webkit-border-top-left-radius: $radii; - -moz-border-top-left-radius: $radii; - -moz-border-radius-topleft: $radii; - -ms-border-top-left-radius: $radii; - -o-border-top-left-radius: $radii; - border-top-left-radius: $radii; -} - -@mixin border-top-right-radius($radii) { - -webkit-border-top-right-radius: $radii; - -moz-border-top-right-radius: $radii; - -moz-border-radius-topright: $radii; - -ms-border-top-right-radius: $radii; - -o-border-top-right-radius: $radii; - border-top-right-radius: $radii; -} - -@mixin border-bottom-left-radius($radii) { - -webkit-border-bottom-left-radius: $radii; - -moz-border-bottom-left-radius: $radii; - -moz-border-radius-bottomleft: $radii; - -ms-border-bottom-left-radius: $radii; - -o-border-bottom-left-radius: $radii; - border-bottom-left-radius: $radii; -} - -@mixin border-bottom-right-radius($radii) { - -webkit-border-bottom-right-radius: $radii; - -moz-border-bottom-right-radius: $radii; - -moz-border-radius-bottomright: $radii; - -ms-border-bottom-right-radius: $radii; - -o-border-bottom-right-radius: $radii; - border-bottom-right-radius: $radii; -} - +//************************************************************************// +// Shorthand Border-radius mixins +//************************************************************************// @mixin border-top-radius($radii) { - @include border-top-left-radius($radii); - @include border-top-right-radius($radii); -} - -@mixin border-right-radius($radii) { - @include border-top-right-radius($radii); - @include border-bottom-right-radius($radii); + @include prefixer(border-top-left-radius, $radii, spec); + @include prefixer(border-top-right-radius, $radii, spec); } @mixin border-bottom-radius($radii) { - @include border-bottom-left-radius($radii); - @include border-bottom-right-radius($radii); + @include prefixer(border-bottom-left-radius, $radii, spec); + @include prefixer(border-bottom-right-radius, $radii, spec); } @mixin border-left-radius($radii) { - @include border-top-left-radius($radii); - @include border-bottom-left-radius($radii); + @include prefixer(border-top-left-radius, $radii, spec); + @include prefixer(border-bottom-left-radius, $radii, spec); +} + +@mixin border-right-radius($radii) { + @include prefixer(border-top-right-radius, $radii, spec); + @include prefixer(border-bottom-right-radius, $radii, spec); } diff --git a/common/static/sass/bourbon/css3/_box-shadow.scss b/common/static/sass/bourbon/css3/_box-shadow.scss deleted file mode 100644 index 327b66d251..0000000000 --- a/common/static/sass/bourbon/css3/_box-shadow.scss +++ /dev/null @@ -1,14 +0,0 @@ -// Box-Shadow Mixin Requires Sass v3.1.1+ -@mixin box-shadow ($shadow-1, - $shadow-2: false, $shadow-3: false, - $shadow-4: false, $shadow-5: false, - $shadow-6: false, $shadow-7: false, - $shadow-8: false, $shadow-9: false) - { - $full: compact($shadow-1, $shadow-2, $shadow-3, $shadow-4, - $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9); - - -webkit-box-shadow: $full; - -moz-box-shadow: $full; - box-shadow: $full; -} diff --git a/common/static/sass/bourbon/css3/_box-sizing.scss b/common/static/sass/bourbon/css3/_box-sizing.scss index d61523b5f1..f07e1d412e 100644 --- a/common/static/sass/bourbon/css3/_box-sizing.scss +++ b/common/static/sass/bourbon/css3/_box-sizing.scss @@ -1,6 +1,4 @@ @mixin box-sizing ($box) { // content-box | border-box | inherit - -webkit-box-sizing: $box; - -moz-box-sizing: $box; - box-sizing: $box; *behavior: url(/static/scripts/boxsizing.htc); + @include prefixer(box-sizing, $box, webkit moz spec); } diff --git a/common/static/sass/bourbon/css3/_columns.scss b/common/static/sass/bourbon/css3/_columns.scss index 2896c91d7f..42274a4eeb 100644 --- a/common/static/sass/bourbon/css3/_columns.scss +++ b/common/static/sass/bourbon/css3/_columns.scss @@ -1,67 +1,47 @@ @mixin columns($arg: auto) { // || - -webkit-columns: $arg; - -moz-columns: $arg; - columns: $arg; + @include prefixer(columns, $arg, webkit moz spec); } @mixin column-count($int: auto) { // auto || integer - -webkit-column-count: $int; - -moz-column-count: $int; - column-count: $int; + @include prefixer(column-count, $int, webkit moz spec); } @mixin column-gap($length: normal) { // normal || length - -webkit-column-gap: $length; - -moz-column-gap: $length; - column-gap: $length; + @include prefixer(column-gap, $length, webkit moz spec); } @mixin column-fill($arg: auto) { // auto || length - -webkit-columns-fill: $arg; - -moz-columns-fill: $arg; - columns-fill: $arg; + @include prefixer(columns-fill, $arg, webkit moz spec); } @mixin column-rule($arg) { // || || - -webkit-column-rule: $arg; - -moz-column-rule: $arg; - column-rule: $arg; + @include prefixer(column-rule, $arg, webkit moz spec); } @mixin column-rule-color($color) { - -webkit-column-rule-color: $color; - -moz-column-rule-color: $color; - column-rule-color: $color; + @include prefixer(column-rule-color, $color, webkit moz spec); } @mixin column-rule-style($style: none) { // none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid - -webkit-column-rule-style: $style; - -moz-column-rule-style: $style; - column-rule-style: $style; + @include prefixer(column-rule-style, $style, webkit moz spec); } @mixin column-rule-width ($width: none) { - -webkit-column-rule-width: $width; - -moz-column-rule-width: $width; - column-rule-width: $width; + @include prefixer(column-rule-width, $width, webkit moz spec); } @mixin column-span($arg: none) { // none || all - -webkit-column-span: $arg; - -moz-column-span: $arg; - column-span: $arg; + @include prefixer(column-span, $arg, webkit moz spec); } @mixin column-width($length: auto) { // auto || length - -webkit-column-width: $length; - -moz-column-width: $length; - column-width: $length; + @include prefixer(column-width, $length, webkit moz spec); } diff --git a/common/static/sass/bourbon/css3/_flex-box.scss b/common/static/sass/bourbon/css3/_flex-box.scss index 44c1dfd789..3e741e6696 100644 --- a/common/static/sass/bourbon/css3/_flex-box.scss +++ b/common/static/sass/bourbon/css3/_flex-box.scss @@ -16,52 +16,37 @@ @mixin box-orient($orient: inline-axis) { // horizontal|vertical|inline-axis|block-axis|inherit - -webkit-box-orient: $orient; - -moz-box-orient: $orient; - box-orient: $orient; + @include prefixer(box-orient, $orient, webkit moz spec); } @mixin box-pack($pack: start) { // start|end|center|justify - -webkit-box-pack: $pack; - -moz-box-pack: $pack; - box-pack: $pack; + @include prefixer(box-pack, $pack, webkit moz spec); } @mixin box-align($align: stretch) { // start|end|center|baseline|stretch - -webkit-box-align: $align; - -moz-box-align: $align; - box-align: $align; + @include prefixer(box-align, $align, webkit moz spec); } @mixin box-direction($direction: normal) { // normal|reverse|inherit - -webkit-box-direction: $direction; - -moz-box-direction: $direction; - box-direction: $direction; -} -@mixin box-lines($lines: single) { -// single|multiple - -webkit-box-lines: $lines; - -moz-box-lines: $lines; - box-lines: $lines; + @include prefixer(box-direction, $direction, webkit moz spec); } -@mixin box-ordinal-group($integer: 1) { - -webkit-box-ordinal-group: $integer; - -moz-box-ordinal-group: $integer; - box-ordinal-group: $integer; +@mixin box-lines($lines: single) { +// single|multiple + @include prefixer(box-lines, $lines, webkit moz spec); +} + +@mixin box-ordinal-group($int: 1) { + @include prefixer(box-ordinal-group, $int, webkit moz spec); } @mixin box-flex($value: 0.0) { - -webkit-box-flex: $value; - -moz-box-flex: $value; - box-flex: $value; + @include prefixer(box-flex, $value, webkit moz spec); } -@mixin box-flex-group($integer: 1) { - -webkit-box-flex-group: $integer; - -moz-box-flex-group: $integer; - box-flex-group: $integer; +@mixin box-flex-group($int: 1) { + @include prefixer(box-flex-group, $int, webkit moz spec); } diff --git a/common/static/sass/bourbon/css3/_font-face.scss b/common/static/sass/bourbon/css3/_font-face.scss new file mode 100644 index 0000000000..029ee8fe88 --- /dev/null +++ b/common/static/sass/bourbon/css3/_font-face.scss @@ -0,0 +1,23 @@ +// Order of the includes matters, and it is: normal, bold, italic, bold+italic. + +@mixin font-face($font-family, $file-path, $weight: normal, $style: normal, $asset-pipeline: false ) { + @font-face { + font-family: $font-family; + font-weight: $weight; + font-style: $style; + + @if $asset-pipeline == true { + src: font-url('#{$file-path}.eot'); + src: font-url('#{$file-path}.eot?#iefix') format('embedded-opentype'), + font-url('#{$file-path}.woff') format('woff'), + font-url('#{$file-path}.ttf') format('truetype'), + font-url('#{$file-path}.svg##{$font-family}') format('svg'); + } @else { + src: url('#{$file-path}.eot'); + src: url('#{$file-path}.eot?#iefix') format('embedded-opentype'), + url('#{$file-path}.woff') format('woff'), + url('#{$file-path}.ttf') format('truetype'), + url('#{$file-path}.svg##{$font-family}') format('svg'); + } + } +} diff --git a/common/static/sass/bourbon/css3/_hidpi-media-query.scss b/common/static/sass/bourbon/css3/_hidpi-media-query.scss new file mode 100644 index 0000000000..111e4009b5 --- /dev/null +++ b/common/static/sass/bourbon/css3/_hidpi-media-query.scss @@ -0,0 +1,10 @@ +// HiDPI mixin. Default value set to 1.3 to target Google Nexus 7 (http://bjango.com/articles/min-device-pixel-ratio/) +@mixin hidpi($ratio: 1.3) { + @media only screen and (-webkit-min-device-pixel-ratio: $ratio), + only screen and (min--moz-device-pixel-ratio: $ratio), + only screen and (-o-min-device-pixel-ratio: #{$ratio}/1), + only screen and (min-resolution: #{round($ratio*96)}dpi), + only screen and (min-resolution: #{$ratio}dppx) { + @content; + } +} diff --git a/common/static/sass/bourbon/css3/_image-rendering.scss b/common/static/sass/bourbon/css3/_image-rendering.scss new file mode 100644 index 0000000000..abc7ee1aa4 --- /dev/null +++ b/common/static/sass/bourbon/css3/_image-rendering.scss @@ -0,0 +1,13 @@ +@mixin image-rendering ($mode:optimizeQuality) { + + @if ($mode == optimize-contrast) { + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: optimize-contrast; + } + + @else { + image-rendering: $mode; + } +} diff --git a/common/static/sass/bourbon/css3/_inline-block.scss b/common/static/sass/bourbon/css3/_inline-block.scss index d79a13c851..3272a0010b 100644 --- a/common/static/sass/bourbon/css3/_inline-block.scss +++ b/common/static/sass/bourbon/css3/_inline-block.scss @@ -1,7 +1,5 @@ // Legacy support for inline-block in IE7 (maybe IE6) @mixin inline-block { - display: -moz-inline-box; - -moz-box-orient: vertical; display: inline-block; vertical-align: baseline; zoom: 1; diff --git a/common/static/sass/bourbon/css3/_keyframes.scss b/common/static/sass/bourbon/css3/_keyframes.scss new file mode 100644 index 0000000000..dca61f2a07 --- /dev/null +++ b/common/static/sass/bourbon/css3/_keyframes.scss @@ -0,0 +1,43 @@ +// Adds keyframes blocks for supported prefixes, removing redundant prefixes in the block's content +@mixin keyframes($name) { + $original-prefix-for-webkit: $prefix-for-webkit; + $original-prefix-for-mozilla: $prefix-for-mozilla; + $original-prefix-for-microsoft: $prefix-for-microsoft; + $original-prefix-for-opera: $prefix-for-opera; + $original-prefix-for-spec: $prefix-for-spec; + + @if $original-prefix-for-webkit { + @include disable-prefix-for-all(); + $prefix-for-webkit: true; + @-webkit-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-mozilla { + @include disable-prefix-for-all(); + $prefix-for-mozilla: true; + @-moz-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-opera { + @include disable-prefix-for-all(); + $prefix-for-opera: true; + @-o-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-spec { + @include disable-prefix-for-all(); + $prefix-for-spec: true; + @keyframes #{$name} { + @content; + } + } + + $prefix-for-webkit: $original-prefix-for-webkit; + $prefix-for-mozilla: $original-prefix-for-mozilla; + $prefix-for-microsoft: $original-prefix-for-microsoft; + $prefix-for-opera: $original-prefix-for-opera; + $prefix-for-spec: $original-prefix-for-spec; +} diff --git a/common/static/sass/bourbon/css3/_linear-gradient.scss b/common/static/sass/bourbon/css3/_linear-gradient.scss index e366a299a9..d5b687b00c 100644 --- a/common/static/sass/bourbon/css3/_linear-gradient.scss +++ b/common/static/sass/bourbon/css3/_linear-gradient.scss @@ -3,15 +3,25 @@ $G5: false, $G6: false, $G7: false, $G8: false, $G9: false, $G10: false, + $deprecated-pos1: left top, + $deprecated-pos2: left bottom, $fallback: false) { // Detect what type of value exists in $pos $pos-type: type-of(nth($pos, 1)); + $pos-spec: null; + $pos-degree: null; // If $pos is missing from mixin, reassign vars and add default position @if ($pos-type == color) or (nth($pos, 1) == "transparent") { $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; - $pos: top; // Default position + $pos: null; + } + + @if $pos { + $positions: _linear-positions-parser($pos); + $pos-degree: nth($positions, 1); + $pos-spec: nth($positions, 2); } $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); @@ -25,17 +35,7 @@ } background-color: $fallback-color; - background-image: deprecated-webkit-gradient(linear, $full); // Safari <= 5.0 - background-image: -webkit-linear-gradient($pos, $full); // Safari 5.1+, Chrome - background-image: -moz-linear-gradient($pos, $full); - background-image: -ms-linear-gradient($pos, $full); - background-image: -o-linear-gradient($pos, $full); - background-image: unquote("linear-gradient(#{$pos}, #{$full})"); + background-image: _deprecated-webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $full); // Safari <= 5.0 + background-image: -webkit-linear-gradient($pos-degree $full); // Safari 5.1+, Chrome + background-image: unquote("linear-gradient(#{$pos-spec}#{$full})"); } - - -// Usage: Gradient position is optional, default is top. Position can be a degree. Color stops are optional as well. -// @include linear-gradient(#1e5799, #2989d8); -// @include linear-gradient(#1e5799, #2989d8, $fallback:#2989d8); -// @include linear-gradient(top, #1e5799 0%, #2989d8 50%); -// @include linear-gradient(50deg, rgba(10, 10, 10, 0.5) 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%); diff --git a/common/static/sass/bourbon/css3/_perspective.scss b/common/static/sass/bourbon/css3/_perspective.scss new file mode 100644 index 0000000000..0e4deb80f3 --- /dev/null +++ b/common/static/sass/bourbon/css3/_perspective.scss @@ -0,0 +1,8 @@ +@mixin perspective($depth: none) { + // none | + @include prefixer(perspective, $depth, webkit moz spec); +} + +@mixin perspective-origin($value: 50% 50%) { + @include prefixer(perspective-origin, $value, webkit moz spec); +} diff --git a/common/static/sass/bourbon/css3/_placeholder.scss b/common/static/sass/bourbon/css3/_placeholder.scss new file mode 100644 index 0000000000..22fd92b4f2 --- /dev/null +++ b/common/static/sass/bourbon/css3/_placeholder.scss @@ -0,0 +1,29 @@ +$placeholders: '-webkit-input-placeholder', + '-moz-placeholder', + '-ms-input-placeholder'; + +@mixin placeholder { + @each $placeholder in $placeholders { + @if $placeholder == "-webkit-input-placeholder" { + &::#{$placeholder} { + @content; + } + } + @else if $placeholder == "-moz-placeholder" { + // FF 18- + &:#{$placeholder} { + @content; + } + + // FF 19+ + &::#{$placeholder} { + @content; + } + } + @else { + &:#{$placeholder} { + @content; + } + } + } +} diff --git a/common/static/sass/bourbon/css3/_radial-gradient.scss b/common/static/sass/bourbon/css3/_radial-gradient.scss index e83cab5234..e87b45a5a1 100644 --- a/common/static/sass/bourbon/css3/_radial-gradient.scss +++ b/common/static/sass/bourbon/css3/_radial-gradient.scss @@ -1,31 +1,44 @@ // Requires Sass 3.1+ -@mixin radial-gradient($pos, $shape-size, - $G1, $G2, +@mixin radial-gradient($G1, $G2, $G3: false, $G4: false, $G5: false, $G6: false, $G7: false, $G8: false, $G9: false, $G10: false, + $pos: null, + $shape-size: null, + $deprecated-pos1: center center, + $deprecated-pos2: center center, + $deprecated-radius1: 0, + $deprecated-radius2: 460, $fallback: false) { + $data: _radial-arg-parser($G1, $G2, $pos, $shape-size); + $G1: nth($data, 1); + $G2: nth($data, 2); + $pos: nth($data, 3); + $shape-size: nth($data, 4); + $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); - // Set $G1 as the default fallback color - $fallback-color: nth($G1, 1); + // Strip deprecated cover/contain for spec + $shape-size-spec: _shape-size-stripper($shape-size); + + // Set $G1 as the default fallback color + $first-color: nth($full, 1); + $fallback-color: nth($first-color, 1); - // If $fallback is a color use that color as the fallback color @if (type-of($fallback) == color) or ($fallback == "transparent") { $fallback-color: $fallback; } - background-color: $fallback-color; - background-image: deprecated-webkit-gradient(radial, $full); // Safari <= 5.0 - background-image: -webkit-radial-gradient($pos, $shape-size, $full); - background-image: -moz-radial-gradient($pos, $shape-size, $full); - background-image: -ms-radial-gradient($pos, $shape-size, $full); - background-image: -o-radial-gradient($pos, $shape-size, $full); - background-image: unquote("radial-gradient(#{$pos}, #{$shape-size}, #{$full})"); -} + // Add Commas and spaces + $shape-size: if($shape-size, '#{$shape-size}, ', null); + $pos: if($pos, '#{$pos}, ', null); + $pos-spec: if($pos, 'at #{$pos}', null); + $shape-size-spec: if(($shape-size-spec != ' ') and ($pos == null), '#{$shape-size-spec}, ', '#{$shape-size-spec} '); -// Usage: Gradient position and shape-size are required. Color stops are optional. -// @include radial-gradient(50% 50%, circle cover, #1e5799, #efefef); -// @include radial-gradient(50% 50%, circle cover, #eee 10%, #1e5799 30%, #efefef); + background-color: $fallback-color; + background-image: _deprecated-webkit-gradient(radial, $deprecated-pos1, $deprecated-pos2, $full, $deprecated-radius1, $deprecated-radius2); // Safari <= 5.0 && IOS 4 + background-image: -webkit-radial-gradient(unquote(#{$pos}#{$shape-size}#{$full})); + background-image: unquote("radial-gradient(#{$shape-size-spec}#{$pos-spec}#{$full})"); +} diff --git a/common/static/sass/bourbon/css3/_transform.scss b/common/static/sass/bourbon/css3/_transform.scss index 8d19e8b88d..8cc35963d5 100644 --- a/common/static/sass/bourbon/css3/_transform.scss +++ b/common/static/sass/bourbon/css3/_transform.scss @@ -1,19 +1,15 @@ @mixin transform($property: none) { // none | - -webkit-transform: $property; - -moz-transform: $property; - -ms-transform: $property; - -o-transform: $property; - transform: $property; + @include prefixer(transform, $property, webkit moz ms o spec); } @mixin transform-origin($axes: 50%) { // x-axis - left | center | right | length | % // y-axis - top | center | bottom | length | % // z-axis - length - -webkit-transform-origin: $axes; - -moz-transform-origin: $axes; - -ms-transform-origin: $axes; - -o-transform-origin: $axes; - transform-origin: $axes; + @include prefixer(transform-origin, $axes, webkit moz ms o spec); +} + +@mixin transform-style ($style: flat) { + @include prefixer(transform-style, $style, webkit moz ms o spec); } diff --git a/common/static/sass/bourbon/css3/_transition.scss b/common/static/sass/bourbon/css3/_transition.scss index 058dbe0e33..180cde6c8a 100644 --- a/common/static/sass/bourbon/css3/_transition.scss +++ b/common/static/sass/bourbon/css3/_transition.scss @@ -3,102 +3,32 @@ // @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s)); // @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s)); -@mixin transition ($property: all, $duration: 0.15s, $timing-function: ease-out, $delay: 0) { - - // Detect # of args passed into each variable - $length-of-property: length($property); - $length-of-duration: length($duration); - $length-of-timing-function: length($timing-function); - $length-of-delay: length($delay); - - @if $length-of-property > 1 { - @include transition-property(zip($property)); } - @else { - @include transition-property( $property); +@mixin transition ($properties...) { + @if length($properties) >= 1 { + @include prefixer(transition, $properties, webkit moz spec); } - @if $length-of-duration > 1 { - @include transition-duration(zip($duration)); } @else { - @include transition-duration( $duration); - } - - @if $length-of-timing-function > 1 { - @include transition-timing-function(zip($timing-function)); } - @else { - @include transition-timing-function( $timing-function); - } - - @if $length-of-delay > 1 { - @include transition-delay(zip($delay)); } - @else { - @include transition-delay( $delay); + $properties: all 0.15s ease-out 0; + @include prefixer(transition, $properties, webkit moz spec); } } - -@mixin transition-property ($prop-1: all, - $prop-2: false, $prop-3: false, - $prop-4: false, $prop-5: false, - $prop-6: false, $prop-7: false, - $prop-8: false, $prop-9: false) - { - $full: compact($prop-1, $prop-2, $prop-3, $prop-4, $prop-5, - $prop-6, $prop-7, $prop-8, $prop-9); - - -webkit-transition-property: $full; - -moz-transition-property: $full; - -ms-transition-property: $full; - -o-transition-property: $full; - transition-property: $full; +@mixin transition-property ($properties...) { + -webkit-transition-property: transition-property-names($properties, 'webkit'); + -moz-transition-property: transition-property-names($properties, 'moz'); + transition-property: transition-property-names($properties, false); } -@mixin transition-duration ($time-1: 0, - $time-2: false, $time-3: false, - $time-4: false, $time-5: false, - $time-6: false, $time-7: false, - $time-8: false, $time-9: false) - { - $full: compact($time-1, $time-2, $time-3, $time-4, $time-5, - $time-6, $time-7, $time-8, $time-9); - - -webkit-transition-duration: $full; - -moz-transition-duration: $full; - -ms-transition-duration: $full; - -o-transition-duration: $full; - transition-duration: $full; +@mixin transition-duration ($times...) { + @include prefixer(transition-duration, $times, webkit moz spec); } -@mixin transition-timing-function ($motion-1: ease, - $motion-2: false, $motion-3: false, - $motion-4: false, $motion-5: false, - $motion-6: false, $motion-7: false, - $motion-8: false, $motion-9: false) - { - $full: compact($motion-1, $motion-2, $motion-3, $motion-4, $motion-5, - $motion-6, $motion-7, $motion-8, $motion-9); - +@mixin transition-timing-function ($motions...) { // ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() - -webkit-transition-timing-function: $full; - -moz-transition-timing-function: $full; - -ms-transition-timing-function: $full; - -o-transition-timing-function: $full; - transition-timing-function: $full; + @include prefixer(transition-timing-function, $motions, webkit moz spec); } -@mixin transition-delay ($time-1: 0, - $time-2: false, $time-3: false, - $time-4: false, $time-5: false, - $time-6: false, $time-7: false, - $time-8: false, $time-9: false) - { - $full: compact($time-1, $time-2, $time-3, $time-4, $time-5, - $time-6, $time-7, $time-8, $time-9); - - -webkit-transition-delay: $full; - -moz-transition-delay: $full; - -ms-transition-delay: $full; - -o-transition-delay: $full; - transition-delay: $full; +@mixin transition-delay ($times...) { + @include prefixer(transition-delay, $times, webkit moz spec); } - diff --git a/common/static/sass/bourbon/css3/_user-select.scss b/common/static/sass/bourbon/css3/_user-select.scss index d5f5749431..1380aa8baa 100644 --- a/common/static/sass/bourbon/css3/_user-select.scss +++ b/common/static/sass/bourbon/css3/_user-select.scss @@ -1,6 +1,3 @@ @mixin user-select($arg: none) { - -webkit-user-select: $arg; - -moz-user-select: $arg; - -ms-user-select: $arg; - user-select: $arg; + @include prefixer(user-select, $arg, webkit moz ms spec); } diff --git a/common/static/sass/bourbon/functions/_compact.scss b/common/static/sass/bourbon/functions/_compact.scss new file mode 100644 index 0000000000..871500e339 --- /dev/null +++ b/common/static/sass/bourbon/functions/_compact.scss @@ -0,0 +1,11 @@ +// Remove `false` values from a list + +@function compact($vars...) { + $list: (); + @each $var in $vars { + @if $var { + $list: append($list, $var, comma); + } + } + @return $list; +} diff --git a/common/static/sass/bourbon/functions/_flex-grid.scss b/common/static/sass/bourbon/functions/_flex-grid.scss index 707f994e15..3bbd866573 100644 --- a/common/static/sass/bourbon/functions/_flex-grid.scss +++ b/common/static/sass/bourbon/functions/_flex-grid.scss @@ -14,13 +14,17 @@ // The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function. // This function takes the fluid grid equation (target / context = result) and uses columns to help define each. // +// The calculation presumes that your column structure will be missing the last gutter: +// +// -- column -- gutter -- column -- gutter -- column +// // $fg-column: 60px; // Column Width // $fg-gutter: 25px; // Gutter Width // $fg-max-columns: 12; // Total Columns For Main Container // // div { -// width: flex-grid(4); // returns (315px / 1020px) = 30.882353%; -// margin-left: flex-gutter(); // returns (25px / 1020px) = 2.45098%; +// width: flex-grid(4); // returns (315px / 995px) = 31.65829%; +// margin-left: flex-gutter(); // returns (25px / 995px) = 2.51256%; // // p { // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; @@ -32,4 +36,4 @@ // float: left; // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; // } -// } +// } \ No newline at end of file diff --git a/common/static/sass/bourbon/functions/_linear-gradient.scss b/common/static/sass/bourbon/functions/_linear-gradient.scss index 3b10ca82a6..c8454d83f0 100644 --- a/common/static/sass/bourbon/functions/_linear-gradient.scss +++ b/common/static/sass/bourbon/functions/_linear-gradient.scss @@ -1,23 +1,13 @@ -@function linear-gradient($pos: top, $G1: false, $G2: false, - $G3: false, $G4: false, - $G5: false, $G6: false, - $G7: false, $G8: false, - $G9: false, $G10: false) { - - // Detect what type of value exists in $pos +@function linear-gradient($pos, $gradients...) { + $type: linear; $pos-type: type-of(nth($pos, 1)); - // If $pos is missing from mixin, reassign vars and add default position + // if $pos doesn't exist, fix $gradient @if ($pos-type == color) or (nth($pos, 1) == "transparent") { - $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; - $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; - $pos: top; // Default position + $gradients: zip($pos $gradients); + $pos: false; } - $type: linear; - $gradient: compact($pos, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); - $type-gradient: append($type, $gradient, comma); - + $type-gradient: $type, $pos, $gradients; @return $type-gradient; } - diff --git a/common/static/sass/bourbon/functions/_px-to-em.scss b/common/static/sass/bourbon/functions/_px-to-em.scss new file mode 100644 index 0000000000..2eb1031c60 --- /dev/null +++ b/common/static/sass/bourbon/functions/_px-to-em.scss @@ -0,0 +1,8 @@ +// Convert pixels to ems +// eg. for a relational value of 12px write em(12) when the parent is 16px +// if the parent is another value say 24px write em(12, 24) + +@function em($pxval, $base: 16) { + @return ($pxval / $base) * 1em; +} + diff --git a/common/static/sass/bourbon/functions/_radial-gradient.scss b/common/static/sass/bourbon/functions/_radial-gradient.scss index 3d5461ad6e..75584060d2 100644 --- a/common/static/sass/bourbon/functions/_radial-gradient.scss +++ b/common/static/sass/bourbon/functions/_radial-gradient.scss @@ -1,15 +1,23 @@ // This function is required and used by the background-image mixin. -@function radial-gradient($pos, $shape-size, - $G1, $G2, +@function radial-gradient($G1, $G2, $G3: false, $G4: false, $G5: false, $G6: false, $G7: false, $G8: false, - $G9: false, $G10: false) { + $G9: false, $G10: false, + $pos: null, + $shape-size: null) { + + $data: _radial-arg-parser($G1, $G2, $pos, $shape-size); + $G1: nth($data, 1); + $G2: nth($data, 2); + $pos: nth($data, 3); + $shape-size: nth($data, 4); $type: radial; - $gradient: compact($pos, $shape-size, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); - $type-gradient: append($type, $gradient, comma); + $gradient: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); + $type-gradient: $type, $shape-size $pos, $gradient; @return $type-gradient; } + diff --git a/common/static/sass/bourbon/functions/_render-gradients.scss b/common/static/sass/bourbon/functions/_render-gradients.scss deleted file mode 100644 index fe7c799ebe..0000000000 --- a/common/static/sass/bourbon/functions/_render-gradients.scss +++ /dev/null @@ -1,14 +0,0 @@ -// User for linear and radial gradients within background-image or border-image properties - -@function render-gradients($gradients, $gradient-type, $vendor: false) { - $vendor-gradients: false; - @if $vendor { - $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient($gradients); - } - - @else if $vendor == false { - $vendor-gradients: "#{$gradient-type}-gradient(#{$gradients})"; - $vendor-gradients: unquote($vendor-gradients); - } - @return $vendor-gradients; -} diff --git a/common/static/sass/bourbon/functions/_transition-property-name.scss b/common/static/sass/bourbon/functions/_transition-property-name.scss new file mode 100644 index 0000000000..54cd422811 --- /dev/null +++ b/common/static/sass/bourbon/functions/_transition-property-name.scss @@ -0,0 +1,22 @@ +// Return vendor-prefixed property names if appropriate +// Example: transition-property-names((transform, color, background), moz) -> -moz-transform, color, background +//************************************************************************// +@function transition-property-names($props, $vendor: false) { + $new-props: (); + + @each $prop in $props { + $new-props: append($new-props, transition-property-name($prop, $vendor), comma); + } + + @return $new-props; +} + +@function transition-property-name($prop, $vendor: false) { + // put other properties that need to be prefixed here aswell + @if $vendor and $prop == transform { + @return unquote('-'+$vendor+'-'+$prop); + } + @else { + @return $prop; + } +} \ No newline at end of file diff --git a/common/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss b/common/static/sass/bourbon/helpers/_deprecated-webkit-gradient.scss similarity index 62% rename from common/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss rename to common/static/sass/bourbon/helpers/_deprecated-webkit-gradient.scss index 1322f6f60e..cd17e2832d 100644 --- a/common/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss +++ b/common/static/sass/bourbon/helpers/_deprecated-webkit-gradient.scss @@ -1,6 +1,9 @@ // Render Deprecated Webkit Gradient - Linear || Radial //************************************************************************// -@function deprecated-webkit-gradient($type, $full) { +@function _deprecated-webkit-gradient($type, + $deprecated-pos1, $deprecated-pos2, + $full, + $deprecated-radius1: false, $deprecated-radius2: false) { $gradient-list: (); $gradient: false; $full-length: length($full); @@ -14,7 +17,7 @@ $color-stop: color-stop(nth($gradient, 2), nth($gradient, 1)); $gradient-list: join($gradient-list, $color-stop, comma); } - @else { + @else if $gradient != null { @if $i == $full-length { $percentage: 100%; } @@ -27,10 +30,10 @@ } @if $type == radial { - $gradient: -webkit-gradient(radial, center center, 0, center center, 460, $gradient-list); + $gradient: -webkit-gradient(radial, $deprecated-pos1, $deprecated-radius1, $deprecated-pos2, $deprecated-radius2, $gradient-list); } @else if $type == linear { - $gradient: -webkit-gradient(linear, left top, left bottom, $gradient-list); + $gradient: -webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $gradient-list); } @return $gradient; } diff --git a/common/static/sass/bourbon/helpers/_gradient-positions-parser.scss b/common/static/sass/bourbon/helpers/_gradient-positions-parser.scss new file mode 100644 index 0000000000..07d30b6cf9 --- /dev/null +++ b/common/static/sass/bourbon/helpers/_gradient-positions-parser.scss @@ -0,0 +1,13 @@ +@function _gradient-positions-parser($gradient-type, $gradient-positions) { + @if $gradient-positions + and ($gradient-type == linear) + and (type-of($gradient-positions) != color) { + $gradient-positions: _linear-positions-parser($gradient-positions); + } + @else if $gradient-positions + and ($gradient-type == radial) + and (type-of($gradient-positions) != color) { + $gradient-positions: _radial-positions-parser($gradient-positions); + } + @return $gradient-positions; +} diff --git a/common/static/sass/bourbon/helpers/_linear-positions-parser.scss b/common/static/sass/bourbon/helpers/_linear-positions-parser.scss new file mode 100644 index 0000000000..d26383edce --- /dev/null +++ b/common/static/sass/bourbon/helpers/_linear-positions-parser.scss @@ -0,0 +1,61 @@ +@function _linear-positions-parser($pos) { + $type: type-of(nth($pos, 1)); + $spec: null; + $degree: null; + $side: null; + $corner: null; + $length: length($pos); + // Parse Side and corner positions + @if ($length > 1) { + @if nth($pos, 1) == "to" { // Newer syntax + $side: nth($pos, 2); + + @if $length == 2 { // eg. to top + // Swap for backwards compatability + $degree: _position-flipper(nth($pos, 2)); + } + @else if $length == 3 { // eg. to top left + $corner: nth($pos, 3); + } + } + @else if $length == 2 { // Older syntax ("top left") + $side: _position-flipper(nth($pos, 1)); + $corner: _position-flipper(nth($pos, 2)); + } + + @if ("#{$side} #{$corner}" == "left top") or ("#{$side} #{$corner}" == "top left") { + $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); + } + @else if ("#{$side} #{$corner}" == "right top") or ("#{$side} #{$corner}" == "top right") { + $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); + } + @else if ("#{$side} #{$corner}" == "right bottom") or ("#{$side} #{$corner}" == "bottom right") { + $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); + } + @else if ("#{$side} #{$corner}" == "left bottom") or ("#{$side} #{$corner}" == "bottom left") { + $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); + } + $spec: to $side $corner; + } + @else if $length == 1 { + // Swap for backwards compatability + @if $type == string { + $degree: $pos; + $spec: to _position-flipper($pos); + } + @else { + $degree: -270 - $pos; //rotate the gradient opposite from spec + $spec: $pos; + } + } + $degree: unquote($degree + ","); + $spec: unquote($spec + ","); + @return $degree $spec; +} + +@function _position-flipper($pos) { + @return if($pos == left, right, null) + if($pos == right, left, null) + if($pos == top, bottom, null) + if($pos == bottom, top, null); +} diff --git a/common/static/sass/bourbon/helpers/_radial-arg-parser.scss b/common/static/sass/bourbon/helpers/_radial-arg-parser.scss new file mode 100644 index 0000000000..3466695bdf --- /dev/null +++ b/common/static/sass/bourbon/helpers/_radial-arg-parser.scss @@ -0,0 +1,69 @@ +@function _radial-arg-parser($G1, $G2, $pos, $shape-size) { + @each $value in $G1, $G2 { + $first-val: nth($value, 1); + $pos-type: type-of($first-val); + $spec-at-index: null; + + // Determine if spec was passed to mixin + @if type-of($value) == list { + $spec-at-index: if(index($value, at), index($value, at), false); + } + @if $spec-at-index { + @if $spec-at-index > 1 { + @for $i from 1 through ($spec-at-index - 1) { + $shape-size: $shape-size nth($value, $i); + } + @for $i from ($spec-at-index + 1) through length($value) { + $pos: $pos nth($value, $i); + } + } + @else if $spec-at-index == 1 { + @for $i from ($spec-at-index + 1) through length($value) { + $pos: $pos nth($value, $i); + } + } + $G1: false; + } + + // If not spec calculate correct values + @else { + @if ($pos-type != color) or ($first-val != "transparent") { + @if ($pos-type == number) + or ($first-val == "center") + or ($first-val == "top") + or ($first-val == "right") + or ($first-val == "bottom") + or ($first-val == "left") { + + $pos: $value; + + @if $pos == $G1 { + $G1: false; + } + } + + @else if + ($first-val == "ellipse") + or ($first-val == "circle") + or ($first-val == "closest-side") + or ($first-val == "closest-corner") + or ($first-val == "farthest-side") + or ($first-val == "farthest-corner") + or ($first-val == "contain") + or ($first-val == "cover") { + + $shape-size: $value; + + @if $value == $G1 { + $G1: false; + } + + @else if $value == $G2 { + $G2: false; + } + } + } + } + } + @return $G1, $G2, $pos, $shape-size; +} diff --git a/common/static/sass/bourbon/helpers/_radial-positions-parser.scss b/common/static/sass/bourbon/helpers/_radial-positions-parser.scss new file mode 100644 index 0000000000..6a5b477778 --- /dev/null +++ b/common/static/sass/bourbon/helpers/_radial-positions-parser.scss @@ -0,0 +1,18 @@ +@function _radial-positions-parser($gradient-pos) { + $shape-size: nth($gradient-pos, 1); + $pos: nth($gradient-pos, 2); + $shape-size-spec: _shape-size-stripper($shape-size); + + $pre-spec: unquote(if($pos, "#{$pos}, ", null)) + unquote(if($shape-size, "#{$shape-size},", null)); + $pos-spec: if($pos, "at #{$pos}", null); + + $spec: "#{$shape-size-spec} #{$pos-spec}"; + + // Add comma + @if ($spec != ' ') { + $spec: "#{$spec}," + } + + @return $pre-spec $spec; +} diff --git a/common/static/sass/bourbon/helpers/_render-gradients.scss b/common/static/sass/bourbon/helpers/_render-gradients.scss new file mode 100644 index 0000000000..5765676838 --- /dev/null +++ b/common/static/sass/bourbon/helpers/_render-gradients.scss @@ -0,0 +1,26 @@ +// User for linear and radial gradients within background-image or border-image properties + +@function _render-gradients($gradient-positions, $gradients, $gradient-type, $vendor: false) { + $pre-spec: null; + $spec: null; + $vendor-gradients: null; + @if $gradient-type == linear { + @if $gradient-positions { + $pre-spec: nth($gradient-positions, 1); + $spec: nth($gradient-positions, 2); + } + } + @else if $gradient-type == radial { + $pre-spec: nth($gradient-positions, 1); + $spec: nth($gradient-positions, 2); + } + + @if $vendor { + $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient(#{$pre-spec} $gradients); + } + @else if $vendor == false { + $vendor-gradients: "#{$gradient-type}-gradient(#{$spec} #{$gradients})"; + $vendor-gradients: unquote($vendor-gradients); + } + @return $vendor-gradients; +} diff --git a/common/static/sass/bourbon/helpers/_shape-size-stripper.scss b/common/static/sass/bourbon/helpers/_shape-size-stripper.scss new file mode 100644 index 0000000000..ee5eda4220 --- /dev/null +++ b/common/static/sass/bourbon/helpers/_shape-size-stripper.scss @@ -0,0 +1,10 @@ +@function _shape-size-stripper($shape-size) { + $shape-size-spec: null; + @each $value in $shape-size { + @if ($value == "cover") or ($value == "contain") { + $value: null; + } + $shape-size-spec: "#{$shape-size-spec} #{$value}"; + } + @return $shape-size-spec; +} diff --git a/common/static/sass/bourbon/lib/bourbon.rb b/common/static/sass/bourbon/lib/bourbon.rb deleted file mode 100644 index 1635be836d..0000000000 --- a/common/static/sass/bourbon/lib/bourbon.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "bourbon/generator" - -module Bourbon - if defined?(Rails) - class Engine < ::Rails::Engine - require 'bourbon/engine' - end - - module Rails - class Railtie < ::Rails::Railtie - rake_tasks do - load "tasks/install.rake" - end - end - end - end -end - -require File.join(File.dirname(__FILE__), "/bourbon/sass_extensions") diff --git a/common/static/sass/bourbon/lib/bourbon/sass_extensions.rb b/common/static/sass/bourbon/lib/bourbon/sass_extensions.rb deleted file mode 100644 index ad567200e3..0000000000 --- a/common/static/sass/bourbon/lib/bourbon/sass_extensions.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Bourbon::SassExtensions -end - -require "sass" - -require File.join(File.dirname(__FILE__), "/sass_extensions/functions") diff --git a/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb b/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb deleted file mode 100644 index daa877650e..0000000000 --- a/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Bourbon::SassExtensions::Functions -end - -require File.join(File.dirname(__FILE__), "/functions/compact") - -module Sass::Script::Functions - include Bourbon::SassExtensions::Functions::Compact -end - -# Wierd that this has to be re-included to pick up sub-modules. Ruby bug? -class Sass::Script::Functions::EvaluationContext - include Sass::Script::Functions -end diff --git a/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb b/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb deleted file mode 100644 index 5192e921e7..0000000000 --- a/common/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Compact function pulled from compass -module Bourbon::SassExtensions::Functions::Compact - - def compact(*args) - sep = :comma - if args.size == 1 && args.first.is_a?(Sass::Script::List) - args = args.first.value - sep = args.first.separator - end - Sass::Script::List.new(args.reject{|a| !a.to_bool}, sep) - end - -end diff --git a/common/static/sass/neat/_neat-helpers.scss b/common/static/sass/neat/_neat-helpers.scss new file mode 100644 index 0000000000..86021b1bff --- /dev/null +++ b/common/static/sass/neat/_neat-helpers.scss @@ -0,0 +1,8 @@ +// Functions +@import "functions/private"; +@import "functions/new-breakpoint"; +@import "functions/px-to-em"; + +// Settings +@import "settings/grid"; +@import "settings/visual-grid"; diff --git a/common/static/sass/neat/_neat.scss b/common/static/sass/neat/_neat.scss new file mode 100644 index 0000000000..cb5876b82d --- /dev/null +++ b/common/static/sass/neat/_neat.scss @@ -0,0 +1,21 @@ +// Bourbon Neat +// MIT Licensed +// Copyright (c) 2012-2013 thoughtbot, inc. + +// Helpers +@import "neat-helpers"; + +// Grid +@import "grid/private"; +@import "grid/reset"; +@import "grid/grid"; +@import "grid/omega"; +@import "grid/outer-container"; +@import "grid/span-columns"; +@import "grid/row"; +@import "grid/shift"; +@import "grid/pad"; +@import "grid/fill-parent"; +@import "grid/media"; +@import "grid/to-deprecate"; +@import "grid/visual-grid"; diff --git a/common/static/sass/neat/functions/_new-breakpoint.scss b/common/static/sass/neat/functions/_new-breakpoint.scss new file mode 100644 index 0000000000..d89dcd101b --- /dev/null +++ b/common/static/sass/neat/functions/_new-breakpoint.scss @@ -0,0 +1,16 @@ +@function new-breakpoint($query:$feature $value $columns, $total-columns: $grid-columns) { + + @if length($query) == 1 { + $query: $default-feature nth($query, 1) $total-columns; + } + + @else if length($query) == 2 or length($query) == 4 { + $query: append($query, $total-columns); + } + + @if not belongs-to($query, $visual-grid-breakpoints) { + $visual-grid-breakpoints: append($visual-grid-breakpoints, $query, comma); + } + + @return $query; +} diff --git a/common/static/sass/neat/functions/_private.scss b/common/static/sass/neat/functions/_private.scss new file mode 100644 index 0000000000..136a6ff3a1 --- /dev/null +++ b/common/static/sass/neat/functions/_private.scss @@ -0,0 +1,107 @@ +// Checks if a number is even +@function is-even($int) { + @if $int%2 == 0 { + @return true; + } + + @return false; +} + +// Checks if an element belongs to a list +@function belongs-to($tested-item, $list) { + @each $item in $list { + @if $item == $tested-item { + @return true; + } + } + + @return false; +} + +// Contains display value +@function contains-display-value($query) { + @if belongs-to(table, $query) or belongs-to(block, $query) or belongs-to(inline-block, $query) or belongs-to(inline, $query) { + @return true; + } + + @return false; +} + +// Parses the first argument of span-columns() +@function container-span($span: $span) { + @if length($span) == 3 { + $container-columns: nth($span, 3); + @return $container-columns; + } + + @else if length($span) == 2 { + $container-columns: nth($span, 2); + @return $container-columns; + } + + @else { + @return $grid-columns; + } +} + +// Generates a striped background +@function gradient-stops($grid-columns, $color: $visual-grid-color) { + $transparent: rgba(0,0,0,0); + + $column-width: flex-grid(1, $grid-columns); + $gutter-width: flex-gutter($grid-columns); + $column-offset: $column-width; + + $values: ($transparent 0, $color 0); + + @for $i from 1 to $grid-columns*2 { + @if is-even($i) { + $values: append($values, $transparent $column-offset); + $values: append($values, $color $column-offset); + $column-offset: $column-offset + $column-width; + } + + @else { + $values: append($values, $color $column-offset); + $values: append($values, $transparent $column-offset); + $column-offset: $column-offset + $gutter-width; + } + } + + @return $values; +} + +// Layout direction +@function get-direction($layout, $default) { + $direction: nil; + + @if $layout == LTR or $layout == RTL { + $direction: direction-from-layout($layout); + } @else { + $direction: direction-from-layout($default); + } + + @return $direction; +} + +@function direction-from-layout($layout) { + $direction: nil; + + @if $layout == LTR { + $direction: right; + } @else { + $direction: left; + } + + @return $direction; +} + +@function get-opposite-direction($direction) { + $opposite-direction: left; + + @if $direction == left { + $opposite-direction: right; + } + + @return $opposite-direction; +} diff --git a/common/static/sass/neat/functions/_px-to-em.scss b/common/static/sass/neat/functions/_px-to-em.scss new file mode 100644 index 0000000000..058e51e8b5 --- /dev/null +++ b/common/static/sass/neat/functions/_px-to-em.scss @@ -0,0 +1,3 @@ +@function em($pxval, $base: 16) { + @return ($pxval / $base) * 1em; +} diff --git a/common/static/sass/neat/grid/_fill-parent.scss b/common/static/sass/neat/grid/_fill-parent.scss new file mode 100644 index 0000000000..859c97790b --- /dev/null +++ b/common/static/sass/neat/grid/_fill-parent.scss @@ -0,0 +1,7 @@ +@mixin fill-parent() { + width: 100%; + + @if $border-box-sizing == false { + @include box-sizing(border-box); + } +} diff --git a/common/static/sass/neat/grid/_grid.scss b/common/static/sass/neat/grid/_grid.scss new file mode 100644 index 0000000000..e074b6c536 --- /dev/null +++ b/common/static/sass/neat/grid/_grid.scss @@ -0,0 +1,5 @@ +@if $border-box-sizing == true { + * { + @include box-sizing(border-box); + } +} diff --git a/common/static/sass/neat/grid/_media.scss b/common/static/sass/neat/grid/_media.scss new file mode 100644 index 0000000000..7c9872fb52 --- /dev/null +++ b/common/static/sass/neat/grid/_media.scss @@ -0,0 +1,51 @@ +@mixin media($query:$feature $value $columns, $total-columns: $grid-columns) { + + @if length($query) == 1 { + @media screen and ($default-feature: nth($query, 1)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 2 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 3 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 3); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 4 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 5 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 5); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else { + @warn "Wrong number of arguments for breakpoint(). Read the documentation for more details."; + } +} diff --git a/common/static/sass/neat/grid/_omega.scss b/common/static/sass/neat/grid/_omega.scss new file mode 100644 index 0000000000..902459bcbc --- /dev/null +++ b/common/static/sass/neat/grid/_omega.scss @@ -0,0 +1,79 @@ +// Remove last element gutter +@mixin omega($query: block, $direction: default) { + $table: if(belongs-to(table, $query), true, false); + $auto: if(belongs-to(auto, $query), true, false); + + @if $direction != default { + @warn "The omega mixin will no longer take a $direction argument. To change the layout direction, use row($direction) or set $default-layout-direction instead." + } @else { + $direction: get-direction($layout-direction, $default-layout-direction); + } + + @if length($query) == 1 { + @if $auto { + &:last-child { + margin-#{$direction}: 0; + } + } + + @else if contains-display-value($query) { + @if $table { + padding-#{$direction}: 0; + } + + @else { + margin-#{$direction}: 0; + } + } + + @else { + @include nth-child($query, $direction); + } + } + + @else if length($query) == 2 { + @if $table { + @if $auto { + &:last-child { + padding-#{$direction}: 0; + } + } + + @else { + &:nth-child(#{nth($query, 1)}) { + padding-#{$direction}: 0; + } + } + } + + @else { + @if $auto { + &:last-child { + margin-#{$direction}: 0; + } + } + + @else { + @include nth-child(nth($query, 1), $direction); + } + } + } + + @else { + @warn "Too many arguments passed to the omega() mixin." + } +} + +@mixin nth-child($query, $direction) { + $opposite-direction: get-opposite-direction($direction); + + &:nth-child(#{$query}) { + margin-#{$direction}: 0; + } + + @if type-of($query) == number { + &:nth-child(#{$query}+1) { + clear: $opposite-direction; + } + } +} diff --git a/common/static/sass/neat/grid/_outer-container.scss b/common/static/sass/neat/grid/_outer-container.scss new file mode 100644 index 0000000000..22c541f455 --- /dev/null +++ b/common/static/sass/neat/grid/_outer-container.scss @@ -0,0 +1,8 @@ +@mixin outer-container { + @include clearfix; + max-width: $max-width; + margin: { + left: auto; + right: auto; + } +} diff --git a/common/static/sass/neat/grid/_pad.scss b/common/static/sass/neat/grid/_pad.scss new file mode 100644 index 0000000000..3ef5d80e45 --- /dev/null +++ b/common/static/sass/neat/grid/_pad.scss @@ -0,0 +1,8 @@ +@mixin pad($padding: flex-gutter()) { + $padding-list: null; + @each $value in $padding { + $value: if($value == 'default', flex-gutter(), $value); + $padding-list: join($padding-list, $value); + } + padding: $padding-list; +} diff --git a/common/static/sass/neat/grid/_private.scss b/common/static/sass/neat/grid/_private.scss new file mode 100644 index 0000000000..acd1b5b74d --- /dev/null +++ b/common/static/sass/neat/grid/_private.scss @@ -0,0 +1,21 @@ +$parent-columns: $grid-columns !default; +$fg-column: $column; +$fg-gutter: $gutter; +$fg-max-columns: $grid-columns; +$container-display-table: false !default; +$layout-direction: nil !default; + +@function flex-grid($columns, $container-columns: $fg-max-columns) { + $width: $columns * $fg-column + ($columns - 1) * $fg-gutter; + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($width / $container-width); +} + +@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) { + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($gutter / $container-width); +} + +@function grid-width($n) { + @return $n * $gw-column + ($n - 1) * $gw-gutter; +} diff --git a/common/static/sass/neat/grid/_reset.scss b/common/static/sass/neat/grid/_reset.scss new file mode 100644 index 0000000000..f670019e4b --- /dev/null +++ b/common/static/sass/neat/grid/_reset.scss @@ -0,0 +1,12 @@ +@mixin reset-display { + $container-display-table: false; +} + +@mixin reset-layout-direction { + $layout-direction: $default-layout-direction; +} + +@mixin reset-all { + @include reset-display; + @include reset-layout-direction; +} diff --git a/common/static/sass/neat/grid/_row.scss b/common/static/sass/neat/grid/_row.scss new file mode 100644 index 0000000000..582603dd01 --- /dev/null +++ b/common/static/sass/neat/grid/_row.scss @@ -0,0 +1,17 @@ +@mixin row($display: block, $direction: $default-layout-direction) { + @include clearfix; + $layout-direction: $direction; + + @if $display == table { + display: table; + @include fill-parent; + table-layout: fixed; + $container-display-table: true; + } + + @else { + display: block; + $container-display-table: false; + } +} + diff --git a/common/static/sass/neat/grid/_shift.scss b/common/static/sass/neat/grid/_shift.scss new file mode 100644 index 0000000000..e39208ef0d --- /dev/null +++ b/common/static/sass/neat/grid/_shift.scss @@ -0,0 +1,10 @@ +@mixin shift($n-columns: 1) { + + $direction: get-direction($layout-direction, $default-layout-direction); + $opposite-direction: get-opposite-direction($direction); + + margin-#{$opposite-direction}: $n-columns * flex-grid(1, $parent-columns) + $n-columns * flex-gutter($parent-columns); + + // Reset nesting context + $parent-columns: $grid-columns; +} diff --git a/common/static/sass/neat/grid/_span-columns.scss b/common/static/sass/neat/grid/_span-columns.scss new file mode 100644 index 0000000000..97902d62c4 --- /dev/null +++ b/common/static/sass/neat/grid/_span-columns.scss @@ -0,0 +1,45 @@ +@mixin span-columns($span: $columns of $container-columns, $display: block) { + + $columns: nth($span, 1); + $container-columns: container-span($span); + $display-table: false; + + $direction: get-direction($layout-direction, $default-layout-direction); + $opposite-direction: get-opposite-direction($direction); + + @if $container-columns != $grid-columns { + $parent-columns: $container-columns; + } @else { + $parent-columns: $grid-columns; + } + + @if $container-display-table == true { + $display-table: true; + } @else if $display == table { + $display-table: true; + } @else { + $display-table: false; + } + + @if $display-table { + display: table-cell; + padding-#{$direction}: flex-gutter($container-columns); + width: flex-grid($columns, $container-columns) + flex-gutter($container-columns); + + &:last-child { + width: flex-grid($columns, $container-columns); + padding-#{$direction}: 0; + } + } + + @else { + display: block; + float: #{$opposite-direction}; + margin-#{$direction}: flex-gutter($container-columns); + width: flex-grid($columns, $container-columns); + + &:last-child { + margin-#{$direction}: 0; + } + } +} diff --git a/common/static/sass/neat/grid/_to-deprecate.scss b/common/static/sass/neat/grid/_to-deprecate.scss new file mode 100644 index 0000000000..d0a681fd12 --- /dev/null +++ b/common/static/sass/neat/grid/_to-deprecate.scss @@ -0,0 +1,57 @@ +@mixin breakpoint($query:$feature $value $columns, $total-columns: $grid-columns) { + @warn "The breakpoint() mixin was renamed to media() in Neat 1.0. Please update your project with the new syntax before the next version bump."; + + @if length($query) == 1 { + @media screen and ($default-feature: nth($query, 1)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 2 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 3 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 3); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 4 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 5 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 5); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else { + @warn "Wrong number of arguments for breakpoint(). Read the documentation for more details."; + } +} + +@mixin nth-omega($nth, $display: block, $direction: default) { + @warn "The nth-omega() mixin is deprecated. Please use omega() instead."; + @include omega($nth $display, $direction); +} diff --git a/common/static/sass/neat/grid/_visual-grid.scss b/common/static/sass/neat/grid/_visual-grid.scss new file mode 100644 index 0000000000..1c822fd322 --- /dev/null +++ b/common/static/sass/neat/grid/_visual-grid.scss @@ -0,0 +1,41 @@ +@mixin grid-column-gradient($values...) { + background-image: deprecated-webkit-gradient(linear, left top, left bottom, $values); + background-image: -webkit-linear-gradient(left, $values); + background-image: -moz-linear-gradient(left, $values); + background-image: -ms-linear-gradient(left, $values); + background-image: -o-linear-gradient(left, $values); + background-image: unquote("linear-gradient(left, #{$values})"); +} + +@if $visual-grid == true or $visual-grid == yes { + body:before { + content: ''; + display: inline-block; + @include grid-column-gradient(gradient-stops($grid-columns)); + height: 100%; + left: 0; + margin: 0 auto; + max-width: $max-width; + opacity: $visual-grid-opacity; + position: fixed; + right: 0; + width: 100%; + pointer-events: none; + + @if $visual-grid-index == back { + z-index: -1; + } + + @else if $visual-grid-index == front { + z-index: 9999; + } + + @each $breakpoint in $visual-grid-breakpoints { + @if $breakpoint != nil { + @include media($breakpoint) { + @include grid-column-gradient(gradient-stops($grid-columns)); + } + } + } + } +} diff --git a/common/static/sass/neat/settings/_grid.scss b/common/static/sass/neat/settings/_grid.scss new file mode 100644 index 0000000000..f1dcda4780 --- /dev/null +++ b/common/static/sass/neat/settings/_grid.scss @@ -0,0 +1,7 @@ +$column: golden-ratio(1em, 3) !default; // Column width +$gutter: golden-ratio(1em, 1) !default; // Gutter between each two columns +$grid-columns: 12 !default; // Total number of columns in the grid +$max-width: em(1088) !default; // Max-width of the outer container +$border-box-sizing: true !default; // Makes all elements have a border-box layout +$default-feature: min-width; // Default @media feature for the breakpoint() mixin +$default-layout-direction: LTR !default; diff --git a/common/static/sass/neat/settings/_visual-grid.scss b/common/static/sass/neat/settings/_visual-grid.scss new file mode 100644 index 0000000000..611c2b3727 --- /dev/null +++ b/common/static/sass/neat/settings/_visual-grid.scss @@ -0,0 +1,5 @@ +$visual-grid: false !default; // Display the base grid +$visual-grid-color: #EEE !default; +$visual-grid-index: back !default; // Show grid behind content (back) or overlay it over the content (front) +$visual-grid-opacity: 0.4 !default; +$visual-grid-breakpoints: () !default; diff --git a/common/templates/hinter_display.html b/common/templates/hinter_display.html new file mode 100644 index 0000000000..6f5d6f37fb --- /dev/null +++ b/common/templates/hinter_display.html @@ -0,0 +1,130 @@ +## The hinter module passes in a field called ${op}, which determines which +## sub-function to render. + + +<%def name="get_hint()"> + % if best_hint != '': +

Hints from students who made similar mistakes:

+
    +
  • ${best_hint}
  • + % endif + % if rand_hint_1 != '': +
  • ${rand_hint_1}
  • + % endif + % if rand_hint_2 != '': +
  • ${rand_hint_2}
  • + % endif +
+ + +<%def name="get_feedback()"> +

Participation in the hinting system is strictly optional, and will not influence your grade.

+

+ Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below: +

+ +
+
    + % for index, answer in index_to_answer.items(): +
  • ${answer}
  • + % endfor +
+ + % for index, answer in index_to_answer.items(): +
+
+ % if index in index_to_hints and len(index_to_hints[index]) > 0: +

+ Which hint would be most effective to show a student who also got ${answer}? +

+ % for hint_text, hint_pk in index_to_hints[index]: +

+ + ${hint_text} +

+ % endfor +

+ Don't like any of the hints above? You can also submit your own. +

+ % endif +

+ What hint would you give a student who made the same mistake you did? Please don't give away the answer. +

+ +

+ +
+ % endfor +
+ +

Read about what makes a good hint.

+ + + + +<%def name="show_votes()"> + % if hint_and_votes is UNDEFINED: + Sorry, but you've already voted! + % else: + Thank you for voting! +
+ % for hint, votes in hint_and_votes: + ${votes} votes. + ${hint} +
+ % endfor + % endif + + +<%def name="simple_message()"> + ${message} + + +% if op == "get_hint": + ${get_hint()} +% endif + +% if op == "get_feedback": + ${get_feedback()} +% endif + +% if op == "submit_hint": + ${simple_message()} +% endif + +% if op == "vote": + ${show_votes()} +% endif + diff --git a/common/test/data/uploads/textbook.pdf b/common/test/data/uploads/textbook.pdf new file mode 100644 index 0000000000..e6e7a031ce Binary files /dev/null and b/common/test/data/uploads/textbook.pdf differ diff --git a/doc/overview.md b/doc/overview.md index 31ddd011ff..c38c61b43e 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -64,6 +64,12 @@ You should be familiar with the following. If you're not, go read some docs... from a Location object, and the ModuleSystem knows how to render things, track events, and complain about 404s + - XModules and XModuleDescriptors are uniquely identified by a Location object, encoding the organization, course, category, name, and possibly revision of the module. + + - XModule initialization: XModules are instantiated by the `XModuleDescriptor.xmodule` method, and given a ModuleSystem, the descriptor which instantiated it, and their relevant model data. + + - XModuleDescriptor initialization: If an XModuleDescriptor is loaded from an XML-based course, the XML data is passed into its `from_xml` method, which is responsible for instantiating a descriptor with the correct attributes. If it's in Mongo, the descriptor is instantiated directly. The module's attributes will be present in the `model_data` dict. + - `course.xml` format. We use python setuptools to connect supported tags with the descriptors that handle them. See `common/lib/xmodule/setup.py`. There are checking and validation tools in `common/validate`. - the xml import+export functionality is in `xml_module.py:XmlDescriptor`, which is a mixin class that's used by the actual descriptor classes. diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst new file mode 100644 index 0000000000..5cf043a3ce --- /dev/null +++ b/doc/public/course_data_formats/jsinput.rst @@ -0,0 +1,151 @@ +############################################################################## +JS Input +############################################################################## + + **NOTE** + *Do not use this feature yet! Its attributes and behaviors may change + without any concern for backwards compatibility. Moreover, it has only been + tested in a very limited context. If you absolutely must, contact Julian + (julian@edx.org). When the feature stabilizes, this note will be removed.* + +This document explains how to write a JSInput input type. JSInput is meant to +allow problem authors to easily turn working standalone HTML files into +problems that can be integrated into the edX platform. Since it's aim is +flexibility, it can be seen as the input and client-side equivalent of +CustomResponse. + +A JSInput input creates an iframe into a static HTML page, and passes the +return value of author-specified functions to the enclosing response type +(generally CustomResponse). JSInput can also stored and retrieve state. + +****************************************************************************** +Format +****************************************************************************** + +A jsinput problem looks like this: + +.. code-block:: xml + + + + + + + + +The accepted attributes are: + +============== ============== ========= ========== +Attribute Name Value Type Required? Default +============== ============== ========= ========== +html_file Url string Yes None +gradefn Function name Yes `gradefn` +set_statefn Function name No None +get_statefn Function name No None +height Integer No `500` +width Integer No `400` +============== ============== ========= ========== + +****************************************************************************** +Required Attributes +****************************************************************************** + +============================================================================== +html_file +============================================================================== + +The `html_file` attribute specifies what html file the iframe will point to. This +should be located in the content directory. + +The iframe is created using the sandbox attribute; while popups, scripts, and +pointer locks are allowed, the iframe cannot access its parent's attributes. + +The html file should contain an accesible gradefn function. To check whether +the gradefn will be accessible to JSInput, check that, in the console,:: + "`gradefn" +Returns the right thing. When used by JSInput, `gradefn` is called with:: + `gradefn`.call(`obj`) +Where `obj` is the object-part of `gradefn`. For example, if `gradefn` is +`myprog.myfn`, JSInput will call `myprog.myfun.call(myprog)`. (This is to +ensure "`this`" continues to refer to what `gradefn` expects.) + +Aside from that, more or less anything goes. Note that currently there is no +support for inheriting css or javascript from the parent (aside from the +Chrome-only `seamless` attribute, which is set to true by default). + +============================================================================== +gradefn +============================================================================== + +The `gradefn` attribute specifies the name of the function that will be called +when a user clicks on the "Check" button, and which should return the student's +answer. This answer will (unless both the get_statefn and set_statefn +attributes are also used) be passed as a string to the enclosing response type. +In the customresponse example above, this means cfn will be passed this answer +as `ans`. + +If the `gradefn` function throws an exception when a student attempts to +submit a problem, the submission is aborted, and the student receives a generic +alert. The alert can be customised by making the exception name `Waitfor +Exception`; in that case, the alert message will be the exception message. + +**IMPORTANT** : the `gradefn` function should not be at all asynchronous, since +this could result in the student's latest answer not being passed correctly. +Moreover, the function should also return promptly, since currently the student +has no indication that her answer is being calculated/produced. + +****************************************************************************** +Option Attributes +****************************************************************************** + +The `height` and `width` attributes are straightforward: they specify the +height and width of the iframe. Both are limited by the enclosing DOM elements, +so for instance there is an implicit max-width of around 900. + +In the future, JSInput may attempt to make these dimensions match the html +file's dimensions (up to the aforementioned limits), but currently it defaults +to `500` and `400` for `height` and `width`, respectively. + +============================================================================== +set_statefn +============================================================================== + +Sometimes a problem author will want information about a student's previous +answers ("state") to be saved and reloaded. If the attribute `set_statefn` is +used, the function given as its value will be passed the state as a string +argument whenever there is a state, and the student returns to a problem. It is +the responsibility of the function to then use this state approriately. + +The state that is passed is: + +1. The previous output of `gradefn` (i.e., the previous answer) if + `get_statefn` is not defined. +2. The previous output of `get_statefn` (see below) otherwise. + +It is the responsibility of the iframe to do proper verification of the +argument that it receives via `set_statefn`. + +============================================================================== +get_statefn +============================================================================== + +Sometimes the state and the answer are quite different. For instance, a problem +that involves using a javascript program that allows the student to alter a +molecule may grade based on the molecule's hidrophobicity, but from the +hidrophobicity it might be incapable of restoring the state. In that case, a +*separate* state may be stored and loaded by `set_statefn`. Note that if +`get_statefn` is defined, the answer (i.e., what is passed to the enclosing +response type) will be a json string with the following format:: + { + answer: `[answer string]` + state: `[state string]` + } + +It is the responsibility of the enclosing response type to then parse this as +json. diff --git a/doc/public/index.rst b/doc/public/index.rst index cda3809237..2af091353e 100644 --- a/doc/public/index.rst +++ b/doc/public/index.rst @@ -29,6 +29,7 @@ Specific Problem Types course_data_formats/word_cloud/word_cloud.rst course_data_formats/custom_response.rst course_data_formats/symbolic_response.rst + course_data_formats/jsinput.rst Internal Data Formats diff --git a/docs/source/conf.py b/docs/source/conf.py index 2c398c1b9a..aa62613370 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,18 +1,11 @@ # -*- coding: utf-8 -*- +""" +EdX documentation build configuration file +""" #pylint: disable=C0103 #pylint: disable=W0622 #pylint: disable=W0212 #pylint: disable=W0613 -""" EdX documentation build configuration file, created by - sphinx-quickstart on Fri Nov 2 15:43:00 2012. - - This file is execfile()d with the current directory set to its containing dir. - - Note that not all possible configuration values are present in this - autogenerated file. - - All configuration values have a default; values that are commented out - serve to show the default.""" import sys import os @@ -21,7 +14,17 @@ import os # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../..')) # mitx folder + +root = os.path.abspath('../..') + +sys.path.append(root) +sys.path.append(os.path.join(root, "common/djangoapps")) +sys.path.append(os.path.join(root, "common/lib")) +sys.path.append(os.path.join(root, "common/lib/sandbox-packages")) +sys.path.append(os.path.join(root, "lms/djangoapps")) +sys.path.append(os.path.join(root, "lms/lib")) +sys.path.append(os.path.join(root, "cms/djangoapps")) +sys.path.append(os.path.join(root, "cms/lib")) # django configuration - careful here os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test' diff --git a/docs/source/lms.rst b/docs/source/lms.rst index 6548cd71a0..c7ba7dde62 100644 --- a/docs/source/lms.rst +++ b/docs/source/lms.rst @@ -39,13 +39,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. automodule:: certificates.tests - :members: - :show-inheritance: - Circuit ======= @@ -67,13 +60,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. automodule:: circuit.tests - :members: - :show-inheritance: - Course_wiki =========== @@ -181,12 +167,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. automodule:: dashboard.tests - :members: - :show-inheritance: Django comment client ===================== @@ -202,12 +182,6 @@ Models :members: :show-inheritance: -Tests ------ - -.. automodule:: django_comment_client.tests - :members: - :show-inheritance: Heartbeat ========= @@ -230,12 +204,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. .. automodule:: instructor.tests -.. :members: -.. :show-inheritance: Lisenses ======== @@ -258,12 +226,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. automodule:: licenses.tests - :members: - :show-inheritance: LMS migration ============= @@ -322,13 +284,6 @@ Static template view :members: :show-inheritance: -Models ------- - -.. automodule:: static_template_view.models - :members: - :show-inheritance: - Views ----- @@ -336,13 +291,6 @@ Views :members: :show-inheritance: -Tests ------ - -.. automodule:: static_template_view.tests - :members: - :show-inheritance: - Static book =========== @@ -364,10 +312,3 @@ Views .. automodule:: staticbook.views :members: :show-inheritance: - -Tests ------ - -.. automodule:: staticbook.tests - :members: - :show-inheritance: diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index af1037f903..78e786e884 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -194,6 +194,7 @@ class XQueueCertInterface(object): # on the queue if self.restricted.filter(user=student).exists(): cert.status = status.restricted + cert.save() else: contents = { 'action': 'create', @@ -202,15 +203,15 @@ class XQueueCertInterface(object): 'name': profile.name, } cert.status = status.generating + cert.save() self._send_to_xqueue(contents, key) - cert.save() else: cert_status = status.notpassing cert.grade = grade['percent'] - cert.status = cert_status cert.user = student cert.course_id = course_id cert.name = profile.name + cert.status = cert_status cert.save() return cert_status diff --git a/lms/djangoapps/certificates/tests.py b/lms/djangoapps/certificates/tests.py deleted file mode 100644 index 501deb776c..0000000000 --- a/lms/djangoapps/certificates/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/lms/djangoapps/circuit/tests.py b/lms/djangoapps/circuit/tests.py deleted file mode 100644 index 501deb776c..0000000000 --- a/lms/djangoapps/circuit/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 50b536d444..8259507617 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -586,7 +586,6 @@ def _has_access_to_location(user, location, access_level, course_context): debug("Deny: user not in groups %s", instructor_groups) else: log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level) - return False diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 5c12725d0a..db7ba1641e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -37,7 +37,7 @@ from courseware.access import has_access from courseware.masquerade import setup_masquerade from courseware.model_data import LmsKeyValueStore, LmsUsage, ModelDataCache from courseware.models import StudentModule - +from util.sandboxing import can_execute_unsafe_code log = logging.getLogger(__name__) @@ -61,9 +61,9 @@ def make_track_function(request): ''' import track.views - def f(event_type, event): + def function(event_type, event): return track.views.server_track(request, event_type, event, page='x_module') - return f + return function def toc_for_course(user, request, course, active_chapter, active_section, model_data_cache): @@ -171,9 +171,9 @@ def get_xqueue_callback_url_prefix(request): should go back to the LMS, not to the worker. """ prefix = '{proto}://{host}'.format( - proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'), - host=request.get_host() - ) + proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'), + host=request.get_host() + ) return settings.XQUEUE_INTERFACE.get('callback_url', prefix) @@ -313,14 +313,6 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours statsd.increment("lms.courseware.question_answered", tags=tags) - def can_execute_unsafe_code(): - # 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 re.match(regex, course_id): - return True - return False - # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from @@ -348,7 +340,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, - can_execute_unsafe_code=can_execute_unsafe_code, + can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) @@ -481,7 +473,8 @@ def modx_dispatch(request, dispatch, location, course_id): error_msg = _check_files_limits(files) if error_msg: return HttpResponse(json.dumps({'success': error_msg})) - data.update(files) # Merge files into data dictionary + for key in files: # Merge files into to data dictionary + data[key] = files.getlist(key) try: descriptor = modulestore().get_instance(course_id, location) diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index bde0c89542..0abbaa02cf 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -32,13 +32,11 @@ class BaseTestXmodule(ModuleStoreTestCase): 1. TEMPLATE_NAME 2. DATA 3. MODEL_DATA - 4. COURSE_DATA and USER_COUNT if needed This class should not contain any tests, because TEMPLATE_NAME should be defined in child class. """ USER_COUNT = 2 - COURSE_DATA = {} # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml TEMPLATE_NAME = "" @@ -47,7 +45,7 @@ class BaseTestXmodule(ModuleStoreTestCase): def setUp(self): - self.course = CourseFactory.create(data=self.COURSE_DATA) + self.course = CourseFactory.create() # Turn off cache. modulestore().request_cache = None diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py new file mode 100644 index 0000000000..6890a6df2a --- /dev/null +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -0,0 +1,134 @@ +import json + +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse + +from student.models import Registration + +from django.test import TestCase + + +def check_for_get_code(self, code, url): + """ + Check that we got the expected code when accessing url via GET. + Returns the HTTP response. + + `self` is a class that subclasses TestCase. + + `code` is a status code for HTTP responses. + + `url` is a url pattern for which we have to test the response. + """ + resp = self.client.get(url) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp + + +def check_for_post_code(self, code, url, data={}): + """ + Check that we got the expected code when accessing url via POST. + Returns the HTTP response. + `self` is a class that subclasses TestCase. + + `code` is a status code for HTTP responses. + + `url` is a url pattern for which we want to test the response. + """ + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp + + +class LoginEnrollmentTestCase(TestCase): + """ + Provides support for user creation, + activation, login, and course enrollment. + """ + def setup_user(self): + """ + Create a user account, activate, and log in. + """ + self.email = 'foo@test.com' + self.password = 'bar' + self.username = 'test' + self.create_account(self.username, + self.email, self.password) + self.activate_user(self.email) + self.login(self.email, self.password) + + # ============ User creation and login ============== + + def login(self, email, password): + """ + Login, check that the corresponding view's response has a 200 status code. + """ + resp = self.client.post(reverse('login'), + {'email': email, 'password': password}) + self.assertEqual(resp.status_code, 200) + data = json.loads(resp.content) + self.assertTrue(data['success']) + + def logout(self): + """ + Logout; check that the HTTP response code indicates redirection + as expected. + """ + # should redirect + check_for_get_code(self, 302, reverse('logout')) + + def create_account(self, username, email, password): + """ + Create the account and check that it worked. + """ + resp = check_for_post_code(self, 200, reverse('create_account'), { + 'username': username, + 'email': email, + 'password': password, + 'name': 'username', + 'terms_of_service': 'true', + 'honor_code': 'true', + }) + data = json.loads(resp.content) + self.assertEqual(data['success'], True) + # Check both that the user is created, and inactive + self.assertFalse(User.objects.get(email=email).is_active) + + def activate_user(self, email): + """ + Look up the activation key for the user, then hit the activate view. + No error checking. + """ + activation_key = Registration.objects.get(user__email=email).activation_key + # and now we try to activate + check_for_get_code(self, 200, reverse('activate', kwargs={'key': activation_key})) + # Now make sure that the user is now actually activated + self.assertTrue(User.objects.get(email=email).is_active) + + def enroll(self, course, verify=False): + """ + Try to enroll and return boolean indicating result. + `course` is an instance of CourseDescriptor. + `verify` is an optional boolean parameter specifying whether we + want to verify that the student was successfully enrolled + in the course. + """ + resp = self.client.post(reverse('change_enrollment'), { + 'enrollment_action': 'enroll', + 'course_id': course.id, + }) + result = resp.status_code == 200 + if verify: + self.assertTrue(result) + return result + + def unenroll(self, course): + """ + Unenroll the currently logged-in user, and check that it worked. + `course` is an instance of CourseDescriptor. + """ + check_for_post_code(self, 200, reverse('change_enrollment'), {'enrollment_action': 'unenroll', + 'course_id': course.id}) diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py new file mode 100644 index 0000000000..80a7b0a7c1 --- /dev/null +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -0,0 +1,8 @@ +from xmodule.modulestore.tests.django_utils import xml_store_config, mongo_store_config, draft_mongo_store_config + +from django.conf import settings + +TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT +TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) diff --git a/lms/djangoapps/courseware/tests/test_draft_modulestore.py b/lms/djangoapps/courseware/tests/test_draft_modulestore.py new file mode 100644 index 0000000000..db6d4c45b5 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_draft_modulestore.py @@ -0,0 +1,21 @@ +from django.test import TestCase +from django.test.utils import override_settings + +from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location + +from modulestore_config import TEST_DATA_DRAFT_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) +class TestDraftModuleStore(TestCase): + def test_get_items_with_course_items(self): + store = modulestore() + + # fix was to allow get_items() to take the course_id parameter + store.get_items(Location(None, None, 'vertical', None, None), + course_id='abc', depth=0) + + # test success is just getting through the above statement. + # The bug was that 'course_id' argument was + # not allowed to be passed in (i.e. was throwing exception) diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 47d437a316..3dc3d2b6b1 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -12,13 +12,15 @@ from django.test.utils import override_settings from django.core.urlresolvers import reverse -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django import json + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ''' @@ -41,7 +43,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.graded_course) @@ -67,7 +69,6 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): self.assertTrue(sdebug in resp.content) - def toggle_masquerade(self): ''' Toggle masquerade state diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 775b6ff0fc..ea31f5110c 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -12,6 +12,7 @@ from xmodule.modulestore.django import modulestore import courseware.module_render as render from courseware.tests.tests import LoginEnrollmentTestCase from courseware.model_data import ModelDataCache +from modulestore_config import TEST_DATA_XML_MODULESTORE from .factories import UserFactory @@ -21,21 +22,6 @@ class Stub: pass -def xml_store_config(data_dir): - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } - } - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) - - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class ModuleRenderTestCase(LoginEnrollmentTestCase): def setUp(self): diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py new file mode 100644 index 0000000000..dd1f00711c --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -0,0 +1,100 @@ +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from helpers import LoginEnrollmentTestCase, check_for_get_code +from modulestore_config import TEST_DATA_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check that navigation state is saved properly. + """ + + STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] + + def setUp(self): + + self.test_course = CourseFactory.create(display_name='Robot_Sub_Course') + self.course = CourseFactory.create(display_name='Robot_Super_Course') + self.chapter0 = ItemFactory.create(parent_location=self.course.location, + display_name='Overview') + self.chapter9 = ItemFactory.create(parent_location=self.course.location, + display_name='factory_chapter') + self.section0 = ItemFactory.create(parent_location=self.chapter0.location, + display_name='Welcome') + self.section9 = ItemFactory.create(parent_location=self.chapter9.location, + display_name='factory_section') + + # Create student accounts and activate them. + for i in range(len(self.STUDENT_INFO)): + email, password = self.STUDENT_INFO[i] + username = 'u{0}'.format(i) + self.create_account(username, email, password) + self.activate_user(email) + + def test_redirects_first_time(self): + """ + Verify that the first time we click on the courseware tab we are + redirected to the 'Welcome' section. + """ + email, password = self.STUDENT_INFO[0] + self.login(email, password) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirects(resp, reverse( + 'courseware_section', kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + def test_redirects_second_time(self): + """ + Verify the accordion remembers we've already visited the Welcome section + and redirects correpondingly. + """ + email, password = self.STUDENT_INFO[0] + self.login(email, password) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + self.client.get(reverse('courseware_section', kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirects(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.id, + 'chapter': 'Overview'})) + + def test_accordion_state(self): + """ + Verify the accordion remembers which chapter you were last viewing. + """ + email, password = self.STUDENT_INFO[0] + self.login(email, password) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # Now we directly navigate to a section in a chapter other than 'Overview'. + check_for_get_code(self, 200, reverse('courseware_section', + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter', + 'section': 'factory_section'})) + + # And now hitting the courseware tab should redirect to 'factory_chapter' + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirects(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter'})) diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py new file mode 100644 index 0000000000..83ae7dc73e --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -0,0 +1,724 @@ +"""Integration tests for submitting problem responses and getting grades.""" + +# text processing dependancies +import json +from textwrap import dedent + +from django.contrib.auth.models import User +from django.test.client import RequestFactory +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +# Need access to internal func to put users in the right group +from courseware import grades +from courseware.model_data import ModelDataCache + +from xmodule.modulestore.django import modulestore + +#import factories and parent testcase modules +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from capa.tests.response_xml_factory import OptionResponseXMLFactory, CustomResponseXMLFactory, SchematicResponseXMLFactory +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check that a course gets graded properly. + """ + + # arbitrary constant + COURSE_SLUG = "100" + COURSE_NAME = "test_course" + + def setUp(self): + + # Create course + self.course = CourseFactory.create(display_name=self.COURSE_NAME, number=self.COURSE_SLUG) + assert self.course, "Couldn't load course %r" % self.COURSE_NAME + + # create a test student + self.student = 'view@test.com' + self.password = 'foo' + self.create_account('u1', self.student, self.password) + self.activate_user(self.student) + self.enroll(self.course) + self.student_user = User.objects.get(email=self.student) + self.factory = RequestFactory() + + def refresh_course(self): + """ + Re-fetch the course from the database so that the object being dealt with has everything added to it. + """ + self.course = modulestore().get_instance(self.course.id, self.course.location) + + def problem_location(self, problem_url_name): + """ + Returns the url of the problem given the problem's name + """ + + return "i4x://"+self.course.org+"/{}/problem/{}".format(self.COURSE_SLUG, problem_url_name) + + def modx_url(self, problem_location, dispatch): + """ + Return the url needed for the desired action. + + problem_location: location of the problem on which we want some action + + dispatch: the the action string that gets passed to the view as a kwarg + example: 'check_problem' for having responses processed + """ + return reverse( + 'modx_dispatch', + kwargs={ + 'course_id': self.course.id, + 'location': problem_location, + 'dispatch': dispatch, + } + ) + + def submit_question_answer(self, problem_url_name, responses): + """ + Submit answers to a question. + + Responses is a dict mapping problem ids to answers: + {'2_1': 'Correct', '2_2': 'Incorrect'} + """ + + problem_location = self.problem_location(problem_url_name) + modx_url = self.modx_url(problem_location, 'problem_check') + + answer_key_prefix = 'input_i4x-' + self.course.org + '-{}-problem-{}_'.format(self.COURSE_SLUG, problem_url_name) + + # format the response dictionary to be sent in the post request by adding the above prefix to each key + response_dict = {(answer_key_prefix + k): v for k, v in responses.items()} + resp = self.client.post(modx_url, response_dict) + + return resp + + def reset_question_answer(self, problem_url_name): + """ + Reset specified problem for current user. + """ + problem_location = self.problem_location(problem_url_name) + modx_url = self.modx_url(problem_location, 'problem_reset') + resp = self.client.post(modx_url) + return resp + + def add_dropdown_to_section(self, section_location, name, num_inputs=2): + """ + Create and return a dropdown problem. + + section_location: location object of section in which to create the problem + (problems must live in a section to be graded properly) + + name: string name of the problem + + num_input: the number of input fields to create in the problem + """ + + problem_template = "i4x://edx/templates/problem/Blank_Common_Problem" + prob_xml = OptionResponseXMLFactory().build_xml( + question_text='The correct answer is Correct', + num_inputs=num_inputs, + weight=num_inputs, + options=['Correct', 'Incorrect'], + correct_option='Correct' + ) + + problem = ItemFactory.create( + parent_location=section_location, + template=problem_template, + data=prob_xml, + metadata={'randomize': 'always'}, + display_name=name + ) + + # re-fetch the course from the database so the object is up to date + self.refresh_course() + return problem + + def add_graded_section_to_course(self, name, section_format='Homework'): + """ + Creates a graded homework section within a chapter and returns the section. + """ + + # if we don't already have a chapter create a new one + if not(hasattr(self, 'chapter')): + self.chapter = ItemFactory.create( + parent_location=self.course.location, + template="i4x://edx/templates/chapter/Empty", + ) + + section = ItemFactory.create( + parent_location=self.chapter.location, + display_name=name, + template="i4x://edx/templates/sequential/Empty", + metadata={'graded': True, 'format': section_format} + ) + + # now that we've added the problem and section to the course + # we fetch the course from the database so the object we are + # dealing with has these additions + self.refresh_course() + return section + + +class TestCourseGrader(TestSubmittingProblems): + """ + Suite of tests for the course grader. + """ + + def add_grading_policy(self, grading_policy): + """ + Add a grading policy to the course. + """ + + course_data = {'grading_policy': grading_policy} + modulestore().update_item(self.course.location, course_data) + self.refresh_course() + + def get_grade_summary(self): + """ + calls grades.grade for current user and course. + + the keywords for the returned object are + - grade : A final letter grade. + - percent : The final percent for the class (rounded up). + - section_breakdown : A breakdown of each section that makes + up the grade. (For display) + - grade_breakdown : A breakdown of the major components that + make up the final grade. (For display) + """ + + model_data_cache = ModelDataCache.cache_for_descriptor_descendents( + self.course.id, self.student_user, self.course) + + fake_request = self.factory.get(reverse('progress', + kwargs={'course_id': self.course.id})) + + return grades.grade(self.student_user, fake_request, + self.course, model_data_cache) + + def get_progress_summary(self): + """ + Return progress summary structure for current user and course. + + Returns + - courseware_summary is a summary of all sections with problems in the course. + It is organized as an array of chapters, each containing an array of sections, + each containing an array of scores. This contains information for graded and + ungraded problems, and is good for displaying a course summary with due dates, + etc. + """ + + model_data_cache = ModelDataCache.cache_for_descriptor_descendents( + self.course.id, self.student_user, self.course) + + fake_request = self.factory.get(reverse('progress', + kwargs={'course_id': self.course.id})) + + progress_summary = grades.progress_summary(self.student_user, + fake_request, + self.course, + model_data_cache) + return progress_summary + + def check_grade_percent(self, percent): + """ + Assert that percent grade is as expected. + """ + grade_summary = self.get_grade_summary() + self.assertEqual(grade_summary['percent'], percent) + + def earned_hw_scores(self): + """ + Global scores, each Score is a Problem Set. + + Returns list of scores: [, , ..., ] + """ + return [s.earned for s in self.get_grade_summary()['totaled_scores']['Homework']] + + def score_for_hw(self, hw_url_name): + """ + Returns list of scores for a given url. + + Returns list of scores for the given homework: + [, , ..., ] + """ + + # list of grade summaries for each section + sections_list = [] + for chapter in self.get_progress_summary(): + sections_list.extend(chapter['sections']) + + # get the first section that matches the url (there should only be one) + hw_section = next(section for section in sections_list if section.get('url_name') == hw_url_name) + return [s.earned for s in hw_section['scores']] + + def basic_setup(self): + """ + Set up a simple course for testing basic grading functionality. + """ + + grading_policy = { + "GRADER": [{ + "type": "Homework", + "min_count": 1, + "drop_count": 0, + "short_label": "HW", + "weight": 1.0 + }], + "GRADE_CUTOFFS": { + 'A': .9, + 'B': .33 + } + } + self.add_grading_policy(grading_policy) + + # set up a simple course with four problems + self.homework = self.add_graded_section_to_course('homework') + self.add_dropdown_to_section(self.homework.location, 'p1', 1) + self.add_dropdown_to_section(self.homework.location, 'p2', 1) + self.add_dropdown_to_section(self.homework.location, 'p3', 1) + self.refresh_course() + + def weighted_setup(self): + """ + Set up a simple course for testing weighted grading functionality. + """ + + grading_policy = { + "GRADER": [{ + "type": "Homework", + "min_count": 1, + "drop_count": 0, + "short_label": "HW", + "weight": 0.25 + }, { + "type": "Final", + "name": "Final Section", + "short_label": "Final", + "weight": 0.75 + }] + } + self.add_grading_policy(grading_policy) + + # set up a structure of 1 homework and 1 final + self.homework = self.add_graded_section_to_course('homework') + self.problem = self.add_dropdown_to_section(self.homework.location, 'H1P1') + self.final = self.add_graded_section_to_course('Final Section', 'Final') + self.final_question = self.add_dropdown_to_section(self.final.location, 'FinalQuestion') + + def dropping_setup(self): + """ + Set up a simple course for testing the dropping grading functionality. + """ + + grading_policy = { + "GRADER": [ + { + "type": "Homework", + "min_count": 3, + "drop_count": 1, + "short_label": "HW", + "weight": 1 + }] + } + self.add_grading_policy(grading_policy) + + # Set up a course structure that just consists of 3 homeworks. + # Since the grading policy drops 1 entire homework, each problem is worth 25% + + # names for the problem in the homeworks + self.hw1_names = ['h1p1', 'h1p2'] + self.hw2_names = ['h2p1', 'h2p2'] + self.hw3_names = ['h3p1', 'h3p2'] + + self.homework1 = self.add_graded_section_to_course('homework1') + self.add_dropdown_to_section(self.homework1.location, self.hw1_names[0], 1) + self.add_dropdown_to_section(self.homework1.location, self.hw1_names[1], 1) + self.homework2 = self.add_graded_section_to_course('homework2') + self.add_dropdown_to_section(self.homework2.location, self.hw2_names[0], 1) + self.add_dropdown_to_section(self.homework2.location, self.hw2_names[1], 1) + self.homework3 = self.add_graded_section_to_course('homework3') + self.add_dropdown_to_section(self.homework3.location, self.hw3_names[0], 1) + self.add_dropdown_to_section(self.homework3.location, self.hw3_names[1], 1) + + def test_none_grade(self): + """ + Check grade is 0 to begin with. + """ + self.basic_setup() + self.check_grade_percent(0) + self.assertEqual(self.get_grade_summary()['grade'], None) + + def test_b_grade_exact(self): + """ + Check that at exactly the cutoff, the grade is B. + """ + self.basic_setup() + self.submit_question_answer('p1', {'2_1': 'Correct'}) + self.check_grade_percent(0.33) + self.assertEqual(self.get_grade_summary()['grade'], 'B') + + def test_b_grade_above(self): + """ + Check grade between cutoffs. + """ + self.basic_setup() + self.submit_question_answer('p1', {'2_1': 'Correct'}) + self.submit_question_answer('p2', {'2_1': 'Correct'}) + self.check_grade_percent(0.67) + self.assertEqual(self.get_grade_summary()['grade'], 'B') + + def test_a_grade(self): + """ + Check that 100 percent completion gets an A + """ + self.basic_setup() + self.submit_question_answer('p1', {'2_1': 'Correct'}) + self.submit_question_answer('p2', {'2_1': 'Correct'}) + self.submit_question_answer('p3', {'2_1': 'Correct'}) + self.check_grade_percent(1.0) + self.assertEqual(self.get_grade_summary()['grade'], 'A') + + def test_wrong_asnwers(self): + """ + Check that answering incorrectly is graded properly. + """ + self.basic_setup() + self.submit_question_answer('p1', {'2_1': 'Correct'}) + self.submit_question_answer('p2', {'2_1': 'Correct'}) + self.submit_question_answer('p3', {'2_1': 'Incorrect'}) + self.check_grade_percent(0.67) + self.assertEqual(self.get_grade_summary()['grade'], 'B') + + def test_weighted_homework(self): + """ + Test that the homework section has proper weight. + """ + self.weighted_setup() + + # Get both parts correct + self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Correct'}) + self.check_grade_percent(0.25) + self.assertEqual(self.earned_hw_scores(), [2.0]) # Order matters + self.assertEqual(self.score_for_hw('homework'), [2.0]) + + def test_weighted_exam(self): + """ + Test that the exam section has the proper weight. + """ + self.weighted_setup() + self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'}) + self.check_grade_percent(0.75) + + def test_weighted_total(self): + """ + Test that the weighted total adds to 100. + """ + self.weighted_setup() + self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Correct'}) + self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'}) + self.check_grade_percent(1.0) + + def dropping_homework_stage1(self): + """ + Get half the first homework correct and all of the second + """ + self.submit_question_answer(self.hw1_names[0], {'2_1': 'Correct'}) + self.submit_question_answer(self.hw1_names[1], {'2_1': 'Incorrect'}) + for name in self.hw2_names: + self.submit_question_answer(name, {'2_1': 'Correct'}) + + def test_dropping_grades_normally(self): + """ + Test that the dropping policy does not change things before it should. + """ + self.dropping_setup() + self.dropping_homework_stage1() + + self.assertEqual(self.score_for_hw('homework1'), [1.0, 0.0]) + self.assertEqual(self.score_for_hw('homework2'), [1.0, 1.0]) + self.assertEqual(self.earned_hw_scores(), [1.0, 2.0, 0]) # Order matters + self.check_grade_percent(0.75) + + def test_dropping_nochange(self): + """ + Tests that grade does not change when making the global homework grade minimum not unique. + """ + self.dropping_setup() + self.dropping_homework_stage1() + self.submit_question_answer(self.hw3_names[0], {'2_1': 'Correct'}) + + self.assertEqual(self.score_for_hw('homework1'), [1.0, 0.0]) + self.assertEqual(self.score_for_hw('homework2'), [1.0, 1.0]) + self.assertEqual(self.score_for_hw('homework3'), [1.0, 0.0]) + self.assertEqual(self.earned_hw_scores(), [1.0, 2.0, 1.0]) # Order matters + self.check_grade_percent(0.75) + + def test_dropping_all_correct(self): + """ + Test that the lowest is dropped for a perfect score. + """ + self.dropping_setup() + + self.dropping_homework_stage1() + for name in self.hw3_names: + self.submit_question_answer(name, {'2_1': 'Correct'}) + + self.check_grade_percent(1.0) + self.assertEqual(self.earned_hw_scores(), [1.0, 2.0, 2.0]) # Order matters + self.assertEqual(self.score_for_hw('homework3'), [1.0, 1.0]) + + +class TestPythonGradedResponse(TestSubmittingProblems): + """ + Check that we can submit a schematic and custom response, and it answers properly. + """ + + SCHEMATIC_SCRIPT = dedent(""" + # for a schematic response, submission[i] is the json representation + # of the diagram and analysis results for the i-th schematic tag + + def get_tran(json,signal): + for element in json: + if element[0] == 'transient': + return element[1].get(signal,[]) + return [] + + def get_value(at,output): + for (t,v) in output: + if at == t: return v + return None + + output = get_tran(submission[0],'Z') + okay = True + + # output should be 1, 1, 1, 1, 1, 0, 0, 0 + if get_value(0.0000004, output) < 2.7: okay = False; + if get_value(0.0000009, output) < 2.7: okay = False; + if get_value(0.0000014, output) < 2.7: okay = False; + if get_value(0.0000019, output) < 2.7: okay = False; + if get_value(0.0000024, output) < 2.7: okay = False; + if get_value(0.0000029, output) > 0.25: okay = False; + if get_value(0.0000034, output) > 0.25: okay = False; + if get_value(0.0000039, output) > 0.25: okay = False; + + correct = ['correct' if okay else 'incorrect']""").strip() + + SCHEMATIC_CORRECT = json.dumps( + [['transient', {'Z': [ + [0.0000004, 2.8], + [0.0000009, 2.8], + [0.0000014, 2.8], + [0.0000019, 2.8], + [0.0000024, 2.8], + [0.0000029, 0.2], + [0.0000034, 0.2], + [0.0000039, 0.2] + ]}]] + ) + + SCHEMATIC_INCORRECT = json.dumps( + [['transient', {'Z': [ + [0.0000004, 2.8], + [0.0000009, 0.0], # wrong. + [0.0000014, 2.8], + [0.0000019, 2.8], + [0.0000024, 2.8], + [0.0000029, 0.2], + [0.0000034, 0.2], + [0.0000039, 0.2] + ]}]] + ) + + CUSTOM_RESPONSE_SCRIPT = dedent(""" + def test_csv(expect, ans): + # Take out all spaces in expected answer + expect = [i.strip(' ') for i in str(expect).split(',')] + # Take out all spaces in student solution + ans = [i.strip(' ') for i in str(ans).split(',')] + + def strip_q(x): + # Strip quotes around strings if students have entered them + stripped_ans = [] + for item in x: + if item[0] == "'" and item[-1]=="'": + item = item.strip("'") + elif item[0] == '"' and item[-1] == '"': + item = item.strip('"') + stripped_ans.append(item) + return stripped_ans + + return strip_q(expect) == strip_q(ans)""").strip() + + CUSTOM_RESPONSE_CORRECT = "0, 1, 2, 3, 4, 5, 'Outside of loop', 6" + CUSTOM_RESPONSE_INCORRECT = "Reading my code I see. I hope you like it :)" + + COMPUTED_ANSWER_SCRIPT = dedent(""" + if submission[0] == "a shout in the street": + correct = ['correct'] + else: + correct = ['incorrect']""").strip() + + COMPUTED_ANSWER_CORRECT = "a shout in the street" + COMPUTED_ANSWER_INCORRECT = "because we never let them in" + + def setUp(self): + super(TestPythonGradedResponse, self).setUp() + self.section = self.add_graded_section_to_course('section') + self.correct_responses = {} + self.incorrect_responses = {} + + def schematic_setup(self, name): + """ + set up an example Circuit_Schematic_Builder problem + """ + + schematic_template = "i4x://edx/templates/problem/Circuit_Schematic_Builder" + script = self.SCHEMATIC_SCRIPT + + xmldata = SchematicResponseXMLFactory().build_xml(answer=script) + ItemFactory.create( + parent_location=self.section.location, + template=schematic_template, + display_name=name, + data=xmldata + ) + + # define the correct and incorrect responses to this problem + self.correct_responses[name] = self.SCHEMATIC_CORRECT + self.incorrect_responses[name] = self.SCHEMATIC_INCORRECT + + # re-fetch the course from the database so the object is up to date + self.refresh_course() + + def custom_response_setup(self, name): + """ + set up an example custom response problem using a check function + """ + + custom_template = "i4x://edx/templates/problem/Custom_Python-Evaluated_Input" + test_csv = self.CUSTOM_RESPONSE_SCRIPT + expect = self.CUSTOM_RESPONSE_CORRECT + cfn_problem_xml = CustomResponseXMLFactory().build_xml(script=test_csv, cfn='test_csv', expect=expect) + + ItemFactory.create( + parent_location=self.section.location, + template=custom_template, + data=cfn_problem_xml, + display_name=name + ) + + # define the correct and incorrect responses to this problem + self.correct_responses[name] = expect + self.incorrect_responses[name] = self.CUSTOM_RESPONSE_INCORRECT + + # re-fetch the course from the database so the object is up to date + self.refresh_course() + + def computed_answer_setup(self, name): + """ + set up an example problem using an answer script''' + """ + + script = self.COMPUTED_ANSWER_SCRIPT + + custom_template = "i4x://edx/templates/problem/Custom_Python-Evaluated_Input" + + computed_xml = CustomResponseXMLFactory().build_xml(answer=script) + + ItemFactory.create( + parent_location=self.section.location, + template=custom_template, + data=computed_xml, + display_name=name + ) + + # define the correct and incorrect responses to this problem + self.correct_responses[name] = self.COMPUTED_ANSWER_CORRECT + self.incorrect_responses[name] = self.COMPUTED_ANSWER_INCORRECT + + # re-fetch the course from the database so the object is up to date + self.refresh_course() + + def _check_correct(self, name): + """ + check that problem named "name" gets evaluated correctly correctly + """ + resp = self.submit_question_answer(name, {'2_1': self.correct_responses[name]}) + + respdata = json.loads(resp.content) + self.assertEqual(respdata['success'], 'correct') + + def _check_incorrect(self, name): + """ + check that problem named "name" gets evaluated incorrectly correctly + """ + resp = self.submit_question_answer(name, {'2_1': self.incorrect_responses[name]}) + + respdata = json.loads(resp.content) + self.assertEqual(respdata['success'], 'incorrect') + + def _check_ireset(self, name): + """ + Check that the problem can be reset + """ + # first, get the question wrong + resp = self.submit_question_answer(name, {'2_1': self.incorrect_responses[name]}) + # reset the question + self.reset_question_answer(name) + # then get it right + resp = self.submit_question_answer(name, {'2_1': self.correct_responses[name]}) + + respdata = json.loads(resp.content) + self.assertEqual(respdata['success'], 'correct') + + def test_schematic_correct(self): + name = "schematic_problem" + self.schematic_setup(name) + self._check_correct(name) + + def test_schematic_incorrect(self): + name = "schematic_problem" + self.schematic_setup(name) + self._check_incorrect(name) + + def test_schematic_reset(self): + name = "schematic_problem" + self.schematic_setup(name) + self._check_ireset(name) + + def test_check_function_correct(self): + name = 'cfn_problem' + self.custom_response_setup(name) + self._check_correct(name) + + def test_check_function_incorrect(self): + name = 'cfn_problem' + self.custom_response_setup(name) + self._check_incorrect(name) + + def test_check_function_reset(self): + name = 'cfn_problem' + self.custom_response_setup(name) + self._check_ireset(name) + + def test_computed_correct(self): + name = 'computed_answer' + self.computed_answer_setup(name) + self._check_correct(name) + + def test_computed_incorrect(self): + name = 'computed_answer' + self.computed_answer_setup(name) + self._check_incorrect(name) + + def test_computed_reset(self): + name = 'computed_answer' + self.computed_answer_setup(name) + self._check_ireset(name) diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py index a6bff60acf..182cbab9e7 100644 --- a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py +++ b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py @@ -52,3 +52,47 @@ class TestVideo(BaseTestXmodule): 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) } self.assertDictEqual(context, expected_context) + + +class TestVideoNonYouTube(TestVideo): + """Integration tests: web client + mongo.""" + + DATA = """ + + + + + + """ + MODEL_DATA = { + 'data': DATA + } + + def test_videoalpha_constructor(self): + """Make sure that if the 'youtube' attribute is omitted in XML, then + the template generates an empty string for the YouTube streams. + """ + + # `get_html` return only context, cause we + # overwrite `system.render_template` + context = self.item_module.get_html() + expected_context = { + 'data_dir': getattr(self, 'data_dir', None), + 'caption_asset_path': '/c4x/MITx/999/asset/subs_', + 'show_captions': self.item_module.show_captions, + 'display_name': self.item_module.display_name_with_default, + 'end': self.item_module.end_time, + 'id': self.item_module.location.html_id(), + 'sources': self.item_module.sources, + 'start': self.item_module.start_time, + 'sub': self.item_module.sub, + 'track': self.item_module.track, + 'youtube_streams': '', + 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) + } + self.assertDictEqual(context, expected_context) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py new file mode 100644 index 0000000000..055c860fcc --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -0,0 +1,374 @@ +import datetime +import pytz + +from mock import patch + +from django.contrib.auth.models import User, Group +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +# Need access to internal func to put users in the right group +from courseware.access import (has_access, _course_staff_group_name, + course_beta_test_group_name, settings as access_settings) + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from helpers import LoginEnrollmentTestCase, check_for_get_code +from modulestore_config import TEST_DATA_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check that view authentication works properly. + """ + + ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] + + @staticmethod + def _reverse_urls(names, course): + """ + Reverse a list of course urls. + + `names` is a list of URL names that correspond to sections in a course. + + `course` is the instance of CourseDescriptor whose section URLs are to be returned. + + Returns a list URLs corresponding to section in the passed in course. + + """ + return [reverse(name, kwargs={'course_id': course.id}) + for name in names] + + def _check_non_staff_light(self, course): + """ + Check that non-staff have access to light urls. + + `course` is an instance of CourseDescriptor. + """ + urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')] + for url in urls: + check_for_get_code(self, 200, url) + + def _check_non_staff_dark(self, course): + """ + Check that non-staff don't have access to dark urls. + """ + + names = ['courseware', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': course.id, + 'book_index': index}) + for index, book in enumerate(course.textbooks) + ]) + for url in urls: + check_for_get_code(self, 404, url) + + def _check_staff(self, course): + """ + Check that access is right for staff in course. + """ + names = ['about_course', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': course.id, + 'book_index': index}) + for index, book in enumerate(course.textbooks) + ]) + for url in urls: + check_for_get_code(self, 200, url) + + # The student progress tab is not accessible to a student + # before launch, so the instructor view-as-student feature + # should return a 404 as well. + # TODO (vshnayder): If this is not the behavior we want, will need + # to make access checking smarter and understand both the effective + # user (the student), and the requesting user (the prof) + url = reverse('student_progress', + kwargs={'course_id': course.id, + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) + check_for_get_code(self, 404, url) + + # The courseware url should redirect, not 200 + url = self._reverse_urls(['courseware'], course)[0] + check_for_get_code(self, 302, url) + + def setUp(self): + + self.course = CourseFactory.create(number='999', display_name='Robot_Super_Course') + self.overview_chapter = ItemFactory.create(display_name='Overview') + self.courseware_chapter = ItemFactory.create(display_name='courseware') + + self.test_course = CourseFactory.create(number='666', display_name='Robot_Sub_Course') + self.sub_courseware_chapter = ItemFactory.create(parent_location=self.test_course.location, + display_name='courseware') + self.sub_overview_chapter = ItemFactory.create(parent_location=self.sub_courseware_chapter.location, + display_name='Overview') + self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, + display_name='Welcome') + + # Create two accounts and activate them. + for i in range(len(self.ACCOUNT_INFO)): + username, email, password = 'u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1] + self.create_account(username, email, password) + self.activate_user(email) + + def test_redirection_unenrolled(self): + """ + Verify unenrolled student is redirected to the 'about' section of the chapter + instead of the 'Welcome' section after clicking on the courseware tab. + """ + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) + response = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + self.assertRedirects(response, + reverse('about_course', + args=[self.course.id])) + + def test_redirection_enrolled(self): + """ + Verify enrolled student is redirected to the 'Welcome' section of + the chapter after clicking on the courseware tab. + """ + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) + self.enroll(self.course) + + response = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirects(response, + reverse('courseware_section', + kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + def test_instructor_page_access_nonstaff(self): + """ + Verify non-staff cannot load the instructor + dashboard, the grade views, and student profile pages. + """ + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) + + self.enroll(self.course) + self.enroll(self.test_course) + + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})] + + # Shouldn't be able to get to the instructor pages + for url in urls: + check_for_get_code(self, 404, url) + + def test_instructor_course_access(self): + """ + Verify instructor can load the instructor dashboard, the grade views, + and student profile pages for their course. + """ + email, password = self.ACCOUNT_INFO[1] + + # Make the instructor staff in self.course + group_name = _course_staff_group_name(self.course.location) + group = Group.objects.create(name=group_name) + group.user_set.add(User.objects.get(email=email)) + + self.login(email, password) + + # Now should be able to get to self.course, but not self.test_course + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + check_for_get_code(self, 200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id}) + check_for_get_code(self, 404, url) + + def test_instructor_as_staff_access(self): + """ + Verify the instructor can load staff pages if he is given + staff permissions. + """ + email, password = self.ACCOUNT_INFO[1] + self.login(email, password) + + # now make the instructor also staff + instructor = User.objects.get(email=email) + instructor.is_staff = True + instructor.save() + + # and now should be able to load both + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})] + + for url in urls: + check_for_get_code(self, 200, url) + + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_enrolled_student(self): + """ + Make sure that before course start, students can't access course + pages. + """ + + student_email, student_password = self.ACCOUNT_INFO[0] + + # Make courses start in the future + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.test_course = self.update_course(self.test_course, test_course_data) + + self.assertFalse(self.course.has_started()) + self.assertFalse(self.test_course.has_started()) + + # First, try with an enrolled student + self.login(student_email, student_password) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # shouldn't be able to get to anything except the light pages + self._check_non_staff_light(self.course) + self._check_non_staff_dark(self.course) + self._check_non_staff_light(self.test_course) + self._check_non_staff_dark(self.test_course) + + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_instructor(self): + """ + Make sure that before course start instructors can access the + page for their course. + """ + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.test_course = self.update_course(self.test_course, test_course_data) + + # Make the instructor staff in self.course + group_name = _course_staff_group_name(self.course.location) + group = Group.objects.create(name=group_name) + group.user_set.add(User.objects.get(email=instructor_email)) + self.logout() + self.login(instructor_email, instructor_password) + # Enroll in the classes---can't see courseware otherwise. + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # should now be able to get to everything for self.course + self._check_non_staff_light(self.test_course) + self._check_non_staff_dark(self.test_course) + self._check_staff(self.course) + + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_staff(self): + """ + Make sure that before course start staff can access + course pages. + """ + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.test_course = self.update_course(self.test_course, test_course_data) + + self.login(instructor_email, instructor_password) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # now also make the instructor staff + instructor = User.objects.get(email=instructor_email) + instructor.is_staff = True + instructor.save() + + # and now should be able to load both + self._check_staff(self.course) + self._check_staff(self.test_course) + + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_enrollment_period(self): + """ + Check that enrollment periods work. + """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + # Make courses start in the future + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + nextday = tomorrow + datetime.timedelta(days=1) + yesterday = now - datetime.timedelta(days=1) + + course_data = {'enrollment_start': tomorrow, 'enrollment_end': nextday} + test_course_data = {'enrollment_start': yesterday, 'enrollment_end': tomorrow} + + # self.course's enrollment period hasn't started + self.course = self.update_course(self.course, course_data) + # test_course course's has + self.test_course = self.update_course(self.test_course, test_course_data) + + # First, try with an enrolled student + self.login(student_email, student_password) + self.assertFalse(self.enroll(self.course)) + self.assertTrue(self.enroll(self.test_course)) + + # Make the instructor staff in the self.course + group_name = _course_staff_group_name(self.course.location) + group = Group.objects.create(name=group_name) + group.user_set.add(User.objects.get(email=instructor_email)) + + self.logout() + self.login(instructor_email, instructor_password) + self.assertTrue(self.enroll(self.course)) + + # now make the instructor global staff, but not in the instructor group + group.user_set.remove(User.objects.get(email=instructor_email)) + instructor = User.objects.get(email=instructor_email) + instructor.is_staff = True + instructor.save() + + # unenroll and try again + self.unenroll(self.course) + self.assertTrue(self.enroll(self.course)) + + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_beta_period(self): + """ + Check that beta-test access works. + """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + # Make courses start in the future + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + + # self.course's hasn't started + self.course = self.update_course(self.course, course_data) + self.assertFalse(self.course.has_started()) + + # but should be accessible for beta testers + self.course.lms.days_early_for_beta = 2 + + # student user shouldn't see it + student_user = User.objects.get(email=student_email) + self.assertFalse(has_access(student_user, self.course, 'load')) + + # now add the student to the beta test group + group_name = course_beta_test_group_name(self.course.location) + group = Group.objects.create(name=group_name) + group.user_set.add(student_user) + + # now the student should see it + self.assertTrue(has_access(student_user, self.course, 'load')) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 25492ad379..37b81aa96f 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -3,7 +3,6 @@ import datetime from django.test import TestCase from django.http import Http404 -from django.conf import settings from django.test.utils import override_settings from django.contrib.auth.models import User from django.test.client import RequestFactory @@ -14,28 +13,13 @@ from xmodule.modulestore.django import modulestore import courseware.views as views from xmodule.modulestore import Location from pytz import UTC +from modulestore_config import TEST_DATA_XML_MODULESTORE class Stub(): pass -# This part is required for modulestore() to work properly -def xml_store_config(data_dir): - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } - } - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) - - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestJumpTo(TestCase): """Check the jumpto link for a course""" @@ -67,8 +51,8 @@ class ViewsTestCase(TestCase): self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC) self.course_id = 'edX/toy/2012_Fall' self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user, - course_id=self.course_id, - created=self.date)[0] + course_id=self.course_id, + created=self.date)[0] self.location = ['tag', 'org', 'course', 'category', 'name'] self._MODULESTORES = {} # This is a CourseDescriptor object diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index e862ed62c3..157cd06d4f 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,298 +1,61 @@ ''' Test for lms courseware app ''' -import logging -import json import random -from urlparse import urlsplit, urlunsplit -from uuid import uuid4 - -from django.contrib.auth.models import User, Group from django.test import TestCase -from django.test.client import RequestFactory -from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings import xmodule.modulestore.django -# Need access to internal func to put users in the right group -from courseware import grades -from courseware.model_data import ModelDataCache -from courseware.access import (has_access, _course_staff_group_name, - course_beta_test_group_name) - -from student.models import Registration from xmodule.error_module import ErrorDescriptor from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml import XMLModuleStore -import datetime -from django.utils.timezone import UTC -log = logging.getLogger("mitx." + __name__) - - -def parse_json(response): - """Parse response, which is assumed to be json""" - return json.loads(response.content) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def get_registration(email): - '''look up registration object by email''' - return Registration.objects.get(user__email=email) - - -def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore - - Use of this config requires mongo to be running - ''' - store = { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } - } - store['direct'] = store['default'] - return store - - -def draft_mongo_store_config(data_dir): - '''Defines default module store using DraftMongoModuleStore''' - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - }, - 'direct': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } - } - - -def xml_store_config(data_dir): - '''Defines default module store using XMLModuleStore''' - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } - } - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) -TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) - - -class LoginEnrollmentTestCase(TestCase): - ''' - Base TestCase providing support for user creation, - activation, login, and course enrollment - ''' - - def assertRedirectsNoFollow(self, response, expected_url): - """ - http://devblog.point2.com/2010/04/23/djangos-assertredirects-little-gotcha/ - - Don't check that the redirected-to page loads--there should be other tests for that. - - Some of the code taken from django.test.testcases.py - """ - self.assertEqual(response.status_code, 302, - 'Response status code was %d instead of 302' - % (response.status_code)) - url = response['Location'] - - e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) - if not (e_scheme or e_netloc): - expected_url = urlunsplit(('http', 'testserver', - e_path, e_query, e_fragment)) - - self.assertEqual(url, expected_url, - "Response redirected to '%s', expected '%s'" % - (url, expected_url)) - - def setup_viewtest_user(self): - '''create a user account, activate, and log in''' - self.viewtest_email = 'view@test.com' - self.viewtest_password = 'foo' - self.viewtest_username = 'viewtest' - self.create_account(self.viewtest_username, - self.viewtest_email, self.viewtest_password) - self.activate_user(self.viewtest_email) - self.login(self.viewtest_email, self.viewtest_password) - - # ============ User creation and login ============== - - def _login(self, email, password): - '''Login. View should always return 200. The success/fail is in the - returned json''' - resp = self.client.post(reverse('login'), - {'email': email, 'password': password}) - self.assertEqual(resp.status_code, 200) - return resp - - def login(self, email, password): - '''Login, check that it worked.''' - resp = self._login(email, password) - data = parse_json(resp) - self.assertTrue(data['success']) - return resp - - def logout(self): - '''Logout, check that it worked.''' - resp = self.client.get(reverse('logout'), {}) - # should redirect - self.assertEqual(resp.status_code, 302) - return resp - - def _create_account(self, username, email, password): - '''Try to create an account. No error checking''' - resp = self.client.post('/create_account', { - 'username': username, - 'email': email, - 'password': password, - 'name': 'Fred Weasley', - 'terms_of_service': 'true', - 'honor_code': 'true', - }) - return resp - - def create_account(self, username, email, password): - '''Create the account and check that it worked''' - resp = self._create_account(username, email, password) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['success'], True) - - # Check both that the user is created, and inactive - self.assertFalse(get_user(email).is_active) - - return resp - - def _activate_user(self, email): - '''Look up the activation key for the user, then hit the activate view. - No error checking''' - activation_key = get_registration(email).activation_key - - # and now we try to activate - url = reverse('activate', kwargs={'key': activation_key}) - resp = self.client.get(url) - return resp - - def activate_user(self, email): - resp = self._activate_user(email) - self.assertEqual(resp.status_code, 200) - # Now make sure that the user is now actually activated - self.assertTrue(get_user(email).is_active) - - def try_enroll(self, course): - """Try to enroll. Return bool success instead of asserting it.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'enroll', - 'course_id': course.id, - }) - print ('Enrollment in %s result status code: %s' - % (course.location.url(), str(resp.status_code))) - return resp.status_code == 200 - - def enroll(self, course): - """Enroll the currently logged-in user, and check that it worked.""" - result = self.try_enroll(course) - self.assertTrue(result) - - def unenroll(self, course): - """Unenroll the currently logged-in user, and check that it worked.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'unenroll', - 'course_id': course.id, - }) - self.assertTrue(resp.status_code == 200) - - def check_for_get_code(self, code, url): - """ - Check that we got the expected code when accessing url via GET. - Returns the response. - """ - resp = self.client.get(url) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp - - def check_for_post_code(self, code, url, data={}): - """ - Check that we got the expected code when accessing url via POST. - Returns the response. - """ - resp = self.client.post(url, data) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp +from helpers import LoginEnrollmentTestCase +from modulestore_config import TEST_DATA_DIR,\ + TEST_DATA_XML_MODULESTORE,\ + TEST_DATA_MONGO_MODULESTORE,\ + TEST_DATA_DRAFT_MONGO_MODULESTORE class ActivateLoginTest(LoginEnrollmentTestCase): - '''Test logging in and logging out''' + """ + Test logging in and logging out. + """ def setUp(self): - self.setup_viewtest_user() + self.setup_user() def test_activate_login(self): - '''Test login -- the setup function does all the work''' + """ + Test login -- the setup function does all the work. + """ pass def test_logout(self): - '''Test logout -- setup function does login''' + """ + Test logout -- setup function does login. + """ self.logout() class PageLoaderTestCase(LoginEnrollmentTestCase): - ''' Base class that adds a function to load all pages in a modulestore ''' + """ + Base class that adds a function to load all pages in a modulestore. + """ def check_random_page_loads(self, module_store): - ''' - Choose a page in the course randomly, and assert that it loads - ''' - # enroll in the course before trying to access pages + """ + Choose a page in the course randomly, and assert that it loads. + """ + # enroll in the course before trying to access pages courses = module_store.get_courses() self.assertEqual(len(courses), 1) course = courses[0] - self.enroll(course) + self.enroll(course, True) course_id = course.id # Search for items in the course @@ -339,12 +102,12 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): def _assert_loads(self, django_url, kwargs, descriptor, expect_redirect=False, check_content=False): - ''' + """ Assert that the url loads correctly. If expect_redirect, then also check that we were redirected. If check_content, then check that we don't get an error message about unavailable modules. - ''' + """ url = reverse(django_url, kwargs=kwargs) response = self.client.get(url, follow=True) @@ -364,11 +127,13 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from XML''' + """ + Check that all pages in test courses load properly from XML. + """ def setUp(self): super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() - self.setup_viewtest_user() + self.setup_user() xmodule.modulestore.django._MODULESTORES = {} def test_toy_course_loads(self): @@ -383,11 +148,13 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from Mongo''' + """ + Check that all pages in test courses load properly from Mongo. + """ def setUp(self): super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() - self.setup_viewtest_user() + self.setup_user() xmodule.modulestore.django._MODULESTORES = {} modulestore().collection.drop() @@ -405,67 +172,6 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): self.assertGreater(len(course.textbooks), 0) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestNavigation(LoginEnrollmentTestCase): - """Check that navigation state is saved properly""" - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - # Assume courses are there - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - self.toy = modulestore().get_course("edX/toy/2012_Fall") - - # Create two accounts - self.student = 'view@test.com' - self.student2 = 'view2@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.student2, self.password) - self.activate_user(self.student) - self.activate_user(self.student2) - - def test_accordion_state(self): - """Make sure that the accordion remembers where you were properly""" - self.login(self.student, self.password) - self.enroll(self.toy) - self.enroll(self.full) - - # First request should redirect to ToyVideos - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) - - # Don't use no-follow, because state should - # only be saved once we actually hit the section - self.assertRedirects(resp, reverse( - 'courseware_section', kwargs={'course_id': self.toy.id, - 'chapter': 'Overview', - 'section': 'Toy_Videos'})) - - # Hitting the couseware tab again should - # redirect to the first chapter: 'Overview' - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) - - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.toy.id, - 'chapter': 'Overview'})) - - # Now we directly navigate to a section in a different chapter - self.check_for_get_code(200, reverse('courseware_section', - kwargs={'course_id': self.toy.id, - 'chapter': 'secret:magic', - 'section': 'toyvideo'})) - - # And now hitting the courseware tab should redirect to 'secret:magic' - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) - - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.toy.id, - 'chapter': 'secret:magic'})) - - @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) class TestDraftModuleStore(TestCase): def test_get_items_with_course_items(self): @@ -478,558 +184,3 @@ class TestDraftModuleStore(TestCase): # test success is just getting through the above statement. # The bug was that 'course_id' argument was # not allowed to be passed in (i.e. was throwing exception) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestViewAuth(LoginEnrollmentTestCase): - """Check that view authentication works properly""" - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - self.toy = modulestore().get_course("edX/toy/2012_Fall") - - # Create two accounts - self.student = 'view@test.com' - self.instructor = 'view2@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.instructor, self.password) - self.activate_user(self.student) - self.activate_user(self.instructor) - - def test_instructor_pages(self): - """Make sure only instructors for the course - or staff can load the instructor - dashboard, the grade views, and student profile pages""" - - # First, try with an enrolled student - self.login(self.student, self.password) - # shouldn't work before enroll - response = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) - - self.assertRedirectsNoFollow(response, - reverse('about_course', - args=[self.toy.id])) - self.enroll(self.toy) - self.enroll(self.full) - # should work now -- redirect to first page - response = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) - self.assertRedirectsNoFollow(response, - reverse('courseware_section', - kwargs={'course_id': self.toy.id, - 'chapter': 'Overview', - 'section': 'Toy_Videos'})) - - def instructor_urls(course): - "list of urls that only instructors/staff should be able to see" - urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( - 'instructor_dashboard', - 'gradebook', - 'grade_summary',)] - - urls.append(reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': get_user(self.student).id})) - return urls - - # Randomly sample an instructor page - url = random.choice(instructor_urls(self.toy) + - instructor_urls(self.full)) - - # Shouldn't be able to get to the instructor pages - print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) - - # Make the instructor staff in the toy course - group_name = _course_staff_group_name(self.toy.location) - group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) - - self.logout() - self.login(self.instructor, self.password) - - # Now should be able to get to the toy course, but not the full course - url = random.choice(instructor_urls(self.toy)) - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) - - url = random.choice(instructor_urls(self.full)) - print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) - - # now also make the instructor staff - instructor = get_user(self.instructor) - instructor.is_staff = True - instructor.save() - - # and now should be able to load both - url = random.choice(instructor_urls(self.toy) + - instructor_urls(self.full)) - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) - - def run_wrapped(self, test): - """ - test.py turns off start dates. Enable them. - Because settings is global, be careful not to mess it up for other tests - (Can't use override_settings because we're only changing part of the - MITX_FEATURES dict) - """ - oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] - - try: - settings.MITX_FEATURES['DISABLE_START_DATES'] = False - test() - finally: - settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD - - def test_dark_launch(self): - """Make sure that before course start, students can't access course - pages, but instructors can""" - self.run_wrapped(self._do_test_dark_launch) - - def test_enrollment_period(self): - """Check that enrollment periods work""" - self.run_wrapped(self._do_test_enrollment_period) - - def test_beta_period(self): - """Check that beta-test access works""" - self.run_wrapped(self._do_test_beta_period) - - def _do_test_dark_launch(self): - """Actually do the test, relying on settings to be right.""" - - # Make courses start in the future - tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) - self.toy.lms.start = tomorrow - self.full.lms.start = tomorrow - - self.assertFalse(self.toy.has_started()) - self.assertFalse(self.full.has_started()) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - - def reverse_urls(names, course): - """Reverse a list of course urls""" - return [reverse(name, kwargs={'course_id': course.id}) - for name in names] - - def dark_student_urls(course): - """ - list of urls that students should be able to see only - after launch, but staff should see before - """ - urls = reverse_urls(['info', 'progress'], course) - urls.extend([ - reverse('book', kwargs={'course_id': course.id, - 'book_index': index}) - for index, book in enumerate(course.textbooks) - ]) - return urls - - def light_student_urls(course): - """ - list of urls that students should be able to see before - launch. - """ - urls = reverse_urls(['about_course'], course) - urls.append(reverse('courses')) - - return urls - - def instructor_urls(course): - """list of urls that only instructors/staff should be able to see""" - urls = reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) - return urls - - def check_non_staff(course): - """Check that access is right for non-staff in course""" - print '=== Checking non-staff access for {0}'.format(course.id) - - # Randomly sample a dark url - url = random.choice(instructor_urls(course) + - dark_student_urls(course) + - reverse_urls(['courseware'], course)) - print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) - - # Randomly sample a light url - url = random.choice(light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) - - def check_staff(course): - """Check that access is right for staff in course""" - print '=== Checking staff access for {0}'.format(course.id) - - # Randomly sample a url - url = random.choice(instructor_urls(course) + - dark_student_urls(course) + - light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) - - # The student progress tab is not accessible to a student - # before launch, so the instructor view-as-student feature - # should return a 404 as well. - # TODO (vshnayder): If this is not the behavior we want, will need - # to make access checking smarter and understand both the effective - # user (the student), and the requesting user (the prof) - url = reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': get_user(self.student).id}) - print 'checking for 404 on view-as-student: {0}'.format(url) - self.check_for_get_code(404, url) - - # The courseware url should redirect, not 200 - url = reverse_urls(['courseware'], course)[0] - self.check_for_get_code(302, url) - - # First, try with an enrolled student - print '=== Testing student access....' - self.login(self.student, self.password) - self.enroll(self.toy) - self.enroll(self.full) - - # shouldn't be able to get to anything except the light pages - check_non_staff(self.toy) - check_non_staff(self.full) - - print '=== Testing course instructor access....' - # Make the instructor staff in the toy course - group_name = _course_staff_group_name(self.toy.location) - group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) - - self.logout() - self.login(self.instructor, self.password) - # Enroll in the classes---can't see courseware otherwise. - self.enroll(self.toy) - self.enroll(self.full) - - # should now be able to get to everything for toy course - check_non_staff(self.full) - check_staff(self.toy) - - print '=== Testing staff access....' - # now also make the instructor staff - instructor = get_user(self.instructor) - instructor.is_staff = True - instructor.save() - - # and now should be able to load both - check_staff(self.toy) - check_staff(self.full) - - def _do_test_enrollment_period(self): - """Actually do the test, relying on settings to be right.""" - - # Make courses start in the future - tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) - nextday = tomorrow + datetime.timedelta(days=1) - yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) - - print "changing" - # toy course's enrollment period hasn't started - self.toy.enrollment_start = tomorrow - self.toy.enrollment_end = nextday - - # full course's has - self.full.enrollment_start = yesterday - self.full.enrollment_end = tomorrow - - print "login" - # First, try with an enrolled student - print '=== Testing student access....' - self.login(self.student, self.password) - self.assertFalse(self.try_enroll(self.toy)) - self.assertTrue(self.try_enroll(self.full)) - - print '=== Testing course instructor access....' - # Make the instructor staff in the toy course - group_name = _course_staff_group_name(self.toy.location) - group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) - - print "logout/login" - self.logout() - self.login(self.instructor, self.password) - print "Instructor should be able to enroll in toy course" - self.assertTrue(self.try_enroll(self.toy)) - - print '=== Testing staff access....' - # now make the instructor global staff, but not in the instructor group - group.user_set.remove(get_user(self.instructor)) - instructor = get_user(self.instructor) - instructor.is_staff = True - instructor.save() - - # unenroll and try again - self.unenroll(self.toy) - self.assertTrue(self.try_enroll(self.toy)) - - def _do_test_beta_period(self): - """Actually test beta periods, relying on settings to be right.""" - - # trust, but verify :) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - - # Make courses start in the future - tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) - - # toy course's hasn't started - self.toy.lms.start = tomorrow - self.assertFalse(self.toy.has_started()) - - # but should be accessible for beta testers - self.toy.lms.days_early_for_beta = 2 - - # student user shouldn't see it - student_user = get_user(self.student) - self.assertFalse(has_access(student_user, self.toy, 'load')) - - # now add the student to the beta test group - group_name = course_beta_test_group_name(self.toy.location) - group = Group.objects.create(name=group_name) - group.user_set.add(student_user) - - # now the student should see it - self.assertTrue(has_access(student_user, self.toy, 'load')) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestSubmittingProblems(LoginEnrollmentTestCase): - """Check that a course gets graded properly""" - - # Subclasses should specify the course slug - course_slug = "UNKNOWN" - course_when = "UNKNOWN" - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - course_name = "edX/%s/%s" % (self.course_slug, self.course_when) - self.course = modulestore().get_course(course_name) - assert self.course, "Couldn't load course %r" % course_name - - # create a test student - self.student = 'view@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.activate_user(self.student) - self.enroll(self.course) - - self.student_user = get_user(self.student) - - self.factory = RequestFactory() - - def problem_location(self, problem_url_name): - return "i4x://edX/{}/problem/{}".format(self.course_slug, problem_url_name) - - def modx_url(self, problem_location, dispatch): - return reverse( - 'modx_dispatch', - kwargs={ - 'course_id': self.course.id, - 'location': problem_location, - 'dispatch': dispatch, - } - ) - - def submit_question_answer(self, problem_url_name, responses): - """ - Submit answers to a question. - - Responses is a dict mapping problem ids (not sure of the right term) - to answers: - {'2_1': 'Correct', '2_2': 'Incorrect'} - - """ - problem_location = self.problem_location(problem_url_name) - modx_url = self.modx_url(problem_location, 'problem_check') - answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name) - resp = self.client.post(modx_url, - { (answer_key_prefix + k): v for k, v in responses.items() } - ) - return resp - - def reset_question_answer(self, problem_url_name): - '''resets specified problem for current user''' - problem_location = self.problem_location(problem_url_name) - modx_url = self.modx_url(problem_location, 'problem_reset') - resp = self.client.post(modx_url) - return resp - - -class TestCourseGrader(TestSubmittingProblems): - """Check that a course gets graded properly""" - - course_slug = "graded" - course_when = "2012_Fall" - - def get_grade_summary(self): - '''calls grades.grade for current user and course''' - model_data_cache = ModelDataCache.cache_for_descriptor_descendents( - self.course.id, self.student_user, self.course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.course.id})) - - return grades.grade(self.student_user, fake_request, - self.course, model_data_cache) - - def get_homework_scores(self): - '''get scores for homeworks''' - return self.get_grade_summary()['totaled_scores']['Homework'] - - def get_progress_summary(self): - '''return progress summary structure for current user and course''' - model_data_cache = ModelDataCache.cache_for_descriptor_descendents( - self.course.id, self.student_user, self.course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.course.id})) - - progress_summary = grades.progress_summary(self.student_user, - fake_request, - self.course, - model_data_cache) - return progress_summary - - def check_grade_percent(self, percent): - '''assert that percent grade is as expected''' - grade_summary = self.get_grade_summary() - self.assertEqual(grade_summary['percent'], percent) - - def test_get_graded(self): - #### Check that the grader shows we have 0% in the course - self.check_grade_percent(0) - - #### Submit the answers to a few problems as ajax calls - def earned_hw_scores(): - """Global scores, each Score is a Problem Set""" - return [s.earned for s in self.get_homework_scores()] - - def score_for_hw(hw_url_name): - """returns list of scores for a given url""" - hw_section = [section for section - in self.get_progress_summary()[0]['sections'] - if section.get('url_name') == hw_url_name][0] - return [s.earned for s in hw_section['scores']] - - # Only get half of the first problem correct - self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'}) - self.check_grade_percent(0.06) - self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters - self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0]) - - # Get both parts of the first problem correct - self.reset_question_answer('H1P1') - self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.13) - self.assertEqual(earned_hw_scores(), [2.0, 0, 0]) - self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0]) - - # This problem is shown in an ABTest - self.submit_question_answer('H1P2', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.25) - self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0]) - self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - - # This problem is hidden in an ABTest. - # Getting it correct doesn't change total grade - self.submit_question_answer('H1P3', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.25) - self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - - # On the second homework, we only answer half of the questions. - # Then it will be dropped when homework three becomes the higher percent - # This problem is also weighted to be 4 points (instead of default of 2) - # If the problem was unweighted the percent would have been 0.38 so we - # know it works. - self.submit_question_answer('H2P1', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.42) - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0]) - - # Third homework - self.submit_question_answer('H3P1', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.42) # Score didn't change - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) - - self.submit_question_answer('H3P2', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.5) # Now homework2 dropped. Score changes - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) - - # Now we answer the final question (worth half of the grade) - self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(1.0) # Hooray! We got 100% - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestSchematicResponse(TestSubmittingProblems): - """Check that we can submit a schematic response, and it answers properly.""" - - course_slug = "embedded_python" - course_when = "2013_Spring" - - def test_schematic(self): - resp = self.submit_question_answer('schematic_problem', - { '2_1': json.dumps( - [['transient', {'Z': [ - [0.0000004, 2.8], - [0.0000009, 2.8], - [0.0000014, 2.8], - [0.0000019, 2.8], - [0.0000024, 2.8], - [0.0000029, 0.2], - [0.0000034, 0.2], - [0.0000039, 0.2] - ]}]] - ) - }) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('schematic_problem') - resp = self.submit_question_answer('schematic_problem', - { '2_1': json.dumps( - [['transient', {'Z': [ - [0.0000004, 2.8], - [0.0000009, 0.0], # wrong. - [0.0000014, 2.8], - [0.0000019, 2.8], - [0.0000024, 2.8], - [0.0000029, 0.2], - [0.0000034, 0.2], - [0.0000039, 0.2] - ]}]] - ) - }) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') - - def test_check_function(self): - resp = self.submit_question_answer('cfn_problem', {'2_1': "0, 1, 2, 3, 4, 5, 'Outside of loop', 6"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('cfn_problem') - - resp = self.submit_question_answer('cfn_problem', {'2_1': "xyzzy!"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') - - def test_computed_answer(self): - resp = self.submit_question_answer('computed_answer', {'2_1': "Xyzzy"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('computed_answer') - - resp = self.submit_question_answer('computed_answer', {'2_1': "NO!"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') diff --git a/lms/djangoapps/dashboard/tests.py b/lms/djangoapps/dashboard/tests.py deleted file mode 100644 index 501deb776c..0000000000 --- a/lms/djangoapps/dashboard/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/lms/djangoapps/django_comment_client/forum/tests.py b/lms/djangoapps/django_comment_client/forum/tests.py new file mode 100644 index 0000000000..bd18ab80d6 --- /dev/null +++ b/lms/djangoapps/django_comment_client/forum/tests.py @@ -0,0 +1,82 @@ +from django.test.utils import override_settings +from django.test.client import Client +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from django.core.urlresolvers import reverse +from util.testing import UrlResetMixin + +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from nose.tools import assert_true +from mock import patch, Mock + +import logging + +log = logging.getLogger(__name__) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): + + @patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUp(self): + + # Patching the ENABLE_DISCUSSION_SERVICE value affects the contents of urls.py, + # so we need to call super.setUp() which reloads urls.py (because + # of the UrlResetMixin) + super(ViewsExceptionTestCase, self).setUp() + + # create a course + self.course = CourseFactory.create(org='MITx', course='999', + display_name='Robot Super Course') + + # Patch the comment client user save method so it does not try + # to create a new cc user when creating a django user + with patch('student.models.cc.User.save'): + uname = 'student' + email = 'student@edx.org' + password = 'test' + + # Create the student + self.student = UserFactory(username=uname, password=password, email=email) + + # Enroll the student in the course + CourseEnrollmentFactory(user=self.student, course_id=self.course.id) + + # Log the student in + self.client = Client() + assert_true(self.client.login(username=uname, password=password)) + + @patch('student.models.cc.User.from_django_user') + @patch('student.models.cc.User.active_threads') + def test_user_profile_exception(self, mock_threads, mock_from_django_user): + + # Mock the code that makes the HTTP requests to the cs_comment_service app + # for the profiled user's active threads + mock_threads.return_value = [], 1, 1 + + # Mock the code that makes the HTTP request to the cs_comment_service app + # that gets the current user's info + mock_from_django_user.return_value = Mock() + + url = reverse('django_comment_client.forum.views.user_profile', + kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345 + self.response = self.client.get(url) + self.assertEqual(self.response.status_code, 404) + + @patch('student.models.cc.User.from_django_user') + @patch('student.models.cc.User.active_threads') + def test_user_followed_threads_exception(self, mock_threads, mock_from_django_user): + + # Mock the code that makes the HTTP requests to the cs_comment_service app + # for the profiled user's active threads + mock_threads.return_value = [], 1, 1 + + # Mock the code that makes the HTTP request to the cs_comment_service app + # that gets the current user's info + mock_from_django_user.return_value = Mock() + + url = reverse('django_comment_client.forum.views.followed_threads', + kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345 + self.response = self.client.get(url) + self.assertEqual(self.response.status_code, 404) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index b04bd787d8..24305a214a 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -114,7 +114,7 @@ def inline_discussion(request, course_id, discussion_id): threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): # TODO (vshnayder): since none of this code seems to be aware of the fact that # sometimes things go wrong, I suspect that the js client is also not # checking for errors on request. Check and fix as needed. @@ -141,8 +141,8 @@ def inline_discussion(request, course_id, discussion_id): if is_moderator: cohorts = get_course_cohorts(course_id) - for c in cohorts: - cohorts_list.append({'name': c.name, 'id': c.id}) + for cohort in cohorts: + cohorts_list.append({'name': cohort.name, 'id': cohort.id}) else: #students don't get to choose @@ -174,11 +174,11 @@ def forum_form_discussion(request, course_id): try: unsafethreads, query_params = get_threads(request, course_id) # This might process a search query threads = [utils.safe_content(thread) for thread in unsafethreads] - except (cc.utils.CommentClientMaintenanceError) as err: + except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response('discussion/maintenance.html', {}) except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: - log.error("Error loading forum discussion threads: %s" % str(err)) + log.error("Error loading forum discussion threads: %s", str(err)) raise Http404 user = cc.User.from_django_user(request.user) @@ -244,7 +244,7 @@ def single_thread(request, course_id, discussion_id, thread_id): try: thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): log.error("Error loading single thread.") raise Http404 @@ -269,7 +269,7 @@ def single_thread(request, course_id, discussion_id, thread_id): try: threads, query_params = get_threads(request, course_id) threads.append(thread.to_dict()) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): log.error("Error loading single thread.") raise Http404 @@ -369,7 +369,7 @@ def user_profile(request, course_id, user_id): } return render_to_response('discussion/user_profile.html', context) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError, User.DoesNotExist): raise Http404 @@ -412,5 +412,5 @@ def followed_threads(request, course_id, user_id): } return render_to_response('discussion/user_profile.html', context) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError, User.DoesNotExist): raise Http404 diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py new file mode 100644 index 0000000000..73c4ba220f --- /dev/null +++ b/lms/djangoapps/instructor/hint_manager.py @@ -0,0 +1,238 @@ +""" +Views for hint management. + +Along with the crowdsource_hinter xmodule, this code is still +experimental, and should not be used in new courses, yet. +""" + +import json +import re + +from django.http import HttpResponse, Http404 +from django_future.csrf import ensure_csrf_cookie + +from mitxmako.shortcuts import render_to_response, render_to_string + +from courseware.courses import get_course_with_access +from courseware.models import XModuleContentField +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore + + +@ensure_csrf_cookie +def hint_manager(request, course_id): + try: + get_course_with_access(request.user, course_id, 'staff', depth=None) + except Http404: + out = 'Sorry, but students are not allowed to access the hint manager!' + return HttpResponse(out) + if request.method == 'GET': + out = get_hints(request, course_id, 'mod_queue') + return render_to_response('courseware/hint_manager.html', out) + field = request.POST['field'] + if not (field == 'mod_queue' or field == 'hints'): + # Invalid field. (Don't let users continue - they may overwrite other db's) + out = 'Error in hint manager - an invalid field was accessed.' + return HttpResponse(out) + + if request.POST['op'] == 'delete hints': + delete_hints(request, course_id, field) + if request.POST['op'] == 'switch fields': + pass + if request.POST['op'] == 'change votes': + change_votes(request, course_id, field) + if request.POST['op'] == 'add hint': + add_hint(request, course_id, field) + if request.POST['op'] == 'approve': + approve(request, course_id, field) + rendered_html = render_to_string('courseware/hint_manager_inner.html', get_hints(request, course_id, field)) + return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) + + +def get_hints(request, course_id, field): + """ + Load all of the hints submitted to the course. + + Args: + `request` -- Django request object. + `course_id` -- The course id, like 'Me/19.002/test_course' + `field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load. + + Keys in returned dict: + - 'field': Same as input + - 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa. + - 'field_label', 'other_field_label': English name for the above. + - 'all_hints': A list of [answer, pk dict] pairs, representing all hints. + Sorted by answer. + - 'id_to_name': A dictionary mapping problem id to problem name. + """ + if field == 'mod_queue': + other_field = 'hints' + field_label = 'Hints Awaiting Moderation' + other_field_label = 'Approved Hints' + elif field == 'hints': + other_field = 'mod_queue' + field_label = 'Approved Hints' + other_field_label = 'Hints Awaiting Moderation' + # The course_id is of the form school/number/classname. + # We want to use the course_id to find all matching definition_id's. + # To do this, just take the school/number part - leave off the classname. + chopped_id = '/'.join(course_id.split('/')[:-1]) + chopped_id = re.escape(chopped_id) + all_hints = XModuleContentField.objects.filter(field_name=field, definition_id__regex=chopped_id) + # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer] + # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer. + big_out_dict = {} + # id_to name maps a problem id to the name of the problem. + # id_to_name[problem id] = Display name of problem + id_to_name = {} + + for hints_by_problem in all_hints: + loc = Location(hints_by_problem.definition_id) + name = location_to_problem_name(loc) + if name is None: + continue + id_to_name[hints_by_problem.definition_id] = name + + def answer_sorter(thing): + """ + `thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains + a dict of hints. This function returns an index based on `thing[0]`, which + is used as a key to sort the list of things. + """ + try: + return float(thing[0]) + except ValueError: + # Put all non-numerical answers first. + return float('-inf') + + # Answer list contains [answer, dict_of_hints] pairs. + answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter) + big_out_dict[hints_by_problem.definition_id] = answer_list + + render_dict = {'field': field, + 'other_field': other_field, + 'field_label': field_label, + 'other_field_label': other_field_label, + 'all_hints': big_out_dict, + 'id_to_name': id_to_name} + return render_dict + + +def location_to_problem_name(loc): + """ + Given the location of a crowdsource_hinter module, try to return the name of the + problem it wraps around. Return None if the hinter no longer exists. + """ + try: + descriptor = modulestore().get_items(loc)[0] + return descriptor.get_children()[0].display_name + except IndexError: + # Sometimes, the problem is no longer in the course. Just + # don't include said problem. + return None + + +def delete_hints(request, course_id, field): + """ + Deletes the hints specified. + + `request.POST` contains some fields keyed by integers. Each such field contains a + [problem_defn_id, answer, pk] tuple. These tuples specify the hints to be deleted. + + Example `request.POST`: + {'op': 'delete_hints', + 'field': 'mod_queue', + 1: ['problem_whatever', '42.0', '3'], + 2: ['problem_whatever', '32.5', '12']} + """ + + for key in request.POST: + if key == 'op' or key == 'field': + continue + problem_id, answer, pk = request.POST.getlist(key) + # Can be optimized - sort the delete list by problem_id, and load each problem + # from the database only once. + this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + problem_dict = json.loads(this_problem.value) + del problem_dict[answer][pk] + this_problem.value = json.dumps(problem_dict) + this_problem.save() + + +def change_votes(request, course_id, field): + """ + Updates the number of votes. + + The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples. + - Very similar to `delete_hints`. Is there a way to merge them? Nah, too complicated. + """ + + for key in request.POST: + if key == 'op' or key == 'field': + continue + problem_id, answer, pk, new_votes = request.POST.getlist(key) + this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + problem_dict = json.loads(this_problem.value) + # problem_dict[answer][pk] points to a [hint_text, #votes] pair. + problem_dict[answer][pk][1] = int(new_votes) + this_problem.value = json.dumps(problem_dict) + this_problem.save() + + +def add_hint(request, course_id, field): + """ + Add a new hint. `request.POST`: + op + field + problem - The problem id + answer - The answer to which a hint will be added + hint - The text of the hint + """ + + problem_id = request.POST['problem'] + answer = request.POST['answer'] + hint_text = request.POST['hint'] + this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + + hint_pk_entry = XModuleContentField.objects.get(field_name='hint_pk', definition_id=problem_id) + this_pk = int(hint_pk_entry.value) + hint_pk_entry.value = this_pk + 1 + hint_pk_entry.save() + + problem_dict = json.loads(this_problem.value) + if answer not in problem_dict: + problem_dict[answer] = {} + problem_dict[answer][this_pk] = [hint_text, 1] + this_problem.value = json.dumps(problem_dict) + this_problem.save() + + +def approve(request, course_id, field): + """ + Approve a list of hints, moving them from the mod_queue to the real + hint list. POST: + op, field + (some number) -> [problem, answer, pk] + """ + + for key in request.POST: + if key == 'op' or key == 'field': + continue + problem_id, answer, pk = request.POST.getlist(key) + # Can be optimized - sort the delete list by problem_id, and load each problem + # from the database only once. + problem_in_mod = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + problem_dict = json.loads(problem_in_mod.value) + hint_to_move = problem_dict[answer][pk] + del problem_dict[answer][pk] + problem_in_mod.value = json.dumps(problem_dict) + problem_in_mod.save() + + problem_in_hints = XModuleContentField.objects.get(field_name='hints', definition_id=problem_id) + problem_dict = json.loads(problem_in_hints.value) + if answer not in problem_dict: + problem_dict[answer] = {} + problem_dict[answer][pk] = hint_to_move + problem_in_hints.value = json.dumps(problem_dict) + problem_in_hints.save() diff --git a/lms/djangoapps/instructor/tests/test_download_csv.py b/lms/djangoapps/instructor/tests/test_download_csv.py index 29e18eee4d..fd5bd562ba 100644 --- a/lms/djangoapps/instructor/tests/test_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_download_csv.py @@ -11,12 +11,13 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/inst from django.test.utils import override_settings # Need access to internal func to put users in the right group -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django @@ -45,7 +46,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.toy) @@ -72,7 +73,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): # All the not-actually-in-the-course hw and labs come from the # default grading policy string in graders.py expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" -"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" +"2","u2","username","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" ''' self.assertEqual(body, expected_body, msg) diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 3b5bdc2ce9..964441c094 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -6,13 +6,11 @@ Unit tests for enrollment methods in views.py from django.test.utils import override_settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse -from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user -from xmodule.modulestore.django import modulestore +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE, LoginEnrollmentTestCase from student.models import CourseEnrollment, CourseEnrollmentAllowed from instructor.views import get_and_clean_student_list, send_mail_to_student from django.core import mail @@ -196,7 +194,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course #Create activated, but not enrolled, user - UserFactory.create(username="student3_0", email="student3_0@test.com") + UserFactory.create(username="student3_0", email="student3_0@test.com", first_name="Jim", last_name="Tester") url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) @@ -211,6 +209,10 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) #Check the outbox self.assertEqual(len(mail.outbox), 3) self.assertEqual(mail.outbox[0].subject, 'You have been enrolled in MITx/999/Robot_Super_Course') + self.assertEqual(mail.outbox[0].body, "Dear Jim Tester\n\nYou have been enrolled in MITx/999/Robot_Super_Course at edx.org by a member of the course staff. " + + "The course should now appear on your edx.org dashboard.\n\n" + + "To start accessing course materials, please visit https://edx.org/courses/MITx/999/Robot_Super_Course\n\n" + + "----\nThis email was automatically sent from edx.org to Jim Tester") self.assertEqual(mail.outbox[1].subject, 'You have been invited to register for MITx/999/Robot_Super_Course') self.assertEqual(mail.outbox[1].body, "Dear student,\n\nYou have been invited to join MITx/999/Robot_Super_Course at edx.org by a member of the course staff.\n\n" + diff --git a/lms/djangoapps/instructor/tests/test_forum_admin.py b/lms/djangoapps/instructor/tests/test_forum_admin.py index 7b4e729867..90dadd569e 100644 --- a/lms/djangoapps/instructor/tests/test_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_forum_admin.py @@ -6,7 +6,7 @@ Unit tests for instructor dashboard forum administration from django.test.utils import override_settings # Need access to internal func to put users in the right group -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \ @@ -14,7 +14,8 @@ from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \ from django_comment_client.utils import has_forum_access from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django @@ -55,7 +56,7 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): group_name = _course_staff_group_name(self.toy.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -146,4 +147,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): added_roles.append(rolename) added_roles.sort() roles = ', '.join(added_roles) - self.assertTrue(response.content.find('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) \ No newline at end of file + self.assertTrue(response.content.find('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index bbdf07f410..5ed0c1d1af 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -27,11 +27,11 @@ class TestGradebook(ModuleStoreTestCase): modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None - course_data = {} + kwargs = {} if self.grading_policy is not None: - course_data['grading_policy'] = self.grading_policy + kwargs['grading_policy'] = self.grading_policy - self.course = CourseFactory.create(data=course_data) + self.course = CourseFactory.create(**kwargs) chapter = ItemFactory.create( parent_location=self.course.location, template="i4x://edx/templates/sequential/Empty", diff --git a/lms/djangoapps/instructor/tests/test_hint_manager.py b/lms/djangoapps/instructor/tests/test_hint_manager.py new file mode 100644 index 0000000000..8f12572875 --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_hint_manager.py @@ -0,0 +1,164 @@ +import json + +from django.test.client import Client, RequestFactory +from django.test.utils import override_settings + +from courseware.models import XModuleContentField +from courseware.tests.factories import ContentFactory +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +import instructor.hint_manager as view +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class HintManagerTest(ModuleStoreTestCase): + + def setUp(self): + """ + Makes a course, which will be the same for all tests. + Set up mako middleware, which is necessary for template rendering to happen. + """ + self.course = CourseFactory.create(org='Me', number='19.002', display_name='test_course') + self.url = '/courses/Me/19.002/test_course/hint_manager' + self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True) + self.c = Client() + self.c.login(username='robot', password='test') + self.problem_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001' + self.course_id = 'Me/19.002/test_course' + ContentFactory.create(field_name='hints', + definition_id=self.problem_id, + value=json.dumps({'1.0': {'1': ['Hint 1', 2], + '3': ['Hint 3', 12]}, + '2.0': {'4': ['Hint 4', 3]} + })) + ContentFactory.create(field_name='mod_queue', + definition_id=self.problem_id, + value=json.dumps({'2.0': {'2': ['Hint 2', 1]}})) + + ContentFactory.create(field_name='hint_pk', + definition_id=self.problem_id, + value=5) + # Mock out location_to_problem_name, which ordinarily accesses the modulestore. + # (I can't figure out how to get fake structures into the modulestore.) + view.location_to_problem_name = lambda loc: "Test problem" + + def test_student_block(self): + """ + Makes sure that students cannot see the hint management view. + """ + c = Client() + UserFactory.create(username='student', email='student@edx.org', password='test') + c.login(username='student', password='test') + out = c.get(self.url) + print out + self.assertTrue('Sorry, but students are not allowed to access the hint manager!' in out.content) + + def test_staff_access(self): + """ + Makes sure that staff can access the hint management view. + """ + out = self.c.get('/courses/Me/19.002/test_course/hint_manager') + print out + self.assertTrue('Hints Awaiting Moderation' in out.content) + + def test_invalid_field_access(self): + """ + Makes sure that field names other than 'mod_queue' and 'hints' are + rejected. + """ + out = self.c.post(self.url, {'op': 'delete hints', 'field': 'all your private data'}) + print out + self.assertTrue('an invalid field was accessed' in out.content) + + def test_switchfields(self): + """ + Checks that the op: 'switch fields' POST request works. + """ + out = self.c.post(self.url, {'op': 'switch fields', 'field': 'mod_queue'}) + print out + self.assertTrue('Hint 2' in out.content) + + def test_gethints(self): + """ + Checks that gethints returns the right data. + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'mod_queue'}) + out = view.get_hints(post, self.course_id, 'mod_queue') + print out + self.assertTrue(out['other_field'] == 'hints') + expected = {self.problem_id: [(u'2.0', {u'2': [u'Hint 2', 1]})]} + self.assertTrue(out['all_hints'] == expected) + + def test_gethints_other(self): + """ + Same as above, with hints instead of mod_queue + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'hints'}) + out = view.get_hints(post, self.course_id, 'hints') + print out + self.assertTrue(out['other_field'] == 'mod_queue') + expected = {self.problem_id: [('1.0', {'1': ['Hint 1', 2], + '3': ['Hint 3', 12]}), + ('2.0', {'4': ['Hint 4', 3]}) + ]} + self.assertTrue(out['all_hints'] == expected) + + def test_deletehints(self): + """ + Checks that delete_hints deletes the right stuff. + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'hints', + 'op': 'delete hints', + 1: [self.problem_id, '1.0', '1']}) + view.delete_hints(post, self.course_id, 'hints') + problem_hints = XModuleContentField.objects.get(field_name='hints', definition_id=self.problem_id).value + self.assertTrue('1' not in json.loads(problem_hints)['1.0']) + + def test_changevotes(self): + """ + Checks that vote changing works. + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'hints', + 'op': 'change votes', + 1: [self.problem_id, '1.0', '1', 5]}) + view.change_votes(post, self.course_id, 'hints') + problem_hints = XModuleContentField.objects.get(field_name='hints', definition_id=self.problem_id).value + # hints[answer][hint_pk (string)] = [hint text, vote count] + print json.loads(problem_hints)['1.0']['1'] + self.assertTrue(json.loads(problem_hints)['1.0']['1'][1] == 5) + + def test_addhint(self): + """ + Check that instructors can add new hints. + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'mod_queue', + 'op': 'add hint', + 'problem': self.problem_id, + 'answer': '3.14', + 'hint': 'This is a new hint.'}) + view.add_hint(post, self.course_id, 'mod_queue') + problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value + self.assertTrue('3.14' in json.loads(problem_hints)) + + def test_approve(self): + """ + Check that instructors can approve hints. (Move them + from the mod_queue to the hints.) + """ + request = RequestFactory() + post = request.post(self.url, {'field': 'mod_queue', + 'op': 'approve', + 1: [self.problem_id, '2.0', '2']}) + view.approve(post, self.course_id, 'mod_queue') + problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value + self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0) + problem_hints = XModuleContentField.objects.get(field_name='hints', definition_id=self.problem_id).value + self.assertTrue(json.loads(problem_hints)['2.0']['2'] == ['Hint 2', 1]) + self.assertTrue(len(json.loads(problem_hints)['2.0']) == 2) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index ea96901bca..1875d380a6 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -200,7 +200,7 @@ def instructor_dashboard(request, course_id): cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:

".format(data_dir) msg += "

{0}

".format(escape(os.popen(cmd).read())) - track.views.server_track(request, 'git pull {0}'.format(data_dir), {}, page='idashboard') + track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") if 'Reload course' in action: log.debug('reloading {0} ({1})'.format(course_id, course)) @@ -208,7 +208,7 @@ def instructor_dashboard(request, course_id): data_dir = getattr(course, 'data_dir') modulestore().try_load_course(data_dir) msg += "

Course reloaded from {0}

".format(data_dir) - track.views.server_track(request, 'reload {0}'.format(data_dir), {}, page='idashboard') + track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_item_errors(course.location) msg += '
    ' for cmsg, cerr in course_errors: @@ -221,37 +221,38 @@ def instructor_dashboard(request, course_id): log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) - track.views.server_track(request, 'list-students', {}, page='idashboard') + track.views.server_track(request, "list-students", {}, page="idashboard") elif 'Dump Grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) - track.views.server_track(request, 'dump-grades', {}, page='idashboard') + track.views.server_track(request, "dump-grades", {}, page="idashboard") elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=True, use_offline=use_offline) datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) - track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard') + track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") elif 'Download CSV of all student grades' in action: - track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard') + track.views.server_track(request, "dump-grades-csv", {}, page="idashboard") return return_csv('grades_{0}.csv'.format(course_id), get_student_grade_summary_data(request, course, course_id, use_offline=use_offline)) elif 'Download CSV of all RAW grades' in action: - track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard') + track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") return return_csv('grades_{0}_raw.csv'.format(course_id), get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: - track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') + track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) elif 'Dump description of graded assignments configuration' in action: - track.views.server_track(request, action, {}, page='idashboard') + # what is "graded assignments configuration"? + track.views.server_track(request, "dump-graded-assignments-config", {}, page="idashboard") msg += dump_grading_context(course) elif "Rescore ALL students' problem submissions" in action: @@ -262,8 +263,7 @@ def instructor_dashboard(request, course_id): if instructor_task is None: msg += 'Failed to create a background task for rescoring "{0}".'.format(problem_url) else: - track_msg = 'rescore problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id) - track.views.server_track(request, track_msg, {}, page='idashboard') + track.views.server_track(request, "rescore-all-submissions", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as e: msg += 'Failed to create a background task for rescoring "{0}": problem not found.'.format(problem_url) except Exception as e: @@ -278,8 +278,7 @@ def instructor_dashboard(request, course_id): if instructor_task is None: msg += 'Failed to create a background task for resetting "{0}".'.format(problem_url) else: - track_msg = 'reset problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id) - track.views.server_track(request, track_msg, {}, page='idashboard') + track.views.server_track(request, "reset-all-attempts", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as e: log.error('Failure to reset: unknown problem "{0}"'.format(e)) msg += 'Failed to create a background task for resetting "{0}": problem not found.'.format(problem_url) @@ -332,9 +331,8 @@ def instructor_dashboard(request, course_id): try: student_module.delete() msg += "Deleted student module state for %s!" % module_state_key - track_format = 'delete student module state for problem {problem} for student {student} in {course}' - track_msg = track_format.format(problem=problem_url, student=unique_student_identifier, course=course_id) - track.views.server_track(request, track_msg, {}, page='idashboard') + event = {"problem": problem_url, "student": unique_student_identifier, "course": course_id} + track.views.server_track(request, "delete-student-module-state", event, page="idashboard") except: msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_urlname) elif "Reset student's attempts" in action: @@ -348,13 +346,12 @@ def instructor_dashboard(request, course_id): # save student_module.state = json.dumps(problem_state) student_module.save() - track_format = '{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}' - track_msg = track_format.format(old_attempts=old_number_of_attempts, - student=student, - problem=student_module.module_state_key, - instructor=request.user, - course=course_id) - track.views.server_track(request, track_msg, {}, page='idashboard') + event = {"old_attempts": old_number_of_attempts, + "student": student, + "problem": student_module.module_state_key, + "instructor": request.user, + "course": course_id} + track.views.server_track(request, "reset-student-attempts", event, page="idashboard") msg += "Module state successfully reset!" except: msg += "Couldn't reset module state. " @@ -365,8 +362,7 @@ def instructor_dashboard(request, course_id): if instructor_task is None: msg += 'Failed to create a background task for rescoring "{0}" for student {1}.'.format(module_state_key, unique_student_identifier) else: - track_msg = 'rescore problem {problem} for student {student} in {course}'.format(problem=module_state_key, student=unique_student_identifier, course=course_id) - track.views.server_track(request, track_msg, {}, page='idashboard') + track.views.server_track(request, "rescore-student-submission", {"problem": module_state_key, "student": unique_student_identifier, "course": course_id}, page="idashboard") except Exception as e: log.exception("Encountered exception from rescore: {0}") msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format(module_state_key, e.message) @@ -378,13 +374,7 @@ def instructor_dashboard(request, course_id): msg += message if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, - '{instructor} requested progress page for {student} in {course}'.format( - student=student, - instructor=request.user, - course=course_id), - {}, - page='idashboard') + track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- @@ -453,7 +443,7 @@ def instructor_dashboard(request, course_id): group = get_staff_group(course) msg += 'Staff group = {0}'.format(group.name) datatable = _group_members_table(group, "List of Staff", course_id) - track.views.server_track(request, 'list-staff', {}, page='idashboard') + track.views.server_track(request, "list-staff", {}, page="idashboard") elif 'List course instructors' in action and request.user.is_staff: group = get_instructor_group(course) @@ -463,7 +453,7 @@ def instructor_dashboard(request, course_id): datatable = {'header': ['Username', 'Full name']} datatable['data'] = [[x.username, x.profile.name] for x in uset] datatable['title'] = 'List of Instructors in course {0}'.format(course_id) - track.views.server_track(request, 'list-instructors', {}, page='idashboard') + track.views.server_track(request, "list-instructors", {}, page="idashboard") elif action == 'Add course staff': uname = request.POST['staffuser'] @@ -482,7 +472,7 @@ def instructor_dashboard(request, course_id): msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) - track.views.server_track(request, 'add-instructor {0}'.format(user), {}, page='idashboard') + track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] @@ -501,7 +491,7 @@ def instructor_dashboard(request, course_id): msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) - track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') + track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") #---------------------------------------- # DataDump @@ -550,7 +540,7 @@ def instructor_dashboard(request, course_id): group = get_beta_group(course) msg += 'Beta test group = {0}'.format(group.name) datatable = _group_members_table(group, "List of beta_testers", course_id) - track.views.server_track(request, 'list-beta-testers', {}, page='idashboard') + track.views.server_track(request, "list-beta-testers", {}, page="idashboard") elif action == 'Add beta testers': users = request.POST['betausers'] @@ -574,55 +564,49 @@ def instructor_dashboard(request, course_id): rolename = FORUM_ROLE_ADMINISTRATOR datatable = {} msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard') + track.views.server_track(request, "list-forum-admins", {"course": course_id}, page="idashboard") elif action == 'Remove forum admin': uname = request.POST['forumadmin'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_ADMINISTRATOR, course_id), - {}, page='idashboard') + track.views.server_track(request, "remove-forum-admin", {"username": uname, "course": course_id}, page="idashboard") elif action == 'Add forum admin': uname = request.POST['forumadmin'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADD) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_ADMINISTRATOR, course_id), - {}, page='idashboard') + track.views.server_track(request, "add-forum-admin", {"username": uname, "course": course_id}, page="idashboard") elif action == 'List course forum moderators': rolename = FORUM_ROLE_MODERATOR datatable = {} msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard') + track.views.server_track(request, "list-forum-mods", {"course": course_id}, page="idashboard") elif action == 'Remove forum moderator': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_REMOVE) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_MODERATOR, course_id), - {}, page='idashboard') + track.views.server_track(request, "remove-forum-mod", {"username": uname, "course": course_id}, page="idashboard") elif action == 'Add forum moderator': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_ADD) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_MODERATOR, course_id), - {}, page='idashboard') + track.views.server_track(request, "add-forum-mod", {"username": uname, "course": course_id}, page="idashboard") elif action == 'List course forum community TAs': rolename = FORUM_ROLE_COMMUNITY_TA datatable = {} msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard') + track.views.server_track(request, "list-forum-community-TAs", {"course": course_id}, page="idashboard") elif action == 'Remove forum community TA': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_REMOVE) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_COMMUNITY_TA, course_id), - {}, page='idashboard') + track.views.server_track(request, "remove-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard") elif action == 'Add forum community TA': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_ADD) - track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id), - {}, page='idashboard') + track.views.server_track(request, "add-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard") #---------------------------------------- # enrollment @@ -674,7 +658,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg - track.views.server_track(request, 'psychometrics {0}'.format(problem), {}, page='idashboard') + track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) @@ -927,8 +911,7 @@ def _add_or_remove_user_group(request, username_or_email, group, group_title, ev else: user.groups.remove(group) event = "add" if do_add else "remove" - track.views.server_track(request, '{event}-{0} {1}'.format(event_name, user, event=event), - {}, page='idashboard') + track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": unicode(user), "event": event}, page="idashboard") return msg @@ -1108,7 +1091,7 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll 'registration_url': registration_url, 'course_id': course_id, 'auto_enroll': auto_enroll, - 'course_url': registration_url + '/courses/' + course_id, + 'course_url': 'https://' + settings.SITE_NAME + '/courses/' + course_id, } for student in new_students: @@ -1251,7 +1234,7 @@ def send_mail_to_student(student, param_dict): """ Construct the email using templates and then send it. `student` is the student's email address (a `str`), - + `param_dict` is a `dict` with keys [ `site_name`: name given to edX instance (a `str`) `registration_url`: url for registration (a `str`) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 99b8b1a929..a46b4b12fe 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -8,7 +8,7 @@ import json from mock import MagicMock, patch, Mock from django.core.urlresolvers import reverse -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.conf import settings from mitxmako.shortcuts import render_to_string @@ -20,7 +20,6 @@ from xmodule.x_module import ModuleSystem from open_ended_grading import staff_grading_service, views from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user import logging @@ -30,6 +29,9 @@ from django.test.utils import override_settings from xmodule.tests import test_util_open_ended from courseware.tests import factories +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code, check_for_post_code + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestStaffGradingService(LoginEnrollmentTestCase): @@ -57,7 +59,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.toy) @@ -74,8 +76,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase): # both get and post should return 404 for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): url = reverse(view_name, kwargs={'course_id': self.course_id}) - self.check_for_get_code(404, url) - self.check_for_post_code(404, url) + check_for_get_code(self, 404, url) + check_for_post_code(self, 404, url) def test_get_next(self): self.login(self.instructor, self.password) @@ -83,7 +85,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) data = {'location': self.location} - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) @@ -112,7 +114,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): if skip: data.update({'skipped': True}) - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success'], str(content)) self.assertEquals(content['submission_id'], self.mock_service.cnt) @@ -129,7 +131,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) data = {} - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success'], str(content)) diff --git a/lms/djangoapps/static_template_view/models.py b/lms/djangoapps/static_template_view/models.py deleted file mode 100644 index 6b20219993..0000000000 --- a/lms/djangoapps/static_template_view/models.py +++ /dev/null @@ -1 +0,0 @@ -# Create your models here. diff --git a/lms/djangoapps/static_template_view/tests.py b/lms/djangoapps/static_template_view/tests.py deleted file mode 100644 index 813a94e294..0000000000 --- a/lms/djangoapps/static_template_view/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.test import TestCase -from django.test.client import Client - - -class SimpleTest(TestCase): - - def setUp(self): - self.client = Client() - - def test_render(self): - """ - Render a normal page, like jobs - """ - response = self.client.get("/jobs") - self.assertEquals(response.status_code, 200) - - - def test_render_press_release(self): - """ - Render press releases from generic URL match - """ - # since I had to remap files, pedantically test all press releases - # published to date. Decent positive test while we're at it. - all_releases = [ - "/press/mit-and-harvard-announce-edx", - "/press/uc-berkeley-joins-edx", - "/press/edX-announces-proctored-exam-testing", - "/press/elsevier-collaborates-with-edx", - "/press/ut-joins-edx", - "/press/cengage-to-provide-book-content", - "/press/gates-foundation-announcement", - "/press/wellesley-college-joins-edx", - "/press/georgetown-joins-edx", - "/press/spring-courses", - "/press/lewin-course-announcement", - "/press/bostonx-announcement", - "/press/eric-lander-secret-of-life", - "/press/edx-expands-internationally", - "/press/xblock_announcement", - "/press/stanford-to-work-with-edx", - ] - - for rel in all_releases: - response = self.client.get(rel) - self.assertNotContains(response, "PAGE NOT FOUND", status_code=200) - - # should work with caps - response = self.client.get("/press/STANFORD-to-work-with-edx") - self.assertContains(response, "Stanford", status_code=200) - - # negative test - response = self.client.get("/press/this-shouldnt-work") - self.assertEqual(response.status_code, 404) - - # can someone do something fishy? no. - response = self.client.get("/press/../homework.html") - self.assertEqual(response.status_code, 404) - - # "." in is ascii 2E - response = self.client.get("/press/%2E%2E/homework.html") - self.assertEqual(response.status_code, 404) - diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py index 501deb776c..deb13ffc9e 100644 --- a/lms/djangoapps/staticbook/tests.py +++ b/lms/djangoapps/staticbook/tests.py @@ -1,16 +1,231 @@ """ -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. +Test the lms/staticbook views. """ -from django.test import TestCase +import textwrap + +import mock +import requests + +from django.test.utils import override_settings +from django.core.urlresolvers import reverse, NoReverseMatch + +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -class SimpleTest(TestCase): - def test_basic_addition(self): +IMAGE_BOOK = ("An Image Textbook", "http://example.com/the_book/") + +PDF_BOOK = { + "tab_title": "Textbook", + "title": "A PDF Textbook", + "chapters": [ + { "title": "Chapter 1 for PDF", "url": "https://somehost.com/the_book/chap1.pdf" }, + { "title": "Chapter 2 for PDF", "url": "https://somehost.com/the_book/chap2.pdf" }, + ], +} + +HTML_BOOK = { + "tab_title": "Textbook", + "title": "An HTML Textbook", + "chapters": [ + { "title": "Chapter 1 for HTML", "url": "https://somehost.com/the_book/chap1.html" }, + { "title": "Chapter 2 for HTML", "url": "https://somehost.com/the_book/chap2.html" }, + ], +} + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class StaticBookTest(ModuleStoreTestCase): + """ + Helpers for the static book tests. + """ + + def __init__(self, *args, **kwargs): + super(StaticBookTest, self).__init__(*args, **kwargs) + self.course = None + + def make_course(self, **kwargs): """ - Tests that 1 + 1 always equals 2. + Make a course with an enrolled logged-in student. """ - self.assertEqual(1 + 1, 2) + self.course = CourseFactory.create(**kwargs) + user = UserFactory.create() + CourseEnrollmentFactory.create(user=user, course_id=self.course.id) + self.client.login(username=user.username, password='test') + + def make_url(self, url_name, **kwargs): + """ + Make a URL for a `url_name` using keyword args for url slots. + + Automatically provides the course id. + + """ + kwargs['course_id'] = self.course.id + url = reverse(url_name, kwargs=kwargs) + return url + + +class StaticImageBookTest(StaticBookTest): + """ + Test the image-based static book view. + """ + + def test_book(self): + # We can access a book. + with mock.patch.object(requests, 'get') as mock_get: + mock_get.return_value.text = textwrap.dedent('''\ + + + + + + + + ''') + + self.make_course(textbooks=[IMAGE_BOOK]) + url = self.make_url('book', book_index=0) + response = self.client.get(url) + + self.assertContains(response, "Contents!?") + self.assertContains(response, "About the Elephants") + + def test_bad_book_id(self): + # A bad book id will be a 404. + self.make_course(textbooks=[IMAGE_BOOK]) + with self.assertRaises(NoReverseMatch): + self.make_url('book', book_index='fooey') + + def test_out_of_range_book_id(self): + self.make_course() + url = self.make_url('book', book_index=0) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + +class StaticPdfBookTest(StaticBookTest): + """ + Test the PDF static book view. + """ + + def test_book(self): + # We can access a book. + self.make_course(pdf_textbooks=[PDF_BOOK]) + url = self.make_url('pdf_book', book_index=0) + response = self.client.get(url) + self.assertContains(response, "Chapter 1 for PDF") + self.assertNotContains(response, "options.chapterNum =") + self.assertNotContains(response, "options.pageNum =") + + def test_book_chapter(self): + # We can access a book at a particular chapter. + self.make_course(pdf_textbooks=[PDF_BOOK]) + url = self.make_url('pdf_book', book_index=0, chapter=2) + response = self.client.get(url) + self.assertContains(response, "Chapter 2 for PDF") + self.assertContains(response, "options.chapterNum = 2;") + self.assertNotContains(response, "options.pageNum =") + + def test_book_page(self): + # We can access a book at a particular page. + self.make_course(pdf_textbooks=[PDF_BOOK]) + url = self.make_url('pdf_book', book_index=0, page=17) + response = self.client.get(url) + self.assertContains(response, "Chapter 1 for PDF") + self.assertNotContains(response, "options.chapterNum =") + self.assertContains(response, "options.pageNum = 17;") + + def test_book_chapter_page(self): + # We can access a book at a particular chapter and page. + self.make_course(pdf_textbooks=[PDF_BOOK]) + url = self.make_url('pdf_book', book_index=0, chapter=2, page=17) + response = self.client.get(url) + self.assertContains(response, "Chapter 2 for PDF") + self.assertContains(response, "options.chapterNum = 2;") + self.assertContains(response, "options.pageNum = 17;") + + def test_bad_book_id(self): + # If the book id isn't an int, we'll get a 404. + self.make_course(pdf_textbooks=[PDF_BOOK]) + with self.assertRaises(NoReverseMatch): + self.make_url('pdf_book', book_index='fooey', chapter=1) + + def test_out_of_range_book_id(self): + # If we have one book, asking for the second book will fail with a 404. + self.make_course(pdf_textbooks=[PDF_BOOK]) + url = self.make_url('pdf_book', book_index=1, chapter=1) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_no_book(self): + # If we have no books, asking for the first book will fail with a 404. + self.make_course() + url = self.make_url('pdf_book', book_index=0, chapter=1) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_chapter_xss(self): + # The chapter in the URL used to go right on the page. + self.make_course(pdf_textbooks=[PDF_BOOK]) + # It's no longer possible to use a non-integer chapter. + with self.assertRaises(NoReverseMatch): + self.make_url('pdf_book', book_index=0, chapter='xyzzy') + + def test_page_xss(self): + # The page in the URL used to go right on the page. + self.make_course(pdf_textbooks=[PDF_BOOK]) + # It's no longer possible to use a non-integer page. + with self.assertRaises(NoReverseMatch): + self.make_url('pdf_book', book_index=0, page='xyzzy') + + def test_chapter_page_xss(self): + # The page in the URL used to go right on the page. + self.make_course(pdf_textbooks=[PDF_BOOK]) + # It's no longer possible to use a non-integer page and a non-integer chapter. + with self.assertRaises(NoReverseMatch): + self.make_url('pdf_book', book_index=0, chapter='fooey', page='xyzzy') + + +class StaticHtmlBookTest(StaticBookTest): + """ + Test the HTML static book view. + """ + + def test_book(self): + # We can access a book. + self.make_course(html_textbooks=[HTML_BOOK]) + url = self.make_url('html_book', book_index=0) + response = self.client.get(url) + self.assertContains(response, "Chapter 1 for HTML") + self.assertNotContains(response, "options.chapterNum =") + + def test_book_chapter(self): + # We can access a book at a particular chapter. + self.make_course(html_textbooks=[HTML_BOOK]) + url = self.make_url('html_book', book_index=0, chapter=2) + response = self.client.get(url) + self.assertContains(response, "Chapter 2 for HTML") + self.assertContains(response, "options.chapterNum = 2;") + + def test_bad_book_id(self): + # If we have one book, asking for the second book will fail with a 404. + self.make_course(html_textbooks=[HTML_BOOK]) + url = self.make_url('html_book', book_index=1, chapter=1) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_no_book(self): + # If we have no books, asking for the first book will fail with a 404. + self.make_course() + url = self.make_url('html_book', book_index=0, chapter=1) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_chapter_xss(self): + # The chapter in the URL used to go right on the page. + self.make_course(pdf_textbooks=[HTML_BOOK]) + # It's no longer possible to use a non-integer chapter. + with self.assertRaises(NoReverseMatch): + self.make_url('html_book', book_index=0, chapter='xyzzy') diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index fcfba9e22c..9ed14bfb6c 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -1,3 +1,7 @@ +""" +Views for serving static textbooks. +""" + from django.contrib.auth.decorators import login_required from django.http import Http404 from mitxmako.shortcuts import render_to_response @@ -10,6 +14,9 @@ from static_replace import replace_static_urls @login_required def index(request, course_id, book_index, page=None): + """ + Serve static image-based textbooks. + """ course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') @@ -22,18 +29,31 @@ def index(request, course_id, book_index, page=None): if page is None: page = textbook.start_page - return render_to_response('staticbook.html', - {'book_index': book_index, 'page': int(page), - 'course': course, - 'book_url': textbook.book_url, - 'table_of_contents': table_of_contents, - 'start_page': textbook.start_page, - 'end_page': textbook.end_page, - 'staff_access': staff_access}) + return render_to_response( + 'staticbook.html', + { + 'book_index': book_index, 'page': int(page), + 'course': course, + 'book_url': textbook.book_url, + 'table_of_contents': table_of_contents, + 'start_page': textbook.start_page, + 'end_page': textbook.end_page, + 'staff_access': staff_access, + }, + ) -def index_shifted(request, course_id, page): - return index(request, course_id=course_id, page=int(page) + 24) +def remap_static_url(original_url, course): + """Remap a URL in the ways the course requires.""" + # Ick: this should be possible without having to quote and unquote the URL... + input_url = "'" + original_url + "'" + output_url = replace_static_urls( + input_url, + getattr(course, 'data_dir', None), + course_namespace=course.location, + ) + # strip off the quotes again... + return output_url[1:-1] @login_required @@ -60,16 +80,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): raise Http404("Invalid book index value: {0}".format(book_index)) textbook = course.pdf_textbooks[book_index] - def remap_static_url(original_url, course): - input_url = "'" + original_url + "'" - output_url = replace_static_urls( - input_url, - getattr(course, 'data_dir', None), - course_namespace=course.location - ) - # strip off the quotes again... - return output_url[1:-1] - if 'url' in textbook: textbook['url'] = remap_static_url(textbook['url'], course) # then remap all the chapter URLs as well, if they are provided. @@ -77,13 +87,17 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): for entry in textbook['chapters']: entry['url'] = remap_static_url(entry['url'], course) - return render_to_response('static_pdfbook.html', - {'book_index': book_index, - 'course': course, - 'textbook': textbook, - 'chapter': chapter, - 'page': page, - 'staff_access': staff_access}) + return render_to_response( + 'static_pdfbook.html', + { + 'book_index': book_index, + 'course': course, + 'textbook': textbook, + 'chapter': chapter, + 'page': page, + 'staff_access': staff_access, + }, + ) @login_required @@ -109,16 +123,6 @@ def html_index(request, course_id, book_index, chapter=None): raise Http404("Invalid book index value: {0}".format(book_index)) textbook = course.html_textbooks[book_index] - def remap_static_url(original_url, course): - input_url = "'" + original_url + "'" - output_url = replace_static_urls( - input_url, - getattr(course, 'data_dir', None), - course_namespace=course.location - ) - # strip off the quotes again... - return output_url[1:-1] - if 'url' in textbook: textbook['url'] = remap_static_url(textbook['url'], course) # then remap all the chapter URLs as well, if they are provided. @@ -126,10 +130,14 @@ def html_index(request, course_id, book_index, chapter=None): for entry in textbook['chapters']: entry['url'] = remap_static_url(entry['url'], course) - return render_to_response('static_htmlbook.html', - {'book_index': book_index, - 'course': course, - 'textbook': textbook, - 'chapter': chapter, - 'staff_access': staff_access, - 'notes_enabled': notes_enabled}) + return render_to_response( + 'static_htmlbook.html', + { + 'book_index': book_index, + 'course': course, + 'textbook': textbook, + 'chapter': chapter, + 'staff_access': staff_access, + 'notes_enabled': notes_enabled, + }, + ) diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 3b87bb4326..087c1ca85c 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -16,13 +16,19 @@ 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() # Use the mongo store for acceptance tests 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', } @@ -42,7 +48,7 @@ CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { 'host': 'localhost', - 'db': 'test_xmodule', + 'db': 'acceptance_xcontent_%s' % seed(), } } @@ -52,14 +58,14 @@ 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(), } } # Set up XQueue information so that the lms will send # requests to a mock XQueue server running locally -XQUEUE_PORT = 8027 +XQUEUE_PORT = random.randint(1024, 65535) XQUEUE_INTERFACE = { "url": "http://127.0.0.1:%d" % XQUEUE_PORT, "django_auth": { @@ -76,4 +82,5 @@ MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) +LETTUCE_SERVER_PORT = random.randint(1024, 65535) LETTUCE_BROWSER = 'chrome' diff --git a/lms/envs/acceptance_static.py b/lms/envs/acceptance_static.py new file mode 100644 index 0000000000..5672ea5bf5 --- /dev/null +++ b/lms/envs/acceptance_static.py @@ -0,0 +1,81 @@ +""" +This config file extends the test environment configuration +so that we can run the lettuce acceptance tests. +""" + +# 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 random + +# Use the mongo store for acceptance tests +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.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options + } +} + +CONTENTSTORE = { + 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', + 'OPTIONS': { + 'host': 'localhost', + 'db': 'acceptance_xcontent', + } +} + +# 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", + } +} + +# Set up XQueue information so that the lms will send +# requests to a mock XQueue server running locally +XQUEUE_PORT = random.randint(1024, 65535) +XQUEUE_INTERFACE = { + "url": "http://127.0.0.1:%d" % XQUEUE_PORT, + "django_auth": { + "username": "lms", + "password": "***REMOVED***" + }, + "basic_auth": ('anant', 'agarwal'), +} + +# Do not display the YouTube videos in the browser while running the +# acceptance tests. This makes them faster and more reliable +MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True + +# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command +INSTALLED_APPS += ('lettuce.django',) +LETTUCE_APPS = ('courseware',) +LETTUCE_SERVER_PORT = random.randint(1024, 65535) +LETTUCE_BROWSER = 'chrome' diff --git a/lms/envs/aws.py b/lms/envs/aws.py index a237788163..a991b97fb5 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -193,7 +193,7 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] -AWS_STORAGE_BUCKET_NAME = 'edxuploads' +AWS_STORAGE_BUCKET_NAME = AUTH_TOKENS.get('AWS_STORAGE_BUCKET_NAME','edxuploads') DATABASES = AUTH_TOKENS['DATABASES'] @@ -227,10 +227,12 @@ ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY") # Celery Broker CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "") CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "") +CELERY_BROKER_VHOST = ENV_TOKENS.get("CELERY_BROKER_VHOST", "") CELERY_BROKER_USER = AUTH_TOKENS.get("CELERY_BROKER_USER", "") CELERY_BROKER_PASSWORD = AUTH_TOKENS.get("CELERY_BROKER_PASSWORD", "") -BROKER_URL = "{0}://{1}:{2}@{3}".format(CELERY_BROKER_TRANSPORT, - CELERY_BROKER_USER, - CELERY_BROKER_PASSWORD, - CELERY_BROKER_HOSTNAME) +BROKER_URL = "{0}://{1}:{2}@{3}/{4}".format(CELERY_BROKER_TRANSPORT, + CELERY_BROKER_USER, + CELERY_BROKER_PASSWORD, + CELERY_BROKER_HOSTNAME, + CELERY_BROKER_VHOST) diff --git a/lms/envs/cms/preview_dev.py b/lms/envs/cms/preview_dev.py index 1cfaec6159..bfa7fec826 100644 --- a/lms/envs/cms/preview_dev.py +++ b/lms/envs/cms/preview_dev.py @@ -10,7 +10,7 @@ from .dev import * MODULESTORE = { 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore', 'OPTIONS': modulestore_options }, } diff --git a/lms/envs/common.py b/lms/envs/common.py index 141bc127be..8b2a1f28cf 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -141,6 +141,9 @@ MITX_FEATURES = { # Enable instructor dash to submit background tasks 'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True, + + # Allow use of the hint managment instructor view. + 'ENABLE_HINTER_INSTRUCTOR_VIEW': False, } # Used for A/B testing diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 813f9cf32c..2ceebf39b8 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -28,6 +28,7 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True +MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True WIKI_ENABLED = True diff --git a/lms/envs/test.py b/lms/envs/test.py index e9b683487e..81505ab0b3 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -27,6 +27,8 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True +MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True + # Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it. WIKI_ENABLED = True @@ -190,3 +192,11 @@ PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.MD5PasswordHasher', # 'django.contrib.auth.hashers.CryptPasswordHasher', ) + +################### Make tests quieter + +# OpenID spews messages like this to stderr, we don't need to see them: +# Generated checkid_setup request to http://testserver/openid/provider/login/ with assocication {HMAC-SHA1}{51d49995}{s/kRmA==} + +import openid.oidutil +openid.oidutil.log = lambda message, level=0: None diff --git a/lms/lib/perfstats/tests.py b/lms/lib/perfstats/tests.py deleted file mode 100644 index 501deb776c..0000000000 --- a/lms/lib/perfstats/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/lms/static/coffee/src/instructor_dashboard_tracking.coffee b/lms/static/coffee/src/instructor_dashboard_tracking.coffee new file mode 100644 index 0000000000..a4eab610c8 --- /dev/null +++ b/lms/static/coffee/src/instructor_dashboard_tracking.coffee @@ -0,0 +1,4 @@ +if $('.instructor-dashboard-wrapper').length == 1 + analytics.track "Loaded an Instructor Dashboard Page", + location: window.location.pathname + dashboard_page: $('.navbar .selectedmode').text() diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index ddfe4a88f1..80f469113d 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -95,7 +95,7 @@ body.discussion { - + .new-post-form-errors { display: none; background: $error-red; @@ -105,7 +105,7 @@ body.discussion { color: #fff; line-height: 1.6; border-radius: 3px; - @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2)); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2); li { padding: 10px 20px 12px 45px; @@ -257,7 +257,6 @@ body.discussion { font-size: 11px; line-height: 16px; color: #333; - outline: 0; } } @@ -491,9 +490,9 @@ body.discussion { section.user-profile { @extend .sidebar; display: table-cell; - @include border-radius(3px 0 0 3px); + border-radius: 3px 0 0 3px; border-right: 1px solid #ddd; - @include box-shadow(none); + box-shadow: none; background-color: $sidebar-color; .user-profile { @@ -549,7 +548,7 @@ body.discussion { font-style: normal; font-size: 0.8em; line-height: 1.6em; - @include border-radius(3px 3px 0 0); + border-radius: 3px 3px 0 0; &::-webkit-input-placeholder { color: #888; @@ -564,9 +563,9 @@ body.discussion { @include box-sizing(border-box); border: 1px solid #c8c8c8; border-top-width: 0; - @include border-radius(0 0 3px 3px); + border-radius: 0 0 3px 3px; overflow: hidden; - @include transition(all, .2s, easeOut); + @include transition(all .2s ease-out 0s); &:before { content: 'PREVIEW'; @@ -592,7 +591,7 @@ body.discussion { padding: 0px; height: 20px; overflow: hidden; - @include transition(all, .2s, easeOut); + @include transition(all .2s ease-out 0s); } .wmd-spacer { @@ -719,7 +718,7 @@ body.discussion { height: 100%; @include linear-gradient(top, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0)); background-color: #dcdcdc; - @include transition(all .2s ease-out); + @include transition(all .2s ease-out 0s); &:hover { background-color: #e9e9e9; @@ -819,7 +818,7 @@ body.discussion { color: #333; text-shadow: 0 1px 0 rgba(255, 255, 255, .8); opacity: 0.0; - @include transition(opacity .2s); + @include transition(opacity .2s linear 0s); } } @@ -848,7 +847,7 @@ body.discussion { border: 1px solid #4b4b4b; border-left: none; border-radius: 0 0 3px 3px; - @include box-shadow(1px 0 0 #4b4b4b inset); + box-shadow: 1px 0 0 #4b4b4b inset; .browse-topic-drop-menu { max-height: 400px; @@ -932,7 +931,6 @@ body.discussion { font-size: 11px; line-height: 16px; color: #333; - outline: 0; } .post-search { @@ -940,7 +938,7 @@ body.discussion { max-width: 30px; margin: auto; @include box-sizing(border-box); - @include transition(all .2s); + @include transition(all .2s linear 0s); } .post-search-field { @@ -959,16 +957,15 @@ body.discussion { font-size: 13px; line-height: 20px; color: #333; - outline: 0; cursor: pointer; pointer-events: none; - @include transition(all .2s ease-out); + @include transition(all .2s ease-out 0s); &::-webkit-input-placeholder, &:-moz-placeholder, &:-ms-input-placeholder { opacity: 0.0; - @include transition(opacity .2s); + @include transition(opacity .2s linear 0s); } &:focus { @@ -1074,7 +1071,7 @@ body.discussion { a { display: block; - position: relative; + position: relative; float: left; clear: both; width: 100%; @@ -1281,8 +1278,8 @@ body.discussion { .discussion-article { position: relative; padding: 40px; - min-height: 468px; - + min-height: 468px; + a { word-wrap: break-word; } @@ -1335,9 +1332,9 @@ body.discussion { background-position: 0 0; } } - - - + + + } .discussion-post { @@ -1382,7 +1379,7 @@ body.discussion { margin-bottom: 20px; } - + .responses { list-style: none; @@ -1612,8 +1609,8 @@ body.discussion { background: #449944; font-size: 9px; font-weight: 700; - font-style: normal; - color: white; + font-style: normal; + color: white; text-transform: uppercase; } @@ -1621,7 +1618,7 @@ body.discussion { padding: 8px 20px; .wmd-input { - @include transition(all .2s); + @include transition(all .2s linear 0s); } .wmd-button { @@ -1641,8 +1638,7 @@ body.discussion { border: 1px solid #b2b2b2; border-radius: 3px; box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset; - @include transition(border-color .1s); - outline: 0; + @include transition(border-color .1s linear 0s); &:focus { border-color: #4697c1; @@ -1730,7 +1726,7 @@ body.discussion { .discussion-reply-new { padding: 20px; @include clearfix; - @include transition(opacity .2s); + @include transition(opacity .2s linear 0s); h4 { font-size: 16px; @@ -1841,7 +1837,7 @@ body.discussion { /* Course content p has a default margin-bottom of 1.416em, this is just to reset that */ .discussion-thread { padding: 0; - @include transition(all .25s); + @include transition(all .25s linear 0s); .dogear { display: none; @@ -1872,7 +1868,7 @@ body.discussion { min-height: 0; padding: 10px 10px 15px 10px; box-shadow: 0 1px 0 #ddd; - @include transition(all .2s); + @include transition(all .2s linear 0s); .discussion-post { padding: 12px 20px 0 20px; @@ -2210,7 +2206,7 @@ body.discussion { font-style: normal; font-size: 0.8em; line-height: 1.6em; - @include border-radius(3px 3px 0 0); + border-radius: 3px 3px 0 0; &::-webkit-input-placeholder { color: #888; @@ -2225,9 +2221,9 @@ body.discussion { @include box-sizing(border-box); border: 1px solid #c8c8c8; border-top-width: 0; - @include border-radius(0 0 3px 3px); + border-radius: 0 0 3px 3px; overflow: hidden; - @include transition(all, .2s, easeOut); + @include transition(all .2s ease-out 0s); &:before { content: 'PREVIEW'; @@ -2253,7 +2249,7 @@ body.discussion { padding: 0px; height: 20px; overflow: hidden; - @include transition(all, .2s, easeOut); + @include transition(all .2s ease-out 0s); } .wmd-spacer { @@ -2452,7 +2448,7 @@ body.discussion { float:right; padding-right: 5px; font-style: italic; - cursor:pointer; + cursor:pointer; margin-right: 10px; opacity: 0.8; @@ -2461,10 +2457,10 @@ body.discussion { } &:hover { - @include transition(opacity .2s); + @include transition(opacity .2s linear 0s); opacity: 1.0; } - } + } .discussion-pin-inline { font-size: 12px; @@ -2476,39 +2472,39 @@ body.discussion { margin-right:35px; margin-top:13px; opacity: 1.0; - } + } .notpinned .icon { - display: block; + display: block; float: left; margin: 3px; - width: 10px; + width: 10px; height: 14px; padding-right: 3px; - background: transparent url('../images/unpinned.png') no-repeat 0 0; + background: transparent url('../images/unpinned.png') no-repeat 0 0; } .pinned .icon { - display: block; + display: block; float: left; margin: 3px; - width: 10px; - height: 14px; + width: 10px; + height: 14px; padding-right: 3px; - background: transparent url('../images/pinned.png') no-repeat 0 0; + background: transparent url('../images/pinned.png') no-repeat 0 0; } .pinned span { color: #B82066; font-style: italic; - //cursor change is here since pins are read-only for inline discussions. + //cursor change is here since pins are read-only for inline discussions. cursor: default; } .notpinned span { color: #888; font-style: italic; - //cursor change is here since pins are read-only for inline discussions. + //cursor change is here since pins are read-only for inline discussions. cursor: default; } @@ -2526,32 +2522,32 @@ display:none; opacity: 0.8; &:hover { - @include transition(opacity .2s); + @include transition(opacity .2s linear 0s); opacity: 1.0; } - } - + } + .notflagged .icon { - display: block; + display: block; float: left; margin: 3px; - width: 10px; + width: 10px; height: 14px; padding-right: 3px; - background: transparent url('../images/notflagged.png') no-repeat 0 0; + background: transparent url('../images/notflagged.png') no-repeat 0 0; } .flagged .icon { - display: block; + display: block; float: left; margin: 3px; - width: 10px; - height: 14px; + width: 10px; + height: 14px; padding-right: 3px; - background: transparent url('../images/flagged.png') no-repeat 0 0; + background: transparent url('../images/flagged.png') no-repeat 0 0; } .flagged span { @@ -2562,4 +2558,4 @@ display:none; .notflagged span { color: #888; font-style: italic; -} \ No newline at end of file +} diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index 9314877249..59a81192f5 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -64,7 +64,7 @@ p { color: $link-color; font: normal 1em/1em $serif; text-decoration: none; - @include transition(all, 0.1s, linear); + @include transition(all 0.1s linear 0s); &:hover { color: $link-color; @@ -77,13 +77,21 @@ a:link, a:visited { color: $link-color; font: normal 1em/1em $sans-serif; text-decoration: none; - @include transition(all, 0.1s, linear); + @include transition(all 0.1s linear 0s); &:hover { text-decoration: underline; } } +a:focus { + /** + * Add general focus styling here + * for example: + * outline: 3px groove $black; + **/ +} + .content-wrapper { width: flex-grid(12); margin: 0 auto; @@ -196,7 +204,7 @@ mark { max-width: 1140px; min-width: 720px; margin: auto; - @include border-radius(0 0 3px 3px); + border-radius: 0 0 3px 3px; background: #f4f4e0; color: #3c3c3c; padding: 5px 20px 8px; @@ -224,7 +232,7 @@ mark { cursor: pointer; border: 1px solid #ccc; border-top-style: none; - @include border-radius(0px 0px 10px 10px); + border-radius: 0px 0px 10px 10px; background: transparentize(#fff, 0.25); color: transparentize(#333, 0.25); font-weight: bold; @@ -253,12 +261,12 @@ mark { &#feedback_link_problem { border-bottom-style: none; - @include border-radius(10px 10px 0px 0px); + border-radius: 10px 10px 0px 0px; } &#feedback_link_question { border-top-style: none; - @include border-radius(0px 0px 10px 10px); + border-radius: 0px 0px 10px 10px; } &:hover { diff --git a/lms/static/sass/base/_reset.scss b/lms/static/sass/base/_reset.scss index aa7fc975c8..e774e7a59b 100644 --- a/lms/static/sass/base/_reset.scss +++ b/lms/static/sass/base/_reset.scss @@ -9,9 +9,6 @@ html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 10 html, button, input, select, textarea { font-family: sans-serif; color: #222; } body { margin: 0; font-size: 1em; line-height: 1.4; } -::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; } -::selection { background: #fe57a1; color: #fff; text-shadow: none; } - a { color: #00e; } a:visited { color: #551a8b; } a:hover { color: #06e; } diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 30d49f5e0e..c0bbcfb9ee 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -61,6 +61,8 @@ $baseFontColor: rgb(60,60,60); $lighter-base-font-color: rgb(100,100,100); $text-color: $dark-gray; +$dark-trans-bg: rgba(0, 0, 0, .75); + $body-bg: rgb(250,250,250); $container-bg: $white; $header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9)); @@ -104,8 +106,6 @@ $border-color-4: rgb(252,252,252); $link-color: $blue; $link-color-d1: $m-blue; $link-hover: $pink; -$selection-color-1: $pink; -$selection-color-2: #444; $site-status-color: $pink; $button-color: $blue; diff --git a/lms/static/sass/course/_discussions-inline.scss b/lms/static/sass/course/_discussions-inline.scss deleted file mode 100644 index f9569a80ff..0000000000 --- a/lms/static/sass/course/_discussions-inline.scss +++ /dev/null @@ -1,535 +0,0 @@ -.discussion-module { - @extend .discussion-body; - margin: 20px 0; - padding: 20px 20px 28px 20px; - background: #f6f6f6 !important; - border-radius: 3px; - - .responses { - margin-top: 40px; - - > li { - margin: 0 20px 30px; - } - } - - .discussion-show { - display: block; - width: 200px; - margin: auto; - font-size: 14px; - text-align: center; - - &.shown { - .show-hide-discussion-icon { - background-position: 0 0; - } - } - - .show-hide-discussion-icon { - display: inline-block; - position: relative; - top: 5px; - margin-right: 6px; - width: 21px; - height: 19px; - background: url(../images/show-hide-discussion-icon.png) no-repeat; - background-position: -21px 0; - } - } - - .new-post-btn { - display: inline-block; - } - - section.discussion { - margin-top: 20px; - - .threads { - margin-top: 20px; - } - - /* Course content p has a default margin-bottom of 1.416em, this is just to reset that */ - .discussion-thread { - padding: 0; - @include transition(all .25s); - - .dogear, - .vote-btn { - display: none; - } - - &.expanded { - padding: 20px 0; - - .dogear, - .vote-btn { - display: block; - } - - .discussion-article { - border: 1px solid #b2b2b2; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); - border-radius: 3px; - } - } - - p { - margin-bottom: 0em; - } - - .discussion-article { - border: 1px solid #ddd; - border-bottom-width: 0; - background: #fff; - min-height: 0; - padding: 10px 10px 15px 10px; - box-shadow: 0 1px 0 #ddd; - @include transition(all .2s); - - .discussion-post { - padding: 12px 20px 0 20px; - @include clearfix; - - header { - padding-bottom: 0; - margin-bottom: 15px; - - h3 { - font-size: 19px; - font-weight: 700; - margin-bottom: 0px; - } - - h4 { - font-size: 16px; - } - } - - .post-body { - font-size: 14px; - clear: both; - } - } - - .post-tools { - margin-left: 20px; - - a { - display: block; - font-size: 12px; - line-height: 30px; - - &.expand-post:before { - content: '▾ '; - } - - &.collapse-post:before { - content: '▴ '; - } - - &.collapse-post { - display: none; - } - } - } - - .responses { - margin-top: 10px; - - header { - padding-bottom: 0em; - margin-bottom: 5px; - - .posted-by { - font-size: 0.8em; - } - } - .response-body { - margin-bottom: 0.2em; - font-size: 14px; - } - } - - .discussion-reply-new { - .wmd-input { - height: 120px; - } - } - - // Content that is hidden by default in the inline view - .post-extended-content{ - display: none; - } - - - } - } - } - - .new-post-article { - display: none; - margin-top: 20px; - - .inner-wrapper { - max-width: 1180px; - min-width: 760px; - margin: auto; - } - - .new-post-form { - width: 100%; - margin-bottom: 20px; - padding: 30px; - border-radius: 3px; - background: rgba(0, 0, 0, .55); - color: #fff; - box-shadow: none; - @include clearfix; - @include box-sizing(border-box); - - .form-row { - margin-bottom: 20px; - } - - .new-post-body .wmd-input { - @include discussion-wmd-input; - position: relative; - width: 100%; - height: 200px; - z-index: 1; - padding: 10px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 3px 3px 0 0; - background: #fff; - font-family: 'Monaco', monospace; - font-size: 13px; - line-height: 1.6; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-body .wmd-preview { - @include discussion-wmd-preview; - position: relative; - width: 100%; - //height: 50px; - margin-top: -1px; - padding: 25px 20px 10px 20px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 0 0 3px 3px; - background: #e6e6e6; - color: #333; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-preview-label { - position: absolute; - top: 4px; - left: 4px; - font-size: 11px; - color: #aaa; - text-transform: uppercase; - } - - .new-post-title, - .new-post-tags { - width: 100%; - height: 40px; - padding: 0 10px; - box-sizing: border-box; - border-radius: 3px; - border: 1px solid #333; - font-size: 16px; - font-family: 'Open Sans', sans-serif; - color: #333; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-title { - font-weight: 700; - } - - .tagsinput { - padding: 10px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 3px; - background: #fff; - font-family: 'Monaco', monospace; - font-size: 13px; - line-height: 1.6; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - - span.tag { - margin-bottom: 0; - } - } - - .submit { - @include blue-button; - float: left; - height: 37px; - margin-top: 10px; - padding-bottom: 2px; - border-color: #333; - - &:hover { - border-color: #222; - } - } - - .new-post-cancel { - @include white-button; - float: left; - margin: 10px 0 0 15px; - border-color: #444; - } - - .options { - margin-top: 5px; - - label { - display: inline; - margin-left: 8px; - font-size: 15px; - color: #fff; - text-shadow: none; - } - } - } - - .thread-tags { - margin-top: 20px; - } - - .thread-tag { - padding: 3px 10px 6px; - border-radius: 3px; - color: #333; - background: #c5eeff; - border: 1px solid #90c4d7; - font-size: 13px; - } - - .thread-title { - display: block; - margin-bottom: 20px; - font-size: 21px; - color: #333; - font-weight: 700; - } - } - - .new-post-btn { - @include blue-button; - display: inline-block; - font-size: 13px; - margin-right: 4px; - } - - .new-post-icon { - display: block; - float: left; - width: 16px; - height: 17px; - margin: 8px 7px 0 0; - background: url(../images/new-post-icon.png) no-repeat; - } - - .moderator-actions { - padding-left: 0 !important; - } - - section.pagination { - margin-top: 30px; - - nav.discussion-paginator { - float: right; - - ol { - li { - list-style: none; - display: inline-block; - padding-right: 0.5em; - a { - @include white-button; - } - } - - li.current-page{ - height: 35px; - padding: 0 15px; - border: 1px solid #ccc; - border-radius: 3px; - font-size: 13px; - font-weight: 700; - line-height: 32px; - color: #333; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - } - } - } - } - - .new-post-body { - .wmd-panel { - width: 100%; - min-width: 500px; - } - - .wmd-button-bar { - width: 100%; - } - - .wmd-input { - height: 150px; - width: 100%; - background-color: #e9e9e9; - border: 1px solid #c8c8c8; - font-family: Monaco, 'Lucida Console', monospace; - font-style: normal; - font-size: 0.8em; - line-height: 1.6em; - @include border-radius(3px 3px 0 0); - - &::-webkit-input-placeholder { - color: #888; - } - } - - .wmd-preview { - position: relative; - font-family: $sans-serif; - padding: 25px 20px 10px 20px; - margin-bottom: 5px; - box-sizing: border-box; - border: 1px solid #c8c8c8; - border-top-width: 0; - @include border-radius(0 0 3px 3px); - overflow: hidden; - @include transition(all, .2s, easeOut); - - &:before { - content: 'PREVIEW'; - position: absolute; - top: 3px; - left: 5px; - font-size: 11px; - color: #bbb; - } - - p { - font-family: $sans-serif; - } - background-color: #fafafa; - } - - .wmd-button-row { - position: relative; - margin-left: 5px; - margin-right: 5px; - margin-bottom: 5px; - margin-top: 10px; - padding: 0px; - height: 20px; - overflow: hidden; - @include transition(all, .2s, easeOut); - } - - .wmd-spacer { - width: 1px; - height: 20px; - margin-left: 14px; - - position: absolute; - background-color: Silver; - display: inline-block; - list-style: none; - } - - .wmd-button { - width: 20px; - height: 20px; - padding-left: 2px; - padding-right: 3px; - position: absolute; - display: inline-block; - list-style: none; - cursor: pointer; - background: none; - } - - .wmd-button > span { - display: inline-block; - background-image: url(../images/new-post-icons-full.png); - background-repeat: no-repeat; - background-position: 0px 0px; - width: 20px; - height: 20px; - } - - .wmd-spacer1 { - left: 50px; - } - .wmd-spacer2 { - left: 175px; - } - - .wmd-spacer3 { - left: 300px; - } - - .wmd-prompt-background { - background-color: Black; - } - - .wmd-prompt-dialog { - @extend .modal; - background: #fff; - } - - .wmd-prompt-dialog { - padding: 20px; - - > div { - font-size: 0.8em; - font-family: arial, helvetica, sans-serif; - } - - b { - font-size: 16px; - } - - > form > input[type="text"] { - border-radius: 3px; - color: #333; - } - - > form > input[type="button"] { - border: 1px solid #888; - font-family: $sans-serif; - font-size: 14px; - } - - > form > input[type="file"] { - margin-bottom: 18px; - } - } - } - - .wmd-button-row { - // this is being hidden now because the inline styles to position the icons are not being written - display: none; - position: relative; - height: 12px; - } - - .wmd-button { - span { - background-image: url("/static/images/wmd-buttons.png"); - display: inline-block; - } - } -} \ No newline at end of file diff --git a/lms/static/sass/course/_gradebook.scss b/lms/static/sass/course/_gradebook.scss index 9817188d34..3355f96260 100644 --- a/lms/static/sass/course/_gradebook.scss +++ b/lms/static/sass/course/_gradebook.scss @@ -2,7 +2,6 @@ $cell-border-color: #e1e1e1; $table-border-color: #c8c8c8; div.gradebook-wrapper { - @extend .table-wrapper; section.gradebook-content { @extend .content; @@ -24,9 +23,9 @@ div.gradebook-wrapper { background: url(../images/search-icon.png) no-repeat 9px center #f6f6f6; font-family: $sans-serif; font-size: 11px; - @include box-shadow(0 1px 4px rgba(0, 0, 0, .12) inset); + box-shadow: 0 1px 4px rgba(0, 0, 0, .12) inset; outline: none; - @include transition(border-color .15s); + @include transition(border-color .15s linear 0s); &::-webkit-input-placeholder, &::-moz-input-placeholder { diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index 741a7f9a22..ce6358c33f 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -1,5 +1,4 @@ div.info-wrapper { - @extend .table-wrapper; section.updates { @extend .content; @@ -67,7 +66,7 @@ div.info-wrapper { > ol { list-style: decimal outside none; - padding: 0 0 0 1em; + padding: 0 0 0 1em; } li { @@ -80,9 +79,9 @@ div.info-wrapper { section.handouts { @extend .sidebar; - @include border-radius(0 4px 4px 0); + border-radius: 0 4px 4px 0; border-left: 1px solid #ddd; - @include box-shadow(none); + box-shadow: none; font-size: 14px; &:after { @@ -114,7 +113,7 @@ div.info-wrapper { &.expandable, &.collapsable { margin: 0 16px 14px 16px; - @include transition(all .2s); + @include transition(all .2s linear 0s); h4 { color: $link-color; @@ -128,7 +127,7 @@ div.info-wrapper { background: #fff; border-radius: 3px; padding: 14px 0; - @include box-shadow(0 0 1px 1px rgba(0, 0, 0, .1), 0 1px 3px rgba(0, 0, 0, .25)); + box-shadow: 0 0 1px 1px rgba(0, 0, 0, .1), 0 1px 3px rgba(0, 0, 0, .25); h4 { margin-bottom: 16px; @@ -199,7 +198,7 @@ div.info-wrapper { h3 { border-bottom: 0; - @include box-shadow(none); + box-shadow: none; color: #888; font-size: 1em; margin-bottom: 0; diff --git a/lms/static/sass/course/_profile.scss b/lms/static/sass/course/_profile.scss index 0683781e44..0f38cedff9 100644 --- a/lms/static/sass/course/_profile.scss +++ b/lms/static/sass/course/_profile.scss @@ -1,11 +1,10 @@ div.profile-wrapper { - @extend .table-wrapper; color: #000; section.user-info { @extend .sidebar; border-left: 1px solid #d3d3d3; - @include border-radius(0px 4px 4px 0); + border-radius: 0px 4px 4px 0; border-right: 0; &:after { @@ -31,13 +30,13 @@ div.profile-wrapper { li { border-bottom: 1px solid #d3d3d3; - @include box-shadow(0 1px 0 #eee); + box-shadow: 0 1px 0 #eee; color: lighten($text-color, 10%); display: block; padding: lh(.5) 0 lh(.5) lh(.5); position: relative; text-decoration: none; - @include transition(); + @include transition(none); div#location_sub, div#language_sub { font-weight: bold; @@ -100,7 +99,7 @@ div.profile-wrapper { input#pwd_reset_button { background: none; border: none; - @include box-shadow(none); + box-shadow: none; color: #999; font-size: 12px; font-weight: normal; @@ -120,7 +119,7 @@ div.profile-wrapper { div#change_password_pop { border-bottom: 1px solid #d3d3d3; - @include box-shadow(0 1px 0 #eee); + box-shadow: 0 1px 0 #eee; color: #4D4D4D; padding: 7px lh(); diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index 83aca09ab6..bc9da1f43f 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -1,8 +1,7 @@ div.book-wrapper { - @extend .table-wrapper; display: table; table-layout: fixed; - padding: 1em 8em; + padding: 1em 8em; #open_close_accordion { @@ -39,13 +38,13 @@ div.book-wrapper { text-align: right; color: #9a9a9a; opacity: 0.0; - @include transition(opacity .15s); + @include transition(opacity .15s linear 0s); } li { background: none; border-bottom: 0; - padding-left: lh(); + padding-left: lh(); a { padding: 0; @@ -62,7 +61,7 @@ div.book-wrapper { div.hitarea { background-image: url('../images/treeview-default.gif'); - + position: relative; top: 4px; @@ -122,7 +121,7 @@ div.book-wrapper { opacity: 0.0; filter: alpha(opacity=0); text-indent: -9999px; - @include transition; + @include transition(none); vertical-align: middle; width: 100%; @@ -175,10 +174,10 @@ div.book-wrapper { text-align: left; line-height: 1.6em; margin: 5px; - + .Paragraph, h2 { margin-top: 10px; - } + } } } } diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index a1c948d4f5..28665a3303 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -36,7 +36,7 @@ a { border-radius: 3px; border: 1px solid $outer-border-color; background: $container-bg; - @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05)); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } } @@ -59,8 +59,8 @@ input[type="email"], input[type="password"] { background: $white; border: 1px solid $border-color-2; - @include border-radius(0); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1)); + border-radius: 0; + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1); @include box-sizing(border-box); font: normal 1em $sans-serif; height: 35px; @@ -74,7 +74,7 @@ input[type="password"] { &:focus { border-color: lighten($link-color, 20%); - @include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15)); + box-shadow: 0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15); outline: none; } } @@ -101,12 +101,6 @@ img { max-width: 100%; } -::selection, ::-moz-selection, ::-webkit-selection { - background: $selection-color-2; - color: #fff; -} - - .tooltip { position: absolute; top: 0; @@ -121,7 +115,7 @@ img { color: #fff; pointer-events: none; opacity: 0; - @include transition(opacity .1s); + @include transition(opacity .1s linear 0s); &:after { content: '▾'; diff --git a/lms/static/sass/course/base/_extends.scss b/lms/static/sass/course/base/_extends.scss index a94a9511fe..dd02e71e54 100644 --- a/lms/static/sass/course/base/_extends.scss +++ b/lms/static/sass/course/base/_extends.scss @@ -42,7 +42,7 @@ h1.top-header { width: flex-grid(9) + flex-gutter(); @media print { - @include box-shadow(none); + box-shadow: none; } } @@ -96,7 +96,7 @@ h1.top-header { a { display: block; text-decoration: none; - @include transition(); + @include transition(none); } &.active { @@ -143,7 +143,7 @@ h1.top-header { a { background: #f6f6f6 url('../images/slide-left-icon.png') center center no-repeat; border: 1px solid #D3D3D3; - @include border-radius(3px 0 0 3px); + border-radius: 3px 0 0 3px; height: 16px; padding: 6px; position: absolute; @@ -185,6 +185,6 @@ h1.top-header { } .tran { - @include transition( all, .2s, $ease-in-out-quad); + @include transition( all .2s $ease-in-out-quad 0s); } diff --git a/lms/static/sass/course/courseware/_amplifier.scss b/lms/static/sass/course/courseware/_amplifier.scss index 18a02f489d..002035bbed 100644 --- a/lms/static/sass/course/courseware/_amplifier.scss +++ b/lms/static/sass/course/courseware/_amplifier.scss @@ -4,7 +4,7 @@ section.tool-wrapper { background: #073642; border-bottom: 1px solid darken(#002b36, 10%); border-top: 1px solid darken(#002b36, 10%); - @include box-shadow(inset 0 0 0 4px darken(#094959, 2%)); + box-shadow: inset 0 0 0 4px darken(#094959, 2%); color: #839496; display: table; margin: lh() (-(lh())) 0; @@ -20,7 +20,7 @@ section.tool-wrapper { .ui-widget-content { background: none; border: none; - @include border-radius(0); + border-radius: 0; } canvas { @@ -30,7 +30,7 @@ section.tool-wrapper { ul.ui-tabs-nav { background: darken(#073642, 2%); border-bottom: 1px solid darken(#073642, 8%); - @include border-radius(0); + border-radius: 0; margin: (-(lh())) (-(lh())) 0; padding: 0; position: relative; @@ -39,7 +39,7 @@ section.tool-wrapper { li { background: none; border: none; - @include border-radius(0); + border-radius: 0; color: #fff; margin-bottom: 0; @@ -76,7 +76,7 @@ section.tool-wrapper { @extend .clearfix; background: darken(#073642, 2%); border-right: 1px solid darken(#002b36, 6%); - @include box-shadow(1px 0 0 lighten(#002b36, 6%), inset 0 0 0 4px darken(#094959, 6%)); + box-shadow: 1px 0 0 lighten(#002b36, 6%), inset 0 0 0 4px darken(#094959, 6%); @include box-sizing(border-box); display: table-cell; padding: lh(); @@ -88,7 +88,7 @@ section.tool-wrapper { div.music-wrapper { @extend .clearfix; border-bottom: 1px solid darken(#073642, 10%); - @include box-shadow(0 1px 0 lighten(#073642, 2%)); + box-shadow: 0 1px 0 lighten(#073642, 2%); margin-bottom: lh(); padding: 0 0 lh(); @@ -101,7 +101,7 @@ section.tool-wrapper { margin-top: 19px; &:active { - @include box-shadow(none); + box-shadow: none; } &[value="Stop"] { @@ -109,7 +109,7 @@ section.tool-wrapper { font: bold 14px $body-font-family; &:active { - @include box-shadow(none); + box-shadow: none; } } } @@ -118,7 +118,7 @@ section.tool-wrapper { div.inputs-wrapper { @extend .clearfix; border-bottom: 1px solid darken(#073642, 10%); - @include box-shadow(0 1px 0 lighten(#073642, 2%)); + box-shadow: 0 1px 0 lighten(#073642, 2%); @include clearfix; margin-bottom: lh(); margin-bottom: lh(); @@ -159,7 +159,7 @@ section.tool-wrapper { } label { - @include border-radius(2px); + border-radius: 2px; color: #fff; font-weight: bold; padding: 3px; @@ -193,7 +193,7 @@ section.tool-wrapper { div.top-sliders { @extend .clearfix; border-bottom: 1px solid darken(#073642, 10%); - @include box-shadow(0 1px 0 lighten(#073642, 2%)); + box-shadow: 0 1px 0 lighten(#073642, 2%); margin-bottom: lh(); padding: 0 0 lh(); @@ -226,14 +226,14 @@ section.tool-wrapper { &.ui-slider-horizontal { background: darken(#002b36, 2%); border: 1px solid darken(#002b36, 8%); - @include box-shadow(none); + box-shadow: none; height: 0.4em; } .ui-slider-handle { background: lighten( #586e75, 5% ) url('../images/amplifier-slider-handle.png') center no-repeat; border: 1px solid darken(#002b36, 8%); - @include box-shadow(inset 0 1px 0 lighten( #586e75, 20% )); + box-shadow: inset 0 1px 0 lighten( #586e75, 20% ); margin-top: -.3em; &:hover, &:active { diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index ab285392ca..200705e8b6 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -4,7 +4,6 @@ html { } div.course-wrapper { - @extend .table-wrapper; section.course-content { @extend .content; @@ -47,7 +46,6 @@ div.course-wrapper { > li { @extend .clearfix; - @extend .problem-set; border-bottom: 1px solid #ddd; margin-bottom: 15px; padding: 0 0 15px; @@ -62,7 +60,7 @@ div.course-wrapper { header { @extend h1.top-header; - @include border-radius(0 4px 0 0); + border-radius: 0 4px 0 0; margin-bottom: -16px; border-bottom: 0; @@ -169,7 +167,7 @@ div.course-wrapper { div.ui-tabs { border: 0; - @include border-radius(0); + border-radius: 0; margin: 0; padding: 0; @@ -180,7 +178,7 @@ div.course-wrapper { } .ui-tabs-panel { - @include border-radius(0); + border-radius: 0; padding: 0; } } diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 24bda451a7..c8e9e233e7 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -1,7 +1,7 @@ section.course-index { @extend .sidebar; @extend .tran; - @include border-radius(3px 0 0 3px); + border-radius: 3px 0 0 3px; border-right: 1px solid $border-color-2; #open_close_accordion { @@ -21,9 +21,9 @@ section.course-index { font-size: 14px; h3 { - @include border-radius(0); + border-radius: 0; margin: 0; - overflow: hidden; + overflow: visible; &:first-child { border: none; @@ -44,8 +44,8 @@ section.course-index { color: #000; a { - @include border-radius(0); - @include box-shadow(none); + border-radius: 0; + box-shadow: none; padding-left: 19px; } @@ -72,8 +72,8 @@ section.course-index { padding: 11px 14px; @include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom); background-color: $sidebar-chapter-bg; - @include box-shadow(0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset); - @include transition(background-color .1s); + box-shadow: 0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset; + @include transition(background-color .1s linear 0s); &.is-open { background: #fff; @@ -85,7 +85,7 @@ section.course-index { &:last-child { border-radius: 0 0 0 3px; - @include box-shadow(0 1px 0 #fff inset); + box-shadow: 0 1px 0 #fff inset; } &:hover { @@ -96,7 +96,7 @@ section.course-index { ul.ui-accordion-content { background: transparent; border: none; - @include border-radius(0); + border-radius: 0; margin: 0; padding: 9px 0 9px 9px; overflow: auto; @@ -104,12 +104,12 @@ section.course-index { li { border-bottom: 0; - @include border-radius(0); + border-radius: 0; margin-bottom: 4px; a { background: transparent; - @include border-radius(4px); + border-radius: 4px; display: block; padding: 5px 36px 5px 10px; position: relative; @@ -143,7 +143,7 @@ section.course-index { } &:active { - @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); + box-shadow: inset 0 1px 14px 0 rgba(0,0,0, 0.1); &:after { opacity: 1.0; @@ -165,12 +165,12 @@ section.course-index { font-weight: normal; color: #333; opacity: 0; - @include transition(); + @include transition(none); } > a { border: 1px solid $border-color-1; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .35) inset); + box-shadow: 0 1px 0 rgba(255, 255, 255, .35) inset; background: $sidebar-active-image; &:after { diff --git a/lms/static/sass/course/instructor/_instructor.scss b/lms/static/sass/course/instructor/_instructor.scss index 070f7bcc9c..ff5cc39a84 100644 --- a/lms/static/sass/course/instructor/_instructor.scss +++ b/lms/static/sass/course/instructor/_instructor.scss @@ -1,5 +1,4 @@ .instructor-dashboard-wrapper { - @extend .table-wrapper; display: table; section.instructor-dashboard-content { diff --git a/lms/static/sass/course/layout/_calculator.scss b/lms/static/sass/course/layout/_calculator.scss index c0a8764a8c..046afbbcf8 100644 --- a/lms/static/sass/course/layout/_calculator.scss +++ b/lms/static/sass/course/layout/_calculator.scss @@ -2,7 +2,7 @@ div.calc-main { bottom: -126px; left: 0; position: fixed; - @include transition(bottom); + @include transition(bottom 0.75s linear 0s); -webkit-appearance: none; width: 100%; z-index: 99; @@ -14,7 +14,7 @@ div.calc-main { a.calc { background: url("../images/calc-icon.png") rgba(#111, .9) no-repeat center; border-bottom: 0; - @include border-radius(3px 3px 0 0); + border-radius: 3px 3px 0 0; color: #fff; float: right; height: 20px; @@ -51,8 +51,8 @@ div.calc-main { input#calculator_button { background: #111; border: 1px solid #000; - @include border-radius(0); - @include box-shadow(none); + border-radius: 0; + box-shadow: none; @include box-sizing(border-box); color: #fff; float: left; @@ -73,7 +73,7 @@ div.calc-main { input#calculator_output { background: #111; border: 0; - @include box-shadow(none); + box-shadow: none; @include box-sizing(border-box); color: #fff; float: left; @@ -94,7 +94,7 @@ div.calc-main { input#calculator_input { border: none; - @include box-shadow(none); + box-shadow: none; @include box-sizing(border-box); font-size: 16px; padding: 10px; @@ -121,8 +121,8 @@ div.calc-main { dl { background: #fff; - @include border-radius(3px); - @include box-shadow(0 0 3px #999); + border-radius: 3px; + box-shadow: 0 0 3px #999; color: #333; display: none; line-height: lh(); @@ -131,7 +131,7 @@ div.calc-main { position: absolute; right: -40px; top: -122px; - @include transition(); + @include transition(none); width: 600px; &.shown { diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index b1c14f930a..8cdf0b0b21 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -43,7 +43,7 @@ nav.course-material { // background: rgba(0, 0, 0, .2); @include linear-gradient(top, rgba(0, 0, 0, .4), rgba(0, 0, 0, .25)); background-color: transparent; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .5), 0 1px 1px rgba(0, 0, 0, .3) inset); + box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 1px 1px rgba(0, 0, 0, .3) inset; color: #fff; text-shadow: 0 1px 0 rgba(0, 0, 0, .4); } @@ -61,7 +61,7 @@ nav.course-material { } header.global.slim { - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); height: auto; padding: 5px 0 10px 0; border-bottom: 1px solid $outer-border-color; @@ -79,9 +79,9 @@ header.global.slim { @include background-image(linear-gradient(-90deg, lighten($link-color, 8%), lighten($link-color, 5%) 50%, $link-color 50%, darken($link-color, 10%) 100%)); border: 1px solid transparent; border-color: darken($link-color, 10%); - @include border-radius(3px); + border-radius: 3px; @include box-sizing(border-box); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); color: #fff; display: inline-block; font-family: $sans-serif; @@ -148,7 +148,7 @@ header.global.slim { float: left; font-size: 0.9em; font-weight: 600; - color: #777; + color: $lighter-base-font-color; letter-spacing: 0; margin-top: 9px; margin-bottom: 0; diff --git a/lms/static/sass/course/wiki/_create.scss b/lms/static/sass/course/wiki/_create.scss index 35b0798501..f1c0473858 100644 --- a/lms/static/sass/course/wiki/_create.scss +++ b/lms/static/sass/course/wiki/_create.scss @@ -35,7 +35,7 @@ form#wiki_revision { #submit_delete { background: none; border: none; - @include box-shadow(none); + box-shadow: none; color: #999; float: right; font-weight: normal; diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss index d064b6d345..467a6a2c3e 100644 --- a/lms/static/sass/course/wiki/_wiki.scss +++ b/lms/static/sass/course/wiki/_wiki.scss @@ -41,10 +41,10 @@ section.wiki { a { float: left; - display: block; + display: block; overflow: hidden; - height: 30px; - line-height: 31px; + height: 30px; + line-height: 31px; max-width: 200px; height: 100%; text-overflow: ellipsis; @@ -56,7 +56,7 @@ section.wiki { display: inline; margin-left: 10px; color: $base-font-color; - height: 30px; + height: 30px; line-height: 31px; } } @@ -106,7 +106,7 @@ section.wiki { font-family: $sans-serif; font-size: 12px; outline: none; - @include transition(border-color .1s); + @include transition(border-color .1s linear 0s); &:-webkit-input-placholder { font-style: italic; @@ -208,7 +208,7 @@ section.wiki { background-color: $sidebar-color; padding: 9px; margin: 10px 0; - @include border-radius(5px); + border-radius: 5px; ul { margin: 0; @@ -234,7 +234,7 @@ section.wiki { .timestamp{ margin-top: 15px; padding: 15px 0 0 10px; - border-top: 1px solid $light-gray; + border-top: 1px solid $light-gray; .label { font-size: 0.7em; @@ -406,7 +406,7 @@ section.wiki { .CodeMirror { background: #fafafa; border: 1px solid #c8c8c8; - @include box-shadow(0 1px 0 0 rgba(255, 255, 255, 0.6), inset 0 0 3px 0 rgba(0, 0, 0, 0.1)); + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.6), inset 0 0 3px 0 rgba(0, 0, 0, 0.1); } .CodeMirror-scroll { @@ -417,7 +417,7 @@ section.wiki { position: relative; canvas { - @include box-shadow(0 0 1px 1px rgba(0, 0, 0, .1), 0 1px 6px rgba(0, 0, 0, .2)); + box-shadow: 0 0 1px 1px rgba(0, 0, 0, .1), 0 1px 6px rgba(0, 0, 0, .2); } &:before { @@ -536,13 +536,13 @@ section.wiki { .modal-header { h1, p { - color: #fff; + color: #fff; } h1 { margin: 3px 12px 8px; font-size: 1.1em; - } + } p { font-size: 0.9em; @@ -610,10 +610,10 @@ section.wiki { border: 1px solid #ccc; @include linear-gradient(top, #eee, #d2d2d2); font-size: 22px; - line-height: 28px; + line-height: 28px; color: #333; text-align: center; - @include box-shadow(0 1px 0 #fff inset, 0 1px 2px rgba(0, 0, 0, .2)); + box-shadow: 0 1px 0 #fff inset, 0 1px 2px rgba(0, 0, 0, .2); } } @@ -688,7 +688,7 @@ section.wiki { overflow-x: scroll; table { - min-width: 100%; + min-width: 100%; } th { @@ -798,7 +798,7 @@ section.wiki { background-color: $sidebar-color; padding: 9px; margin: 0 -9px 20px; - @include border-radius(5px); + border-radius: 5px; .well-small { @include clearfix; @@ -837,7 +837,7 @@ section.wiki { padding: 8px; overflow: hidden; } - + a.list-children { margin-left: 3px; } @@ -858,7 +858,7 @@ section.wiki { .attachment-options { height: 40px; - margin: 40px 0 30px; + margin: 40px 0 30px; } .attachment-list { @@ -871,7 +871,7 @@ section.wiki { margin-bottom: 15px; border: 1px solid #DDD; background: #F9F9F9; - @include border-radius(5px); + border-radius: 5px; } header, @@ -881,7 +881,7 @@ section.wiki { .attachment-details { background: #eee; - @include border-radius(0 0 5px 5px); + border-radius: 0 0 5px 5px; } h3 { @@ -987,7 +987,7 @@ section.wiki { overflow: hidden; background: $pink; padding: lh(); - @include box-shadow(inset 0 0 0 1px lighten($pink, 10%)); + box-shadow: inset 0 0 0 1px lighten($pink, 10%); border: 1px solid darken($pink, 15%); p { @@ -1002,7 +1002,7 @@ section.wiki { color: #fff; font-weight: bold; font-size: em(18); - @include transition; + @include transition(none); text-align: center; -webkit-font-smoothing: antialiased; @@ -1046,13 +1046,13 @@ section.wiki { .modal-header { h1, p { - color: #fff; + color: #fff; } h1 { margin: 3px 12px 8px; font-size: 1.1em; - } + } p { font-size: 0.9em; @@ -1086,7 +1086,7 @@ section.wiki { @include button; font-size: 0.8em; } - + margin-right: 10px; } } diff --git a/lms/static/sass/ie.scss b/lms/static/sass/ie.scss index e03b711bae..87a8b07fe6 100644 --- a/lms/static/sass/ie.scss +++ b/lms/static/sass/ie.scss @@ -65,7 +65,7 @@ header.global { } -.home .university-partners .partners { +.home .university-partners .partners { width: 660px; li.partner { @@ -118,7 +118,7 @@ header.global { &:hover { background: rgb(245,245,245); border-color: rgb(170,170,170); - @include box-shadow(0 1px 16px 0 rgba($blue, 0.4)); + box-shadow: 0 1px 16px 0 rgba($blue, 0.4); .info { top: 0; diff --git a/lms/static/sass/multicourse/_about_pages.scss b/lms/static/sass/multicourse/_about_pages.scss index a72a77c89f..6c6bcfb01b 100644 --- a/lms/static/sass/multicourse/_about_pages.scss +++ b/lms/static/sass/multicourse/_about_pages.scss @@ -21,7 +21,7 @@ letter-spacing: 1px; margin: 0px 15px; padding: 20px 10px; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); text-transform: lowercase; &:hover, &.active { diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 5fcff57074..9de0a1fca8 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -62,7 +62,7 @@ // specific examples - buttons .button-primary { - @include border-radius(0); + border-radius: 0; @include linear-gradient(saturate($link-color-d1,15%) 5%, shade($link-color-d1,15%) 95%); display: inline-block; padding: $baseline/2 $baseline*2.5; @@ -139,7 +139,7 @@ } a { - @include transition(color 0.15s ease-in-out, border 0.15s ease-in-out); + @include transition(color 0.15s ease-in-out 0s, border 0.15s ease-in-out 0s); &:link, &:visited, &:hover, &:active { color: $link-color-d1; @@ -249,7 +249,7 @@ // elements label, input, textarea { - @include border-radius(0); + border-radius: 0; display: block; height: auto; font-family: $sans-serif; @@ -259,16 +259,16 @@ } label { - @include transition(color 0.15s ease-in-out); + @include transition(color 0.15s ease-in-out 0s); margin: 0 0 ($baseline/4) 0; color: tint($black, 20%); } .tip { - @include transition(color 0.15s ease-in-out); + @include transition(color 0.15s ease-in-out 0s); display: block; margin-top: ($baseline/4); - color: tint($outer-border-color, 50%); + color: $lighter-base-font-color; font-size: em(13); } @@ -447,7 +447,7 @@ } .submission-error, .system-error { - @include box-shadow(inset 0 -1px 2px 0 tint($red, 85%)); + box-shadow: inset 0 -1px 2px 0 tint($red, 85%); border-bottom: 3px solid shade($red, 10%); background: tint($red,95%); @@ -539,11 +539,11 @@ // modal password reset form #forgot-password-modal { - @include border-radius(2px); + border-radius: 2px; .inner-wrapper { - @include border-radius(2px); + border-radius: 2px; background: $body-bg; padding-bottom: 0 !important; } @@ -579,8 +579,8 @@ } form { - @include border-radius(0); - @include box-shadow(none); + border-radius: 0; + box-shadow: none; margin: 0; border: none; padding: 0; @@ -610,7 +610,7 @@ .modal-form-error { @extend .body-text; - @include box-shadow(inset 0 -1px 2px 0 tint($red, 85%)); + box-shadow: inset 0 -1px 2px 0 tint($red, 85%); @include box-sizing(border-box); margin: $baseline 0 ($baseline/2) 0 !important; padding: $baseline; diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss index 096ecd6db2..bceaaa280a 100644 --- a/lms/static/sass/multicourse/_course_about.scss +++ b/lms/static/sass/multicourse/_course_about.scss @@ -7,9 +7,9 @@ background: $course-profile-bg; @include background-image(url($homepage-bg-image)); background-size: cover; - @include box-shadow(0 1px 80px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 1px 80px 0 rgba(0,0,0, 0.5); border-bottom: 1px solid $border-color-3; - @include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.1)); + box-shadow: inset 0 1px 5px 0 rgba(0,0,0, 0.1); height: 280px; margin-top: $header_image_margin; padding-top: 150px; @@ -20,7 +20,7 @@ .intro-inner-wrapper { background: $course-header-bg; border: 1px solid $border-color-3; - @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 4px 25px 0 rgba(0,0,0, 0.5); @include box-sizing(border-box); @include clearfix; margin: 0 auto; @@ -45,7 +45,7 @@ > hgroup { border-bottom: 1px solid $border-color-2; - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); margin-bottom: 20px; padding-bottom: 20px; width: 100%; @@ -95,13 +95,13 @@ @include clearfix; float: left; margin-right: flex-gutter(); - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: flex-grid(12); > a.find-courses, a.register { @include button(shiny, $button-color); @include box-sizing(border-box); - @include border-radius(3px); + border-radius: 3px; display: block; font: normal 1.2rem/1.6rem $sans-serif; letter-spacing: 1px; @@ -124,7 +124,7 @@ strong { @include button(shiny, $button-color); @include box-sizing(border-box); - @include border-radius(3px); + border-radius: 3px; display: block; float: left; font: normal 1.2rem/1.6rem $sans-serif; @@ -151,7 +151,7 @@ text-align: center; float: left; margin: 1px flex-gutter(8) 0 0; - @include transition(); + @include transition(none); width: flex-grid(5, 8); } @@ -183,8 +183,8 @@ .play-intro { @include background-image(linear-gradient(-90deg, rgba(0,0,0, 0.65), rgba(0,0,0, 0.75))); - @include border-radius(4px); - @include box-shadow(0 1px 12px 0 rgba(0,0,0, 0.4)); + border-radius: 4px; + box-shadow: 0 1px 12px 0 rgba(0,0,0, 0.4); border: 2px solid rgba(255,255,255, 0.8); height: 80px; left: 50%; @@ -219,7 +219,7 @@ .play-intro { @include background-image(linear-gradient(-90deg, rgba(0,0,0, 0.75), rgba(0,0,0, 0.8))); - @include box-shadow(0 1px 12px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 1px 12px 0 rgba(0,0,0, 0.5); border-color: rgba(255,255,255, 0.9); &::after { @@ -350,7 +350,7 @@ width: flex-grid(4); > section { - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15)); + box-shadow: inset 0 0 3px 0 rgba(0,0,0, 0.15); border: 1px solid $border-color-2; &.course-summary { @@ -438,8 +438,8 @@ @include background-image(linear-gradient(-90deg, rgba(0,0,0, 0.9) 0%, rgba(0,0,0, 0.7) 100%)); border: 1px solid rgba(0,0,0, 0.5); - @include border-radius(4px); - @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5)); + border-radius: 4px; + box-shadow: 0 4px 25px 0 rgba(0,0,0, 0.5); @include box-sizing(border-box); color: rgb(255,255,255); float: right; @@ -452,7 +452,7 @@ padding: 6px 10px; position: absolute; text-align: center; - @include transition(all, 0.15s, ease-out); + @include transition(all 0.15s ease-out 0s); top: 65px; width: 220px; @@ -466,7 +466,7 @@ @include inline-block; margin-right: 10px; opacity: 0.5; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 44px; &:hover { @@ -530,7 +530,7 @@ height: 19px; margin: 2px 10px 0 0; opacity: 0.6; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 19px; &.start { diff --git a/lms/static/sass/multicourse/_courses.scss b/lms/static/sass/multicourse/_courses.scss index 83680c06a0..3b349db950 100644 --- a/lms/static/sass/multicourse/_courses.scss +++ b/lms/static/sass/multicourse/_courses.scss @@ -8,7 +8,7 @@ @include background-image(url($homepage-bg-image)); background-position: center top !important; border-bottom: 1px solid $border-color-3; - @include box-shadow(inset 0 -1px 8px 0 rgba(0,0,0, 0.2), inset 0 1px 12px 0 rgba(0,0,0, 0.3)); + box-shadow: inset 0 -1px 8px 0 rgba(0,0,0, 0.2), inset 0 1px 12px 0 rgba(0,0,0, 0.3); height: 430px; margin-top: $header_image_margin; width: 100%; @@ -27,7 +27,7 @@ background: #FFF; background: $course-header-bg; border: 1px solid $border-color-3; - @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 4px 25px 0 rgba(0,0,0, 0.5); padding: 20px 30px; position: relative; z-index: 2; diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index c0dac89199..cd58d4d8e4 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -5,7 +5,7 @@ .dashboard-banner { background: $yellow; border: 1px solid rgb(200,200,200); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); padding: 10px; margin-bottom: 30px; @@ -33,7 +33,7 @@ @include background-image($dashboard-profile-header-image); background-color: $dashboard-profile-header-color; border: 1px solid $border-color-2; - @include border-radius(4px); + border-radius: 4px; @include box-sizing(border-box); width: flex-grid(12); @@ -59,7 +59,7 @@ border-top: none; //@include border-bottom-radius(4px); @include box-sizing(border-box); - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15)); + box-shadow: inset 0 0 3px 0 rgba(0,0,0, 0.15); @include clearfix; margin: 0px; padding: 20px 10px 10px; @@ -93,7 +93,7 @@ height: 19px; margin: 0 6px 0 0; opacity: 0.6; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 19px; &.email-icon { @@ -131,7 +131,7 @@ margin: 30px 10px 0; border: 1px solid $border-color-2; background: $dashboard-profile-color; - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15)); + box-shadow: inset 0 0 3px 0 rgba(0,0,0, 0.15); * { font-family: $sans-serif; @@ -250,8 +250,8 @@ @include background-image($button-bg-image); background-color: $button-bg-color; border: 1px solid $border-color-2; - @include border-radius(4px); - @include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1)); + border-radius: 4px; + box-shadow: 0 1px 8px 0 rgba(0,0,0, 0.1); @include box-sizing(border-box); color: $base-font-color; font-family: $sans-serif; @@ -278,13 +278,13 @@ position: relative; width: flex-grid(12); z-index: 20; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); &:last-child { margin-bottom: none; } - .cover { + .cover { @include box-sizing(border-box); float: left; height: 100%; @@ -292,7 +292,7 @@ margin: 0px; overflow: hidden; position: relative; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 200px; height: 120px; @@ -346,7 +346,7 @@ .course-status { background: $yellow; border: 1px solid $border-color-2; - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); margin-top: 17px; margin-right: flex-gutter(); padding: 5px; @@ -378,7 +378,7 @@ .enter-course { @include button(simple, $button-color); @include box-sizing(border-box); - @include border-radius(3px); + border-radius: 3px; display: block; float: left; font: normal 15px/1.6rem $sans-serif; @@ -406,7 +406,7 @@ .message-status { @include clearfix; - @include border-radius(3px); + border-radius: 3px; display: none; z-index: 10; margin: 20px 0 10px; @@ -431,7 +431,7 @@ strong { font-weight: 700; - + a { font-weight: 700; } @@ -454,7 +454,7 @@ .btn { @include box-sizing(border-box); - @include border-radius(3px); + border-radius: 3px; float: left; font: normal 0.8rem/1.2rem $sans-serif; letter-spacing: 1px; @@ -561,7 +561,7 @@ float: right; display: block; font-style: italic; - color: #a0a0a0; + color: $lighter-base-font-color; text-decoration: underline; font-size: .8em; margin-top: 32px; diff --git a/lms/static/sass/multicourse/_edge.scss b/lms/static/sass/multicourse/_edge.scss index b0b7450940..30d5c0815a 100644 --- a/lms/static/sass/multicourse/_edge.scss +++ b/lms/static/sass/multicourse/_edge.scss @@ -13,8 +13,8 @@ $paleYellow: #fffcf1; font-weight: 700; text-transform: none; letter-spacing: 0; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0)); - @include transition(background-color .15s, box-shadow .15s); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0); + @include transition(background-color .15s linear 0s, box-shadow .15s linear 0s); &.disabled { border: 1px solid $lightGrey !important; @@ -29,7 +29,7 @@ $paleYellow: #fffcf1; } &:hover { - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15)); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15); text-decoration: none; } } @@ -54,7 +54,7 @@ $paleYellow: #fffcf1; border-radius: 3px; @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); background-color: #d1dae3; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; color: #6d788b; &:hover { @@ -63,7 +63,7 @@ $paleYellow: #fffcf1; } } -.edge-landing { +.edge-landing { border-top: 5px solid $blue; header { @@ -85,7 +85,7 @@ $paleYellow: #fffcf1; background: #fff; border: 1px solid $darkGrey; border-radius: 3px; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); @include clearfix; } @@ -257,4 +257,4 @@ $paleYellow: #fffcf1; color: #fff; } } -} \ No newline at end of file +} diff --git a/lms/static/sass/multicourse/_home.scss b/lms/static/sass/multicourse/_home.scss index 05285262f5..1d0c543fa4 100644 --- a/lms/static/sass/multicourse/_home.scss +++ b/lms/static/sass/multicourse/_home.scss @@ -11,7 +11,7 @@ @include background-image(url($homepage-bg-image)); background-size: cover; border-bottom: 1px solid $border-color-3; - @include box-shadow(0 1px 0 0 $course-header-bg, inset 0 -1px 5px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 1px 0 0 $course-header-bg, inset 0 -1px 5px 0 rgba(0,0,0, 0.1); @include clearfix; height: 460px; overflow: hidden; @@ -33,14 +33,14 @@ background: #FFF; background: $course-header-bg; border: 1px solid $border-color-3; - @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 4px 25px 0 rgba(0,0,0, 0.5); @include box-sizing(border-box); min-height: 120px; margin-left: grid-width(2) + $gw-gutter; width: flex-grid(6); float: left; position: relative; - @include transition(all, 0.2s, linear); + @include transition(all 0.2s linear 0s); vertical-align: top; &:hover { @@ -56,7 +56,7 @@ opacity: 1.0; padding: 20px 30px; top: 0px; - @include transition(all, 0.2s, linear); + @include transition(all 0.2s linear 0s); text-align: left; h1 { @@ -84,7 +84,7 @@ border: 1px solid $border-color-3; border-left: 0; @include box-sizing(border-box); - // @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5)); + // box-shadow: 0 4px 25px 0 rgba(0,0,0, 0.5); height: 120px; float: left; padding: 4px; @@ -102,12 +102,12 @@ overflow: hidden; position: relative; background: url($video-thumb-url) center no-repeat; - @include background-size(cover); + background-size: cover; .play-intro { @include background-image(linear-gradient(-90deg, rgba(0,0,0, 0.65), rgba(0,0,0, 0.75))); - @include border-radius(4px); - @include box-shadow(0 1px 12px 0 rgba(0,0,0, 0.4)); + border-radius: 4px; + box-shadow: 0 1px 12px 0 rgba(0,0,0, 0.4); @include box-sizing(border-box); border: 2px solid rgba(255,255,255, 0.8); height: 60px; @@ -116,7 +116,7 @@ margin-left: -30px; position: absolute; top: 50%; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 60px; &::after { @@ -143,7 +143,7 @@ &:hover { .play-intro { @include background-image(linear-gradient(-90deg, rgba(0,0,0, 0.75), rgba(0,0,0, 0.8))); - @include box-shadow(0 1px 12px 0 rgba(0,0,0, 0.5)); + box-shadow: 0 1px 12px 0 rgba(0,0,0, 0.5); border-color: rgba(255,255,255, 0.9); &::after { @@ -165,9 +165,9 @@ > h2 { @include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230))); border: 1px solid $border-color-2; - @include border-radius(4px); + border-radius: 4px; border-top-color: $border-color-1; - @include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.4), 0 0px 12px 0 rgba(0,0,0, 0.2)); + box-shadow: inset 0 0 0 1px rgba(255,255,255, 0.4), 0 0px 12px 0 rgba(0,0,0, 0.2); color: $lighter-base-font-color; letter-spacing: 1px; margin-bottom: 0px; @@ -262,7 +262,7 @@ } a { - @include transition(all, 0.25s, ease-in-out); + @include transition(all 0.25s ease-in-out 0s); &::before { @include background-image(radial-gradient(50% 50%, circle closest-side, rgba(255,255,255, 1) 0%, rgba(255,255,255, 0) 100%)); @@ -275,7 +275,7 @@ opacity: 0; width: 200px; position: absolute; - @include transition(all, 0.25s, ease-in-out); + @include transition(all 0.25s ease-in-out 0s); top: 50%; z-index: 1; } @@ -285,7 +285,7 @@ left: 0px; position: absolute; text-align: center; - @include transition(all, 0.25s, ease-in-out); + @include transition(all 0.25s ease-in-out 0s); width: 100%; z-index: 2; @@ -293,7 +293,7 @@ color: $base-font-color; font: 800 italic 1.4em/1.4em $sans-serif; text-shadow: 0 1px rgba(255,255,255, 0.6); - @include transition(all, 0.15s, ease-in-out); + @include transition(all 0.15s ease-in-out 0s); &:hover { color: $lighter-base-font-color; @@ -303,7 +303,7 @@ img { position: relative; - @include transition(all, 0.25s, ease-in-out); + @include transition(all 0.25s ease-in-out 0s); vertical-align: middle; z-index: 2; } @@ -410,7 +410,7 @@ .news { @include box-sizing(border-box); - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15)); + box-shadow: inset 0 0 3px 0 rgba(0,0,0, 0.15); padding: 20px; width: flex-grid(12); @@ -428,13 +428,13 @@ float: left; margin-right: flex-gutter(); padding: 10px; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: flex-grid(4); &:hover { background: $body-bg; border: 1px solid $border-color-2; - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.1)); + box-shadow: inset 0 0 3px 0 rgba(0,0,0, 0.1); } &:last-child { diff --git a/lms/static/sass/multicourse/_media-kit.scss b/lms/static/sass/multicourse/_media-kit.scss index db73029fd3..ef6abc3696 100644 --- a/lms/static/sass/multicourse/_media-kit.scss +++ b/lms/static/sass/multicourse/_media-kit.scss @@ -9,9 +9,9 @@ $white: rgb(255,255,255); width: 980px; .wrapper-mediakit { - @include border-radius(4px); + border-radius: 4px; @include box-sizing(border-box); - @include box-shadow(0 1px 10px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 1px 10px 0 rgba(0,0,0, 0.1); margin: ($baseline*3) 0 0 0; border: 1px solid $border-color; padding: ($baseline*2) ($baseline*3); @@ -24,7 +24,7 @@ $white: rgb(255,255,255); } header { - + } } } @@ -58,7 +58,7 @@ $white: rgb(255,255,255); color: $blue; font-family: $sans-serif; text-decoration: none; - @include transition(all, 0.1s, linear); + @include transition(all 0.1s linear 0s); .note { position: relative; @@ -66,7 +66,7 @@ $white: rgb(255,255,255); font-family: $sans-serif; font-size: 13px; text-decoration: none; - @include transition(all, 0.1s, linear); + @include transition(all 0.1s linear 0s); &:before { position: relative; @@ -113,9 +113,9 @@ $white: rgb(255,255,255); } aside { - @include border-radius(2px); + border-radius: 2px; @include box-sizing(border-box); - @include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.2)); + box-shadow: 0 1px 4px 0 rgba(0,0,0, 0.2); width: 330px; float: left; border: 3px solid tint(rgb(96, 155, 216), 35%); @@ -142,7 +142,7 @@ $white: rgb(255,255,255); .note { width: 100%; display: inline-block; - text-align: center; + text-align: center; } } @@ -171,9 +171,9 @@ $white: rgb(255,255,255); // library section .library { - @include border-radius(2px); + border-radius: 2px; @include box-sizing(border-box); - @include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.2)); + box-shadow: 0 1px 4px 0 rgba(0,0,0, 0.2); border: 3px solid tint($light-gray,50%); padding: 0; background: tint($light-gray,50%); @@ -220,9 +220,9 @@ $white: rgb(255,255,255); figure { a { - @include border-radius(2px); + border-radius: 2px; @include box-sizing(border-box); - @include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 1px 2px 0 rgba(0,0,0, 0.1); display: block; min-height: 380px; border: 2px solid tint($light-gray,75%); @@ -257,4 +257,4 @@ $white: rgb(255,255,255); .share { } -} \ No newline at end of file +} diff --git a/lms/static/sass/multicourse/_password_reset.scss b/lms/static/sass/multicourse/_password_reset.scss index 9f145351d1..0e3ea15573 100644 --- a/lms/static/sass/multicourse/_password_reset.scss +++ b/lms/static/sass/multicourse/_password_reset.scss @@ -1,9 +1,9 @@ .password-reset { background: rgb(245,245,245); border: 1px solid rgb(200,200,200); - @include border-radius(4px); + border-radius: 4px; @include box-sizing(border-box); - @include box-shadow(0 5px 50px 0 rgba(0,0,0, 0.3)); + box-shadow: 0 5px 50px 0 rgba(0,0,0, 0.3); margin: 120px auto 0; padding: 0px 40px 40px; width: flex-grid(5); diff --git a/lms/static/sass/multicourse/_press_release.scss b/lms/static/sass/multicourse/_press_release.scss index 7ee362617d..6efa4d65c3 100644 --- a/lms/static/sass/multicourse/_press_release.scss +++ b/lms/static/sass/multicourse/_press_release.scss @@ -35,9 +35,9 @@ > article { border: 1px solid rgb(220,220,220); - @include border-radius(10px); + border-radius: 10px; @include box-sizing(border-box); - @include box-shadow(0 2px 16px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 2px 16px 0 rgba(0,0,0, 0.1); margin: 0 auto; padding: 80px 80px 40px 80px; width: flex-grid(10); diff --git a/lms/static/sass/multicourse/_testcenter-register.scss b/lms/static/sass/multicourse/_testcenter-register.scss index 01405d7fc1..754b1428de 100644 --- a/lms/static/sass/multicourse/_testcenter-register.scss +++ b/lms/static/sass/multicourse/_testcenter-register.scss @@ -39,7 +39,7 @@ .introduction { header { - + h2 { margin: 0; font-family: $sans-serif; @@ -51,7 +51,7 @@ font-family: $sans-serif; font-size: 34px; text-align: left; - } + } } } @@ -63,13 +63,13 @@ // form .form-fields-primary, .form-fields-secondary { border-bottom: 1px solid rgba(0,0,0,0.25); - @include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 1px 2px 0 rgba(0,0,0, 0.1); } form { border: 1px solid rgb(216, 223, 230); - @include border-radius(3px); - @include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.2)); + border-radius: 3px; + box-shadow: 0 1px 2px 0 rgba(0,0,0, 0.2); .instructions, .note { margin: 0; @@ -100,7 +100,7 @@ display: block; @include button(simple, $blue); @include box-sizing(border-box); - @include border-radius(3px); + border-radius: 3px; font: bold 15px/1.6rem $sans-serif; letter-spacing: 0; padding: ($baseline*0.75) $baseline; @@ -181,7 +181,7 @@ } .value { - @include border-radius(3px); + border-radius: 3px; border: 1px solid #C8C8C8; padding: $baseline ($baseline*0.75); background: #FAFAFA; @@ -219,7 +219,7 @@ label { margin: 0 0 ($baseline/4) 0; - @include transition(color, 0.15s, ease-in-out); + @include transition(color 0.15s ease-in-out 0s); &.is-focused { color: $blue; @@ -229,7 +229,7 @@ input, textarea { height: 100%; width: 100%; - padding: ($baseline/2); + padding: ($baseline/2); &.long { width: 100%; @@ -336,7 +336,7 @@ padding-left: $baseline; .message-status { - @include border-radius(3px); + border-radius: 3px; margin: 0 0 ($baseline*2) 0; border: 1px solid #ccc; padding: 0; @@ -406,13 +406,13 @@ } .details, .item, .instructions { - @include transition(opacity, 0.10s, ease-in-out); + @include transition(opacity 0.10s ease-in-out 0s); font-size: 13px; opacity: 0.65; } &:before { - @include border-radius($baseline); + border-radius: $baseline; position: relative; top: 3px; display: block; @@ -512,7 +512,7 @@ } .actions { - @include box-shadow(inset 0 1px 1px 0px rgba(0,0,0,0.2)); + box-shadow: inset 0 1px 1px 0px rgba(0,0,0,0.2); border-top: 1px solid tint(rgb(0,0,0), 90%); padding-top: ($baseline*0.75); background: tint($yellow,70%); @@ -523,7 +523,7 @@ } .label, .value { - display: inline-block; + display: inline-block; } .label { @@ -559,7 +559,7 @@ letter-spacing: 0; } - + } .registration-processed { @@ -627,7 +627,7 @@ // status messages .message { - @include border-radius(3px); + border-radius: 3px; display: none; margin: $baseline 0; padding: ($baseline/2) $baseline; @@ -642,7 +642,7 @@ // registration status &.message-flash { - @include border-radius(3px); + border-radius: 3px; position: relative; margin: 0 0 ($baseline*2) 0; border: 1px solid #ccc; @@ -787,4 +787,4 @@ .is-hidden { display: none; } -} \ No newline at end of file +} diff --git a/lms/static/sass/shared/_activation_messages.scss b/lms/static/sass/shared/_activation_messages.scss index bfcc1d278c..c9f61f12fb 100644 --- a/lms/static/sass/shared/_activation_messages.scss +++ b/lms/static/sass/shared/_activation_messages.scss @@ -21,8 +21,8 @@ .message { background: rgb(252,252,252); border: 1px solid rgb(200,200,200); - @include box-shadow(0 3px 20px 0 rgba(0,0,0, 0.2)); - @include border-radius(4px); + box-shadow: 0 3px 20px 0 rgba(0,0,0, 0.2); + border-radius: 4px; margin: 0 auto; padding: 40px; width: flex-grid(6); diff --git a/lms/static/sass/shared/_course_filter.scss b/lms/static/sass/shared/_course_filter.scss index ff283b1e86..a8bf7f9fdc 100644 --- a/lms/static/sass/shared/_course_filter.scss +++ b/lms/static/sass/shared/_course_filter.scss @@ -3,7 +3,7 @@ nav { @include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230))); - @include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.4), inset 0 0 0 -1px rgba(255,255,255, 0.4)); + box-shadow: inset 0 0 0 1px rgba(255,255,255, 0.4), inset 0 0 0 -1px rgba(255,255,255, 0.4); @include box-sizing(border-box); border: 1px solid rgb(190,190,190); border-bottom-color: rgb(200,200,200); @@ -16,7 +16,7 @@ z-index: 9; &.fixed-top { - @include box-shadow(0 1px 15px 0 rgba(0,0,0, 0.2), inset 0 0 0 1px rgba(255,255,255, 0.4)); + box-shadow: 0 1px 15px 0 rgba(0,0,0, 0.2), inset 0 0 0 1px rgba(255,255,255, 0.4); max-width: 1200px; position: fixed; top: 0px; @@ -30,9 +30,9 @@ .filter-heading { @include background-image(linear-gradient(-90deg, rgb(250,250,250) 0%, rgb(245,245,245) 50%, rgb(235,235,235) 50%, rgb(230,230,230) 100%)); - @include border-radius(4px); + border-radius: 4px; @include box-sizing(border-box); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.4), inset 0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.4), inset 0 1px 0 0 rgba(255,255,255, 0.6); border: 1px solid rgb(200,200,200); color: $base-font-color; cursor: pointer; @@ -47,9 +47,9 @@ ul { background: rgb(255,255,255); - @include border-radius(0px 4px 4px 4px); + border-radius: 0px 4px 4px 4px; border: 1px solid rgb(200,200,200); - @include box-shadow(0 2px 15px 0 rgba(0,0,0, 0.2)); + box-shadow: 0 2px 15px 0 rgba(0,0,0, 0.2); padding: 20px 0px 5px 20px; position: absolute; visibility: hidden; @@ -66,9 +66,9 @@ .filter-heading { background: rgb(255,255,255); @include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(255,255,255))); - @include border-radius(4px 4px 0px 0px); + border-radius: 4px 4px 0px 0px; border-bottom: 1px dotted rgb(200,200,200); - @include box-shadow(0 2px 0 -1px rgb(255,255,255)); + box-shadow: 0 2px 0 -1px rgb(255,255,255); color: $base-font-color; height: 40px; } @@ -83,14 +83,14 @@ float: right; input[type="text"] { - @include border-radius(3px 0px 0px 3px); + border-radius: 3px 0px 0px 3px; float: left; height: 36px; width: 200px; } input[type="submit"] { - @include border-radius(0px 3px 3px 0px); + border-radius: 0px 3px 3px 0px; float: left; height: 36px; padding: 2px 20px; diff --git a/lms/static/sass/shared/_course_object.scss b/lms/static/sass/shared/_course_object.scss index bd4a8dc049..3321afd92d 100644 --- a/lms/static/sass/shared/_course_object.scss +++ b/lms/static/sass/shared/_course_object.scss @@ -33,13 +33,13 @@ .course { background: $body-bg; border: 1px solid $border-color-1; - @include border-radius(2px); + border-radius: 2px; @include box-sizing(border-box); - @include box-shadow(0 1px 10px 0 rgba(0,0,0, 0.15), inset 0 0 0 1px rgba(255,255,255, 0.9)); + box-shadow: 0 1px 10px 0 rgba(0,0,0, 0.15), inset 0 0 0 1px rgba(255,255,255, 0.9); margin-bottom: 30px; position: relative; width: 100%; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); .status { background: $link-color; @@ -47,7 +47,7 @@ font-size: 10px; left: 10px; padding: 2px 10px; - @include border-radius(2px); + border-radius: 2px; position: absolute; text-transform: uppercase; top: -6px; @@ -66,20 +66,25 @@ width: 0; } + a { + position: relative; + display: block; + } + a:hover { text-decoration: none; } .meta-info { - background: rgba(0,0,0, 0.6); + background: $dark-trans-bg; bottom: 6px; border: 1px solid rgba(0,0,0, 0.5); @include border-right-radius(2px); - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.15)); + box-shadow: 0 1px 5px 0 rgba(0,0,0, 0.15); @include clearfix; position: absolute; right: -4px; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); p { color: rgb(255,255,255); @@ -106,7 +111,7 @@ // > a { @include background-image(linear-gradient(-90deg, rgba(255,255,255, 1), rgba(255,255,255, 0.85))); - @include box-shadow(inset 0 -1px 0 0 rgba(255,255,255, 0.2)); + box-shadow: inset 0 -1px 0 0 rgba(255,255,255, 0.2); border-bottom: 1px solid rgba(150,150,150, 0.7); display: block; height: 50px; @@ -181,7 +186,7 @@ left: 0px; position: absolute; top: 0px; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 100%; overflow: hidden; @@ -242,7 +247,7 @@ &:hover { background: $course-profile-bg; border-color: $border-color-1; - @include box-shadow(0 1px 16px 0 rgba($shadow-color, 0.4)); + box-shadow: 0 1px 16px 0 rgba($shadow-color, 0.4); .info { top: -150px; diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index 3c89c54faf..888f19ac88 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -1,5 +1,5 @@ .wrapper-footer { - @include box-shadow(0 -1px 5px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 -1px 5px 0 rgba(0,0,0, 0.1); border-top: 1px solid tint($m-gray,50%); padding: 25px ($baseline/2) ($baseline*1.5) ($baseline/2); background: $footer-bg; @@ -16,7 +16,7 @@ } a { - @include transition(link-color 0.15s ease-in-out, border 0.15s ease-in-out); + @include transition(link-color 0.15s ease-in-out 0s, border 0.15s ease-in-out 0s); &:link, &:visited, &:hover, &:active { border-bottom: none; diff --git a/lms/static/sass/shared/_forms.scss b/lms/static/sass/shared/_forms.scss index 3350081850..6a011b22ef 100644 --- a/lms/static/sass/shared/_forms.scss +++ b/lms/static/sass/shared/_forms.scss @@ -17,8 +17,8 @@ input[type="password"], input[type="tel"] { background: $form-bg-color; border: 1px solid $border-color-2; - @include border-radius(3px); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1)); + border-radius: 3px; + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1); @include box-sizing(border-box); font: italic 300 1rem/1.6rem $serif; height: 35px; @@ -32,7 +32,7 @@ input[type="tel"] { &:focus { border-color: darken($button-archive-color, 50%); - @include box-shadow(0 0 6px 0 darken($button-archive-color, 50%), inset 0 0 4px 0 rgba(0,0,0, 0.15)); + box-shadow: 0 0 6px 0 darken($button-archive-color, 50%), inset 0 0 4px 0 rgba(0,0,0, 0.15); outline: none; } } @@ -45,7 +45,7 @@ input[type="submit"], input[type="button"], button, .button { - @include border-radius(3px); + border-radius: 3px; @include button(shiny, $button-color); font: normal 1.2rem/1.6rem $sans-serif; letter-spacing: 1px; diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 0e161b6327..7da89ccc1c 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -1,6 +1,6 @@ header.global { border-bottom: 1px solid $m-gray; - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); + box-shadow: 0 1px 5px 0 rgba(0,0,0, 0.1); background: $header-bg; height: 76px; position: relative; @@ -79,9 +79,9 @@ header.global { @include background-image($button-bg-image); background-color: $button-bg-color; border: 1px solid $border-color-2; - @include border-radius(3px); + border-radius: 3px; @include box-sizing(border-box); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); color: $base-font-color; display: inline-block; font-family: $sans-serif; @@ -120,7 +120,7 @@ header.global { &:last-child { > a { - @include border-radius(0 4px 4px 0); + border-radius: 0 4px 4px 0; border-left: none; padding: 5px 8px 7px 8px; } @@ -145,7 +145,7 @@ header.global { overflow: hidden; position: absolute; top: 4px; - @include transition(all, 0.15s, linear); + @include transition(all 0.15s linear 0s); width: 26px; } @@ -158,8 +158,8 @@ header.global { ul.dropdown-menu { background: $border-color-4; - @include border-radius(4px); - @include box-shadow(0 2px 24px 0 rgba(0,0,0, 0.3)); + border-radius: 4px; + box-shadow: 0 2px 24px 0 rgba(0,0,0, 0.3); border: 1px solid $border-color-3; display: none; padding: 5px 10px; @@ -181,7 +181,7 @@ header.global { bottom: 6px solid transparent; left: 6px solid transparent; } - @include box-shadow(1px 0 0 0 $border-color-3, 0 -1px 0 0 $border-color-3); + box-shadow: 1px 0 0 0 $border-color-3, 0 -1px 0 0 $border-color-3; content: ""; display: block; height: 0px; @@ -195,16 +195,16 @@ header.global { li { display: block; border-top: 1px dotted $border-color-2; - @include box-shadow(inset 0 1px 0 0 rgba(255,255,255, 0.05)); + box-shadow: inset 0 1px 0 0 rgba(255,255,255, 0.05); &:first-child { border: none; - @include box-shadow(none); + box-shadow: none; } > a { border: 1px solid transparent; - @include border-radius(3px); + border-radius: 3px; @include box-sizing(border-box); color: $link-color; cursor: pointer; @@ -213,7 +213,7 @@ header.global { overflow: hidden; padding: 3px 5px 4px; text-overflow: ellipsis; - @include transition(padding, 0.15s, linear); + @include transition(padding 0.15s linear 0s); white-space: nowrap; width: 100%; @@ -279,7 +279,7 @@ header.global { display: inline-block; a { - @include border-radius(0); + border-radius: 0; @include linear-gradient(saturate($link-color-d1,15%) 5%, shade($link-color-d1,15%) 95%); display: inline-block; padding: $baseline/2 $baseline*2.5; diff --git a/lms/static/sass/shared/_modal.scss b/lms/static/sass/shared/_modal.scss index 9777c582da..f3233b6f00 100644 --- a/lms/static/sass/shared/_modal.scss +++ b/lms/static/sass/shared/_modal.scss @@ -13,8 +13,8 @@ .modal { background: rgba(0,0,0, 0.6); border: 1px solid rgba(0, 0, 0, 0.9); - @include border-radius(0px); - @include box-shadow(0 15px 80px 15px rgba(0,0,0, 0.5)); + border-radius: 0px; + box-shadow: 0 15px 80px 15px rgba(0,0,0, 0.5); color: #fff; display: none; left: 50%; @@ -30,7 +30,7 @@ .inner-wrapper { background: #000; - @include box-shadow(none); + box-shadow: none; height: 315px; padding: 10px; width: 560px; @@ -44,7 +44,7 @@ .inner-wrapper { background: #000; - @include box-shadow(none); + box-shadow: none; height: 360px; padding: 10px; width: 640px; @@ -53,9 +53,9 @@ .inner-wrapper { background: $modal-bg-color; - @include border-radius(0px); + border-radius: 0px; border: 1px solid rgba(0, 0, 0, 0.9); - @include box-shadow(inset 0 1px 0 0 rgba(255, 255, 255, 0.7)); + box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.7); overflow: hidden; padding-left: 10px; padding-right: 10px; @@ -143,7 +143,7 @@ .input-group { @include clearfix; border-bottom: 1px solid rgb(210,210,210); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); margin-bottom: 30px; padding-bottom: 10px; } @@ -189,8 +189,8 @@ label.honor-code { background: rgb(233,233,233); border: 1px solid rgb(200,200,200); - @include border-radius(3px); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + border-radius: 3px; + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); display: block; margin-bottom: 20px; padding: 8px 10px; @@ -274,7 +274,7 @@ } .close-modal { - @include border-radius(2px); + border-radius: 2px; cursor: pointer; @include inline-block; padding: 10px; @@ -289,7 +289,7 @@ font: normal 1.2rem/1.2rem $sans-serif; text-align: center; text-shadow: 0 1px rgba(255,255,255, 0.8); - @include transition(all, 0.15s, ease-out); + @include transition(all 0.15s ease-out 0s); } } diff --git a/lms/templates/course.html b/lms/templates/course.html index e8c7cd5875..e3dd9baf43 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -19,7 +19,7 @@
    - + ${course.number} ${get_course_about_section(course, 'title')} Cover Image

    ${get_course_about_section(course, 'short_description')}

    diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 7f0d596f4b..f731f0d989 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -10,9 +10,9 @@
    % if self.stanford_theme_enabled(): diff --git a/lms/templates/courseware/hint_manager.html b/lms/templates/courseware/hint_manager.html new file mode 100644 index 0000000000..ebd7091a09 --- /dev/null +++ b/lms/templates/courseware/hint_manager.html @@ -0,0 +1,124 @@ +<%inherit file="/main.html" /> +<%namespace name='static' file='/static_content.html'/> +<%namespace name="content" file="/courseware/hint_manager_inner.html"/> + + +<%block name="headextra"> + <%static:css group='course'/> + + + + + + + + + + +
    +
    + +
    + ${content.main()} +
    + +
    +
    diff --git a/lms/templates/courseware/hint_manager_inner.html b/lms/templates/courseware/hint_manager_inner.html new file mode 100644 index 0000000000..c69539522f --- /dev/null +++ b/lms/templates/courseware/hint_manager_inner.html @@ -0,0 +1,45 @@ +<%block name="main"> + + +

    ${field_label}

    +Switch to ${other_field_label} + + +% for definition_id in all_hints: +

    Problem: ${id_to_name[definition_id]}

    + % for answer, hint_dict in all_hints[definition_id]: + % if len(hint_dict) > 0: +

    Answer: ${answer}

    + % endif + % for pk, hint in hint_dict.items(): +

    + ${hint[0]} +
    + Votes: +

    +

    + % endfor + % if len(hint_dict) > 0: +

    + % endif + % endfor + +

    Add a hint to this problem

    +

    Answer:

    + + (Be sure to format your answer in the same way as the other answers you see here.) +
    + Hint:
    + +
    + +
    +% endfor + + + +% if field == 'mod_queue': + +% endif + + \ No newline at end of file diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index bc49cda427..2994dc16be 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -10,7 +10,7 @@ %if instructor_tasks is not None: - > + %endif @@ -104,7 +104,7 @@ function goto( mode)

    Instructor Dashboard

    -

    [ Grades | +

    -