bringing up to master

This commit is contained in:
Juho Kim
2013-06-21 17:39:38 -04:00
756 changed files with 73574 additions and 9502 deletions

13
.gitignore vendored
View File

@@ -4,11 +4,14 @@
*.swp
*.orig
*.DS_Store
*.mo
:2e_*
:2e#
.AppleDouble
database.sqlite
private-requirements.txt
requirements/private.txt
lms/envs/private.py
cms/envs/private.py
courseware/static/js/mathjax/*
flushdb.sh
build
@@ -22,7 +25,11 @@ reports/
*.egg-info
Gemfile.lock
.env/
conf/locale/en/LC_MESSAGES/*.po
!messages.po
lms/static/sass/*.css
lms/static/sass/application.scss
lms/static/sass/course.scss
cms/static/sass/*.css
lms/lib/comment_client/python
nosetests.xml
@@ -33,3 +40,7 @@ chromedriver.log
/nbproject
ghostdriver.log
node_modules
.pip_download_cache/
.prereqs_cache
autodeploy.properties
.ws_migrations_complete

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "common/test/phantom-jasmine"]
path = common/test/phantom-jasmine
url = https://github.com/jcarver989/phantom-jasmine.git

2
.reviewboardrc Normal file
View File

@@ -0,0 +1,2 @@
REVIEWBOARD_URL = "https://rbcommons.com/s/edx/"
GUESS_FIELDS = True

View File

@@ -1 +1 @@
mitx
edx-platform

26
.tx/config Normal file
View File

@@ -0,0 +1,26 @@
[main]
host = https://www.transifex.com
[edx-studio.django-partial]
file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po
source_file = conf/locale/en/LC_MESSAGES/django-partial.po
source_lang = en
type = PO
[edx-studio.djangojs]
file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = conf/locale/en/LC_MESSAGES/djangojs.po
source_lang = en
type = PO
[edx-studio.mako]
file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po
source_file = conf/locale/en/LC_MESSAGES/mako.po
source_lang = en
type = PO
[edx-studio.messages]
file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po
source_file = conf/locale/en/LC_MESSAGES/messages.po
source_lang = en
type = PO

77
AUTHORS Normal file
View File

@@ -0,0 +1,77 @@
Piotr Mitros <pmitros@edx.org>
Kyle Fiedler <kyle@kylefiedler.com>
Ernie Park <eipark@mit.edu>
Bridger Maxwell <bridger@mit.edu>
Lyla Fischer <lyla@edx.org>
David Ormsbee <dave@edx.org>
Chris Terman <cjt@edx.org>
Reda Lemeden <reda@thoughtbot.com>
Anant Agarwal <agarwal@edx.org>
Jean-Michel Claus <jmc@edx.org>
Calen Pennington <calen.pennington@gmail.com>
JM Van Thong <jm@edx.org>
Prem Sichanugrist <psichanugrist@thoughtbot.com>
Isaac Chuang <ichuang@mit.edu>
Galen Frechette <galen@thoughtbot.com>
Edward Loveall <edward@edwardloveall.com>
Matt Jankowski <mjankowski@thoughtbot.com>
John Jarvis <jarv@edx.org>
Victor Shnayder <victor@edx.org>
Matthew Mongeau <halogenandtoast@gmail.com>
Tony Kim <kimth@edx.org>
Arjun Singh <arjun810@gmail.com>
John Hess <mgojohn@gmail.com>
Carlos Andrés Rocha <rocha@edx.org>
Mike Chen <ccp0101@gmail.com>
Rocky Duan <dementrock@gmail.com>
Sidhanth Rao <sidhanth@mitx.mit.edu>
Brittany Cheng <bcheng42@gmail.com>
Dhaval Adjodah <dhaval@mit.edu>
Tom Giannattasio <tom@mitx.mit.edu>
Ibrahim Awwal <ibrahim.awwal@gmail.com>
Sarina Canelake <sarina@edx.org>
Mark L. Chang <mark.chang@gmail.com>
Dean Dieker <ddieker@gmail.com>
Tommy MacWilliam <tmacwilliam@cs.harvard.edu>
Nate Hardison <natehardison@gmail.com>
Chris Dodge <cdodge@edx.org>
Kevin Chugh <kevinchugh@edx.org>
Ned Batchelder <ned@nedbatchelder.com>
Alexander Kryklia <kryklia@gmail.com>
Vik Paruchuri <vik@edx.org>
Louis Sobel <sobel@edx.org>
Brian Wilson <brian@edx.org>
Ashley Penney <apenney@edx.org>
Don Mitchell <dmitchell@edx.org>
Aaron Culich <aculich@edx.org>
Brian Talbot <btalbot@edx.org>
Jay Zoldak <jzoldak@edx.org>
Valera Rozuvan <valera.rozuvan@gmail.com>
Diana Huang <dkh@edx.org>
Marco Morales <marcotuts@gmail.com>
Christina Roberts <christina@edx.org>
Robert Chirwa <robert@edx.org>
Ed Zarecor <ed@edx.org>
Deena Wang <thedeenawang@gmail.com>
Jean Manuel-Nater <jnater@edx.org>
Emily Zhang <1800.ehz.hang@gmail.com>
Jennifer Akana <jaakana@gmail.com>
Peter Baratta <peter.baratta@gmail.com>
Julian Arni <julian@edx.org>
Arthur Barrett <abarrett@edx.org>
Vasyl Nakvasiuk <vaxxxa@gmail.com>
Will Daly <will@edx.org>
James Tauber <jtauber@jtauber.com>
Greg Price <gprice@edx.org>
Joe Blaylock <jrbl@stanford.edu>
Sef Kloninger <sef@kloninger.com>
Anto Stupak <s2pak.anton@gmail.com>
David Adams <dcadams@stanford.edu>
Steve Strassmann <straz@edx.org>
Giulio Gratta <giulio@giuliogratta.com>
David Baumgold <david@davidbaumgold.com>
Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com>

View File

@@ -1,19 +1,18 @@
This is edX, a platform for online course delivery. The project is primarily
written in [Python](http://python.org/), using the
[Django](https://www.djangoproject.com/) framework. We also use some
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
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
============
The installation process is a bit messy at the moment. Here's a high-level
overview of what you should do to get started.
**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
that you understand what the script is doing, and why, by reading this document.
There is a `scripts/create-dev-env.sh` that will attempt to set up a development
environment.
If you want to better understand what the script is doing, keep reading.
Directory Hierarchy
-------------------
This code assumes that it is checked out in a directory that has three sibling
directories: `data` (used for XML course data), `db` (used to hold a
[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
@@ -77,11 +76,17 @@ environment), and Node has a library installer called
Once you've got your languages and virtual environments set up, install
the libraries like so:
$ pip install -r pre-requirements.txt
$ pip install -r requirements.txt
$ pip install -r requirements/edx/pre.txt
$ pip install -r requirements/edx/base.txt
$ pip install -r requirements/edx/post.txt
$ bundle install
$ npm install
You can also use [`rake`](http://rake.rubyforge.org/) to get all of the prerequisites (or to update)
them if they've changed
$ rake install_prereqs
Other Dependencies
------------------
You'll also need to install [MongoDB](http://www.mongodb.org/), since our
@@ -106,7 +111,7 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb]
$ rake django-admin[migrate]
$ rake django-admin[update_templates]
$ rake cms:update_templates
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing
@@ -137,12 +142,30 @@ Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
There's also an older version of the LMS that saves its information in XML files
in the `data` directory, instead of in Mongo. To run this older version, run:
$ rake lms
$ rake lms
Further Documentation
=====================
Once you've got your project up and running, you can check out the `docs`
directory to see more documentation about how edX is structured.
License
-------
The code in this repository is licensed under version 3 of the AGPL unless
otherwise noted.
Please see ``LICENSE.txt`` for details.
How to Contribute
-----------------
Contributions are very welcome. The easiest way is to fork this repo, and then
make a pull request from your fork. The first time you make a pull request, you
may be asked to sign a Contributor Agreement.
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@edx.org
Mailing List and IRC Channel
----------------------------
You can discuss this code on the [edx-code Google Group](https://groups.google.com/forum/#!forum/edx-code) or in the
`edx-code` IRC channel on Freenode.

View File

@@ -11,7 +11,6 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
@skip-phantom
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
@@ -20,7 +19,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
@@ -28,7 +26,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
@skip-phantom
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
@@ -36,7 +33,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
@skip-phantom
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes

View File

@@ -19,9 +19,7 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
world.click_course_settings()
link_css = 'li.nav-course-settings-advanced a'
world.css_click(link_css)
@@ -44,8 +42,9 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
g._element.send_keys(Keys.ARROW_LEFT, ' ', 'X')
@step(u'I edit the value of a policy key and save$')
@@ -125,10 +124,12 @@ def get_display_name_value():
def change_display_name_value(step, new_value):
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")

View File

@@ -10,8 +10,6 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
@@ -19,8 +17,6 @@ Feature: Course checklists
And I press the browser back button
Then I am brought back to the course outline in the correct state
@skip-phantom
@skip-firefox
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page

View File

@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_equal
from nose.tools import assert_true, assert_equal, assert_in
from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
@@ -10,9 +10,7 @@ from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
world.click_tools()
link_css = 'li.nav-course-tools-checklists a'
world.css_click(link_css)
@@ -63,7 +61,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_in('Course Outline', world.css_find('.outline .page-header')[0].text)
assert_equal(1, len(world.browser.windows))

View File

@@ -5,8 +5,6 @@ from lettuce import world, step
from nose.tools import assert_true
from nose.tools import assert_equal
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category):
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
open_new_course()
####### HELPER FUNCTIONS ##############
def open_new_course():
world.clear_courses()
log_into_studio()
create_a_course()
####### HELPER FUNCTIONS ##############
def create_studio_user(
uname='robot',
email='robot+studio@edx.org',
password='test',
is_staff=False):
studio_user = world.UserFactory.build(
studio_user = world.UserFactory(
username=uname,
email=email,
password=password,
is_staff=is_staff)
studio_user.set_password(password)
studio_user.save()
registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user)
registration.activate()
user_profile = world.UserProfileFactory(user=studio_user)
def fill_in_course_info(
name='Robot Super Course',
@@ -153,4 +151,21 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
time.sleep(float(1))
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
'i4x://edx/templates/video/default',
'.xmodule_VideoModule'
)
@step('I have clicked the new unit button')
def open_new_unit(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')

View File

@@ -0,0 +1,92 @@
# disable missing docstring
#pylint: disable=C0111
from lettuce import world
from nose.tools import assert_equal
from terrain.steps import reload_the_page
@world.absorb
def create_component_instance(step, component_button_css, instance_id, expected_css):
click_new_component_button(step, component_button_css)
click_component_from_menu(instance_id, expected_css)
@world.absorb
def click_new_component_button(step, component_button_css):
step.given('I have clicked the new unit button')
world.css_click(component_button_css)
@world.absorb
def click_component_from_menu(instance_id, expected_css):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
component. Components with only one template are created as soon
as the user clicks the appropriate button, so we assert that the
expected component is present.
"""
elem_css = "a[data-location='%s']" % instance_id
elements = world.css_find(elem_css)
assert(len(elements) == 1)
if elements[0]['id'] == instance_id: # If this is a component with multiple templates
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
world.css_click('a.edit-button')
world.css_click('#settings-mode')
@world.absorb
def verify_setting_entry(setting, display_name, value, explicitly_set):
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
settingClearButton = setting.find_by_css('.setting-clear')[0]
assert_equal(explicitly_set, settingClearButton.has_class('active'))
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
@world.absorb
def verify_all_setting_entries(expected_entries):
settings = world.browser.find_by_css('.wrapper-comp-setting')
assert_equal(len(expected_entries), len(settings))
for (counter, setting) in enumerate(settings):
world.verify_setting_entry(
setting, expected_entries[counter][0],
expected_entries[counter][1], expected_entries[counter][2]
)
@world.absorb
def save_component_and_reopen(step):
world.css_click("a.save-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
reload_the_page(step)
edit_component_and_select_settings()
@world.absorb
def cancel_component(step):
world.css_click("a.cancel-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes were not persisted.
reload_the_page(step)
@world.absorb
def revert_setting_entry(label):
get_setting_entry(label).find_by_css('.setting-clear')[0].click()
@world.absorb
def get_setting_entry(label):
settings = world.browser.find_by_css('.wrapper-comp-setting')
for setting in settings:
if setting.find_by_css('.setting-label')[0].value == label:
return setting
return None

View File

@@ -1,20 +1,17 @@
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date
Given I have set course dates
And I clear the course start date

View File

@@ -25,9 +25,7 @@ DEFAULT_TIME = "00:00"
############### ACTIONS ####################
@step('I select Schedule and Details$')
def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
world.click_course_settings()
link_css = 'li.nav-course-settings-schedule a'
world.css_click(link_css)

View File

@@ -47,12 +47,6 @@ def i_see_the_course_in_my_courses(step):
assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$')
def course_is_loaded(step):
class_css = 'a.class-name'
assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1'
@@ -62,4 +56,4 @@ def i_am_on_tab(step, tab_name):
@step('I see a link for adding a new section$')
def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button'
assert world.css_has_text(link_css, '+ New Section')
assert world.css_has_text(link_css, 'New Section')

View File

@@ -0,0 +1,17 @@
Feature: Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
Given I have created a Discussion Tag
And I edit and select Settings
Then I see three alphabetized settings and their expected values
Scenario: User can modify display name
Given I have created a Discussion Tag
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: Creating a discussion takes a single click
Given I have clicked the new unit button
Then creating a discussion takes a single click

View File

@@ -0,0 +1,30 @@
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_component_instance(
step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag',
'.xmodule_DiscussionModule'
)
@step('I see three alphabetized settings and their expected values$')
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", True],
['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule'))
world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']")
assert(world.is_css_present('.xmodule_DiscussionModule'))

View File

@@ -0,0 +1,13 @@
Feature: HTML Editor
As a course author, I want to be able to create HTML blocks.
Scenario: User can view metadata
Given I have created a Blank HTML Page
And I edit and select Settings
Then I see only the HTML display name setting
Scenario: User can modify display name
Given I have created a Blank HTML Page
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save

View File

@@ -0,0 +1,17 @@
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])

View File

@@ -0,0 +1,67 @@
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
Scenario: User can view metadata
Given I have created a Blank Common Problem
And I edit and select Settings
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
Scenario: User can modify String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can revert the display name to unset
And my display name is unset on save
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can select Per Student for Randomization
And my change to randomization is persisted
And I can revert to the default value for randomization
Scenario: User can modify float input values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And my change to weight is persisted
And I can revert to the default value of unset for weight
Scenario: User cannot type letters in float number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the weight to "abc", it remains unset
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
And 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"
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And I can modify the display name
Then If I press Cancel my changes are not persisted
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
And I edit and select Settings
Then Edit High Level Source is visible

View File

@@ -0,0 +1,187 @@
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
from nose.tools import assert_equal
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer"
############### ACTIONS ####################
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_component_instance(
step,
'.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule'
)
@step('I edit and select Settings$')
def i_edit_and_select_settings(step):
world.edit_component_and_select_settings()
@step('I see five alphabetized settings and their expected values$')
def i_see_five_settings_with_values(step):
world.verify_all_setting_entries(
[
[DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", True],
[SHOW_ANSWER, "Finished", True]
])
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
verify_modified_display_name()
@step('my display name change is persisted on save')
def my_display_name_change_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name()
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
verify_modified_display_name_with_special_chars()
@step('my special characters and persisted on save')
def special_chars_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name_with_special_chars()
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name()
@step('my display name is unset on save')
def my_display_name_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_unset_display_name()
@step('I can select Per Student for Randomization')
def i_can_select_per_student_for_randomization(step):
world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization()
@step('my change to randomization is persisted')
def my_change_to_randomization_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_randomization()
@step('I can revert to the default value for randomization')
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
@step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight):
set_weight(weight)
verify_modified_weight()
@step('my change to weight is persisted')
def my_change_to_weight_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_weight()
@step('I can revert to the default value of unset for weight')
def i_can_revert_to_default_for_unset_weight(step):
world.revert_setting_entry(PROBLEM_WEIGHT)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the weight to "(.*)", it remains unset')
def set_the_weight_to_abc(step, bad_weight):
set_weight(bad_weight)
# We show the clear button immediately on type, hence the "True" here.
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
world.save_component_and_reopen(step)
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the max attempts to "(.*)", it displays initially as "(.*)", and is persisted as "(.*)"')
def set_the_max_attempts(step, max_attempts_set, max_attempts_displayed, max_attempts_persisted):
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill(max_attempts_set)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_displayed, True)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_persisted, True)
@step('Edit High Level Source is not visible')
def edit_high_level_source_not_visible(step):
verify_high_level_source(step, False)
@step('Edit High Level Source is visible')
def edit_high_level_source_visible(step):
verify_high_level_source(step, True)
@step('If I press Cancel my changes are not persisted')
def cancel_does_not_save_changes(step):
world.cancel_component(step)
step.given("I edit and select Settings")
step.given("I see five alphabetized settings and their expected values")
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab (waiting for the tab to be visible)
world.css_find('#ui-id-2')
world.css_click('#ui-id-2')
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
def verify_high_level_source(step, visible):
assert_equal(visible, world.is_css_present('.launch-latex-compiler'))
world.cancel_component(step)
assert_equal(visible, world.is_css_present('.upload-button'))
def verify_modified_weight():
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
def verify_modified_randomization():
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
def verify_modified_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
def verify_modified_display_name_with_special_chars():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
def set_weight(weight):
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill(weight)

View File

@@ -3,7 +3,6 @@ Feature: Create Section
As a course author
I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course
Given I have opened a new course in Studio
When I click the New Section link
@@ -27,10 +26,9 @@ Feature: Create Section
And I save a new section release date
Then the section release date is updated
@skip-phantom
Scenario: Delete section
Given I have opened a new course in Studio
And I have added a new section
When I press the "section" delete icon
And I confirm the alert
When I will confirm all alerts
And I press the "section" delete icon
Then the section does not exist

View File

@@ -9,34 +9,34 @@ from nose.tools import assert_equal
@step('I click the new section link$')
def i_click_new_section_link(step):
def i_click_new_section_link(_step):
link_css = 'a.new-courseware-section-button'
world.css_click(link_css)
@step('I enter the section name and click save$')
def i_save_section_name(step):
def i_save_section_name(_step):
save_section_name('My Section')
@step('I enter a section name with a quote and click save$')
def i_save_section_name_with_quote(step):
def i_save_section_name_with_quote(_step):
save_section_name('Section with "Quote"')
@step('I have added a new section$')
def i_have_added_new_section(step):
def i_have_added_new_section(_step):
add_section()
@step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step):
def i_click_the_edit_link_for_the_release_date(_step):
button_css = 'div.section-published-date a.edit-button'
world.css_click(button_css)
@step('I save a new section release date$')
def i_save_a_new_section_release_date(step):
def i_save_a_new_section_release_date(_step):
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
'input.start-time.time.ui-timepicker-input', '00:00')
world.browser.click_link_by_text('Save')
@@ -46,35 +46,35 @@ def i_save_a_new_section_release_date(step):
@step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(step):
def i_see_my_section_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('My Section')
@step('I see my section name with a quote on the Courseware page$')
def i_see_my_section_name_with_quote_on_the_courseware_page(step):
def i_see_my_section_name_with_quote_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('Section with "Quote"')
@step('I click to edit the section name$')
def i_click_to_edit_section_name(step):
def i_click_to_edit_section_name(_step):
world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name'
def i_see_complete_section_name_with_quote_in_editor(_step):
css = '.section-name-edit input[type=text]'
assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
@step('the section does not exist$')
def section_does_not_exist(step):
css = 'span.section-name-span'
assert world.browser.is_element_not_present_by_css(css)
def section_does_not_exist(_step):
css = 'h3[data-name="My Section"]'
assert world.is_css_not_present(css)
@step('I see a release date for my section$')
def i_see_a_release_date_for_my_section(step):
def i_see_a_release_date_for_my_section(_step):
import re
css = 'span.published-status'
@@ -83,26 +83,32 @@ def i_see_a_release_date_for_my_section(step):
# e.g. 11/06/2012 at 16:25
msg = 'Will Release:'
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]'
time_regex = '[0-2][0-9]:[0-5][0-9]'
match_string = '%s %s at %s' % (msg, date_regex, time_regex)
date_regex = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d\d?, \d{4}'
if not re.search(date_regex, status_text):
print status_text, date_regex
time_regex = r'[0-2]\d:[0-5]\d( \w{3})?'
if not re.search(time_regex, status_text):
print status_text, time_regex
match_string = r'%s\s+%s at %s' % (msg, date_regex, time_regex)
if not re.match(match_string, status_text):
print status_text, match_string
assert re.match(match_string, status_text)
@step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step):
def i_see_a_link_to_create_a_new_subsection(_step):
css = 'a.new-subsection-item'
assert world.is_css_present(css)
@step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step):
def the_section_release_date_picker_not_visible(_step):
css = 'div.edit-subsection-publish-settings'
assert not world.css_visible(css)
@step('the section release date is updated$')
def the_section_release_date_is_updated(step):
def the_section_release_date_is_updated(_step):
css = 'span.published-status'
status_text = world.css_text(css)
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')

View File

@@ -18,10 +18,7 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = world.css_find(submit_css)
e.type(' ')
world.css_click(submit_css)
@step('I should see be on the studio home page$')

View File

@@ -1,61 +1,59 @@
Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections
In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author
I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
Then I see the "Collapse All Sections" link
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page
When I will confirm all alerts
And I press the "section" delete icon
Then I see the "Collapse All Sections" link
Scenario: Collapsing all sections when all sections are expanded
Given I navigate to the courseware page of a course with multiple sections
And all sections are expanded
When I click the "Collapse All Sections" link
Then I see the "Expand All Sections" link
And all sections are collapsed
Scenario: Collapsing all sections when all sections are expanded
Given I navigate to the courseware page of a course with multiple sections
And all sections are expanded
When I click the "Collapse All Sections" link
Then I see the "Expand All Sections" link
And all sections are collapsed
Scenario: Collapsing all sections when 1 or more sections are already collapsed
Given I navigate to the courseware page of a course with multiple sections
And all sections are expanded
When I collapse the first section
And I click the "Collapse All Sections" link
Then I see the "Expand All Sections" link
And all sections are collapsed
Scenario: Collapsing all sections when 1 or more sections are already collapsed
Given I navigate to the courseware page of a course with multiple sections
And all sections are expanded
When I collapse the first section
And I click the "Collapse All Sections" link
Then I see the "Expand All Sections" link
And all sections are collapsed
Scenario: Expanding all sections when all sections are collapsed
Given I navigate to the courseware page of a course with multiple sections
And I click the "Collapse All Sections" link
When I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expanding all sections when all sections are collapsed
Given I navigate to the courseware page of a course with multiple sections
And I click the "Collapse All Sections" link
When I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expanding all sections when 1 or more sections are already expanded
Given I navigate to the courseware page of a course with multiple sections
And I click the "Collapse All Sections" link
When I expand the first section
And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expanding all sections when 1 or more sections are already expanded
Given I navigate to the courseware page of a course with multiple sections
And I click the "Collapse All Sections" link
When I expand the first section
And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded

View File

@@ -112,7 +112,7 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$')
def all_sections_are_expanded(step):
def all_sections_are_collapsed(step):
subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator)
for s in subsections:

View File

@@ -3,14 +3,12 @@ Feature: Create Subsection
As a course author
I want to create and edit subsections
@skip-phantom
Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
@skip-phantom
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
@@ -27,7 +25,6 @@ Feature: Create Subsection
And I reload the page
Then I see it marked as Homework
@skip-phantom
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
@@ -35,11 +32,10 @@ Feature: Create Subsection
And I reload the page
Then I see the correct dates
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection
And I see my subsection on the Courseware page
When I press the "subsection" delete icon
And I confirm the alert
When I will confirm all alerts
And I press the "subsection" delete icon
Then the subsection does not exist

View File

@@ -10,9 +10,7 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step):
world.clear_courses()
log_into_studio()
create_a_course()
open_new_course()
add_section()
@@ -63,14 +61,6 @@ def test_have_set_dates_in_different_years(step):
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('03:00', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('04:00', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$')
def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle')
@@ -101,8 +91,20 @@ def the_subsection_does_not_exist(step):
assert world.browser.is_element_not_present_by_css(css)
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', get_date('input#start_date'))
assert_equal('03:00', get_date('input#start_time'))
assert_equal('01/02/2012', get_date('input#due_date'))
assert_equal('04:00', get_date('input#due_time'))
############ HELPER METHODS ###################
def get_date(css):
return world.css_find(css).first.value.strip()
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'

View File

@@ -0,0 +1,13 @@
Feature: Video Component Editor
As a course author, I want to be able to create video components.
Scenario: User can view metadata
Given I have created a Video component
And I edit and select Settings
Then I see only the Video display name setting
Scenario: User can modify display name
Given I have created a Video component
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save

View File

@@ -0,0 +1,9 @@
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I see only the video display name setting$')
def i_see_only_the_video_display_name(step):
world.verify_all_setting_entries([['Display Name', "default", True]])

View File

@@ -0,0 +1,10 @@
Feature: Video Component
As a course author, I want to be able to view my created videos in Studio.
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click

View File

@@ -0,0 +1,18 @@
#pylint: disable=C0111
from lettuce import world, step
############### ACTIONS ####################
@step('when I view the video it does not have autoplay enabled')
def does_not_autoplay(step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play')
@step('creating a video takes a single click')
def video_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule'))

View File

@@ -34,6 +34,13 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
import datetime
from pytz import UTC
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
@@ -45,7 +52,7 @@ class MongoCollectionFindWrapper(object):
self.counter = 0
def find(self, query, *args, **kwargs):
self.counter = self.counter+1
self.counter = self.counter + 1
return self.original(query, *args, **kwargs)
@@ -73,8 +80,62 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
def check_components_on_page(self, component_types, expected_types):
"""
Ensure that the right types end up on the page.
component_types is the list of advanced components.
expected_types is the list of elements that should appear on the page.
expected_types and component_types should be similar, but not
exactly the same -- for example, 'videoalpha' in
component_types should cause 'Video Alpha' to be present.
"""
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
course = store.get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
course.advanced_modules = component_types
store.update_metadata(course.location, own_metadata(course))
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
for expected in expected_types:
self.assertIn(expected, resp.content)
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha',
'Word cloud',
'Annotation',
'Open Ended Response',
'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha'])
def test_malformed_edit_unit_request(self):
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
location = descriptor.location._replace(name='.' + descriptor.location.name)
resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
self.assertEqual(resp.status_code, 400)
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
print "Checking ", descriptor.location.url()
@@ -101,7 +162,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
Unfortunately, None = published for the revision field, so get_items() would return
both draft and non-draft copies.
'''
store = modulestore()
store = modulestore('direct')
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
@@ -128,7 +189,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
properly computed
'''
store = modulestore()
store = modulestore('direct')
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
@@ -186,7 +247,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
@@ -210,7 +271,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
)
self.assertTrue(getattr(draft_problem, 'is_draft', False))
#now requery with depth
# now requery with depth
course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None
@@ -220,10 +281,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
def test_import_textbook_as_content_element(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
@@ -245,10 +314,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
def test_import_polls(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
found = False
import_from_xml(module_store, 'common/test/data/', ['full'])
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
found = len(items) > 0
@@ -262,9 +329,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertGreater(err_cnt, 0)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full'])
sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
@@ -293,14 +359,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours')
@@ -309,9 +375,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
@@ -326,14 +391,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'display_name': 'Robot Super Course',
}
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
@@ -348,19 +413,45 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999')
new_loc = descriptor.location.replace(org='MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200)
def test_illegal_draft_crud_ops(self):
draft_store = modulestore('draft')
direct_store = modulestore('direct')
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
location = Location('i4x://MITx/999/chapter/neuvo')
self.assertRaises(InvalidVersionError, draft_store.clone_item, 'i4x://edx/templates/chapter/Empty',
location)
direct_store.clone_item('i4x://edx/templates/chapter/Empty', location)
self.assertRaises(InvalidVersionError, draft_store.clone_item, location,
location)
self.assertRaises(InvalidVersionError, draft_store.update_item, location,
'chapter data')
# taking advantage of update_children and other functions never checking that the ids are valid
self.assertRaises(InvalidVersionError, draft_store.update_children, location,
['i4x://MITx/999/problem/doesntexist'])
self.assertRaises(InvalidVersionError, draft_store.update_metadata, location,
{'due': datetime.datetime.now(UTC)})
self.assertRaises(InvalidVersionError, draft_store.unpublish, location)
def test_bad_contentstore_request(self):
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
@@ -371,15 +462,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
fs = OSFS(root_dir / 'test_export')
self.assertTrue(fs.exists(dirname))
filesystem = OSFS(root_dir / 'test_export')
self.assertTrue(filesystem.exists(dirname))
query_loc = Location('i4x', location.org, location.course, category_name, None)
items = modulestore.get_items(query_loc)
for item in items:
fs = OSFS(root_dir / ('test_export/' + dirname))
self.assertTrue(fs.exists(item.location.name + filename_suffix))
filesystem = OSFS(root_dir / ('test_export/' + dirname))
self.assertTrue(filesystem.exists(item.location.name + filename_suffix))
def test_export_course(self):
module_store = modulestore('direct')
@@ -411,7 +502,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# add private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
private_location_no_draft = private_vertical.location._replace(revision=None)
private_location_no_draft = private_vertical.location.replace(revision=None)
module_store.update_children(sequential.location, sequential.children +
[private_location_no_draft.url()])
@@ -435,21 +526,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for about content
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(filesystem.exists('grading_policy.json'))
course = module_store.get_item(location)
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy:
with filesystem.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# check for policy.json
self.assertTrue(filesystem.exists('policy.json'))
# compare what's on disk to what we have in the course module
with fs.open('policy.json', 'r') as course_policy:
with filesystem.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
@@ -490,6 +584,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
# make sure the textbook survived the export/import
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
@@ -511,8 +610,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_prefetch_children(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
@@ -598,6 +698,14 @@ class ContentStoreTest(ModuleStoreTestCase):
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
def test_create_course_check_forum_seeding(self):
"""Test new course creation and verify forum seeding """
resp = self.client.post(reverse('create_new_course'), self.course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course'))
def test_create_course_duplicate_course(self):
"""Test new course creation - error path"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
@@ -634,7 +742,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('index'))
self.assertContains(
resp,
'<h1 class="title-1">My Courses</h1>',
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True
)
@@ -724,7 +832,7 @@ class ContentStoreTest(ModuleStoreTestCase):
Import and walk through some common URL endpoints. This just verifies non-500 and no other
correct behavior, so it is not a deep test
"""
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
resp = self.client.get(reverse('course_index',
kwargs={'org': loc.org,
@@ -791,44 +899,46 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(200, resp.status_code)
# go look at a subsection page
subsection_location = loc._replace(category='sequential', name='test_sequence')
subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()}))
self.assertEqual(200, resp.status_code)
# go look at the Edit page
unit_location = loc._replace(category='vertical', name='test_vertical')
unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()}))
self.assertEqual(200, resp.status_code)
# delete a component
del_loc = loc._replace(category='html', name='test_html')
del_loc = loc.replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a unit
del_loc = loc._replace(category='vertical', name='test_vertical')
del_loc = loc.replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a unit
del_loc = loc._replace(category='sequential', name='test_sequence')
del_loc = loc.replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a chapter
del_loc = loc._replace(category='chapter', name='chapter_2')
del_loc = loc.replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['simple'])
did_load_item = False
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
@@ -840,8 +950,9 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertTrue(did_load_item)
def test_forum_id_generation(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
@@ -853,9 +964,8 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
def test_update_modulestore_signal_did_fire(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
try:
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
@@ -879,9 +989,9 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertTrue(self.got_signal)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])

View File

@@ -17,7 +17,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
@@ -55,6 +54,7 @@ class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self):
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))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
@@ -68,7 +68,6 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails)
self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=")
# Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense.
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 ")
@@ -77,6 +76,23 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_ooc_encoder(self):
"""
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())}
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails)
self.assertIn('location', jsondetails)
self.assertIn('org', jsondetails['location'])
self.assertEquals('org', jsondetails['location'][1])
self.assertEquals(1, jsondetails['number'])
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)
@@ -117,11 +133,8 @@ class CourseDetailsViewTest(CourseTestCase):
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(datetime):
if datetime is not None:
return datetime.isoformat("T")
else:
return None
def convert_datetime_to_iso(dt):
return Date().to_json(dt)
def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location)
@@ -152,22 +165,12 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(*struct_time[:6], tzinfo=UTC())
def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None:
encoded_encoded = date.from_json(encoded[field])
dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
if isinstance(details[field], datetime.datetime):
dt2 = details[field]
else:
details_encoded = date.from_json(details[field])
dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
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)
@@ -256,7 +259,7 @@ class CourseMetadataEditingTest(CourseTestCase):
def setUp(self):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(modulestore(), '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):

View File

@@ -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 = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 8, 2013'}
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(payload['content'], content)
first_update_url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': payload['id']})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>'
payload['content'] = content
resp = self.client.post(first_update_url, json.dumps(payload),
"application/json")
"application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div")
"iframe w/ div")
# now put in an evil update
content = '<ol/>'
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(content, payload['content'], "self closing ol")
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2)
# can't test non-json paylod b/c expect_json throws error
# try json w/o required fields
self.assertContains(
self.client.post(url, json.dumps({'garbage': 1}),
"application/json"),
'Failed to save', status_code=400)
self.assertContains(self.client.post(url, json.dumps({'garbage': 1}),
"application/json"),
'Failed to save', status_code=400)
# now try to update a non-existent update
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '9'})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '9'})
content = 'blah blah'
payload = {'content': content,
'date': 'January 21, 2013'}
@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"),
@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase):
# now try to delete a non-existent update
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '19'})
'course': self.course_location.course,
'provided_id': '19'})
payload = {'content': content,
'date': 'January 21, 2013'}
self.assertContains(self.client.delete(url), "delete", status_code=400)
@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content,
'date': 'January 28, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
this_id = payload['id']
self.assertHTMLEqual(content, payload['content'], "single iframe")
# first count the entries
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
before_delete = len(payload)
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': this_id})
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': this_id})
resp = self.client.delete(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1)

View File

@@ -6,6 +6,7 @@ from django.test.client import Client
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class InternationalizationTest(ModuleStoreTestCase):
"""
Tests to validate Internationalization.
@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
}
def test_course_plain_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True)
def test_course_explicit_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='en'
)
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
'<h1 class="page-header">My Courses</h1>',
status_code=200,
html=True)
# ****
# NOTE:
@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase):
# This test will break when we replace this fake 'test' language
# with actual French. This test will need to be updated with
# actual French at that time.
# Test temporarily disable since it depends on creation of dummy strings
@skip
def test_course_with_accents (self):
def test_course_with_accents(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='fr'
@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase):
TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>'
self.assertContains(resp,
TEST_STRING,
status_code=200,
html=True)
html=True)

View File

@@ -0,0 +1,29 @@
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
class DeleteItem(CourseTestCase):
def setUp(self):
""" Creates the test course with a static page in it. """
super(DeleteItem, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course')
def testDeleteStaticPage(self):
# Add static tab
data = {
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'template': 'i4x://edx/templates/static_tab/Empty'
}
resp = self.client.post(reverse('clone_item'), data)
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200)

View File

@@ -1,7 +1,10 @@
""" Tests for utils. """
from contentstore import utils
import mock
import collections
import copy
from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -9,11 +12,52 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """
def about_page_test(self):
""" Get URL for about page. """
""" Get URL for about page, no marketing site """
# default for ENABLE_MKTG_SITE is False.
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def about_page_marketing_site_test(self):
""" Get URL for about page, marketing root present. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//dummy-root/courses/mitX/101/test/about")
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
def about_page_marketing_site_remove_http_test(self):
""" Get URL for about page, marketing root present, remove http://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
def about_page_marketing_site_remove_https_test(self):
""" Get URL for about page, marketing root present, remove https://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
def about_page_marketing_site_https__edge_test(self):
""" Get URL for about page, only remove https:// at the beginning of the string. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={})
def about_page_marketing_urls_not_set_test(self):
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), None)
@override_settings(LMS_BASE=None)
def about_page_no_lms_base_test(self):
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """
self.assertEquals(self.get_about_page_link(), None)
def get_about_page_link(self):
""" create mock course and return the about page link """
location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
return utils.get_lms_link_for_about_page(location)
def lms_link_test(self):
""" Tests get_lms_link_for_item. """
@@ -24,7 +68,7 @@ class LMSLinksTestCase(TestCase):
link = utils.get_lms_link_for_item(location, True)
self.assertEquals(
link,
"//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
"//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
)
@@ -70,3 +114,79 @@ class UrlReverseTestCase(ModuleStoreTestCase):
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """
def get_tab_type_dicts(self, tab_types):
""" Returns an array of tab dictionaries. """
if tab_types:
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
else:
return []
def get_course_with_tabs(self, tabs=[]):
""" Returns a mock course object with a tabs attribute. """
course = collections.namedtuple('MockCourse', ['tabs'])
if isinstance(tabs, basestring):
course.tabs = self.get_tab_type_dicts(tabs)
else:
course.tabs = tabs
return course
def test_add_extra_panel_tab(self):
""" Tests if a tab can be added to a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test adding with changed = True
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
expected_tabs.append(tab)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test adding with changed = False
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
def test_remove_extra_panel_tab(self):
""" Tests if a tab can be removed from a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test removing with changed = True
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test removing with changed = False
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)

View File

@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase):
# Now login should work
self.login(self.email, self.pw)
def test_login_link_on_activation_age(self):
self.create_account(self.username, self.email, self.pw)
# we want to test the rendering of the activation page when the user isn't logged in
self.client.logout()
resp = self._activate_user(self.email)
self.assertEqual(resp.status_code, 200)
# check the the HTML has links to the right login page. Note that this is merely a content
# check and thus could be fragile should the wording change on this page
expected = 'You can now <a href="' + reverse('login') + '">login</a>.'
self.assertIn(expected, resp.content)
def test_private_pages_auth(self):
"""Make sure pages that do require login work."""
auth_pages = (

View File

@@ -4,11 +4,16 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse
import copy
import logging
import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
log = logging.getLogger(__name__)
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location):
@@ -86,7 +91,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None:
if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
else:
lms_base = settings.LMS_BASE
@@ -105,9 +110,29 @@ def get_lms_link_for_about_page(location):
"""
Returns the url to the course about page from the location tuple.
"""
if settings.LMS_BASE is not None:
lms_link = "//{lms_base}/courses/{course_id}/about".format(
lms_base=settings.LMS_BASE,
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
if not hasattr(settings, 'MKTG_URLS'):
log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
about_base = None
else:
marketing_urls = settings.MKTG_URLS
if marketing_urls.get('ROOT', None) is None:
log.exception('There is no ROOT defined in MKTG_URLS')
about_base = None
else:
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
about_base = marketing_urls.get('ROOT')
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
about_base = re.sub(r"^https?://", "", about_base)
elif settings.LMS_BASE is not None:
about_base = settings.LMS_BASE
else:
about_base = None
if about_base is not None:
lms_link = "//{about_base_url}/courses/{course_id}/about".format(
about_base_url=about_base,
course_id=get_course_id(location)
)
else:
@@ -192,9 +217,10 @@ class CoursePageNames:
Checklists = "checklists"
def add_open_ended_panel_tab(course):
def add_extra_panel_tab(tab_type, course):
"""
Used to add the open ended panel tab to a course if it does not exist.
Used to add the panel tab to a course if it does not exist.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -202,16 +228,19 @@ def add_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL)
course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
def remove_extra_panel_tab(tab_type, course):
"""
Used to remove the open ended panel tab from a course if it exists.
Used to remove the panel tab from a course if it exists.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -219,8 +248,10 @@ def remove_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs

View File

@@ -1,1685 +0,0 @@
from util.json_request import expect_json
import json
import logging
import os
import sys
import time
import tarfile
import shutil
from collections import defaultdict
from uuid import uuid4
from path import path
from xmodule.modulestore.xml_exporter import export_to_xml
from tempfile import mkdtemp
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
import static_replace
from external_auth.views import ssl_login_shortcut
from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError, ProcessingError
from functools import partial
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
remove_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \
update_course_updates, delete_course_update
from cache_toolbox.core import del_cached_content
from contentstore.module_info_model import get_module_info, set_module_info
from models.settings.course_details import CourseDetails, \
CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.shortcuts import redirect
from models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
# ==== Public views ==================================================
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})
# static/proof-of-concept views
def ux_alerts(request):
return render_to_response('ux-alerts.html', {})
# ==== Views for any logged-in user ==================================
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
# ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
"""
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course = modulestore().get_item(location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
'active_tab': 'courseware',
'context_course': course,
'lms_link': lms_link,
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
})
@login_required
def edit_subsection(request, location):
# check that we have permissions to edit this item
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest()
parent_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict(
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
for unit in subsection_units:
state = compute_unit_state(unit)
if state == UnitState.public or state == UnitState.draft:
can_view_live = True
break
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
@login_required
def edit_unit(request, location):
"""
Display an editing page for the specified module.
Expects a GET request with the parameter 'id'.
id: A Location URL
"""
course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied()
item = modulestore().get_item(location, depth=1)
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
))
components = [
component.location.url()
for component
in item.get_children()
]
# TODO (cpennington): If we share units between courses,
# this will need to change to check permissions correctly so as
# to pick the correct parent subsection
containing_subsection_locs = modulestore().get_parent_locations(location, None)
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
# so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this
index = 1
for child in containing_subsection.get_children():
if child.location == item.location:
break
index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
unit_state = compute_unit_state(item)
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
'unit': item,
'unit_location': location,
'components': components,
'component_templates': component_templates,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
@expect_json
@login_required
@ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name):
'''
CRUD operations on assignment types for sections and subsections and anything else gradable.
'''
location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location):
raise HttpResponseForbidden()
if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json")
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest()
# construct a location from the passed in path
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
try:
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent
filename = request.FILES['file'].name
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location)
# now store thumbnail location only if we could create it
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
# readback the saved content - we need the database timestamp
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
@login_required
@ensure_csrf_cookie
def course_info(request, org, course, name, provided_id=None):
"""
Send models and views as well as html for editing the course info to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
return render_to_response('course_info.html', {
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_info_updates(request, org, course, provided_id=None):
"""
restful CRUD operations on course_info updates.
org, course: Attributes of the Location for the item to edit
provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
"""
# ??? No way to check for access permission afaik
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '':
provided_id = None
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
@expect_json
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
location = Location(module_location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
else:
return HttpResponseBadRequest()
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings.html', {
'context_course': course_module,
'course_location': location,
'details_url': reverse(course_settings_updates,
kwargs={"org": org,
"course": course,
"name": name,
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
"""
Send models and views as well as html for editing the advanced course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section):
"""
restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
# # NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_advanced_updates(request, org, course, name):
"""
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
the payload is either a key or a list of keys to delete.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
request_body = json.loads(request.body)
#Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
#module, and to remove it if they have removed the open ended elements.
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Check to see if the user instantiated any open ended components
found_oe_type = False
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add an open ended tab to the course if needed
changed, new_tabs = add_open_ended_panel_tab(course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the open ended tab removal code below.
found_oe_type = True
break
#If we did not find an open ended module type in the advanced settings,
# we may need to remove the open ended tab from the course.
if not found_oe_type:
#Remove open ended tab to the course if needed
changed, new_tabs = remove_open_ended_panel_tab(course_module)
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
"""
Display an editable asset library
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
asset_display = []
for asset in assets:
id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
@login_required
@expect_json
def create_new_course(request):
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
except InvalidLocationError as e:
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
# see if the course already exists
existing_course = None
try:
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
if existing_course is not None:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
new_course = modulestore('direct').clone_item(template, dest_location)
# clone a default 'about' module as well
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
dest_about_location = dest_location._replace(category='about', name='overview')
modulestore('direct').clone_item(about_template_location, dest_about_location)
if display_name is not None:
new_course.display_name = display_name
# set a default start date to now
new_course.start = time.gmtime()
initialize_course_tabs(new_course)
create_all_course_groups(request.user, new_course.location)
return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
# at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tf = tarfile.open(temp_filepath)
tf.extractall(course_dir + '/')
# find the 'course.xml' file
for r, d, f in os.walk(course_dir):
for files in f:
if files == 'course.xml':
break
if files == 'course.xml':
break
if files != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(r))
if r != course_dir:
for fname in os.listdir(r):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
# export out to a tempdir
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz')
tf.add(root_dir / name, arcname=name)
tf.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'active_tab': 'export',
'successful_import_redirect_url': ''
})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-)
'''
return HttpResponse(True)
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method

View File

@@ -0,0 +1,15 @@
# pylint: disable=W0401, W0511
# Disable warnings about import from wildcard
# All files below declare exports with __all__
from .assets import *
from .checklist import *
from .component import *
from .course import *
from .error import *
from .item import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *

View File

@@ -0,0 +1,36 @@
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from django.core.exceptions import PermissionDenied
from ..utils import get_course_location_for_item
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access

View File

@@ -0,0 +1,264 @@
import logging
import json
import os
import tarfile
import shutil
from tempfile import mkdtemp
from path import path
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from auth.authz import create_all_course_groups
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from ..utils import get_url_reverse
from .access import get_location_and_verify_access
__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
"""
Display an editable asset library
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
asset_display = []
for asset in assets:
asset_id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'])
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest()
# construct a location from the passed in path
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
try:
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
return HttpResponseBadRequest()
if 'file' not in request.FILES:
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent
filename = request.FILES['file'].name
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location)
# now store thumbnail location only if we could create it
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
# readback the saved content - we need the database timestamp
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tar_file = tarfile.open(temp_filepath)
tar_file.extractall(course_dir + '/')
# find the 'course.xml' file
for dirpath, _dirnames, filenames in os.walk(course_dir):
for files in filenames:
if files == 'course.xml':
break
if files == 'course.xml':
break
if files != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(dirpath))
if dirpath != course_dir:
for fname in os.listdir(dirpath):
shutil.move(dirpath / fname, course_dir)
_module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
# export out to a tempdir
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
logging.debug('tar file being generated at {0}'.format(export_file.name))
tar_file = tarfile.open(name=export_file.name, mode='w:gz')
tar_file.add(root_dir / name, arcname=name)
tar_file.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'active_tab': 'export',
'successful_import_redirect_url': ''
})

View File

@@ -0,0 +1,104 @@
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .requests import get_request_method
from .access import get_location_and_verify_access
__all__ = ['get_checklists', 'update_checklist']
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified

View File

@@ -0,0 +1,316 @@
import json
import logging
from collections import defaultdict
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.util.date_utils import get_default_time_display
from xblock.core import Scope
from util.json_request import expect_json
from contentstore.module_info_model import get_module_info, set_module_info
from contentstore.utils import get_modulestore, get_lms_link_for_item, \
compute_unit_state, UnitState, get_course_for_item
from models.settings.course_grading import CourseGradingModel
from .requests import get_request_method, _xmodule_recurse
from .access import has_access
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
'edit_subsection',
'edit_unit',
'assignment_type_update',
'create_draft',
'publish_draft',
'unpublish_unit',
'module_info']
log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required
def edit_subsection(request, location):
# check that we have permissions to edit this item
try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location):
raise PermissionDenied()
try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest()
parent_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict(
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
for unit in subsection_units:
state = compute_unit_state(unit)
if state == UnitState.public or state == UnitState.draft:
can_view_live = True
break
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
@login_required
def edit_unit(request, location):
"""
Display an editing page for the specified module.
Expects a GET request with the parameter 'id'.
id: A Location URL
"""
try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location):
raise PermissionDenied()
try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None
))
components = [
component.location.url()
for component
in item.get_children()
]
# TODO (cpennington): If we share units between courses,
# this will need to change to check permissions correctly so as
# to pick the correct parent subsection
containing_subsection_locs = modulestore().get_parent_locations(location, None)
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
# so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this
index = 1
for child in containing_subsection.get_children():
if child.location == item.location:
break
index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
unit_state = compute_unit_state(item)
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
'unit': item,
'unit_location': location,
'components': components,
'component_templates': component_templates,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@expect_json
@login_required
@ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name):
'''
CRUD operations on assignment types for sections and subsections and anything else gradable.
'''
location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location):
raise HttpResponseForbidden()
if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json")
@login_required
@expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@expect_json
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
location = Location(module_location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
else:
return HttpResponseBadRequest()

View File

@@ -0,0 +1,409 @@
"""
Views related to operations on course objects
"""
import json
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore import Location
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from auth.authz import create_all_course_groups
from util.json_request import expect_json
from .access import has_access, get_location_and_verify_access
from .requests import get_request_method
from .tabs import initialize_course_tabs
from .component import OPEN_ENDED_COMPONENT_TYPES, \
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC
# TODO: should explicitly enumerate exports with __all__
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
'course_config_advanced_page',
'course_settings_updates',
'course_grader_updates',
'course_advanced_updates']
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
"""
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course = modulestore().get_item(location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
'active_tab': 'courseware',
'context_course': course,
'lms_link': lms_link,
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
})
@login_required
@expect_json
def create_new_course(request):
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
except InvalidLocationError as error:
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" +
display_name + "'.\n\n" + error.message}))
# see if the course already exists
existing_course = None
try:
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
if existing_course is not None:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
new_course = modulestore('direct').clone_item(template, dest_location)
# clone a default 'about' module as well
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
dest_about_location = dest_location._replace(category='about', name='overview')
modulestore('direct').clone_item(about_template_location, dest_about_location)
if display_name is not None:
new_course.display_name = display_name
# set a default start date to now
new_course.start = datetime.datetime.now(UTC())
initialize_course_tabs(new_course)
create_all_course_groups(request.user, new_course.location)
# seed the forums
seed_permissions_roles(new_course.location.course_id)
return HttpResponse(json.dumps({'id': new_course.location.url()}))
@login_required
@ensure_csrf_cookie
def course_info(request, org, course, name, provided_id=None):
"""
Send models and views as well as html for editing the course info to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
return render_to_response('course_info.html', {
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_info_updates(request, org, course, provided_id=None):
"""
restful CRUD operations on course_info updates.
org, course: Attributes of the Location for the item to edit
provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
"""
# ??? No way to check for access permission afaik
# get current updates
location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '':
provided_id = None
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings.html', {
'context_course': course_module,
'course_location': location,
'details_url': reverse(course_settings_updates,
kwargs={"org": org,
"course": course,
"name": name,
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
"""
Send models and views as well as html for editing the advanced course settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section):
"""
restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
# # NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_advanced_updates(request, org, course, name):
"""
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
the payload is either a key or a list of keys to delete.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)),
mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location,
json.loads(request.body))),
mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json
# cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
request_body = json.loads(request.body)
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
# Check to see if the user instantiated any advanced components. This is a hack
# that does the following :
# 1) adds/removes the open ended panel tab to a course automatically if the user
# has indicated that they want to edit the combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if the user has
# indicated that they want the notes module enabled in their course
# TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
# Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
# Maps tab types to components
tab_component_map = {
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES,
}
# Check to see if the user instantiated any notes or open ended components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
for ac_type in component_types:
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
# Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
# Set this flag to avoid the tab removal code below.
found_ac_type = True
break
# If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type:
# Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
# Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body,
filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")

View File

@@ -0,0 +1,20 @@
from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))

View File

@@ -0,0 +1,138 @@
import json
from uuid import uuid4
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
__all__ = ['save_item', 'clone_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = get_modulestore(item_location)
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()

View File

@@ -0,0 +1,177 @@
import logging
import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .access import has_access
__all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html

View File

@@ -0,0 +1,51 @@
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks']
"""
Public views
"""
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})

View File

@@ -0,0 +1,60 @@
import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-)
'''
return HttpResponse(status=204)
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)

View File

@@ -0,0 +1,28 @@
from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session

View File

@@ -0,0 +1,132 @@
from access import has_access
from util.json_request import expect_json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
# at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})

View File

@@ -0,0 +1,144 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from .access import has_access
from .requests import create_json_response
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()

View File

@@ -3,26 +3,26 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
from models.settings import course_grading
from contentstore.utils import update_item
from xmodule.fields import Date
import re
import logging
import datetime
class CourseDetails(object):
def __init__(self, location):
self.course_location = location # a Location obj
self.course_location = location # a Location obj
self.start_date = None # 'start'
self.end_date = None # 'end'
self.end_date = None # 'end'
self.enrollment_start = None
self.enrollment_end = None
self.syllabus = None # a pdf file asset
self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer
self.effort = None # int hours/week
self.syllabus = None # a pdf file asset
self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer
self.effort = None # int hours/week
@classmethod
def fetch(cls, course_location):
@@ -73,9 +73,9 @@ class CourseDetails(object):
"""
Decode the json into CourseDetails and save any changed attrs to the db
"""
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
# TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
course_location = jsondict['course_location']
## Will probably want to cache the inflight courses because every blur generates an update
# Will probably want to cache the inflight courses because every blur generates an update
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
@@ -181,7 +181,7 @@ class CourseSettingsEncoder(json.JSONEncoder):
return obj.__dict__
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, time.struct_time):
elif isinstance(obj, datetime.datetime):
return Date().to_json(obj)
else:
return JSONEncoder.default(self, obj)

View File

@@ -2,33 +2,52 @@
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
# Show the courses that are in the data directory
COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# MODULESTORE = {
# 'default': {
# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
# 'OPTIONS': {
# 'data_dir': DATA_DIR,
# 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
# }
# }
# }
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string'
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
# 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': ENV_ROOT / "db" / "test_mitx.db",
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
}
}

View File

@@ -1,6 +1,11 @@
"""
This is the default template for our main set of AWS servers.
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import json
from .common import *
@@ -28,12 +33,55 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
###################################### CELERY ################################
# Don't use a connection pool, since connections are dropped by ELB.
BROKER_POOL_LIMIT = 0
BROKER_CONNECTION_TIMEOUT = 1
# For the Result Store, use the django cache named 'celery'
CELERY_RESULT_BACKEND = 'cache'
CELERY_CACHE_BACKEND = 'celery'
# When the broker is behind an ELB, use a heartbeat to refresh the
# connection and to detect if it has been dropped.
BROKER_HEARTBEAT = 10.0
BROKER_HEARTBEAT_CHECKRATE = 2
# Each worker should only fetch one message at a time
CELERYD_PREFETCH_MULTIPLIER = 1
# Skip djcelery migrations, since we don't use the database as the broker
SOUTH_MIGRATION_MODULES = {
'djcelery': 'ignore',
}
# Rename the exchange and queues for each variant
QUEUE_VARIANT = CONFIG_PREFIX.lower()
CELERY_DEFAULT_EXCHANGE = 'edx.{0}core'.format(QUEUE_VARIANT)
HIGH_PRIORITY_QUEUE = 'edx.{0}core.high'.format(QUEUE_VARIANT)
DEFAULT_PRIORITY_QUEUE = 'edx.{0}core.default'.format(QUEUE_VARIANT)
LOW_PRIORITY_QUEUE = 'edx.{0}core.low'.format(QUEUE_VARIANT)
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
# Note that MITX_FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.
SITE_NAME = ENV_TOKENS['SITE_NAME']
@@ -43,11 +91,19 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# allow for environments to specify what cookie name our login subsystem should use
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
# NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str()
SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME'))
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
@@ -78,3 +134,14 @@ CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Celery Broker
CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "")
CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "")
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)

View File

@@ -19,8 +19,13 @@ Longer TODO:
multiple sites, but we do need a way to map their data assets.
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import sys
import lms.envs.common
from lms.envs.common import USE_TZ
from path import path
############################ FEATURE CONFIGURATION #############################
@@ -30,10 +35,16 @@ MITX_FEATURES = {
'GITHUB_PUSH': False,
'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True,
# Enable URL that shows information about the status of various services
'ENABLE_SERVICE_STATUS': False,
# Don't autoplay videos for course authors
'AUTOPLAY_VIDEOS': False
}
ENABLE_JASMINE = False
@@ -173,7 +184,7 @@ STATICFILES_DIRS = [
# Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
USE_I18N = True
USE_L10N = True
@@ -214,7 +225,10 @@ PIPELINE_JS = {
'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js'],
) + ['js/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js'],
'output_filename': 'js/cms-application.js',
'test_order': 0
},
@@ -240,6 +254,51 @@ STATICFILES_IGNORE_PATTERNS = (
PIPELINE_YUI_BINARY = 'yui-compressor'
################################# CELERY ######################################
# Message configuration
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_MESSAGE_COMPRESSION = 'gzip'
# Results configuration
CELERY_IGNORE_RESULT = False
CELERY_STORE_ERRORS_EVEN_IF_IGNORED = True
# Events configuration
CELERY_TRACK_STARTED = True
CELERY_SEND_EVENTS = True
CELERY_SEND_TASK_SENT_EVENT = True
# Exchange configuration
CELERY_DEFAULT_EXCHANGE = 'edx.core'
CELERY_DEFAULT_EXCHANGE_TYPE = 'direct'
# Queues configuration
HIGH_PRIORITY_QUEUE = 'edx.core.high'
DEFAULT_PRIORITY_QUEUE = 'edx.core.default'
LOW_PRIORITY_QUEUE = 'edx.core.low'
CELERY_QUEUE_HA_POLICY = 'all'
CELERY_CREATE_MISSING_QUEUES = True
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############################ APPS #####################################
INSTALLED_APPS = (
@@ -249,8 +308,12 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'djcelery',
'south',
# Monitor the status of services
'service_status',
# For CMS
'contentstore',
'auth',
@@ -261,7 +324,26 @@ INSTALLED_APPS = (
'track',
# For asset pipelining
'mitxmako',
'pipeline',
'staticfiles',
'static_replace',
# comment common
'django_comment_common',
)
################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin'
MKTG_URLS = {}
MKTG_URL_LINK_MAP = {
'ABOUT': 'about_edx',
'CONTACT': 'contact',
'FAQ': 'help_edx',
'COURSES': 'courses',
'ROOT': 'root',
'TOS': 'tos',
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}

View File

@@ -1,6 +1,10 @@
"""
This config file runs the simplest dev environment"""
# 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 .common import *
from logsettings import get_logger_config
@@ -18,7 +22,7 @@ modulestore_options = {
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
'render_template': 'mitxmako.shortcuts.render_to_string'
}
MODULESTORE = {
@@ -51,6 +55,7 @@ DATABASES = {
}
LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000"
REPOS = {
'edx4edx': {
@@ -59,7 +64,7 @@ REPOS = {
},
'content-mit-6002x': {
'branch': 'master',
#'origin': 'git@github.com:MITx/6002x-fall-2012.git',
# 'origin': 'git@github.com:MITx/6002x-fall-2012.git',
'origin': 'git@github.com:MITx/content-mit-6002x.git',
},
'6.00x': {
@@ -116,10 +121,14 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions
CELERY_ALWAYS_EAGER = True
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = (
@@ -151,5 +160,16 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# Enable URL that shows information about the status of variuous services
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg'
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
from .private import *
except ImportError:
pass

View File

@@ -1,3 +1,7 @@
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# dev environment for ichuang/mit
# FORCE_SCRIPT_NAME = '/cms'

View File

@@ -0,0 +1,39 @@
"""
This config file follows the dev enviroment, but adds the
requirement of a celery worker running in the background to process
celery tasks.
The worker can be executed using:
django_admin.py celery worker
"""
# 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 dev import *
################################# CELERY ######################################
# Requires a separate celery worker
CELERY_ALWAYS_EAGER = False
# Use django db as the broker and result store
BROKER_URL = 'django://'
INSTALLED_APPS += ('djcelery.transport', )
CELERY_RESULT_BACKEND = 'database'
DJKOMBU_POLLING_INTERVAL = 1.0
# Disable transaction management because we are using a worker. Views
# that request a task and wait for the result will deadlock otherwise.
MIDDLEWARE_CLASSES = tuple(
c for c in MIDDLEWARE_CLASSES
if c != 'django.middleware.transaction.TransactionMiddleware')
# Note: other alternatives for disabling transactions don't work in 1.4
# https://code.djangoproject.com/ticket/2304
# https://code.djangoproject.com/ticket/16039

View File

@@ -2,6 +2,10 @@
This configuration is used for running jasmine 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 *
from logsettings import get_logger_config
@@ -32,8 +36,13 @@ PIPELINE_JS['spec'] = {
}
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/cms/jasmine')
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
# Remove the localization middleware class because it requires the test database
# to be sync'd and migrated in order to run the jasmine tests interactively
@@ -41,4 +50,4 @@ STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
if e != 'django.middleware.locale.LocaleMiddleware')
INSTALLED_APPS += ('django_jasmine', )
INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')

View File

@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo
/log # Where we're going to write log files
"""
# 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 .common import *
import os
from path import path
@@ -41,14 +46,14 @@ MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
'collection': 'test_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string'
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
@@ -77,6 +82,7 @@ DATABASES = {
}
LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview"
CACHES = {
# This is the cache used for most things. Askbot will not work without a
@@ -108,8 +114,14 @@ CACHES = {
}
}
################################# CELERY ######################################
CELERY_ALWAYS_EAGER = True
CELERY_RESULT_BACKEND = 'cache'
BROKER_TRANSPORT = 'memory'
################### Make tests faster
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
# http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
@@ -121,3 +133,4 @@ SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True

11
cms/pydev_manage.py Normal file
View File

@@ -0,0 +1,11 @@
'''
Used for pydev eclipse. Should be innocuous for everyone else.
Created on May 8, 2013
@author: dmitchell
'''
#!/home/<username>/mitx_all/python/bin/python
from django.core import management
if __name__ == '__main__':
management.execute_from_command_line()

View File

@@ -11,11 +11,11 @@
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
<i class="icon-caret-down ui-toggle-expansion"></i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
<i class="icon-ok"></i>
</span>
</header>
@@ -58,4 +58,4 @@
<% taskIndex+=1; }) %>
</ul>
</section>
</section>

View File

@@ -8,6 +8,8 @@
"js/vendor/json2.js",
"js/vendor/underscore-min.js",
"js/vendor/backbone-min.js",
"js/vendor/jquery.leanModal.min.js"
"js/vendor/jquery.leanModal.min.js",
"js/vendor/sinon-1.7.1.js",
"js/test/i18n.js"
]
}

View File

@@ -0,0 +1 @@
../../../templates/js/metadata-editor.underscore

View File

@@ -0,0 +1 @@
../../../templates/js/metadata-number-entry.underscore

View File

@@ -0,0 +1 @@
../../../templates/js/metadata-option-entry.underscore

View File

@@ -0,0 +1 @@
../../../templates/js/metadata-string-entry.underscore

View File

@@ -0,0 +1 @@
../../../templates/js/section-name-edit.underscore

View File

@@ -0,0 +1 @@
../../../templates/js/system-feedback.underscore

View File

@@ -1,3 +1,5 @@
jasmine.getFixtures().fixturesPath = 'fixtures'
# Stub jQuery.cookie
@stubCookies =
csrftoken: "stubCSRFToken"

View File

@@ -22,3 +22,37 @@ describe "main helper", ->
it "setup AJAX CSRF token", ->
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
describe "AJAX Errors", ->
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
appendSetFixtures(sandbox({id: "page-notification"}))
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "successful AJAX request does not pop an error notification", ->
expect($("#page-notification")).toBeEmpty()
$.ajax("/test")
expect($("#page-notification")).toBeEmpty()
@requests[0].respond(200)
expect($("#page-notification")).toBeEmpty()
it "AJAX request with error should pop an error notification", ->
$.ajax("/test")
@requests[0].respond(500)
expect($("#page-notification")).not.toBeEmpty()
expect($("#page-notification")).toContain('div.wrapper-notification-error')
it "can override AJAX request with error so it does not pop an error notification", ->
$.ajax
url: "/test"
notifyOnError: false
@requests[0].respond(500)
expect($("#page-notification")).toBeEmpty()

View File

@@ -0,0 +1,34 @@
describe "CMS.Models.SystemFeedback", ->
beforeEach ->
@model = new CMS.Models.SystemFeedback()
it "should have an empty message by default", ->
expect(@model.get("message")).toEqual("")
it "should have an empty title by default", ->
expect(@model.get("title")).toEqual("")
it "should not have an intent set by default", ->
expect(@model.get("intent")).toBeNull()
describe "CMS.Models.WarningMessage", ->
beforeEach ->
@model = new CMS.Models.WarningMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("warning")
describe "CMS.Models.ErrorMessage", ->
beforeEach ->
@model = new CMS.Models.ErrorMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("error")
describe "CMS.Models.ConfirmationMessage", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("confirmation")

View File

@@ -0,0 +1,58 @@
describe "CMS.Models.Metadata", ->
it "knows when the value has not been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isModified()).toBeFalsy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('original')
expect(model.isModified()).toBeFalsy()
it "knows when the value has been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
model.setValue('original')
expect(model.isModified()).toBeTruthy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('modified')
expect(model.isModified()).toBeTruthy()
it "tracks when values have been explicitly set", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isExplicitlySet()).toBeFalsy()
model.setValue('original')
expect(model.isExplicitlySet()).toBeTruthy()
it "has both 'display value' and a 'value' methods", ->
model = new CMS.Models.Metadata(
{'value': 'default', 'explicitly_set': false})
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
model.setValue('modified')
expect(model.getValue()).toBe('modified')
expect(model.getDisplayValue()).toBe('modified')
it "has a clear method for reverting to the default", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
model.clear()
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
expect(model.isExplicitlySet()).toBeFalsy()
it "has a getter for field name", ->
model = new CMS.Models.Metadata({'field_name': 'foo'})
expect(model.getFieldName()).toBe('foo')
it "has a getter for options", ->
model = new CMS.Models.Metadata({'options': ['foo', 'bar']})
expect(model.getOptions()).toEqual(['foo', 'bar'])
it "has a getter for type", ->
model = new CMS.Models.Metadata({'type': 'Integer'})
expect(model.getType()).toBe(CMS.Models.Metadata.INTEGER_TYPE)

View File

@@ -0,0 +1,53 @@
describe "CMS.Models.Section", ->
describe "basic", ->
beforeEach ->
@model = new CMS.Models.Section({
id: 42,
name: "Life, the Universe, and Everything"
})
it "should take an id argument", ->
expect(@model.get("id")).toEqual(42)
it "should take a name argument", ->
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
it "should have a URL set", ->
expect(@model.url).toEqual("/save_item")
it "should serialize to JSON correctly", ->
expect(@model.toJSON()).toEqual({
id: 42,
metadata: {
display_name: "Life, the Universe, and Everything"
}
})
describe "XHR", ->
beforeEach ->
spyOn(CMS.Models.Section.prototype, 'showNotification')
spyOn(CMS.Models.Section.prototype, 'hideNotification')
@model = new CMS.Models.Section({
id: 42,
name: "Life, the Universe, and Everything"
})
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "show/hide a notification when it saves to the server", ->
@model.save()
expect(CMS.Models.Section.prototype.showNotification).toHaveBeenCalled()
@requests[0].respond(200)
expect(CMS.Models.Section.prototype.hideNotification).toHaveBeenCalled()
it "don't hide notification when saving fails", ->
# this is handled by the global AJAX error handler
@model.save()
@requests[0].respond(500)
expect(CMS.Models.Section.prototype.hideNotification).not.toHaveBeenCalled()

View File

@@ -0,0 +1,179 @@
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures(sandbox({id: "page-alert"}))
appendSetFixtures(sandbox({id: "page-notification"}))
appendSetFixtures(sandbox({id: "page-prompt"}))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
@addMatchers
toBeShown: ->
@actual.hasClass("is-shown") and not @actual.hasClass("is-hiding")
toBeHiding: ->
@actual.hasClass("is-hiding") and not @actual.hasClass("is-shown")
toContainText: (text) ->
# remove this when we upgrade jasmine-jquery
trimmedText = $.trim(@actual.text())
if text and $.isFunction(text.test)
return text.test(trimmedText)
else
return trimmedText.indexOf(text) != -1;
describe "CMS.Views.Alert as base class", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage({
title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
})
# it will be interesting to see when this.render is called, so lets spy on it
spyOn(CMS.Views.Alert.prototype, 'render').andCallThrough()
it "renders on initalize", ->
view = new CMS.Views.Alert({model: @model})
expect(view.render).toHaveBeenCalled()
it "renders the template", ->
view = new CMS.Views.Alert({model: @model})
expect(view.$(".action-close")).toBeDefined()
expect(view.$('.wrapper')).toBeShown()
expect(view.$el).toContainText(@model.get("title"))
expect(view.$el).toContainText(@model.get("message"))
it "close button sends a .hide() message", ->
spyOn(CMS.Views.Alert.prototype, 'hide').andCallThrough()
view = new CMS.Views.Alert({model: @model})
view.$(".action-close").click()
expect(CMS.Views.Alert.prototype.hide).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage({
title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
})
# for some reason, expect($("body")) blows up the test runner, so this test
# just exercises the Prompt rather than asserting on anything. Best I can
# do for now. :(
it "changes class on body", ->
# expect($("body")).not.toHaveClass("prompt-is-shown")
view = new CMS.Views.Prompt({model: @model})
# expect($("body")).toHaveClass("prompt-is-shown")
view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown")
describe "CMS.Views.Alert click events", ->
beforeEach ->
@model = new CMS.Models.WarningMessage(
title: "Unsaved",
message: "Your content is currently Unsaved.",
actions:
primary:
text: "Save",
class: "save-button",
click: jasmine.createSpy('primaryClick')
secondary: [{
text: "Revert",
class: "cancel-button",
click: jasmine.createSpy('secondaryClick')
}]
)
@view = new CMS.Views.Alert({model: @model})
it "should trigger the primary event on a primary click", ->
@view.primaryClick()
expect(@model.get('actions').primary.click).toHaveBeenCalled()
it "should trigger the secondary event on a secondary click", ->
@view.secondaryClick()
expect(@model.get('actions').secondary[0].click).toHaveBeenCalled()
it "should apply class to primary action", ->
expect(@view.$(".action-primary")).toHaveClass("save-button")
it "should apply class to secondary action", ->
expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
describe "CMS.Views.Notification minShown and maxShown", ->
beforeEach ->
@model = new CMS.Models.SystemFeedback(
intent: "saving"
title: "Saving"
)
spyOn(CMS.Views.Notification.prototype, 'show').andCallThrough()
spyOn(CMS.Views.Notification.prototype, 'hide').andCallThrough()
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "a minShown view should not hide too quickly", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# call hide() on it, but the minShown should prevent it from hiding right away
view.hide()
expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again
@clock.tick(1001)
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view should hide by itself", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait for the maxShown timeout to expire, and check again
@clock.tick(1001)
expect(view.$('.wrapper')).toBeHiding()
it "a minShown view can stay visible longer", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again
@clock.tick(1001)
expect(CMS.Views.Notification.prototype.hide).not.toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# can now hide immediately
view.hide()
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view can hide early", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait 50 milliseconds, and hide it early
@clock.tick(50)
view.hide()
expect(view.$('.wrapper')).toBeHiding()
# wait for timeout to expire, make sure it doesn't do anything weird
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()
it "a view can have both maxShown and minShown", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000, maxShown: 2000})
# can't hide early
@clock.tick(50)
view.hide()
expect(view.$('.wrapper')).toBeShown()
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()
# show it again, and let it hide automatically
view.show()
@clock.tick(1050)
expect(view.$('.wrapper')).toBeShown()
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()

View File

@@ -0,0 +1,300 @@
describe "Test Metadata Editor", ->
editorTemplate = readFixtures('metadata-editor.underscore')
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
beforeEach ->
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
genericEntry = {
default_value: 'default value',
display_name: "Display Name",
explicitly_set: true,
field_name: "display_name",
help: "Specifies the name for this component.",
inheritable: false,
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
value: "Word cloud"
}
selectEntry = {
default_value: "answered",
display_name: "Show Answer",
explicitly_set: false,
field_name: "show_answer",
help: "When should you show the answer",
inheritable: true,
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}
],
type: CMS.Models.Metadata.SELECT_TYPE,
value: "always"
}
integerEntry = {
default_value: 5,
display_name: "Inputs",
explicitly_set: false,
field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.",
inheritable: false,
options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE,
value: 5
}
floatEntry = {
default_value: 2.7,
display_name: "Weight",
explicitly_set: true,
field_name: "weight",
help: "Weight for this problem",
inheritable: true,
options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE,
value: 10.2
}
# Test for the editor that creates the individual views.
describe "CMS.Views.Metadata.Editor creates editors for each field", ->
beforeEach ->
@model = new CMS.Models.MetadataCollection(
[
integerEntry,
floatEntry,
selectEntry,
genericEntry,
{
default_value: null,
display_name: "Unknown",
explicitly_set: true,
field_name: "unknown_type",
help: "Mystery property.",
inheritable: false,
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}],
type: "unknown type",
value: null
}
]
)
it "creates child views on initialize, and sorts them alphabetically", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
childModels = view.collection.models
expect(childModels.length).toBe(5)
childViews = view.$el.find('.setting-input')
expect(childViews.length).toBe(5)
verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name)
expect(childViews[index].type).toBe(type)
verifyEntry(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number')
verifyEntry(2, 'Show Answer', 'select-one')
verifyEntry(3, 'Unknown', 'text')
verifyEntry(4, 'Weight', 'number')
it "returns its display name", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
expect(view.getDisplayName()).toBe("Word cloud")
it "returns an empty string if there is no display name property with a valid value", ->
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
expect(view.getDisplayName()).toBe("")
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
{
default_value: null,
display_name: "Display Name",
explicitly_set: false,
field_name: "display_name",
help: "",
inheritable: false,
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
value: null
}])
})
expect(view.getDisplayName()).toBe("")
it "has no modified values by default", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
expect(view.getModifiedMetadataValues()).toEqual({})
it "returns modified values only", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
childModels = view.collection.models
childModels[0].setValue('updated display name')
childModels[1].setValue(20)
expect(view.getModifiedMetadataValues()).toEqual({
display_name : 'updated display name',
num_inputs: 20
})
# Tests for individual views.
assertInputType = (view, expectedType) ->
input = view.$el.find('.setting-input')
expect(input.length).toBe(1)
expect(input[0].type).toBe(expectedType)
assertValueInView = (view, expectedValue) ->
expect(view.getValueFromEditor()).toBe(expectedValue)
assertCanUpdateView = (view, newValue) ->
view.setValueInEditor(newValue)
expect(view.getValueFromEditor()).toBe(newValue)
assertClear = (view, modelValue, editorValue=modelValue) ->
view.clear()
expect(view.model.getValue()).toBe(null)
expect(view.model.getDisplayValue()).toBe(modelValue)
expect(view.getValueFromEditor()).toBe(editorValue)
assertUpdateModel = (view, originalValue, newValue) ->
view.setValueInEditor(newValue)
expect(view.model.getValue()).toBe(originalValue)
view.updateModel()
expect(view.model.getValue()).toBe(newValue)
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(genericEntry)
@view = new CMS.Views.Metadata.String({model: model})
it "uses a text input type", ->
assertInputType(@view, 'text')
it "returns the intial value upon initialization", ->
assertValueInView(@view, 'Word cloud')
it "can update its value in the view", ->
assertCanUpdateView(@view, "updated ' \" &")
it "has a clear method to revert to the model default", ->
assertClear(@view, 'default value')
it "has an update model method", ->
assertUpdateModel(@view, 'Word cloud', 'updated')
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(selectEntry)
@view = new CMS.Views.Metadata.Option({model: model})
it "uses a select input type", ->
assertInputType(@view, 'select-one')
it "returns the intial value upon initialization", ->
assertValueInView(@view, 'always')
it "can update its value in the view", ->
assertCanUpdateView(@view, "never")
it "has a clear method to revert to the model default", ->
assertClear(@view, 'answered')
it "has an update model method", ->
assertUpdateModel(@view, null, 'never')
it "does not update to a value that is not an option", ->
@view.setValueInEditor("not an option")
expect(@view.getValueFromEditor()).toBe('always')
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
beforeEach ->
integerModel = new CMS.Models.Metadata(integerEntry)
@integerView = new CMS.Views.Metadata.Number({model: integerModel})
floatModel = new CMS.Models.Metadata(floatEntry)
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
it "uses a number input type", ->
assertInputType(@integerView, 'number')
assertInputType(@floatView, 'number')
it "returns the intial value upon initialization", ->
assertValueInView(@integerView, '5')
assertValueInView(@floatView, '10.2')
it "can update its value in the view", ->
assertCanUpdateView(@integerView, "12")
assertCanUpdateView(@floatView, "-2.4")
it "has a clear method to revert to the model default", ->
assertClear(@integerView, 5, '5')
assertClear(@floatView, 2.7, '2.7')
it "has an update model method", ->
assertUpdateModel(@integerView, null, '90')
assertUpdateModel(@floatView, 10.2, '-9.5')
it "knows the difference between integer and float", ->
expect(@integerView.isIntegerField()).toBeTruthy()
expect(@floatView.isIntegerField()).toBeFalsy()
it "sets attribtues related to min, max, and step", ->
verifyAttributes = (view, min, step, max=null) ->
inputEntry = view.$el.find('input')
expect(Number(inputEntry.attr('min'))).toEqual(min)
expect(Number(inputEntry.attr('step'))).toEqual(step)
if max is not null
expect(Number(inputEntry.attr('max'))).toEqual(max)
verifyAttributes(@integerView, 1, 1)
verifyAttributes(@floatView, 1.3, .1, 100.2)
it "corrects values that are out of range", ->
verifyValueAfterChanged = (view, value, expectedResult) ->
view.setValueInEditor(value)
view.changed()
expect(view.getValueFromEditor()).toBe(expectedResult)
verifyValueAfterChanged(@integerView, '-4', '1')
verifyValueAfterChanged(@integerView, '1', '1')
verifyValueAfterChanged(@integerView, '0', '1')
verifyValueAfterChanged(@integerView, '3001', '3001')
verifyValueAfterChanged(@floatView, '-4', '1.3')
verifyValueAfterChanged(@floatView, '1.3', '1.3')
verifyValueAfterChanged(@floatView, '1.2', '1.3')
verifyValueAfterChanged(@floatView, '100.2', '100.2')
verifyValueAfterChanged(@floatView, '100.3', '100.2')
it "disallows invalid characters", ->
verifyValueAfterKeyPressed = (view, character, reject) ->
event = {
type : 'keypress',
which : character.charCodeAt(0),
keyCode: character.charCodeAt(0),
preventDefault : () -> 'no op'
}
spyOn(event, 'preventDefault')
view.$el.find('input').trigger(event)
if (reject)
expect(event.preventDefault).toHaveBeenCalled()
else
expect(event.preventDefault).not.toHaveBeenCalled()
verifyDisallowedChars = (view) ->
verifyValueAfterKeyPressed(view, 'a', true)
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
verifyValueAfterKeyPressed(view, '[', true)
verifyValueAfterKeyPressed(view, '@', true)
for i in [0...9]
verifyValueAfterKeyPressed(view, String(i), false)
verifyDisallowedChars(@integerView)
verifyDisallowedChars(@floatView)

View File

@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})

View File

@@ -0,0 +1,85 @@
describe "CMS.Views.SectionShow", ->
describe "Basic", ->
beforeEach ->
spyOn(CMS.Views.SectionShow.prototype, "switchToEditView")
.andCallThrough()
@model = new CMS.Models.Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionShow({model: @model})
@view.render()
it "should contain the model name", ->
expect(@view.$el).toHaveText(@model.get('name'))
it "should call switchToEditView when clicked", ->
@view.$el.click()
expect(@view.switchToEditView).toHaveBeenCalled()
it "should pass the same element to SectionEdit when switching views", ->
spyOn(CMS.Views.SectionEdit.prototype, 'initialize').andCallThrough()
@view.switchToEditView()
expect(CMS.Views.SectionEdit.prototype.initialize).toHaveBeenCalled()
expect(CMS.Views.SectionEdit.prototype.initialize.mostRecentCall.args[0].el).toEqual(@view.el)
describe "CMS.Views.SectionEdit", ->
describe "Basic", ->
tpl = readFixtures('section-name-edit.underscore')
feedback_tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedback_tpl))
spyOn(CMS.Views.SectionEdit.prototype, "switchToShowView")
.andCallThrough()
spyOn(CMS.Views.SectionEdit.prototype, "showInvalidMessage")
.andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@model = new CMS.Models.Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionEdit({model: @model})
@view.render()
afterEach ->
@xhr.restore()
delete window.analytics
delete window.course_location_analytics
it "should have the model name as the default text value", ->
expect(@view.$("input[type=text]").val()).toEqual(@model.get('name'))
it "should call switchToShowView when cancel button is clicked", ->
@view.$("input.cancel-button").click()
expect(@view.switchToShowView).toHaveBeenCalled()
it "should save model when save button is clicked", ->
spyOn(@model, 'save')
@view.$("input[type=submit]").click()
expect(@model.save).toHaveBeenCalled()
it "should call switchToShowView when save() is successful", ->
@view.$("input[type=submit]").click()
@requests[0].respond(200)
expect(@view.switchToShowView).toHaveBeenCalled()
it "should call showInvalidMessage when validation is unsuccessful", ->
spyOn(@model, 'validate').andReturn("BLARRGH")
@view.$("input[type=submit]").click()
expect(@view.showInvalidMessage).toHaveBeenCalledWith(
jasmine.any(Object), "BLARRGH", jasmine.any(Object))
expect(@view.switchToShowView).not.toHaveBeenCalled()
it "should not save when validation is unsuccessful", ->
spyOn(@model, 'validate').andReturn("BLARRGH")
@view.$("input[type=text]").val("changed")
@view.$("input[type=submit]").click()
expect(@model.get('name')).not.toEqual("changed")

View File

@@ -15,6 +15,15 @@ $ ->
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
dataType: 'json'
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
if ajaxSettings.notifyOnError is false
return
msg = new CMS.Models.ErrorMessage(
"title": gettext("Studio's having trouble saving your work")
"message": jqXHR.responseText || gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
)
new CMS.Views.Notification({model: msg})
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i

View File

@@ -1,12 +1,14 @@
class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'li'
className: 'component'
editorMode: 'editor-mode'
events:
"click .component-editor .cancel-button": 'clickCancelButton'
"click .component-editor .save-button": 'clickSaveButton'
"click .component-actions .edit-button": 'clickEditButton'
"click .component-actions .delete-button": 'onDelete'
"click .mode a": 'clickModeButton'
initialize: ->
@onDelete = @options.onDelete
@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: ->
if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
metadataEditor = @$el.find('.metadata_edit')
metadataData = metadataEditor.data('metadata')
models = [];
for key of metadataData
models.push(metadataData[key])
@metadataEditor = new CMS.Views.Metadata.Editor({
el: metadataEditor,
collection: new CMS.Models.MetadataCollection(models)
})
metadata: ->
# cdodge: package up metadata which is separated into a number of input fields
# there's probably a better way to do this, but at least this lets me continue to move onwards
_metadata = {}
# Need to update set "active" class on data editor if there is one.
# If we are only showing settings, hide the data editor controls and update settings accordingly.
if @hasDataEditor()
@selectMode(@editorMode)
else
@hideDataEditor()
$metadata = @$component_editor().find('.metadata_edit')
if $metadata
# walk through the set of elments which have the 'xmetadata_name' attribute and
# build up a object to pass back to the server on the subsequent POST
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
return _metadata
title = interpolate(gettext('<em>Editing:</em> %s'),
[@metadataEditor.getDisplayName()])
@$el.find('.component-name').html(title)
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
return @metadataEditor.getModifiedMetadataValues()
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
@render()
@$el.removeClass('editing')
).fail( ->
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
)
clickCancelButton: (event) ->
@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
$modalCover.show().addClass('is-fixed')
@$component_editor().slideDown(150)
@loadEdit()
clickModeButton: (event) ->
event.preventDefault()
if not @hasDataEditor()
return
@selectMode(event.currentTarget.parentElement.id)
hasDataEditor: =>
return @$el.find('.wrapper-comp-editor').length > 0
selectMode: (mode) =>
dataEditor = @$el.find('.wrapper-comp-editor')
settingsEditor = @$el.find('.wrapper-comp-settings')
editorModeButton = @$el.find('#editor-mode').find("a")
settingsModeButton = @$el.find('#settings-mode').find("a")
if mode == @editorMode
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
# we have to use a class of is-inactive instead of is-active.
dataEditor.removeClass('is-inactive')
editorModeButton.addClass('is-set')
settingsEditor.removeClass('is-active')
settingsModeButton.removeClass('is-set')
else
dataEditor.addClass('is-inactive')
editorModeButton.removeClass('is-set')
settingsEditor.addClass('is-active')
settingsModeButton.addClass('is-set')
hideDataEditor: =>
editorModeButtonParent = @$el.find('#editor-mode')
editorModeButtonParent.addClass('inactive-mode')
editorModeButtonParent.removeClass('active-mode')
@$el.find('.wrapper-comp-settings').addClass('is-active')
@$el.find('#settings-mode').find("a").addClass('is-set')

View File

@@ -1,6 +1,7 @@
class CMS.Views.UnitEdit extends Backbone.View
events:
'click .new-component .new-component-type a': 'showComponentTemplates'
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
'click .new-component .new-component-type a.single-template': 'saveNewComponent'
'click .new-component .cancel-button': 'closeNewComponent'
'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent'

BIN
cms/static/fonts/vendor/FontAwesome.otf vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,339 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="fontawesomeregular" horiz-adv-x="1536" >
<font-face units-per-em="1792" ascent="1536" descent="-256" />
<missing-glyph horiz-adv-x="448" />
<glyph unicode=" " horiz-adv-x="448" />
<glyph unicode="&#x09;" horiz-adv-x="448" />
<glyph unicode="&#xa0;" horiz-adv-x="448" />
<glyph unicode="&#xa8;" horiz-adv-x="1792" />
<glyph unicode="&#xa9;" horiz-adv-x="1792" />
<glyph unicode="&#xae;" horiz-adv-x="1792" />
<glyph unicode="&#xb4;" horiz-adv-x="1792" />
<glyph unicode="&#xc6;" horiz-adv-x="1792" />
<glyph unicode="&#x2000;" horiz-adv-x="768" />
<glyph unicode="&#x2001;" />
<glyph unicode="&#x2002;" horiz-adv-x="768" />
<glyph unicode="&#x2003;" />
<glyph unicode="&#x2004;" horiz-adv-x="512" />
<glyph unicode="&#x2005;" horiz-adv-x="384" />
<glyph unicode="&#x2006;" horiz-adv-x="256" />
<glyph unicode="&#x2007;" horiz-adv-x="256" />
<glyph unicode="&#x2008;" horiz-adv-x="192" />
<glyph unicode="&#x2009;" horiz-adv-x="307" />
<glyph unicode="&#x200a;" horiz-adv-x="85" />
<glyph unicode="&#x202f;" horiz-adv-x="307" />
<glyph unicode="&#x205f;" horiz-adv-x="384" />
<glyph unicode="&#x2122;" horiz-adv-x="1792" />
<glyph unicode="&#x221e;" horiz-adv-x="1792" />
<glyph unicode="&#x2260;" horiz-adv-x="1792" />
<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#xf000;" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" />
<glyph unicode="&#xf001;" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf002;" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" />
<glyph unicode="&#xf003;" horiz-adv-x="1792" d="M1664 32v768q-32 -36 -69 -66q-268 -206 -426 -338q-51 -43 -83 -67t-86.5 -48.5t-102.5 -24.5h-1h-1q-48 0 -102.5 24.5t-86.5 48.5t-83 67q-158 132 -426 338q-37 30 -69 66v-768q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1664 1083v11v13.5t-0.5 13 t-3 12.5t-5.5 9t-9 7.5t-14 2.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5q0 -168 147 -284q193 -152 401 -317q6 -5 35 -29.5t46 -37.5t44.5 -31.5t50.5 -27.5t43 -9h1h1q20 0 43 9t50.5 27.5t44.5 31.5t46 37.5t35 29.5q208 165 401 317q54 43 100.5 115.5t46.5 131.5z M1792 1120v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf004;" horiz-adv-x="1792" d="M896 -128q-26 0 -44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124t127 -344q0 -221 -229 -450l-623 -600 q-18 -18 -44 -18z" />
<glyph unicode="&#xf005;" horiz-adv-x="1664" d="M1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -21 -10.5 -35.5t-30.5 -14.5q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455 l502 -73q56 -9 56 -46z" />
<glyph unicode="&#xf006;" horiz-adv-x="1664" d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -50 -41 -50q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500 l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455l502 -73q56 -9 56 -46z" />
<glyph unicode="&#xf007;" horiz-adv-x="1408" d="M1408 131q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q9 0 42 -21.5t74.5 -48t108 -48t133.5 -21.5t133.5 21.5t108 48t74.5 48t42 21.5q61 0 111.5 -20t85.5 -53.5t62 -81 t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" />
<glyph unicode="&#xf008;" horiz-adv-x="1920" d="M384 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 320v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 704v128q0 26 -19 45t-45 19h-128 q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 -64v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM384 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45 t45 -19h128q26 0 45 19t19 45zM1792 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 704v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1792 320v128 q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 704v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19 t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1920 1248v-1344q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1344q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf009;" horiz-adv-x="1664" d="M768 512v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM768 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 512v-384q0 -52 -38 -90t-90 -38 h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" />
<glyph unicode="&#xf00a;" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 288v-192q0 -40 -28 -68t-68 -28h-320 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf00b;" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-960 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h960q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf00c;" horiz-adv-x="1792" d="M1671 970q0 -40 -28 -68l-724 -724l-136 -136q-28 -28 -68 -28t-68 28l-136 136l-362 362q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -295l656 657q28 28 68 28t68 -28l136 -136q28 -28 28 -68z" />
<glyph unicode="&#xf00d;" horiz-adv-x="1408" d="M1298 214q0 -40 -28 -68l-136 -136q-28 -28 -68 -28t-68 28l-294 294l-294 -294q-28 -28 -68 -28t-68 28l-136 136q-28 28 -28 68t28 68l294 294l-294 294q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -294l294 294q28 28 68 28t68 -28l136 -136q28 -28 28 -68 t-28 -68l-294 -294l294 -294q28 -28 28 -68z" />
<glyph unicode="&#xf00e;" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-224q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v224h-224q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h224v224q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5v-224h224 q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5 t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" />
<glyph unicode="&#xf010;" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-576q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h576q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5z M1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z " />
<glyph unicode="&#xf011;" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61t-298 61t-245 164t-164 245t-61 298q0 182 80.5 343t226.5 270q43 32 95.5 25t83.5 -50q32 -42 24.5 -94.5t-49.5 -84.5q-98 -74 -151.5 -181t-53.5 -228q0 -104 40.5 -198.5t109.5 -163.5t163.5 -109.5 t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5q0 121 -53.5 228t-151.5 181q-42 32 -49.5 84.5t24.5 94.5q31 43 84 50t95 -25q146 -109 226.5 -270t80.5 -343zM896 1408v-640q0 -52 -38 -90t-90 -38t-90 38t-38 90v640q0 52 38 90t90 38t90 -38t38 -90z" />
<glyph unicode="&#xf012;" horiz-adv-x="1792" d="M256 96v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 224v-320q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 480v-576q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1408 864v-960q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1376v-1472q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1472q0 14 9 23t23 9h192q14 0 23 -9t9 -23z" />
<glyph unicode="&#xf013;" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5t-8 23.5v222q0 12 8 23t19 13 l186 28q14 46 39 92q-40 57 -107 138q-10 12 -10 24q0 10 9 23q26 36 98.5 107.5t94.5 71.5q13 0 26 -10l138 -107q44 23 91 38q16 136 29 186q7 28 36 28h222q14 0 24.5 -8.5t11.5 -21.5l28 -184q49 -16 90 -37l142 107q9 9 24 9q13 0 25 -10q129 -119 165 -170q7 -8 7 -22 q0 -12 -8 -23q-15 -21 -51 -66.5t-54 -70.5q26 -50 41 -98l183 -28q13 -2 21 -12.5t8 -23.5z" />
<glyph unicode="&#xf014;" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832 q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" />
<glyph unicode="&#xf015;" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" />
<glyph unicode="&#xf016;" horiz-adv-x="1280" d="M128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280zM768 896h299l-299 299v-299zM1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h544q40 0 88 -20t76 -48l408 -408q28 -28 48 -76t20 -88z" />
<glyph unicode="&#xf017;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM1024 640q26 0 45 -19t19 -45v-96q0 -26 -19 -45t-45 -19h-416q-26 0 -45 19t-19 45v480q0 26 19 45t45 19h96q26 0 45 -19t19 -45v-320h256z" />
<glyph unicode="&#xf018;" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t26 -33l417 -1044q26 -62 26 -116z" />
<glyph unicode="&#xf019;" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q42 0 59 -39z" />
<glyph unicode="&#xf01a;" d="M1120 608q0 -12 -10 -24l-319 -319q-11 -9 -23 -9t-23 9l-320 320q-15 16 -7 35q8 20 30 20h192v352q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-352h192q14 0 23 -9t9 -23zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273 t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf01b;" d="M1118 660q-8 -20 -30 -20h-192v-352q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v352h-192q-14 0 -23 9t-9 23q0 12 10 24l319 319q11 9 23 9t23 -9l320 -320q15 -16 7 -35zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198 t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf01c;" d="M1023 576h316q-1 3 -2.5 8t-2.5 8l-212 496h-708l-212 -496q-1 -2 -2.5 -8t-2.5 -8h316l95 -192h320zM1536 546v-482q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v482q0 62 25 123l238 552q10 25 36.5 42t52.5 17h832q26 0 52.5 -17t36.5 -42l238 -552 q25 -61 25 -123z" />
<glyph unicode="&#xf01d;" d="M1184 640q0 -37 -32 -55l-544 -320q-15 -9 -32 -9q-16 0 -32 8q-32 19 -32 56v640q0 37 32 56q33 18 64 -1l544 -320q32 -18 32 -55zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf01e;" d="M1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l138 138q-148 137 -349 137q-104 0 -198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5q119 0 225 52t179 147q7 10 23 12q14 0 25 -9 l137 -138q9 -8 9.5 -20.5t-7.5 -22.5q-109 -132 -264 -204.5t-327 -72.5q-156 0 -298 61t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q147 0 284.5 -55.5t244.5 -156.5l130 129q29 31 70 14q39 -17 39 -59z" />
<glyph unicode="&#xf021;" d="M1511 480q0 -5 -1 -7q-64 -268 -268 -434.5t-478 -166.5q-146 0 -282.5 55t-243.5 157l-129 -129q-19 -19 -45 -19t-45 19t-19 45v448q0 26 19 45t45 19h448q26 0 45 -19t19 -45t-19 -45l-137 -137q71 -66 161 -102t187 -36q134 0 250 65t186 179q11 17 53 117 q8 23 30 23h192q13 0 22.5 -9.5t9.5 -22.5zM1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-26 0 -45 19t-19 45t19 45l138 138q-148 137 -349 137q-134 0 -250 -65t-186 -179q-11 -17 -53 -117q-8 -23 -30 -23h-199q-13 0 -22.5 9.5t-9.5 22.5v7q65 268 270 434.5t480 166.5 q146 0 284 -55.5t245 -156.5l130 129q19 19 45 19t45 -19t19 -45z" />
<glyph unicode="&#xf022;" horiz-adv-x="1792" d="M384 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M384 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1536 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5z M1536 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5zM1536 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5 t9.5 -22.5zM1664 160v832q0 13 -9.5 22.5t-22.5 9.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1792 1248v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47 t47 -113z" />
<glyph unicode="&#xf023;" horiz-adv-x="1152" d="M704 512q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5q0 -37 19 -67t51 -47l-69 -229q-5 -15 5 -28t26 -13h192q16 0 26 13t5 28l-69 229q32 17 51 47t19 67zM320 768h512v192q0 106 -75 181t-181 75t-181 -75t-75 -181v-192zM1152 672v-576q0 -40 -28 -68 t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v192q0 184 132 316t316 132t316 -132t132 -316v-192h32q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf024;" horiz-adv-x="1792" d="M320 1280q0 -72 -64 -110v-1266q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v1266q-64 38 -64 110q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -25 -12.5 -38.5t-39.5 -27.5q-215 -116 -369 -116q-61 0 -123.5 22t-108.5 48 t-115.5 48t-142.5 22q-192 0 -464 -146q-17 -9 -33 -9q-26 0 -45 19t-19 45v742q0 32 31 55q21 14 79 43q236 120 421 120q107 0 200 -29t219 -88q38 -19 88 -19q54 0 117.5 21t110 47t88 47t54.5 21q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf025;" horiz-adv-x="1664" d="M1664 650q0 -166 -60 -314l-20 -49l-185 -33q-22 -83 -90.5 -136.5t-156.5 -53.5v-32q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-32q71 0 130 -35.5t93 -95.5l68 12q29 95 29 193q0 148 -88 279t-236.5 209t-315.5 78 t-315.5 -78t-236.5 -209t-88 -279q0 -98 29 -193l68 -12q34 60 93 95.5t130 35.5v32q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v32q-88 0 -156.5 53.5t-90.5 136.5l-185 33l-20 49q-60 148 -60 314q0 151 67 291t179 242.5 t266 163.5t320 61t320 -61t266 -163.5t179 -242.5t67 -291z" />
<glyph unicode="&#xf026;" horiz-adv-x="768" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z" />
<glyph unicode="&#xf027;" horiz-adv-x="1152" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142z" />
<glyph unicode="&#xf028;" horiz-adv-x="1664" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56 29 76 44q74 54 115.5 135.5t41.5 173.5t-41.5 173.5 t-115.5 135.5q-20 15 -76 44q-39 20 -39 59q0 26 19 45t45 19q13 0 26 -5q140 -59 225 -188.5t85 -282.5zM1664 640q0 -230 -127 -422.5t-338 -283.5q-13 -5 -26 -5q-26 0 -45 19t-19 45q0 36 39 59q7 4 22.5 10.5t22.5 10.5q46 25 82 51q123 91 192 227t69 289t-69 289 t-192 227q-36 26 -82 51q-7 4 -22.5 10.5t-22.5 10.5q-39 23 -39 59q0 26 19 45t45 19q13 0 26 -5q211 -91 338 -283.5t127 -422.5z" />
<glyph unicode="&#xf029;" horiz-adv-x="1408" d="M384 384v-128h-128v128h128zM384 1152v-128h-128v128h128zM1152 1152v-128h-128v128h128zM128 129h384v383h-384v-383zM128 896h384v384h-384v-384zM896 896h384v384h-384v-384zM640 640v-640h-640v640h640zM1152 128v-128h-128v128h128zM1408 128v-128h-128v128h128z M1408 640v-384h-384v128h-128v-384h-128v640h384v-128h128v128h128zM640 1408v-640h-640v640h640zM1408 1408v-640h-640v640h640z" />
<glyph unicode="&#xf02a;" horiz-adv-x="1792" d="M63 0h-63v1408h63v-1408zM126 1h-32v1407h32v-1407zM220 1h-31v1407h31v-1407zM377 1h-31v1407h31v-1407zM534 1h-62v1407h62v-1407zM660 1h-31v1407h31v-1407zM723 1h-31v1407h31v-1407zM786 1h-31v1407h31v-1407zM943 1h-63v1407h63v-1407zM1100 1h-63v1407h63v-1407z M1226 1h-63v1407h63v-1407zM1352 1h-63v1407h63v-1407zM1446 1h-63v1407h63v-1407zM1635 1h-94v1407h94v-1407zM1698 1h-32v1407h32v-1407zM1792 0h-63v1408h63v-1408z" />
<glyph unicode="&#xf02b;" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91z" />
<glyph unicode="&#xf02c;" horiz-adv-x="1920" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91zM1899 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-36 0 -59 14t-53 45l470 470q37 37 37 90q0 52 -37 91l-715 714q-38 38 -102 64.5t-117 26.5h224q53 0 117 -26.5t102 -64.5l715 -714q37 -39 37 -91z" />
<glyph unicode="&#xf02d;" horiz-adv-x="1664" d="M1639 1058q40 -57 18 -129l-275 -906q-19 -64 -76.5 -107.5t-122.5 -43.5h-923q-77 0 -148.5 53.5t-99.5 131.5q-24 67 -2 127q0 4 3 27t4 37q1 8 -3 21.5t-3 19.5q2 11 8 21t16.5 23.5t16.5 23.5q23 38 45 91.5t30 91.5q3 10 0.5 30t-0.5 28q3 11 17 28t17 23 q21 36 42 92t25 90q1 9 -2.5 32t0.5 28q4 13 22 30.5t22 22.5q19 26 42.5 84.5t27.5 96.5q1 8 -3 25.5t-2 26.5q2 8 9 18t18 23t17 21q8 12 16.5 30.5t15 35t16 36t19.5 32t26.5 23.5t36 11.5t47.5 -5.5l-1 -3q38 9 51 9h761q74 0 114 -56t18 -130l-274 -906 q-36 -119 -71.5 -153.5t-128.5 -34.5h-869q-27 0 -38 -15q-11 -16 -1 -43q24 -70 144 -70h923q29 0 56 15.5t35 41.5l300 987q7 22 5 57q38 -15 59 -43zM575 1056q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5 t-16.5 -22.5zM492 800q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5t-16.5 -22.5z" />
<glyph unicode="&#xf02e;" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
<glyph unicode="&#xf02f;" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" />
<glyph unicode="&#xf030;" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
<glyph unicode="&#xf031;" horiz-adv-x="1664" d="M725 977l-170 -450q73 -1 153.5 -2t119 -1.5t52.5 -0.5l29 2q-32 95 -92 241q-53 132 -92 211zM21 -128h-21l2 79q22 7 80 18q89 16 110 31q20 16 48 68l237 616l280 724h75h53l11 -21l205 -480q103 -242 124 -297q39 -102 96 -235q26 -58 65 -164q24 -67 65 -149 q22 -49 35 -57q22 -19 69 -23q47 -6 103 -27q6 -39 6 -57q0 -14 -1 -26q-80 0 -192 8q-93 8 -189 8q-79 0 -135 -2l-200 -11l-58 -2q0 45 4 78l131 28q56 13 68 23q12 12 12 27t-6 32l-47 114l-92 228l-450 2q-29 -65 -104 -274q-23 -64 -23 -84q0 -31 17 -43 q26 -21 103 -32q3 0 13.5 -2t30 -5t40.5 -6q1 -28 1 -58q0 -17 -2 -27q-66 0 -349 20l-48 -8q-81 -14 -167 -14z" />
<glyph unicode="&#xf032;" horiz-adv-x="1408" d="M555 15q76 -32 140 -32q131 0 216 41t122 113q38 70 38 181q0 114 -41 180q-58 94 -141 126q-80 32 -247 32q-74 0 -101 -10v-144l-1 -173l3 -270q0 -15 12 -44zM541 761q43 -7 109 -7q175 0 264 65t89 224q0 112 -85 187q-84 75 -255 75q-52 0 -130 -13q0 -44 2 -77 q7 -122 6 -279l-1 -98q0 -43 1 -77zM0 -128l2 94q45 9 68 12q77 12 123 31q17 27 21 51q9 66 9 194l-2 497q-5 256 -9 404q-1 87 -11 109q-1 4 -12 12q-18 12 -69 15q-30 2 -114 13l-4 83l260 6l380 13l45 1q5 0 14 0.5t14 0.5q1 0 21.5 -0.5t40.5 -0.5h74q88 0 191 -27 q43 -13 96 -39q57 -29 102 -76q44 -47 65 -104t21 -122q0 -70 -32 -128t-95 -105q-26 -20 -150 -77q177 -41 267 -146q92 -106 92 -236q0 -76 -29 -161q-21 -62 -71 -117q-66 -72 -140 -108q-73 -36 -203 -60q-82 -15 -198 -11l-197 4q-84 2 -298 -11q-33 -3 -272 -11z" />
<glyph unicode="&#xf033;" horiz-adv-x="1024" d="M0 -126l17 85q4 1 77 20q76 19 116 39q29 37 41 101l27 139l56 268l12 64q8 44 17 84.5t16 67t12.5 46.5t9 30.5t3.5 11.5l29 157l16 63l22 135l8 50v38q-41 22 -144 28q-28 2 -38 4l19 103l317 -14q39 -2 73 -2q66 0 214 9q33 2 68 4.5t36 2.5q-2 -19 -6 -38 q-7 -29 -13 -51q-55 -19 -109 -31q-64 -16 -101 -31q-12 -31 -24 -88q-9 -44 -13 -82q-44 -199 -66 -306l-61 -311l-38 -158l-43 -235l-12 -45q-2 -7 1 -27q64 -15 119 -21q36 -5 66 -10q-1 -29 -7 -58q-7 -31 -9 -41q-18 0 -23 -1q-24 -2 -42 -2q-9 0 -28 3q-19 4 -145 17 l-198 2q-41 1 -174 -11q-74 -7 -98 -9z" />
<glyph unicode="&#xf034;" horiz-adv-x="1792" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l215 -1h293l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -42.5 2t-103.5 -1t-111 -1 q-34 0 -67 -5q-10 -97 -8 -136l1 -152v-332l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-88 0 -233 -14q-48 -4 -70 -4q-2 22 -2 26l-1 26v9q21 33 79 49 q139 38 159 50q9 21 12 56q8 192 6 433l-5 428q-1 62 -0.5 118.5t0.5 102.5t-2 57t-6 15q-6 5 -14 6q-38 6 -148 6q-43 0 -100 -13.5t-73 -24.5q-13 -9 -22 -33t-22 -75t-24 -84q-6 -19 -19.5 -32t-20.5 -13q-44 27 -56 44v297v86zM1744 128q33 0 42 -18.5t-11 -44.5 l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80z" />
<glyph unicode="&#xf035;" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l446 -1h318l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -58.5 2t-138.5 -1t-128 -1 q-94 0 -127 -5q-10 -97 -8 -136l1 -152v52l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-82 0 -233 -13q-45 -5 -70 -5q-2 22 -2 26l-1 26v9q21 33 79 49 q139 38 159 50q9 21 12 56q6 137 6 433l-5 44q0 265 -2 278q-2 11 -6 15q-6 5 -14 6q-38 6 -148 6q-50 0 -168.5 -14t-132.5 -24q-13 -9 -22 -33t-22 -75t-24 -84q-6 -19 -19.5 -32t-20.5 -13q-44 27 -56 44v297v86zM1505 113q26 -20 26 -49t-26 -49l-162 -126 q-26 -20 -44.5 -11t-18.5 42v80h-1024v-80q0 -33 -18.5 -42t-44.5 11l-162 126q-26 20 -26 49t26 49l162 126q26 20 44.5 11t18.5 -42v-80h1024v80q0 33 18.5 42t44.5 -11z" />
<glyph unicode="&#xf036;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf037;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf038;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf039;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf03a;" horiz-adv-x="1792" d="M256 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM256 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5 t9.5 -22.5zM256 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344 q13 0 22.5 -9.5t9.5 -22.5zM256 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192 q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5z" />
<glyph unicode="&#xf03b;" horiz-adv-x="1792" d="M384 992v-576q0 -13 -9.5 -22.5t-22.5 -9.5q-14 0 -23 9l-288 288q-9 9 -9 23t9 23l288 288q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" />
<glyph unicode="&#xf03c;" horiz-adv-x="1792" d="M352 704q0 -14 -9 -23l-288 -288q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v576q0 13 9.5 22.5t22.5 9.5q14 0 23 -9l288 -288q9 -9 9 -23zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" />
<glyph unicode="&#xf03d;" horiz-adv-x="1792" d="M1792 1184v-1088q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-403 403v-166q0 -119 -84.5 -203.5t-203.5 -84.5h-704q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h704q119 0 203.5 -84.5t84.5 -203.5v-165l403 402q18 19 45 19q12 0 25 -5 q39 -17 39 -59z" />
<glyph unicode="&#xf03e;" horiz-adv-x="1920" d="M640 960q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5v1216 q0 13 -9.5 22.5t-22.5 9.5zM1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf040;" d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928q0 22 -22 22q-10 0 -17 -7l-542 -542q-7 -7 -7 -17q0 -22 22 -22q10 0 17 7l542 542q7 7 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024q0 -53 -37 -90l-166 -166l-416 416l166 165q36 38 90 38 q53 0 91 -38l235 -234q37 -39 37 -91z" />
<glyph unicode="&#xf041;" horiz-adv-x="1024" d="M768 896q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1024 896q0 -109 -33 -179l-364 -774q-16 -33 -47.5 -52t-67.5 -19t-67.5 19t-46.5 52l-365 774q-33 70 -33 179q0 212 150 362t362 150t362 -150t150 -362z" />
<glyph unicode="&#xf042;" d="M768 96v1088q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf043;" horiz-adv-x="1024" d="M512 384q0 36 -20 69q-1 1 -15.5 22.5t-25.5 38t-25 44t-21 50.5q-4 16 -21 16t-21 -16q-7 -23 -21 -50.5t-25 -44t-25.5 -38t-15.5 -22.5q-20 -33 -20 -69q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 512q0 -212 -150 -362t-362 -150t-362 150t-150 362 q0 145 81 275q6 9 62.5 90.5t101 151t99.5 178t83 201.5q9 30 34 47t51 17t51.5 -17t33.5 -47q28 -93 83 -201.5t99.5 -178t101 -151t62.5 -90.5q81 -127 81 -275z" />
<glyph unicode="&#xf044;" horiz-adv-x="1792" d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072q-16 16 -33 -1l-350 -350q-17 -17 -1 -33t33 1l350 350q17 17 1 33zM1408 478v-190q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-14 -14 -32 -8q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v126q0 13 9 22l64 64q15 15 35 7t20 -29zM1312 1216l288 -288l-672 -672h-288v288zM1756 1084l-92 -92 l-288 288l92 92q28 28 68 28t68 -28l152 -152q28 -28 28 -68t-28 -68z" />
<glyph unicode="&#xf045;" horiz-adv-x="1664" d="M1408 547v-259q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h255v0q13 0 22.5 -9.5t9.5 -22.5q0 -27 -26 -32q-77 -26 -133 -60q-10 -4 -16 -4h-112q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832 q66 0 113 47t47 113v214q0 19 18 29q28 13 54 37q16 16 35 8q21 -9 21 -29zM1645 1043l-384 -384q-18 -19 -45 -19q-12 0 -25 5q-39 17 -39 59v192h-160q-323 0 -438 -131q-119 -137 -74 -473q3 -23 -20 -34q-8 -2 -12 -2q-16 0 -26 13q-10 14 -21 31t-39.5 68.5t-49.5 99.5 t-38.5 114t-17.5 122q0 49 3.5 91t14 90t28 88t47 81.5t68.5 74t94.5 61.5t124.5 48.5t159.5 30.5t196.5 11h160v192q0 42 39 59q13 5 25 5q26 0 45 -19l384 -384q19 -19 19 -45t-19 -45z" />
<glyph unicode="&#xf046;" horiz-adv-x="1664" d="M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263 -263l647 647q24 24 57 24t57 -24l110 -110 q24 -24 24 -57t-24 -57z" />
<glyph unicode="&#xf047;" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-384v-384h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v384h-384v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45 t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h384v384h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45t-19 -45t-45 -19h-128v-384h384v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" />
<glyph unicode="&#xf048;" horiz-adv-x="1024" d="M979 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19z" />
<glyph unicode="&#xf049;" horiz-adv-x="1792" d="M1747 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19l710 710 q19 19 32 13t13 -32v-710q4 11 13 19z" />
<glyph unicode="&#xf04a;" horiz-adv-x="1664" d="M1619 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-8 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-19 19 -19 45t19 45l710 710q19 19 32 13t13 -32v-710q5 11 13 19z" />
<glyph unicode="&#xf04b;" horiz-adv-x="1408" d="M1384 609l-1328 -738q-23 -13 -39.5 -3t-16.5 36v1472q0 26 16.5 36t39.5 -3l1328 -738q23 -13 23 -31t-23 -31z" />
<glyph unicode="&#xf04c;" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45zM640 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf04d;" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf04e;" horiz-adv-x="1664" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q19 -19 19 -45t-19 -45l-710 -710q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" />
<glyph unicode="&#xf050;" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" />
<glyph unicode="&#xf051;" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" />
<glyph unicode="&#xf052;" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" />
<glyph unicode="&#xf053;" horiz-adv-x="1152" d="M742 -37l-652 651q-37 37 -37 90.5t37 90.5l652 651q37 37 90.5 37t90.5 -37l75 -75q37 -37 37 -90.5t-37 -90.5l-486 -486l486 -485q37 -38 37 -91t-37 -90l-75 -75q-37 -37 -90.5 -37t-90.5 37z" />
<glyph unicode="&#xf054;" horiz-adv-x="1152" d="M1099 704q0 -52 -37 -91l-652 -651q-37 -37 -90 -37t-90 37l-76 75q-37 39 -37 91q0 53 37 90l486 486l-486 485q-37 39 -37 91q0 53 37 90l76 75q36 38 90 38t90 -38l652 -651q37 -37 37 -90z" />
<glyph unicode="&#xf055;" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf056;" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
<glyph unicode="&#xf057;" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf058;" d="M1284 802q0 28 -18 46l-91 90q-19 19 -45 19t-45 -19l-408 -407l-226 226q-19 19 -45 19t-45 -19l-91 -90q-18 -18 -18 -46q0 -27 18 -45l362 -362q19 -19 45 -19q27 0 46 19l543 543q18 18 18 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf059;" d="M896 160v192q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h192q14 0 23 9t9 23zM1152 832q0 88 -55.5 163t-138.5 116t-170 41q-243 0 -371 -213q-15 -24 8 -42l132 -100q7 -6 19 -6q16 0 25 12q53 68 86 92q34 24 86 24q48 0 85.5 -26t37.5 -59 q0 -38 -20 -61t-68 -45q-63 -28 -115.5 -86.5t-52.5 -125.5v-36q0 -14 9 -23t23 -9h192q14 0 23 9t9 23q0 19 21.5 49.5t54.5 49.5q32 18 49 28.5t46 35t44.5 48t28 60.5t12.5 81zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf05a;" d="M1024 160v160q0 14 -9 23t-23 9h-96v512q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h96v-320h-96q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h448q14 0 23 9t9 23zM896 1056v160q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23 t23 -9h192q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf05b;" d="M1197 512h-109q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h109q-32 108 -112.5 188.5t-188.5 112.5v-109q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v109q-108 -32 -188.5 -112.5t-112.5 -188.5h109q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-109 q32 -108 112.5 -188.5t188.5 -112.5v109q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-109q108 32 188.5 112.5t112.5 188.5zM1536 704v-128q0 -26 -19 -45t-45 -19h-143q-37 -161 -154.5 -278.5t-278.5 -154.5v-143q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v143 q-161 37 -278.5 154.5t-154.5 278.5h-143q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h143q37 161 154.5 278.5t278.5 154.5v143q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-143q161 -37 278.5 -154.5t154.5 -278.5h143q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf05c;" d="M1097 457l-146 -146q-10 -10 -23 -10t-23 10l-137 137l-137 -137q-10 -10 -23 -10t-23 10l-146 146q-10 10 -10 23t10 23l137 137l-137 137q-10 10 -10 23t10 23l146 146q10 10 23 10t23 -10l137 -137l137 137q10 10 23 10t23 -10l146 -146q10 -10 10 -23t-10 -23 l-137 -137l137 -137q10 -10 10 -23t-10 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5 t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf05d;" d="M1171 723l-422 -422q-19 -19 -45 -19t-45 19l-294 294q-19 19 -19 45t19 45l102 102q19 19 45 19t45 -19l147 -147l275 275q19 19 45 19t45 -19l102 -102q19 -19 19 -45t-19 -45zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198 t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf05e;" d="M1312 643q0 161 -87 295l-754 -753q137 -89 297 -89q111 0 211.5 43.5t173.5 116.5t116 174.5t43 212.5zM313 344l755 754q-135 91 -300 91q-148 0 -273 -73t-198 -199t-73 -274q0 -162 89 -299zM1536 643q0 -157 -61 -300t-163.5 -246t-245 -164t-298.5 -61t-298.5 61 t-245 164t-163.5 246t-61 300t61 299.5t163.5 245.5t245 164t298.5 61t298.5 -61t245 -164t163.5 -245.5t61 -299.5z" />
<glyph unicode="&#xf060;" d="M1536 640v-128q0 -53 -32.5 -90.5t-84.5 -37.5h-704l293 -294q38 -36 38 -90t-38 -90l-75 -76q-37 -37 -90 -37q-52 0 -91 37l-651 652q-37 37 -37 90q0 52 37 91l651 650q38 38 91 38q52 0 90 -38l75 -74q38 -38 38 -91t-38 -91l-293 -293h704q52 0 84.5 -37.5 t32.5 -90.5z" />
<glyph unicode="&#xf061;" d="M1472 576q0 -54 -37 -91l-651 -651q-39 -37 -91 -37q-51 0 -90 37l-75 75q-38 38 -38 91t38 91l293 293h-704q-52 0 -84.5 37.5t-32.5 90.5v128q0 53 32.5 90.5t84.5 37.5h704l-293 294q-38 36 -38 90t38 90l75 75q38 38 90 38q53 0 91 -38l651 -651q37 -35 37 -90z" />
<glyph unicode="&#xf062;" horiz-adv-x="1664" d="M1611 565q0 -51 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-294 293v-704q0 -52 -37.5 -84.5t-90.5 -32.5h-128q-53 0 -90.5 32.5t-37.5 84.5v704l-294 -293q-36 -38 -90 -38t-90 38l-75 75q-38 38 -38 90q0 53 38 91l651 651q35 37 90 37q54 0 91 -37l651 -651 q37 -39 37 -91z" />
<glyph unicode="&#xf063;" horiz-adv-x="1664" d="M1611 704q0 -53 -37 -90l-651 -652q-39 -37 -91 -37q-53 0 -90 37l-651 652q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l294 -294v704q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-704l294 294q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" />
<glyph unicode="&#xf064;" horiz-adv-x="1792" d="M1792 896q0 -26 -19 -45l-512 -512q-19 -19 -45 -19t-45 19t-19 45v256h-224q-98 0 -175.5 -6t-154 -21.5t-133 -42.5t-105.5 -69.5t-80 -101t-48.5 -138.5t-17.5 -181q0 -55 5 -123q0 -6 2.5 -23.5t2.5 -26.5q0 -15 -8.5 -25t-23.5 -10q-16 0 -28 17q-7 9 -13 22 t-13.5 30t-10.5 24q-127 285 -127 451q0 199 53 333q162 403 875 403h224v256q0 26 19 45t45 19t45 -19l512 -512q19 -19 19 -45z" />
<glyph unicode="&#xf065;" d="M755 480q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23zM1536 1344v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332 q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf066;" d="M768 576v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45zM1523 1248q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45 t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23z" />
<glyph unicode="&#xf067;" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-416v-416q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v416h-416q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68 -28t28 -68v-416h416q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf068;" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-1216q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h1216q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf069;" horiz-adv-x="1664" d="M1482 486q46 -26 59.5 -77.5t-12.5 -97.5l-64 -110q-26 -46 -77.5 -59.5t-97.5 12.5l-266 153v-307q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v307l-266 -153q-46 -26 -97.5 -12.5t-77.5 59.5l-64 110q-26 46 -12.5 97.5t59.5 77.5l266 154l-266 154 q-46 26 -59.5 77.5t12.5 97.5l64 110q26 46 77.5 59.5t97.5 -12.5l266 -153v307q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-307l266 153q46 26 97.5 12.5t77.5 -59.5l64 -110q26 -46 12.5 -97.5t-59.5 -77.5l-266 -154z" />
<glyph unicode="&#xf06a;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM896 161v190q0 14 -9 23.5t-22 9.5h-192q-13 0 -23 -10t-10 -23v-190q0 -13 10 -23t23 -10h192 q13 0 22 9.5t9 23.5zM894 505l18 621q0 12 -10 18q-10 8 -24 8h-220q-14 0 -24 -8q-10 -6 -10 -18l17 -621q0 -10 10 -17.5t24 -7.5h185q14 0 23.5 7.5t10.5 17.5z" />
<glyph unicode="&#xf06b;" d="M928 180v56v468v192h-320v-192v-468v-56q0 -25 18 -38.5t46 -13.5h192q28 0 46 13.5t18 38.5zM472 1024h195l-126 161q-26 31 -69 31q-40 0 -68 -28t-28 -68t28 -68t68 -28zM1160 1120q0 40 -28 68t-68 28q-43 0 -69 -31l-125 -161h194q40 0 68 28t28 68zM1536 864v-320 q0 -14 -9 -23t-23 -9h-96v-416q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v416h-96q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h440q-93 0 -158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5q107 0 168 -77l128 -165l128 165q61 77 168 77q93 0 158.5 -65.5t65.5 -158.5 t-65.5 -158.5t-158.5 -65.5h440q14 0 23 -9t9 -23z" />
<glyph unicode="&#xf06c;" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19q-172 0 -318 -49.5t-259.5 -134t-235.5 -219.5q-19 -21 -19 -45q0 -26 19 -45t45 -19q24 0 45 19q27 24 74 71t67 66q137 124 268.5 176t313.5 52q26 0 45 19t19 45zM1792 1030q0 -95 -20 -193q-46 -224 -184.5 -383t-357.5 -268 q-214 -108 -438 -108q-148 0 -286 47q-15 5 -88 42t-96 37q-16 0 -39.5 -32t-45 -70t-52.5 -70t-60 -32q-30 0 -51 11t-31 24t-27 42q-2 4 -6 11t-5.5 10t-3 9.5t-1.5 13.5q0 35 31 73.5t68 65.5t68 56t31 48q0 4 -14 38t-16 44q-9 51 -9 104q0 115 43.5 220t119 184.5 t170.5 139t204 95.5q55 18 145 25.5t179.5 9t178.5 6t163.5 24t113.5 56.5l29.5 29.5t29.5 28t27 20t36.5 16t43.5 4.5q39 0 70.5 -46t47.5 -112t24 -124t8 -96z" />
<glyph unicode="&#xf06d;" horiz-adv-x="1408" d="M1408 -160v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1152 896q0 -78 -24.5 -144t-64 -112.5t-87.5 -88t-96 -77.5t-87.5 -72t-64 -81.5t-24.5 -96.5q0 -96 67 -224l-4 1l1 -1 q-90 41 -160 83t-138.5 100t-113.5 122.5t-72.5 150.5t-27.5 184q0 78 24.5 144t64 112.5t87.5 88t96 77.5t87.5 72t64 81.5t24.5 96.5q0 94 -66 224l3 -1l-1 1q90 -41 160 -83t138.5 -100t113.5 -122.5t72.5 -150.5t27.5 -184z" />
<glyph unicode="&#xf06e;" horiz-adv-x="1792" d="M1664 576q-152 236 -381 353q61 -104 61 -225q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 121 61 225q-229 -117 -381 -353q133 -205 333.5 -326.5t434.5 -121.5t434.5 121.5t333.5 326.5zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5 t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1792 576q0 -34 -20 -69q-140 -230 -376.5 -368.5t-499.5 -138.5t-499.5 139t-376.5 368q-20 35 -20 69t20 69q140 229 376.5 368t499.5 139t499.5 -139t376.5 -368q20 -35 20 -69z" />
<glyph unicode="&#xf070;" horiz-adv-x="1792" d="M555 201l78 141q-87 63 -136 159t-49 203q0 121 61 225q-229 -117 -381 -353q167 -258 427 -375zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1307 1151q0 -7 -1 -9 q-105 -188 -315 -566t-316 -567l-49 -89q-10 -16 -28 -16q-12 0 -134 70q-16 10 -16 28q0 12 44 87q-143 65 -263.5 173t-208.5 245q-20 31 -20 69t20 69q153 235 380 371t496 136q89 0 180 -17l54 97q10 16 28 16q5 0 18 -6t31 -15.5t33 -18.5t31.5 -18.5t19.5 -11.5 q16 -10 16 -27zM1344 704q0 -139 -79 -253.5t-209 -164.5l280 502q8 -45 8 -84zM1792 576q0 -35 -20 -69q-39 -64 -109 -145q-150 -172 -347.5 -267t-419.5 -95l74 132q212 18 392.5 137t301.5 307q-115 179 -282 294l63 112q95 -64 182.5 -153t144.5 -184q20 -34 20 -69z " />
<glyph unicode="&#xf071;" horiz-adv-x="1792" d="M1024 161v190q0 14 -9.5 23.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -23.5v-190q0 -14 9.5 -23.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 23.5zM1022 535l18 459q0 12 -10 19q-13 11 -24 11h-220q-11 0 -24 -11q-10 -7 -10 -21l17 -457q0 -10 10 -16.5t24 -6.5h185 q14 0 23.5 6.5t10.5 16.5zM1008 1469l768 -1408q35 -63 -2 -126q-17 -29 -46.5 -46t-63.5 -17h-1536q-34 0 -63.5 17t-46.5 46q-37 63 -2 126l768 1408q17 31 47 49t65 18t65 -18t47 -49z" />
<glyph unicode="&#xf072;" horiz-adv-x="1408" d="M1376 1376q44 -52 12 -148t-108 -172l-225 -225l160 -696q5 -19 -12 -33l-128 -96q-7 -6 -19 -6q-4 0 -7 1q-15 3 -21 16l-279 508l-195 -195l53 -194q5 -17 -8 -31l-96 -96q-9 -9 -23 -9h-2q-15 2 -24 13l-189 252l-252 189q-11 7 -13 23q-1 13 9 25l96 97q9 9 23 9 q6 0 8 -1l194 -53l195 195l-508 279q-14 8 -17 24q-2 16 9 27l128 128q14 13 30 8l665 -159l224 224q76 76 172 108t148 -12z" />
<glyph unicode="&#xf073;" horiz-adv-x="1664" d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z M512 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h288v288h-288v-288zM1280 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64 q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47 h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
<glyph unicode="&#xf074;" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5 v192h-256q-48 0 -87 -15t-69 -45t-51 -61.5t-45 -77.5q-32 -62 -78 -171q-29 -66 -49.5 -111t-54 -105t-64 -100t-74 -83t-90 -68.5t-106.5 -42t-128 -16.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q48 0 87 15t69 45t51 61.5t45 77.5q32 62 78 171q29 66 49.5 111 t54 105t64 100t74 83t90 68.5t106.5 42t128 16.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" />
<glyph unicode="&#xf075;" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" />
<glyph unicode="&#xf076;" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf077;" horiz-adv-x="1664" d="M1611 320q0 -53 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-486 485l-486 -485q-36 -38 -90 -38t-90 38l-75 75q-38 36 -38 90q0 53 38 91l651 651q37 37 90 37q52 0 91 -37l650 -651q38 -38 38 -91z" />
<glyph unicode="&#xf078;" horiz-adv-x="1664" d="M1611 832q0 -53 -37 -90l-651 -651q-38 -38 -91 -38q-54 0 -90 38l-651 651q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l486 -486l486 486q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" />
<glyph unicode="&#xf079;" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " />
<glyph unicode="&#xf07a;" horiz-adv-x="1664" d="M640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5 l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5 t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf07b;" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
<glyph unicode="&#xf07c;" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" />
<glyph unicode="&#xf07d;" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" />
<glyph unicode="&#xf07e;" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" />
<glyph unicode="&#xf080;" horiz-adv-x="1920" d="M512 512v-384h-256v384h256zM896 1024v-896h-256v896h256zM1280 768v-640h-256v640h256zM1664 1152v-1024h-256v1024h256zM1792 32v1216q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5z M1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf081;" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf082;" d="M1307 618l23 219h-198v109q0 49 15.5 68.5t71.5 19.5h110v219h-175q-152 0 -218 -72t-66 -213v-131h-131v-219h131v-635h262v635h175zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf083;" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" />
<glyph unicode="&#xf084;" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" />
<glyph unicode="&#xf085;" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" />
<glyph unicode="&#xf086;" horiz-adv-x="1792" d="M1408 768q0 -139 -94 -257t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224 q0 139 94 257t256.5 186.5t353.5 68.5t353.5 -68.5t256.5 -186.5t94 -257zM1792 512q0 -120 -71 -224.5t-195 -176.5q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7 q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230z" />
<glyph unicode="&#xf087;" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 768q0 51 -39 89.5t-89 38.5h-352q0 58 48 159.5t48 160.5q0 98 -32 145t-128 47q-26 -26 -38 -85t-30.5 -125.5t-59.5 -109.5q-22 -23 -77 -91q-4 -5 -23 -30t-31.5 -41t-34.5 -42.5 t-40 -44t-38.5 -35.5t-40 -27t-35.5 -9h-32v-640h32q13 0 31.5 -3t33 -6.5t38 -11t35 -11.5t35.5 -12.5t29 -10.5q211 -73 342 -73h121q192 0 192 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5q32 1 53.5 47t21.5 81zM1536 769 q0 -89 -49 -163q9 -33 9 -69q0 -77 -38 -144q3 -21 3 -43q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5h-36h-93q-96 0 -189.5 22.5t-216.5 65.5q-116 40 -138 40h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h274q36 24 137 155q58 75 107 128 q24 25 35.5 85.5t30.5 126.5t62 108q39 37 90 37q84 0 151 -32.5t102 -101.5t35 -186q0 -93 -48 -192h176q104 0 180 -76t76 -179z" />
<glyph unicode="&#xf088;" d="M256 1088q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 512q0 35 -21.5 81t-53.5 47q15 17 25 47.5t10 55.5q0 69 -53 119q18 32 18 69t-17.5 73.5t-47.5 52.5q5 30 5 56q0 85 -49 126t-136 41h-128q-131 0 -342 -73q-5 -2 -29 -10.5 t-35.5 -12.5t-35 -11.5t-38 -11t-33 -6.5t-31.5 -3h-32v-640h32q16 0 35.5 -9t40 -27t38.5 -35.5t40 -44t34.5 -42.5t31.5 -41t23 -30q55 -68 77 -91q41 -43 59.5 -109.5t30.5 -125.5t38 -85q96 0 128 47t32 145q0 59 -48 160.5t-48 159.5h352q50 0 89 38.5t39 89.5z M1536 511q0 -103 -76 -179t-180 -76h-176q48 -99 48 -192q0 -118 -35 -186q-35 -69 -102 -101.5t-151 -32.5q-51 0 -90 37q-34 33 -54 82t-25.5 90.5t-17.5 84.5t-31 64q-48 50 -107 127q-101 131 -137 155h-274q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5 h288q22 0 138 40q128 44 223 66t200 22h112q140 0 226.5 -79t85.5 -216v-5q60 -77 60 -178q0 -22 -3 -43q38 -67 38 -144q0 -36 -9 -69q49 -74 49 -163z" />
<glyph unicode="&#xf089;" horiz-adv-x="896" d="M832 1504v-1339l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41z" />
<glyph unicode="&#xf08a;" horiz-adv-x="1792" d="M1664 940q0 81 -21.5 143t-55 98.5t-81.5 59.5t-94 31t-98 8t-112 -25.5t-110.5 -64t-86.5 -72t-60 -61.5q-18 -22 -49 -22t-49 22q-24 28 -60 61.5t-86.5 72t-110.5 64t-112 25.5t-98 -8t-94 -31t-81.5 -59.5t-55 -98.5t-21.5 -143q0 -168 187 -355l581 -560l580 559 q188 188 188 356zM1792 940q0 -221 -229 -450l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5 q224 0 351 -124t127 -344z" />
<glyph unicode="&#xf08b;" horiz-adv-x="1664" d="M640 96q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h320q13 0 22.5 -9.5t9.5 -22.5q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-66 0 -113 -47t-47 -113v-704 q0 -66 47 -113t113 -47h288h11h13t11.5 -1t11.5 -3t8 -5.5t7 -9t2 -13.5zM1568 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45z" />
<glyph unicode="&#xf08c;" d="M237 122h231v694h-231v-694zM483 1030q-1 52 -36 86t-93 34t-94.5 -34t-36.5 -86q0 -51 35.5 -85.5t92.5 -34.5h1q59 0 95 34.5t36 85.5zM1068 122h231v398q0 154 -73 233t-193 79q-136 0 -209 -117h2v101h-231q3 -66 0 -694h231v388q0 38 7 56q15 35 45 59.5t74 24.5 q116 0 116 -157v-371zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf08d;" horiz-adv-x="1152" d="M480 672v448q0 14 -9 23t-23 9t-23 -9t-9 -23v-448q0 -14 9 -23t23 -9t23 9t9 23zM1152 320q0 -26 -19 -45t-45 -19h-429l-51 -483q-2 -12 -10.5 -20.5t-20.5 -8.5h-1q-27 0 -32 27l-76 485h-404q-26 0 -45 19t-19 45q0 123 78.5 221.5t177.5 98.5v512q-52 0 -90 38 t-38 90t38 90t90 38h640q52 0 90 -38t38 -90t-38 -90t-90 -38v-512q99 0 177.5 -98.5t78.5 -221.5z" />
<glyph unicode="&#xf08e;" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf090;" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf091;" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf092;" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf093;" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" />
<glyph unicode="&#xf094;" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" />
<glyph unicode="&#xf095;" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" />
<glyph unicode="&#xf096;" horiz-adv-x="1408" d="M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf097;" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
<glyph unicode="&#xf098;" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf099;" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" />
<glyph unicode="&#xf09a;" horiz-adv-x="768" d="M511 980h257l-30 -284h-227v-824h-341v824h-170v284h170v171q0 182 86 275.5t283 93.5h227v-284h-142q-39 0 -62.5 -6.5t-34 -23.5t-13.5 -34.5t-3 -49.5v-142z" />
<glyph unicode="&#xf09b;" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf09c;" horiz-adv-x="1664" d="M704 160q0 6 -15 57t-35 115.5t-20 65.5q32 16 51 47t19 67q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5q0 -36 19 -66.5t51 -47.5q0 -2 -20 -66t-35 -115t-15 -57q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1664 960v-256q0 -26 -19 -45t-45 -19 h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5z" />
<glyph unicode="&#xf09d;" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" />
<glyph unicode="&#xf09e;" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" />
<glyph unicode="&#xf0a0;" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q-66 0 -113 47t-47 113v320q0 25 16 75 l197 606q17 53 63 86t101 33h782q55 0 101 -33t63 -86l197 -606q16 -50 16 -75z" />
<glyph unicode="&#xf0a1;" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" />
<glyph unicode="&#xf0a2;" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM183 128h1298q-164 181 -246.5 411.5t-82.5 484.5q0 256 -320 256t-320 -256q0 -254 -82.5 -484.5t-246.5 -411.5zM1664 128q0 -52 -38 -90t-90 -38 h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117t96 -262q0 -262 97 -498.5t287 -397.5z" />
<glyph unicode="&#xf0a3;" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 -188l186 53q41 12 70 -19q31 -29 19 -70 l-53 -186l188 -48q40 -10 52 -51q10 -42 -20 -70z" />
<glyph unicode="&#xf0a4;" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-180.5 -76h-169q-4 -62 -37 -119q3 -21 3 -43 q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5q-133 0 -322 69q-164 59 -223 59h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h288q10 0 21.5 4.5t23.5 14t22.5 18t24 22.5t20.5 21.5t19 21.5t14 17q65 74 100 129q13 21 33 62t37 72t40.5 63t55 49.5 t69.5 17.5q125 0 206.5 -67t81.5 -189q0 -68 -22 -128h374q104 0 180 -76t76 -179z" />
<glyph unicode="&#xf0a5;" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32zM1664 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45 t45 -19t45 19t19 45zM1792 768v-640q0 -53 -37.5 -90.5t-90.5 -37.5h-288q-59 0 -223 -59q-190 -69 -317 -69q-142 0 -230 77.5t-87 217.5l1 5q-61 76 -61 178q0 22 3 43q-33 57 -37 119h-169q-105 0 -180.5 76t-75.5 181q0 103 76 179t180 76h374q-22 60 -22 128 q0 122 81.5 189t206.5 67q38 0 69.5 -17.5t55 -49.5t40.5 -63t37 -72t33 -62q35 -55 100 -129q2 -3 14 -17t19 -21.5t20.5 -21.5t24 -22.5t22.5 -18t23.5 -14t21.5 -4.5h288q53 0 90.5 -37.5t37.5 -90.5z" />
<glyph unicode="&#xf0a6;" d="M1280 -64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 700q0 189 -167 189q-26 0 -56 -5q-16 30 -52.5 47.5t-73.5 17.5t-69 -18q-50 53 -119 53q-25 0 -55.5 -10t-47.5 -25v331q0 52 -38 90t-90 38q-51 0 -89.5 -39t-38.5 -89v-576 q-20 0 -48.5 15t-55 33t-68 33t-84.5 15q-67 0 -97.5 -44.5t-30.5 -115.5q0 -24 139 -90q44 -24 65 -37q64 -40 145 -112q81 -71 106 -101q57 -69 57 -140v-32h640v32q0 72 32 167t64 193.5t32 179.5zM1536 705q0 -133 -69 -322q-59 -164 -59 -223v-288q0 -53 -37.5 -90.5 t-90.5 -37.5h-640q-53 0 -90.5 37.5t-37.5 90.5v288q0 10 -4.5 21.5t-14 23.5t-18 22.5t-22.5 24t-21.5 20.5t-21.5 19t-17 14q-74 65 -129 100q-21 13 -62 33t-72 37t-63 40.5t-49.5 55t-17.5 69.5q0 125 67 206.5t189 81.5q68 0 128 -22v374q0 104 76 180t179 76 q105 0 181 -75.5t76 -180.5v-169q62 -4 119 -37q21 3 43 3q101 0 178 -60q139 1 219.5 -85t80.5 -227z" />
<glyph unicode="&#xf0a7;" d="M1408 576q0 84 -32 183t-64 194t-32 167v32h-640v-32q0 -35 -12 -67.5t-37 -62.5t-46 -50t-54 -49q-9 -8 -14 -12q-81 -72 -145 -112q-22 -14 -68 -38q-3 -1 -22.5 -10.5t-36 -18.5t-35.5 -20t-30.5 -21.5t-11.5 -18.5q0 -71 30.5 -115.5t97.5 -44.5q43 0 84.5 15t68 33 t55 33t48.5 15v-576q0 -50 38.5 -89t89.5 -39q52 0 90 38t38 90v331q46 -35 103 -35q69 0 119 53q32 -18 69 -18t73.5 17.5t52.5 47.5q24 -4 56 -4q85 0 126 48.5t41 135.5zM1280 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 580 q0 -142 -77.5 -230t-217.5 -87l-5 1q-76 -61 -178 -61q-22 0 -43 3q-54 -30 -119 -37v-169q0 -105 -76 -180.5t-181 -75.5q-103 0 -179 76t-76 180v374q-54 -22 -128 -22q-121 0 -188.5 81.5t-67.5 206.5q0 38 17.5 69.5t49.5 55t63 40.5t72 37t62 33q55 35 129 100 q3 2 17 14t21.5 19t21.5 20.5t22.5 24t18 22.5t14 23.5t4.5 21.5v288q0 53 37.5 90.5t90.5 37.5h640q53 0 90.5 -37.5t37.5 -90.5v-288q0 -59 59 -223q69 -190 69 -317z" />
<glyph unicode="&#xf0a8;" d="M1280 576v128q0 26 -19 45t-45 19h-502l189 189q19 19 19 45t-19 45l-91 91q-18 18 -45 18t-45 -18l-362 -362l-91 -91q-18 -18 -18 -45t18 -45l91 -91l362 -362q18 -18 45 -18t45 18l91 91q18 18 18 45t-18 45l-189 189h502q26 0 45 19t19 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf0a9;" d="M1285 640q0 27 -18 45l-91 91l-362 362q-18 18 -45 18t-45 -18l-91 -91q-18 -18 -18 -45t18 -45l189 -189h-502q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h502l-189 -189q-19 -19 -19 -45t19 -45l91 -91q18 -18 45 -18t45 18l362 362l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf0aa;" d="M1284 641q0 27 -18 45l-362 362l-91 91q-18 18 -45 18t-45 -18l-91 -91l-362 -362q-18 -18 -18 -45t18 -45l91 -91q18 -18 45 -18t45 18l189 189v-502q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v502l189 -189q19 -19 45 -19t45 19l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf0ab;" d="M1284 639q0 27 -18 45l-91 91q-18 18 -45 18t-45 -18l-189 -189v502q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-502l-189 189q-19 19 -45 19t-45 -19l-91 -91q-18 -18 -18 -45t18 -45l362 -362l91 -91q18 -18 45 -18t45 18l91 91l362 362q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf0ac;" d="M1193 993q11 7 25 22v-1q0 -2 -9.5 -10t-11.5 -12q-1 1 -4 1zM1187 992q-1 1 -2.5 3t-1.5 3q3 -2 10 -5q-6 -4 -6 -1zM728 1175q-16 2 -26 5q1 0 6.5 -1t10.5 -2t9 -2zM773 1212q7 4 13.5 2.5t7.5 -7.5q-5 3 -21 5zM765 1206l-3 2q-2 3 -5.5 5t-4.5 2q2 -1 21 -3 q-6 -4 -8 -6zM663 1290v2q1 -2 3 -5.5t3 -5.5zM558 1250q0 -2 -1 -2l-1 2h2zM933 206v-1v1zM768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1240 162 l5 5q-7 10 -29 12q1 12 -14 26.5t-27 15.5q0 4 -10.5 11t-17.5 8q-9 2 -27 -9q-7 -3 -4 -5q-3 3 -12 11t-16 11q-2 1 -7.5 1t-8.5 2q-1 1 -6 4.5t-7 4.5t-6.5 3t-7.5 1.5t-7.5 -2.5t-8.5 -6t-4.5 -15.5t-2.5 -14.5q-8 6 -0.5 20t1.5 20q-7 7 -21 0.5t-21 -15.5 q-1 -1 -9.5 -5.5t-11.5 -7.5q-4 -6 -9 -17.5t-6 -13.5q0 2 -2.5 6.5t-2.5 6.5q-12 -2 -16 3q5 -16 8 -17l-4 2q-1 -6 3 -15t4 -11q1 -5 -1.5 -13t-2.5 -11q0 -2 5 -11q4 -19 -2 -32q0 -1 -3.5 -7t-6.5 -11l-2 -5l-2 1q-1 1 -2 0q-1 -6 -9 -13t-10 -11q-15 -23 -9 -38 q3 -8 10 -10q3 -1 3 2q1 -9 -11 -27q1 -1 4 -3q-17 0 -10 -14q202 36 352 181h-3zM680 347q16 3 30.5 -16t22.5 -23q41 -20 59 -11q0 -9 14 -28q3 -4 6.5 -11.5t5.5 -10.5q5 -7 19 -16t19 -16q6 3 9 9q13 -35 24 -34q5 0 8 8q0 -1 -0.5 -3t-1.5 -3q7 15 5 26l6 4q5 4 5 5 q-6 6 -9 -3q-30 -14 -48 22q-2 3 -4.5 8t-5 12t-1.5 11.5t6 4.5q11 0 12.5 1.5t-2.5 6t-4 7.5q-1 4 -1.5 12.5t-1.5 12.5l-5 6q-5 6 -11.5 13.5t-7.5 9.5q-4 -10 -16.5 -8.5t-18.5 9.5q1 -2 -0.5 -6.5t-1.5 -6.5q-14 0 -17 1q1 6 3 21t4 22q1 5 5.5 13.5t8 15.5t4.5 14 t-4.5 10.5t-18.5 2.5q-20 -1 -29 -22q-1 -3 -3 -11.5t-5 -12.5t-9 -7q-8 -3 -27 -2t-26 5q-14 8 -24 30.5t-11 41.5q0 10 3 27.5t3 27t-6 26.5q3 2 10 10.5t11 11.5q2 2 5 2h5t4 2t3 6q-1 1 -4 3q-3 3 -4 3q4 -3 19 -1t19 2q0 1 22 0q17 -13 24 2q0 1 -2.5 10.5t-0.5 14.5 q5 -29 32 -10q3 -4 16.5 -6t18.5 -5q3 -2 7 -5.5t6 -5t6 -0.5t9 7q11 -17 13 -25q11 -43 20 -48q8 -2 12.5 -2t5 10.5t0 15.5t-1.5 13l-2 37q-16 3 -20 12.5t1.5 20t16.5 19.5q1 1 16.5 8t21.5 12q24 19 17 39q9 -2 11 9l-5 3q-4 3 -8 5.5t-5 1.5q11 7 2 18q5 3 8 11.5 t9 11.5q9 -14 22 -3q8 9 2 18q5 8 22 11.5t20 9.5q5 -1 7 0t2 4.5v7.5t1 8.5t3 7.5q4 6 16 10.5t14 5.5l19 12q4 4 0 4q18 -2 32 11q13 12 -5 23q2 7 -4 10.5t-16 5.5q3 1 12 0.5t12 1.5q15 11 -7 17q-20 5 -47 -13q-3 -2 -13 -12t-17 -11q15 18 5 22q8 -1 22.5 9t15.5 11 q4 2 10.5 2.5t8.5 1.5q71 25 92 -1q8 11 11 15t9.5 9t15.5 8q21 7 23 9l1 23q-12 -1 -18 8t-7 22l-6 -8q0 6 -3.5 7.5t-7.5 0.5t-9.5 -2t-7.5 0q-9 2 -19.5 15.5t-14.5 16.5q9 0 9 5q-2 5 -10 8q1 6 -2 8t-9 0q-2 12 -1 13q-6 1 -11 11t-8 10q-2 0 -4.5 -2t-5 -5.5l-5 -7 t-3.5 -5.5l-2 -2q-12 6 -24 -10q-9 1 -17 -2q15 6 2 13q-11 5 -21 2q12 5 10 14t-12 16q1 0 4 -1t4 -1q-1 5 -9.5 9.5t-19.5 9t-14 6.5q-7 5 -36 10.5t-36 1.5q-5 -3 -6 -6t1.5 -8.5t3.5 -8.5q6 -23 5 -27q-1 -3 -8.5 -8t-5.5 -12q1 -4 11.5 -10t12.5 -12q5 -13 -4 -25 q-4 -5 -15 -11t-14 -10q-5 -5 -3.5 -11.5t0.5 -9.5q1 1 1 2.5t1 2.5q0 -13 11 -22q8 -6 -16 -18q-20 -11 -20 -4q1 8 -7.5 16t-10.5 12t-3.5 19t-9.5 21q-6 4 -19 4t-18 -5q0 10 -49 30q-17 8 -58 4q7 1 0 17q-8 16 -21 12q-8 25 -4 35q2 5 9 14t9 15q1 3 15.5 6t16.5 8 q1 4 -2.5 6.5t-9.5 4.5q53 -6 63 18q5 9 3 14q0 -1 2 -1t2 -1q12 3 7 17q19 8 26 8q5 -1 11 -6t10 -5q17 -3 21.5 10t-9.5 23q7 -4 7 6q-1 13 -7 19q-3 2 -6.5 2.5t-6.5 0t-7 0.5q-1 0 -8 2q-1 -1 -2 -1h-8q-4 -2 -4 -5v-1q-1 -3 4 -6l5 -1l3 -2q-1 0 -2.5 -2.5t-2.5 -2.5 q0 -3 3 -5q-2 -1 -14 -7.5t-17 -10.5q-1 -1 -4 -2.5t-4 -2.5q-2 -1 -4 2t-4 9t-4 11.5t-4.5 10t-5.5 4.5q-12 0 -18 -17q3 10 -13 17.5t-25 7.5q20 15 -9 30l-1 1q-30 -4 -45 -7q-2 -6 3 -12q-1 -7 6 -9q0 -1 0.5 -1t0.5 -1q0 1 -0.5 1t-0.5 1q3 -1 10.5 -1.5t9.5 -1.5 q3 -1 4.5 -2l7.5 -5t5.5 -6t-2.5 -5q-2 -1 -9 -4t-12.5 -5.5t-6.5 -3.5q-3 -5 0 -16t-2 -15q-5 5 -10 18.5t-8 17.5q8 -9 -30 -6l-8 1q-4 0 -15 -2t-16 -1q-7 0 -29 6q7 17 5 25q5 0 7 2l-6 3q-3 -1 -25 -9q2 -3 8 -9.5t9 -11.5q-22 6 -27 -2q0 -1 -9 0q-25 1 -24 -7 q1 -4 9 -12q0 -9 -1 -9q-27 22 -30 23q-172 -83 -276 -248q1 -2 2.5 -11t3.5 -8.5t11 4.5q9 -9 3 -21q2 2 36 -21q56 -40 22 -53v5.5t1 6.5q-9 -1 -19 5q-3 -6 0.5 -20t11.5 -14q-8 0 -10.5 -17t-2.5 -38.5t-1 -25.5l2 -1q-3 -13 6 -37.5t24 -20.5q-4 -18 5 -21q-1 -4 0 -8 t4.5 -8.5t6 -7l7.5 -7.5l6 -6q28 -11 41 -29q4 -6 10.5 -24.5t15.5 -25.5q-2 -6 10 -21.5t11 -25.5q-1 0 -2.5 -0.5t-2.5 -0.5q3 -8 16.5 -16t16.5 -14q2 -3 2.5 -10.5t3 -12t8.5 -2.5q3 24 -26 68q-16 27 -18 31q-3 5 -5.5 16.5t-4.5 15.5q27 -9 26 -13q-5 -10 26 -52 q2 -3 10 -10t11 -12q3 -4 9.5 -14.5t10.5 -15.5q-1 0 -3 -2l-3 -3q4 -2 9 -5t8 -4.5t7.5 -5t7.5 -7.5q16 -18 20 -33q1 -4 0.5 -15.5t1.5 -16.5q2 -6 6 -11t11.5 -10t11.5 -7t14.5 -6.5t11.5 -5.5q2 -1 18 -11t25 -14q10 -4 16.5 -4.5t16 2.5t15.5 4z" />
<glyph unicode="&#xf0ad;" horiz-adv-x="1664" d="M384 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1028 484l-682 -682q-37 -37 -90 -37q-52 0 -91 37l-106 108q-38 36 -38 90q0 53 38 91l681 681q39 -98 114.5 -173.5t173.5 -114.5zM1662 919q0 -39 -23 -106q-47 -134 -164.5 -217.5 t-258.5 -83.5q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q58 0 121.5 -16.5t107.5 -46.5q16 -11 16 -28t-16 -28l-293 -169v-224l193 -107q5 3 79 48.5t135.5 81t70.5 35.5q15 0 23.5 -10t8.5 -25z" />
<glyph unicode="&#xf0ae;" horiz-adv-x="1792" d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 832v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19 t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0b0;" horiz-adv-x="1408" d="M1403 1241q17 -41 -14 -70l-493 -493v-742q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-256 256q-19 19 -19 45v486l-493 493q-31 29 -14 70q17 39 59 39h1280q42 0 59 -39z" />
<glyph unicode="&#xf0b1;" horiz-adv-x="1792" d="M640 1280h512v128h-512v-128zM1792 640v-480q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v480h672v-160q0 -26 19 -45t45 -19h320q26 0 45 19t19 45v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384q0 66 47 113t113 47h352v160q0 40 28 68 t68 28h576q40 0 68 -28t28 -68v-160h352q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf0b2;" d="M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0 -42 -39 -59q-13 -5 -25 -5q-26 0 -45 19z " />
<glyph unicode="&#xf0c0;" horiz-adv-x="1920" d="M593 640q-162 -5 -265 -128h-134q-82 0 -138 40.5t-56 118.5q0 353 124 353q6 0 43.5 -21t97.5 -42.5t119 -21.5q67 0 133 23q-5 -37 -5 -66q0 -139 81 -256zM1664 3q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q10 0 43 -21.5t73 -48t107 -48t135 -21.5t135 21.5t107 48t73 48t43 21.5q61 0 111.5 -20t85.5 -53.5t62 -81t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM640 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75 t75 -181zM1344 896q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5zM1920 671q0 -78 -56 -118.5t-138 -40.5h-134q-103 123 -265 128q81 117 81 256q0 29 -5 66q66 -23 133 -23q59 0 119 21.5t97.5 42.5 t43.5 21q124 0 124 -353zM1792 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181z" />
<glyph unicode="&#xf0c1;" horiz-adv-x="1664" d="M1456 320q0 40 -28 68l-208 208q-28 28 -68 28q-42 0 -72 -32q3 -3 19 -18.5t21.5 -21.5t15 -19t13 -25.5t3.5 -27.5q0 -40 -28 -68t-68 -28q-15 0 -27.5 3.5t-25.5 13t-19 15t-21.5 21.5t-18.5 19q-33 -31 -33 -73q0 -40 28 -68l206 -207q27 -27 68 -27q40 0 68 26 l147 146q28 28 28 67zM753 1025q0 40 -28 68l-206 207q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l208 -208q27 -27 68 -27q42 0 72 31q-3 3 -19 18.5t-21.5 21.5t-15 19t-13 25.5t-3.5 27.5q0 40 28 68t68 28q15 0 27.5 -3.5t25.5 -13t19 -15 t21.5 -21.5t18.5 -19q33 31 33 73zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-206 207q-83 83 -83 203q0 123 88 209l-88 88q-86 -88 -208 -88q-120 0 -204 84l-208 208q-84 84 -84 204t85 203l147 146q83 83 203 83q121 0 204 -85l206 -207 q83 -83 83 -203q0 -123 -88 -209l88 -88q86 88 208 88q120 0 204 -84l208 -208q84 -84 84 -204z" />
<glyph unicode="&#xf0c2;" horiz-adv-x="1920" d="M1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088q-185 0 -316.5 131.5t-131.5 316.5q0 132 71 241.5t187 163.5q-2 28 -2 43q0 212 150 362t362 150q158 0 286.5 -88t187.5 -230q70 62 166 62q106 0 181 -75t75 -181q0 -75 -41 -138q129 -30 213 -134.5t84 -239.5z " />
<glyph unicode="&#xf0c3;" horiz-adv-x="1664" d="M1527 88q56 -89 21.5 -152.5t-140.5 -63.5h-1152q-106 0 -140.5 63.5t21.5 152.5l503 793v399h-64q-26 0 -45 19t-19 45t19 45t45 19h512q26 0 45 -19t19 -45t-19 -45t-45 -19h-64v-399zM748 813l-272 -429h712l-272 429l-20 31v37v399h-128v-399v-37z" />
<glyph unicode="&#xf0c4;" horiz-adv-x="1792" d="M960 640q26 0 45 -19t19 -45t-19 -45t-45 -19t-45 19t-19 45t19 45t45 19zM1260 576l507 -398q28 -20 25 -56q-5 -35 -35 -51l-128 -64q-13 -7 -29 -7q-17 0 -31 8l-690 387l-110 -66q-8 -4 -12 -5q14 -49 10 -97q-7 -77 -56 -147.5t-132 -123.5q-132 -84 -277 -84 q-136 0 -222 78q-90 84 -79 207q7 76 56 147t131 124q132 84 278 84q83 0 151 -31q9 13 22 22l122 73l-122 73q-13 9 -22 22q-68 -31 -151 -31q-146 0 -278 84q-82 53 -131 124t-56 147q-5 59 15.5 113t63.5 93q85 79 222 79q145 0 277 -84q83 -52 132 -123t56 -148 q4 -48 -10 -97q4 -1 12 -5l110 -66l690 387q14 8 31 8q16 0 29 -7l128 -64q30 -16 35 -51q3 -36 -25 -56zM579 836q46 42 21 108t-106 117q-92 59 -192 59q-74 0 -113 -36q-46 -42 -21 -108t106 -117q92 -59 192 -59q74 0 113 36zM494 91q81 51 106 117t-21 108 q-39 36 -113 36q-100 0 -192 -59q-81 -51 -106 -117t21 -108q39 -36 113 -36q100 0 192 59zM672 704l96 -58v11q0 36 33 56l14 8l-79 47l-26 -26q-3 -3 -10 -11t-12 -12q-2 -2 -4 -3.5t-3 -2.5zM896 480l96 -32l736 576l-128 64l-768 -431v-113l-160 -96l9 -8q2 -2 7 -6 q4 -4 11 -12t11 -12l26 -26zM1600 64l128 64l-520 408l-177 -138q-2 -3 -13 -7z" />
<glyph unicode="&#xf0c5;" horiz-adv-x="1792" d="M1696 1152q40 0 68 -28t28 -68v-1216q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v288h-544q-40 0 -68 28t-28 68v672q0 40 20 88t48 76l408 408q28 28 76 48t88 20h416q40 0 68 -28t28 -68v-328q68 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323l-299 -299 h299v299zM708 676l316 316v416h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h512v256q0 40 20 88t48 76zM1664 -128v1152h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h896z" />
<glyph unicode="&#xf0c6;" horiz-adv-x="1408" d="M1404 151q0 -117 -79 -196t-196 -79q-135 0 -235 100l-777 776q-113 115 -113 271q0 159 110 270t269 111q158 0 273 -113l605 -606q10 -10 10 -22q0 -16 -30.5 -46.5t-46.5 -30.5q-13 0 -23 10l-606 607q-79 77 -181 77q-106 0 -179 -75t-73 -181q0 -105 76 -181 l776 -777q63 -63 145 -63q64 0 106 42t42 106q0 82 -63 145l-581 581q-26 24 -60 24q-29 0 -48 -19t-19 -48q0 -32 25 -59l410 -410q10 -10 10 -22q0 -16 -31 -47t-47 -31q-12 0 -22 10l-410 410q-63 61 -63 149q0 82 57 139t139 57q88 0 149 -63l581 -581q100 -98 100 -235 z" />
<glyph unicode="&#xf0c7;" d="M384 0h768v384h-768v-384zM1280 0h128v896q0 14 -10 38.5t-20 34.5l-281 281q-10 10 -34 20t-39 10v-416q0 -40 -28 -68t-68 -28h-576q-40 0 -68 28t-28 68v416h-128v-1280h128v416q0 40 28 68t68 28h832q40 0 68 -28t28 -68v-416zM896 928v320q0 13 -9.5 22.5t-22.5 9.5 h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1536 896v-928q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h928q40 0 88 -20t76 -48l280 -280q28 -28 48 -76t20 -88z" />
<glyph unicode="&#xf0c8;" d="M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf0c9;" d="M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0ca;" horiz-adv-x="1792" d="M384 128q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 640q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1152q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z M1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" />
<glyph unicode="&#xf0cb;" horiz-adv-x="1792" d="M381 -84q0 -80 -54.5 -126t-135.5 -46q-106 0 -172 66l57 88q49 -45 106 -45q29 0 50.5 14.5t21.5 42.5q0 64 -105 56l-26 56q8 10 32.5 43.5t42.5 54t37 38.5v1q-16 0 -48.5 -1t-48.5 -1v-53h-106v152h333v-88l-95 -115q51 -12 81 -49t30 -88zM383 543v-159h-362 q-6 36 -6 54q0 51 23.5 93t56.5 68t66 47.5t56.5 43.5t23.5 45q0 25 -14.5 38.5t-39.5 13.5q-46 0 -81 -58l-85 59q24 51 71.5 79.5t105.5 28.5q73 0 123 -41.5t50 -112.5q0 -50 -34 -91.5t-75 -64.5t-75.5 -50.5t-35.5 -52.5h127v60h105zM1792 224v-192q0 -13 -9.5 -22.5 t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1123v-99h-335v99h107q0 41 0.5 122t0.5 121v12h-2q-8 -17 -50 -54l-71 76l136 127h106v-404h108zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5 t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" />
<glyph unicode="&#xf0cc;" horiz-adv-x="1792" d="M1760 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1728q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h1728zM483 704q-28 35 -51 80q-48 97 -48 188q0 181 134 309q133 127 393 127q50 0 167 -19q66 -12 177 -48q10 -38 21 -118q14 -123 14 -183q0 -18 -5 -45l-12 -3l-84 6 l-14 2q-50 149 -103 205q-88 91 -210 91q-114 0 -182 -59q-67 -58 -67 -146q0 -73 66 -140t279 -129q69 -20 173 -66q58 -28 95 -52h-743zM990 448h411q7 -39 7 -92q0 -111 -41 -212q-23 -55 -71 -104q-37 -35 -109 -81q-80 -48 -153 -66q-80 -21 -203 -21q-114 0 -195 23 l-140 40q-57 16 -72 28q-8 8 -8 22v13q0 108 -2 156q-1 30 0 68l2 37v44l102 2q15 -34 30 -71t22.5 -56t12.5 -27q35 -57 80 -94q43 -36 105 -57q59 -22 132 -22q64 0 139 27q77 26 122 86q47 61 47 129q0 84 -81 157q-34 29 -137 71z" />
<glyph unicode="&#xf0cd;" d="M48 1313q-37 2 -45 4l-3 88q13 1 40 1q60 0 112 -4q132 -7 166 -7q86 0 168 3q116 4 146 5q56 0 86 2l-1 -14l2 -64v-9q-60 -9 -124 -9q-60 0 -79 -25q-13 -14 -13 -132q0 -13 0.5 -32.5t0.5 -25.5l1 -229l14 -280q6 -124 51 -202q35 -59 96 -92q88 -47 177 -47 q104 0 191 28q56 18 99 51q48 36 65 64q36 56 53 114q21 73 21 229q0 79 -3.5 128t-11 122.5t-13.5 159.5l-4 59q-5 67 -24 88q-34 35 -77 34l-100 -2l-14 3l2 86h84l205 -10q76 -3 196 10l18 -2q6 -38 6 -51q0 -7 -4 -31q-45 -12 -84 -13q-73 -11 -79 -17q-15 -15 -15 -41 q0 -7 1.5 -27t1.5 -31q8 -19 22 -396q6 -195 -15 -304q-15 -76 -41 -122q-38 -65 -112 -123q-75 -57 -182 -89q-109 -33 -255 -33q-167 0 -284 46q-119 47 -179 122q-61 76 -83 195q-16 80 -16 237v333q0 188 -17 213q-25 36 -147 39zM1536 -96v64q0 14 -9 23t-23 9h-1472 q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h1472q14 0 23 9t9 23z" />
<glyph unicode="&#xf0ce;" horiz-adv-x="1664" d="M512 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23 v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 160v192 q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192 q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1664 1248v-1088q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1344q66 0 113 -47t47 -113 z" />
<glyph unicode="&#xf0d0;" horiz-adv-x="1664" d="M1190 955l293 293l-107 107l-293 -293zM1637 1248q0 -27 -18 -45l-1286 -1286q-18 -18 -45 -18t-45 18l-198 198q-18 18 -18 45t18 45l1286 1286q18 18 45 18t45 -18l198 -198q18 -18 18 -45zM286 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM636 1276 l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1566 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM926 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" />
<glyph unicode="&#xf0d1;" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0d2;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf0d3;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" />
<glyph unicode="&#xf0d4;" d="M678 -57q0 -38 -10 -71h-380q-95 0 -171.5 56.5t-103.5 147.5q24 45 69 77.5t100 49.5t107 24t107 7q32 0 49 -2q6 -4 30.5 -21t33 -23t31 -23t32 -25.5t27.5 -25.5t26.5 -29.5t21 -30.5t17.5 -34.5t9.5 -36t4.5 -40.5zM385 294q-234 -7 -385 -85v433q103 -118 273 -118 q32 0 70 5q-21 -61 -21 -86q0 -67 63 -149zM558 805q0 -100 -43.5 -160.5t-140.5 -60.5q-51 0 -97 26t-78 67.5t-56 93.5t-35.5 104t-11.5 99q0 96 51.5 165t144.5 69q66 0 119 -41t84 -104t47 -130t16 -128zM1536 896v-736q0 -119 -84.5 -203.5t-203.5 -84.5h-468 q39 73 39 157q0 66 -22 122.5t-55.5 93t-72 71t-72 59.5t-55.5 54.5t-22 59.5q0 36 23 68t56 61.5t65.5 64.5t55.5 93t23 131t-26.5 145.5t-75.5 118.5q-6 6 -14 11t-12.5 7.5t-10 9.5t-10.5 17h135l135 64h-437q-138 0 -244.5 -38.5t-182.5 -133.5q0 126 81 213t207 87h960 q119 0 203.5 -84.5t84.5 -203.5v-96h-256v256h-128v-256h-256v-128h256v-256h128v256h256z" />
<glyph unicode="&#xf0d5;" horiz-adv-x="1664" d="M876 71q0 21 -4.5 40.5t-9.5 36t-17.5 34.5t-21 30.5t-26.5 29.5t-27.5 25.5t-32 25.5t-31 23t-33 23t-30.5 21q-17 2 -50 2q-54 0 -106 -7t-108 -25t-98 -46t-69 -75t-27 -107q0 -68 35.5 -121.5t93 -84t120.5 -45.5t127 -15q59 0 112.5 12.5t100.5 39t74.5 73.5 t27.5 110zM756 933q0 60 -16.5 127.5t-47 130.5t-84 104t-119.5 41q-93 0 -144 -69t-51 -165q0 -47 11.5 -99t35.5 -104t56 -93.5t78 -67.5t97 -26q97 0 140.5 60.5t43.5 160.5zM625 1408h437l-135 -79h-135q71 -45 110 -126t39 -169q0 -74 -23 -131.5t-56 -92.5t-66 -64.5 t-56 -61t-23 -67.5q0 -26 16.5 -51t43 -48t58.5 -48t64 -55.5t58.5 -66t43 -85t16.5 -106.5q0 -160 -140 -282q-152 -131 -420 -131q-59 0 -119.5 10t-122 33.5t-108.5 58t-77 89t-30 121.5q0 61 37 135q32 64 96 110.5t145 71t155 36t150 13.5q-64 83 -64 149q0 12 2 23.5 t5 19.5t8 21.5t7 21.5q-40 -5 -70 -5q-149 0 -255.5 98t-106.5 246q0 140 95 250.5t234 141.5q94 20 187 20zM1664 1152v-128h-256v-256h-128v256h-256v128h256v256h128v-256h256z" />
<glyph unicode="&#xf0d6;" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0d7;" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0d8;" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
<glyph unicode="&#xf0d9;" horiz-adv-x="640" d="M640 1088v-896q0 -26 -19 -45t-45 -19t-45 19l-448 448q-19 19 -19 45t19 45l448 448q19 19 45 19t45 -19t19 -45z" />
<glyph unicode="&#xf0da;" horiz-adv-x="640" d="M576 640q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19t-19 45v896q0 26 19 45t45 19t45 -19l448 -448q19 -19 19 -45z" />
<glyph unicode="&#xf0db;" horiz-adv-x="1664" d="M160 0h608v1152h-640v-1120q0 -13 9.5 -22.5t22.5 -9.5zM1536 32v1120h-640v-1152h608q13 0 22.5 9.5t9.5 22.5zM1664 1248v-1216q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1344q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf0dc;" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45zM1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
<glyph unicode="&#xf0dd;" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0de;" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
<glyph unicode="&#xf0e0;" horiz-adv-x="1792" d="M1792 826v-794q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v794q44 -49 101 -87q362 -246 497 -345q57 -42 92.5 -65.5t94.5 -48t110 -24.5h1h1q51 0 110 24.5t94.5 48t92.5 65.5q170 123 498 345q57 39 100 87zM1792 1120q0 -79 -49 -151t-122 -123 q-376 -261 -468 -325q-10 -7 -42.5 -30.5t-54 -38t-52 -32.5t-57.5 -27t-50 -9h-1h-1q-23 0 -50 9t-57.5 27t-52 32.5t-54 38t-42.5 30.5q-91 64 -262 182.5t-205 142.5q-62 42 -117 115.5t-55 136.5q0 78 41.5 130t118.5 52h1472q65 0 112.5 -47t47.5 -113z" />
<glyph unicode="&#xf0e1;" d="M349 911v-991h-330v991h330zM370 1217q1 -73 -50.5 -122t-135.5 -49h-2q-82 0 -132 49t-50 122q0 74 51.5 122.5t134.5 48.5t133 -48.5t51 -122.5zM1536 488v-568h-329v530q0 105 -40.5 164.5t-126.5 59.5q-63 0 -105.5 -34.5t-63.5 -85.5q-11 -30 -11 -81v-553h-329 q2 399 2 647t-1 296l-1 48h329v-144h-2q20 32 41 56t56.5 52t87 43.5t114.5 15.5q171 0 275 -113.5t104 -332.5z" />
<glyph unicode="&#xf0e2;" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298z" />
<glyph unicode="&#xf0e3;" horiz-adv-x="1792" d="M1771 0q0 -53 -37 -90l-107 -108q-39 -37 -91 -37q-53 0 -90 37l-363 364q-38 36 -38 90q0 53 43 96l-256 256l-126 -126q-14 -14 -34 -14t-34 14q2 -2 12.5 -12t12.5 -13t10 -11.5t10 -13.5t6 -13.5t5.5 -16.5t1.5 -18q0 -38 -28 -68q-3 -3 -16.5 -18t-19 -20.5 t-18.5 -16.5t-22 -15.5t-22 -9t-26 -4.5q-40 0 -68 28l-408 408q-28 28 -28 68q0 13 4.5 26t9 22t15.5 22t16.5 18.5t20.5 19t18 16.5q30 28 68 28q10 0 18 -1.5t16.5 -5.5t13.5 -6t13.5 -10t11.5 -10t13 -12.5t12 -12.5q-14 14 -14 34t14 34l348 348q14 14 34 14t34 -14 q-2 2 -12.5 12t-12.5 13t-10 11.5t-10 13.5t-6 13.5t-5.5 16.5t-1.5 18q0 38 28 68q3 3 16.5 18t19 20.5t18.5 16.5t22 15.5t22 9t26 4.5q40 0 68 -28l408 -408q28 -28 28 -68q0 -13 -4.5 -26t-9 -22t-15.5 -22t-16.5 -18.5t-20.5 -19t-18 -16.5q-30 -28 -68 -28 q-10 0 -18 1.5t-16.5 5.5t-13.5 6t-13.5 10t-11.5 10t-13 12.5t-12 12.5q14 -14 14 -34t-14 -34l-126 -126l256 -256q43 43 96 43q52 0 91 -37l363 -363q37 -39 37 -91z" />
<glyph unicode="&#xf0e4;" horiz-adv-x="1792" d="M384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM576 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1004 351l101 382q6 26 -7.5 48.5t-38.5 29.5 t-48 -6.5t-30 -39.5l-101 -382q-60 -5 -107 -43.5t-63 -98.5q-20 -77 20 -146t117 -89t146 20t89 117q16 60 -6 117t-72 91zM1664 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 1024q0 53 -37.5 90.5 t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1472 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 384q0 -261 -141 -483q-19 -29 -54 -29h-1402q-35 0 -54 29 q-141 221 -141 483q0 182 71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
<glyph unicode="&#xf0e5;" horiz-adv-x="1792" d="M896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640 q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 174 120 321.5 t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" />
<glyph unicode="&#xf0e6;" horiz-adv-x="1792" d="M704 1152q-153 0 -286 -52t-211.5 -141t-78.5 -191q0 -82 53 -158t149 -132l97 -56l-35 -84q34 20 62 39l44 31l53 -10q78 -14 153 -14q153 0 286 52t211.5 141t78.5 191t-78.5 191t-211.5 141t-286 52zM704 1280q191 0 353.5 -68.5t256.5 -186.5t94 -257t-94 -257 t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224q0 139 94 257t256.5 186.5 t353.5 68.5zM1526 111q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129 q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230q0 -120 -71 -224.5t-195 -176.5z" />
<glyph unicode="&#xf0e7;" horiz-adv-x="896" d="M885 970q18 -20 7 -44l-540 -1157q-13 -25 -42 -25q-4 0 -14 2q-17 5 -25.5 19t-4.5 30l197 808l-406 -101q-4 -1 -12 -1q-18 0 -31 11q-18 15 -13 39l201 825q4 14 16 23t28 9h328q19 0 32 -12.5t13 -29.5q0 -8 -5 -18l-171 -463l396 98q8 2 12 2q19 0 34 -15z" />
<glyph unicode="&#xf0e8;" horiz-adv-x="1792" d="M1792 288v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192q0 52 38 90t90 38h512v192h-96q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h320q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-96v-192h512q52 0 90 -38t38 -90v-192h96q40 0 68 -28t28 -68 z" />
<glyph unicode="&#xf0e9;" horiz-adv-x="1664" d="M896 708v-580q0 -104 -76 -180t-180 -76t-180 76t-76 180q0 26 19 45t45 19t45 -19t19 -45q0 -50 39 -89t89 -39t89 39t39 89v580q33 11 64 11t64 -11zM1664 681q0 -13 -9.5 -22.5t-22.5 -9.5q-11 0 -23 10q-49 46 -93 69t-102 23q-68 0 -128 -37t-103 -97 q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -28 -17q-18 0 -29 17q-4 6 -14.5 24t-17.5 28q-43 60 -102.5 97t-127.5 37t-127.5 -37t-102.5 -97q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -29 -17q-17 0 -28 17q-4 6 -14.5 24t-17.5 28q-43 60 -103 97t-128 37q-58 0 -102 -23t-93 -69 q-12 -10 -23 -10q-13 0 -22.5 9.5t-9.5 22.5q0 5 1 7q45 183 172.5 319.5t298 204.5t360.5 68q140 0 274.5 -40t246.5 -113.5t194.5 -187t115.5 -251.5q1 -2 1 -7zM896 1408v-98q-42 2 -64 2t-64 -2v98q0 26 19 45t45 19t45 -19t19 -45z" />
<glyph unicode="&#xf0ea;" horiz-adv-x="1792" d="M768 -128h896v640h-416q-40 0 -68 28t-28 68v416h-384v-1152zM1024 1312v64q0 13 -9.5 22.5t-22.5 9.5h-704q-13 0 -22.5 -9.5t-9.5 -22.5v-64q0 -13 9.5 -22.5t22.5 -9.5h704q13 0 22.5 9.5t9.5 22.5zM1280 640h299l-299 299v-299zM1792 512v-672q0 -40 -28 -68t-68 -28 h-960q-40 0 -68 28t-28 68v160h-544q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1088q40 0 68 -28t28 -68v-328q21 -13 36 -28l408 -408q28 -28 48 -76t20 -88z" />
<glyph unicode="&#xf0eb;" horiz-adv-x="1024" d="M736 960q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5q0 46 -54 71t-106 25q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5q50 0 99.5 -16t87 -54t37.5 -90zM896 960q0 72 -34.5 134t-90 101.5t-123 62t-136.5 22.5t-136.5 -22.5t-123 -62t-90 -101.5t-34.5 -134 q0 -101 68 -180q10 -11 30.5 -33t30.5 -33q128 -153 141 -298h228q13 145 141 298q10 11 30.5 33t30.5 33q68 79 68 180zM1024 960q0 -155 -103 -268q-45 -49 -74.5 -87t-59.5 -95.5t-34 -107.5q47 -28 47 -82q0 -37 -25 -64q25 -27 25 -64q0 -52 -45 -81q13 -23 13 -47 q0 -46 -31.5 -71t-77.5 -25q-20 -44 -60 -70t-87 -26t-87 26t-60 70q-46 0 -77.5 25t-31.5 71q0 24 13 47q-45 29 -45 81q0 37 25 64q-25 27 -25 64q0 54 47 82q-4 50 -34 107.5t-59.5 95.5t-74.5 87q-103 113 -103 268q0 99 44.5 184.5t117 142t164 89t186.5 32.5 t186.5 -32.5t164 -89t117 -142t44.5 -184.5z" />
<glyph unicode="&#xf0ec;" horiz-adv-x="1792" d="M1792 352v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5q-12 0 -24 10l-319 320q-9 9 -9 22q0 14 9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h1376q13 0 22.5 -9.5t9.5 -22.5zM1792 896q0 -14 -9 -23l-320 -320q-9 -9 -23 -9 q-13 0 -22.5 9.5t-9.5 22.5v192h-1376q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1376v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" />
<glyph unicode="&#xf0ed;" horiz-adv-x="1920" d="M1280 608q0 14 -9 23t-23 9h-224v352q0 13 -9.5 22.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-352h-224q-13 0 -22.5 -9.5t-9.5 -22.5q0 -14 9 -23l352 -352q9 -9 23 -9t23 9l351 351q10 12 10 24zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" />
<glyph unicode="&#xf0ee;" horiz-adv-x="1920" d="M1280 672q0 14 -9 23l-352 352q-9 9 -23 9t-23 -9l-351 -351q-10 -12 -10 -24q0 -14 9 -23t23 -9h224v-352q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5v352h224q13 0 22.5 9.5t9.5 22.5zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" />
<glyph unicode="&#xf0f0;" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 38 90t90 38t90 -38t38 -90v-89q-32 -29 -32 -71q0 -40 28 -68 t68 -28t68 28t28 68q0 42 -32 71v89q0 68 -34.5 127.5t-93.5 93.5q0 10 0.5 42.5t0 48t-2.5 41.5t-7 47t-13 40q68 -15 120 -60.5t81 -103t47.5 -132.5t24 -138t5.5 -131zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" />
<glyph unicode="&#xf0f1;" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 -48q10 2 16 2q26 0 45 -19t19 -45v-512q0 -144 -110 -252 t-274 -128v-132q0 -106 94 -181t226 -75t226 75t94 181v395q-57 21 -92.5 70t-35.5 111q0 80 56 136t136 56t136 -56t56 -136z" />
<glyph unicode="&#xf0f2;" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" />
<glyph unicode="&#xf0f3;" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1664 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5 q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117t96 -262q0 -262 97 -498.5t287 -397.5z" />
<glyph unicode="&#xf0f4;" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" />
<glyph unicode="&#xf0f5;" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0f6;" horiz-adv-x="1280" d="M1024 352v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM1024 608v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280z M768 896h299l-299 299v-299zM1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h544q40 0 88 -20t76 -48l408 -408q28 -28 48 -76t20 -88z" />
<glyph unicode="&#xf0f7;" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1536h-1152v-1536h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM1408 1472v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0f8;" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1152h-256v-32q0 -40 -28 -68t-68 -28h-448q-40 0 -68 28t-28 68v32h-256v-1152h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM896 1056v320q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-96h-128v96q0 13 -9.5 22.5 t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5v96h128v-96q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1408 1088v-1280q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1280q0 26 19 45t45 19h320 v288q0 40 28 68t68 28h448q40 0 68 -28t28 -68v-288h320q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0f9;" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM1920 1344v-1152 q0 -26 -19 -45t-45 -19h-192q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-128q-26 0 -45 19t-19 45t19 45t45 19v416q0 26 13 58t32 51l198 198q19 19 51 32t58 13h160v320q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf0fa;" horiz-adv-x="1792" d="M1280 416v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM640 1152h512v128h-512v-128zM256 1152v-1280h-32 q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h32zM1440 1152v-1280h-1088v1280h160v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h160zM1792 928v-832q0 -92 -66 -158t-158 -66h-32v1280h32q92 0 158 -66t66 -158z" />
<glyph unicode="&#xf0fb;" horiz-adv-x="1920" d="M1920 576q-1 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-96h-160h-64v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h64h160h96 q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-69l293 -352h64l224 -64l352 -32q261 -58 287 -93z" />
<glyph unicode="&#xf0fc;" horiz-adv-x="1664" d="M640 640v384h-256v-256q0 -53 37.5 -90.5t90.5 -37.5h128zM1664 192v-192h-1152v192l128 192h-128q-159 0 -271.5 112.5t-112.5 271.5v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" />
<glyph unicode="&#xf0fd;" d="M1280 192v896q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-512v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-896q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h512v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf0fe;" d="M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf100;" horiz-adv-x="1024" d="M627 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23zM1011 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23z" />
<glyph unicode="&#xf101;" horiz-adv-x="1024" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM979 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23 l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
<glyph unicode="&#xf102;" horiz-adv-x="1152" d="M1075 224q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM1075 608q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393 q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
<glyph unicode="&#xf103;" horiz-adv-x="1152" d="M1075 672q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23zM1075 1056q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
<glyph unicode="&#xf104;" horiz-adv-x="640" d="M627 992q0 -13 -10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
<glyph unicode="&#xf105;" horiz-adv-x="640" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
<glyph unicode="&#xf106;" horiz-adv-x="1152" d="M1075 352q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
<glyph unicode="&#xf107;" horiz-adv-x="1152" d="M1075 800q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
<glyph unicode="&#xf108;" horiz-adv-x="1920" d="M1792 544v832q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1376v-1088q0 -66 -47 -113t-113 -47h-544q0 -37 16 -77.5t32 -71t16 -43.5q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19 t-19 45q0 14 16 44t32 70t16 78h-544q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf109;" horiz-adv-x="1920" d="M416 256q-66 0 -113 47t-47 113v704q0 66 47 113t113 47h1088q66 0 113 -47t47 -113v-704q0 -66 -47 -113t-113 -47h-1088zM384 1120v-704q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5v704q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5z M1760 192h160v-96q0 -40 -47 -68t-113 -28h-1600q-66 0 -113 28t-47 68v96h160h1600zM1040 96q16 0 16 16t-16 16h-160q-16 0 -16 -16t16 -16h160z" />
<glyph unicode="&#xf10a;" horiz-adv-x="1152" d="M640 128q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1024 288v960q0 13 -9.5 22.5t-22.5 9.5h-832q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h832q13 0 22.5 9.5t9.5 22.5zM1152 1248v-1088q0 -66 -47 -113t-113 -47h-832 q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h832q66 0 113 -47t47 -113z" />
<glyph unicode="&#xf10b;" horiz-adv-x="768" d="M464 128q0 33 -23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5t56.5 23.5t23.5 56.5zM672 288v704q0 13 -9.5 22.5t-22.5 9.5h-512q-13 0 -22.5 -9.5t-9.5 -22.5v-704q0 -13 9.5 -22.5t22.5 -9.5h512q13 0 22.5 9.5t9.5 22.5zM480 1136 q0 16 -16 16h-160q-16 0 -16 -16t16 -16h160q16 0 16 16zM768 1152v-1024q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v1024q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" />
<glyph unicode="&#xf10c;" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf10d;" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" />
<glyph unicode="&#xf10e;" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" />
<glyph unicode="&#xf110;" horiz-adv-x="1568" d="M496 192q0 -60 -42.5 -102t-101.5 -42q-60 0 -102 42t-42 102t42 102t102 42q59 0 101.5 -42t42.5 -102zM928 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -66 -47 -113t-113 -47t-113 47t-47 113 t47 113t113 47t113 -47t47 -113zM1360 192q0 -46 -33 -79t-79 -33t-79 33t-33 79t33 79t79 33t79 -33t33 -79zM528 1088q0 -73 -51.5 -124.5t-124.5 -51.5t-124.5 51.5t-51.5 124.5t51.5 124.5t124.5 51.5t124.5 -51.5t51.5 -124.5zM992 1280q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1536 640q0 -40 -28 -68t-68 -28t-68 28t-28 68t28 68t68 28t68 -28t28 -68zM1328 1088q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5z" />
<glyph unicode="&#xf111;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf112;" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" />
<glyph unicode="&#xf113;" horiz-adv-x="1664" />
<glyph unicode="&#xf114;" horiz-adv-x="1664" d="M1536 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68v-960q0 -40 28 -68t68 -28h1216q40 0 68 28t28 68zM1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320 q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
<glyph unicode="&#xf115;" horiz-adv-x="1920" d="M1781 605q0 35 -53 35h-1088q-40 0 -85.5 -21.5t-71.5 -52.5l-294 -363q-18 -24 -18 -40q0 -35 53 -35h1088q40 0 86 22t71 53l294 363q18 22 18 39zM640 768h768v160q0 40 -28 68t-68 28h-576q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68 v-853l256 315q44 53 116 87.5t140 34.5zM1909 605q0 -62 -46 -120l-295 -363q-43 -53 -116 -87.5t-140 -34.5h-1088q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158v-160h192q54 0 99 -24.5t67 -70.5q15 -32 15 -68z " />
<glyph unicode="&#xf116;" horiz-adv-x="1152" d="M896 608v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h224q14 0 23 -9t9 -23zM1024 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 -28 t-28 -68v-704q0 -40 28 -68t68 -28h704q40 0 68 28t28 68zM1152 928v-704q0 -92 -65.5 -158t-158.5 -66h-704q-93 0 -158.5 66t-65.5 158v704q0 93 65.5 158.5t158.5 65.5h704q93 0 158.5 -65.5t65.5 -158.5z" />
<glyph unicode="&#xf117;" horiz-adv-x="1152" d="M928 1152q93 0 158.5 -65.5t65.5 -158.5v-704q0 -92 -65.5 -158t-158.5 -66h-704q-93 0 -158.5 66t-65.5 158v704q0 93 65.5 158.5t158.5 65.5h704zM1024 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 -28t-28 -68v-704q0 -40 28 -68t68 -28h704q40 0 68 28t28 68z M864 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-576q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h576z" />
<glyph unicode="&#xf118;" d="M1134 461q-37 -121 -138 -195t-228 -74t-228 74t-138 195q-8 25 4 48.5t38 31.5q25 8 48.5 -4t31.5 -38q25 -80 92.5 -129.5t151.5 -49.5t151.5 49.5t92.5 129.5q8 26 32 38t49 4t37 -31.5t4 -48.5zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5 t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf119;" d="M1134 307q8 -25 -4 -48.5t-37 -31.5t-49 4t-32 38q-25 80 -92.5 129.5t-151.5 49.5t-151.5 -49.5t-92.5 -129.5q-8 -26 -31.5 -38t-48.5 -4q-26 8 -38 31.5t-4 48.5q37 121 138 195t228 74t228 -74t138 -195zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204 t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf11a;" d="M1152 448q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h640q26 0 45 -19t19 -45zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf11b;" horiz-adv-x="1920" d="M832 448v128q0 14 -9 23t-23 9h-192v192q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-192h-192q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h192v-192q0 -14 9 -23t23 -9h128q14 0 23 9t9 23v192h192q14 0 23 9t9 23zM1408 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1920 512q0 -212 -150 -362t-362 -150q-192 0 -338 128h-220q-146 -128 -338 -128q-212 0 -362 150 t-150 362t150 362t362 150h896q212 0 362 -150t150 -362z" />
<glyph unicode="&#xf11c;" horiz-adv-x="1920" d="M384 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM512 624v-96q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h224q16 0 16 -16zM384 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 368v-96q0 -16 -16 -16 h-864q-16 0 -16 16v96q0 16 16 16h864q16 0 16 -16zM768 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM640 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1024 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16 h96q16 0 16 -16zM896 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1280 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1152 880v-96 q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 880v-352q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h112v240q0 16 16 16h96q16 0 16 -16zM1792 128v896h-1664v-896 h1664zM1920 1024v-896q0 -53 -37.5 -90.5t-90.5 -37.5h-1664q-53 0 -90.5 37.5t-37.5 90.5v896q0 53 37.5 90.5t90.5 37.5h1664q53 0 90.5 -37.5t37.5 -90.5z" />
<glyph unicode="&#xf11d;" horiz-adv-x="1792" d="M1664 491v616q-169 -91 -306 -91q-82 0 -145 32q-100 49 -184 76.5t-178 27.5q-173 0 -403 -127v-599q245 113 433 113q55 0 103.5 -7.5t98 -26t77 -31t82.5 -39.5l28 -14q44 -22 101 -22q120 0 293 92zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9 h-64q-14 0 -23 9t-9 23v1266q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102 q-15 -9 -33 -9q-16 0 -32 8q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" />
<glyph unicode="&#xf11e;" horiz-adv-x="1792" d="M832 536v192q-181 -16 -384 -117v-185q205 96 384 110zM832 954v197q-172 -8 -384 -126v-189q215 111 384 118zM1664 491v184q-235 -116 -384 -71v224q-20 6 -39 15q-5 3 -33 17t-34.5 17t-31.5 15t-34.5 15.5t-32.5 13t-36 12.5t-35 8.5t-39.5 7.5t-39.5 4t-44 2 q-23 0 -49 -3v-222h19q102 0 192.5 -29t197.5 -82q19 -9 39 -15v-188q42 -17 91 -17q120 0 293 92zM1664 918v189q-169 -91 -306 -91q-45 0 -78 8v-196q148 -42 384 90zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v1266 q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102q-15 -9 -33 -9q-16 0 -32 8 q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" />
<glyph unicode="&#xf120;" horiz-adv-x="1664" d="M585 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23zM1664 96v-64q0 -14 -9 -23t-23 -9h-960q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h960q14 0 23 -9 t9 -23z" />
<glyph unicode="&#xf121;" horiz-adv-x="1920" d="M617 137l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23zM1208 1204l-373 -1291q-4 -13 -15.5 -19.5t-23.5 -2.5l-62 17q-13 4 -19.5 15.5t-2.5 24.5 l373 1291q4 13 15.5 19.5t23.5 2.5l62 -17q13 -4 19.5 -15.5t2.5 -24.5zM1865 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23z" />
<glyph unicode="&#xf122;" horiz-adv-x="1792" d="M640 454v-70q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-69l-397 -398q-19 -19 -19 -45t19 -45zM1792 416q0 -58 -17 -133.5t-38.5 -138t-48 -125t-40.5 -90.5l-20 -40q-8 -17 -28 -17q-6 0 -9 1 q-25 8 -23 34q43 400 -106 565q-64 71 -170.5 110.5t-267.5 52.5v-251q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-262q411 -28 599 -221q169 -173 169 -509z" />
<glyph unicode="&#xf123;" horiz-adv-x="1664" d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500q5 -33 -6 -51.5t-34 -18.5q-17 0 -40 12l-449 236l-449 -236q-23 -12 -40 -12q-23 0 -34 18.5t-6 51.5l86 500l-364 354q-32 32 -23 59.5t54 34.5 l502 73l225 455q20 41 49 41q28 0 49 -41l225 -455l502 -73q45 -7 54 -34.5t-24 -59.5z" />
<glyph unicode="&#xf124;" horiz-adv-x="1408" d="M1401 1187l-640 -1280q-17 -35 -57 -35q-5 0 -15 2q-22 5 -35.5 22.5t-13.5 39.5v576h-576q-22 0 -39.5 13.5t-22.5 35.5t4 42t29 30l1280 640q13 7 29 7q27 0 45 -19q15 -14 18.5 -34.5t-6.5 -39.5z" />
<glyph unicode="&#xf125;" horiz-adv-x="1664" d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v224h-864q-14 0 -23 9t-9 23v864h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224v224q0 14 9 23t23 9h192q14 0 23 -9t9 -23 v-224h851l246 247q10 9 23 9t23 -9q9 -10 9 -23t-9 -23l-247 -246v-851h224q14 0 23 -9t9 -23z" />
<glyph unicode="&#xf126;" horiz-adv-x="1024" d="M288 64q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM288 1216q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM928 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1024 1088q0 -52 -26 -96.5t-70 -69.5 q-2 -287 -226 -414q-68 -38 -203 -81q-128 -40 -169.5 -71t-41.5 -100v-26q44 -25 70 -69.5t26 -96.5q0 -80 -56 -136t-136 -56t-136 56t-56 136q0 52 26 96.5t70 69.5v820q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136q0 -52 -26 -96.5t-70 -69.5v-497 q54 26 154 57q55 17 87.5 29.5t70.5 31t59 39.5t40.5 51t28 69.5t8.5 91.5q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136z" />
<glyph unicode="&#xf127;" horiz-adv-x="1664" d="M439 265l-256 -256q-10 -9 -23 -9q-12 0 -23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23zM608 224v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM384 448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23t9 23t23 9h320 q14 0 23 -9t9 -23zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-334 335q-21 21 -42 56l239 18l273 -274q27 -27 68 -27.5t68 26.5l147 146q28 28 28 67q0 40 -28 68l-274 275l18 239q35 -21 56 -42l336 -336q84 -86 84 -204zM1031 1044l-239 -18 l-273 274q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l274 -274l-18 -240q-35 21 -56 42l-336 336q-84 86 -84 204q0 120 85 203l147 146q83 83 203 83q121 0 204 -85l334 -335q21 -21 42 -56zM1664 960q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9 t-9 23t9 23t23 9h320q14 0 23 -9t9 -23zM1120 1504v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM1527 1353l-256 -256q-11 -9 -23 -9t-23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" />
<glyph unicode="&#xf128;" horiz-adv-x="1024" d="M704 280v-240q0 -16 -12 -28t-28 -12h-240q-16 0 -28 12t-12 28v240q0 16 12 28t28 12h240q16 0 28 -12t12 -28zM1020 880q0 -54 -15.5 -101t-35 -76.5t-55 -59.5t-57.5 -43.5t-61 -35.5q-41 -23 -68.5 -65t-27.5 -67q0 -17 -12 -32.5t-28 -15.5h-240q-15 0 -25.5 18.5 t-10.5 37.5v45q0 83 65 156.5t143 108.5q59 27 84 56t25 76q0 42 -46.5 74t-107.5 32q-65 0 -108 -29q-35 -25 -107 -115q-13 -16 -31 -16q-12 0 -25 8l-164 125q-13 10 -15.5 25t5.5 28q160 266 464 266q80 0 161 -31t146 -83t106 -127.5t41 -158.5z" />
<glyph unicode="&#xf129;" horiz-adv-x="640" d="M640 192v-128q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64v384h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-576h64q26 0 45 -19t19 -45zM512 1344v-192q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v192 q0 26 19 45t45 19h256q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf12a;" horiz-adv-x="640" d="M512 288v-224q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v224q0 26 19 45t45 19h256q26 0 45 -19t19 -45zM542 1344l-28 -768q-1 -26 -20.5 -45t-45.5 -19h-256q-26 0 -45.5 19t-20.5 45l-28 768q-1 26 17.5 45t44.5 19h320q26 0 44.5 -19t17.5 -45z" />
<glyph unicode="&#xf12b;" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1534 846v-206h-514l-3 27 q-4 28 -4 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q83 65 188 65q110 0 178 -59.5t68 -158.5q0 -56 -24.5 -103t-62 -76.5t-81.5 -58.5t-82 -50.5t-65.5 -51.5t-30.5 -63h232v80 h126z" />
<glyph unicode="&#xf12c;" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1536 -50v-206h-514l-4 27 q-3 45 -3 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q80 65 188 65q110 0 178 -59.5t68 -158.5q0 -66 -34.5 -118.5t-84 -86t-99.5 -62.5t-87 -63t-41 -73h232v80h126z" />
<glyph unicode="&#xf12d;" horiz-adv-x="1920" d="M896 128l336 384h-768l-336 -384h768zM1909 1205q15 -34 9.5 -71.5t-30.5 -65.5l-896 -1024q-38 -44 -96 -44h-768q-38 0 -69.5 20.5t-47.5 54.5q-15 34 -9.5 71.5t30.5 65.5l896 1024q38 44 96 44h768q38 0 69.5 -20.5t47.5 -54.5z" />
<glyph unicode="&#xf12e;" horiz-adv-x="1664" d="M1664 438q0 -81 -44.5 -135t-123.5 -54q-41 0 -77.5 17.5t-59 38t-56.5 38t-71 17.5q-110 0 -110 -124q0 -39 16 -115t15 -115v-5q-22 0 -33 -1q-34 -3 -97.5 -11.5t-115.5 -13.5t-98 -5q-61 0 -103 26.5t-42 83.5q0 37 17.5 71t38 56.5t38 59t17.5 77.5q0 79 -54 123.5 t-135 44.5q-84 0 -143 -45.5t-59 -127.5q0 -43 15 -83t33.5 -64.5t33.5 -53t15 -50.5q0 -45 -46 -89q-37 -35 -117 -35q-95 0 -245 24q-9 2 -27.5 4t-27.5 4l-13 2q-1 0 -3 1q-2 0 -2 1v1024q2 -1 17.5 -3.5t34 -5t21.5 -3.5q150 -24 245 -24q80 0 117 35q46 44 46 89 q0 22 -15 50.5t-33.5 53t-33.5 64.5t-15 83q0 82 59 127.5t144 45.5q80 0 134 -44.5t54 -123.5q0 -41 -17.5 -77.5t-38 -59t-38 -56.5t-17.5 -71q0 -57 42 -83.5t103 -26.5q64 0 180 15t163 17v-2q-1 -2 -3.5 -17.5t-5 -34t-3.5 -21.5q-24 -150 -24 -245q0 -80 35 -117 q44 -46 89 -46q22 0 50.5 15t53 33.5t64.5 33.5t83 15q82 0 127.5 -59t45.5 -143z" />
<glyph unicode="&#xf130;" horiz-adv-x="1152" d="M1152 832v-128q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-217 24 -364.5 187.5t-147.5 384.5v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -185 131.5 -316.5t316.5 -131.5 t316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45zM896 1216v-512q0 -132 -94 -226t-226 -94t-226 94t-94 226v512q0 132 94 226t226 94t226 -94t94 -226z" />
<glyph unicode="&#xf131;" horiz-adv-x="1408" d="M271 591l-101 -101q-42 103 -42 214v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -53 15 -113zM1385 1193l-361 -361v-128q0 -132 -94 -226t-226 -94q-55 0 -109 19l-96 -96q97 -51 205 -51q185 0 316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45v-128 q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-125 13 -235 81l-254 -254q-10 -10 -23 -10t-23 10l-82 82q-10 10 -10 23t10 23l1234 1234q10 10 23 10t23 -10l82 -82q10 -10 10 -23 t-10 -23zM1005 1325l-621 -621v512q0 132 94 226t226 94q102 0 184.5 -59t116.5 -152z" />
<glyph unicode="&#xf132;" horiz-adv-x="1280" d="M1088 576v640h-448v-1137q119 63 213 137q235 184 235 360zM1280 1344v-768q0 -86 -33.5 -170.5t-83 -150t-118 -127.5t-126.5 -103t-121 -77.5t-89.5 -49.5t-42.5 -20q-12 -6 -26 -6t-26 6q-16 7 -42.5 20t-89.5 49.5t-121 77.5t-126.5 103t-118 127.5t-83 150 t-33.5 170.5v768q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
<glyph unicode="&#xf133;" horiz-adv-x="1664" d="M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
<glyph unicode="&#xf134;" horiz-adv-x="1408" d="M512 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 1376v-320q0 -16 -12 -25q-8 -7 -20 -7q-4 0 -7 1l-448 96q-11 2 -18 11t-7 20h-256v-102q111 -23 183.5 -111t72.5 -203v-800q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v800 q0 106 62.5 190.5t161.5 114.5v111h-32q-59 0 -115 -23.5t-91.5 -53t-66 -66.5t-40.5 -53.5t-14 -24.5q-17 -35 -57 -35q-16 0 -29 7q-23 12 -31.5 37t3.5 49q5 10 14.5 26t37.5 53.5t60.5 70t85 67t108.5 52.5q-25 42 -25 86q0 66 47 113t113 47t113 -47t47 -113 q0 -33 -14 -64h302q0 11 7 20t18 11l448 96q3 1 7 1q12 0 20 -7q12 -9 12 -25z" />
<glyph unicode="&#xf135;" horiz-adv-x="1664" d="M1440 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1664 1376q0 -249 -75.5 -430.5t-253.5 -360.5q-81 -80 -195 -176l-20 -379q-2 -16 -16 -26l-384 -224q-7 -4 -16 -4q-12 0 -23 9l-64 64q-13 14 -8 32l85 276l-281 281l-276 -85q-3 -1 -9 -1 q-14 0 -23 9l-64 64q-17 19 -5 39l224 384q10 14 26 16l379 20q96 114 176 195q188 187 358 258t431 71q14 0 24 -9.5t10 -22.5z" />
<glyph unicode="&#xf136;" horiz-adv-x="1792" d="M1708 881l-188 -881h-304l181 849q4 21 1 43q-4 20 -16 35q-10 14 -28 24q-18 9 -40 9h-197l-205 -960h-303l204 960h-304l-205 -960h-304l272 1280h1139q157 0 245 -118q86 -116 52 -281z" />
<glyph unicode="&#xf137;" d="M909 141l102 102q19 19 19 45t-19 45l-307 307l307 307q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf138;" d="M717 141l454 454q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l307 -307l-307 -307q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf139;" d="M1165 397l102 102q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l307 307l307 -307q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf13a;" d="M813 237l454 454q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-307 -307l-307 307q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf13b;" horiz-adv-x="1408" d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" />
<glyph unicode="&#xf13c;" horiz-adv-x="1792" d="M275 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" />
<glyph unicode="&#xf13d;" horiz-adv-x="1792" d="M960 1280q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1790 276q-8 -20 -30 -20h-112q0 -137 -99.5 -251t-272 -179.5t-380.5 -65.5t-380.5 65.5t-272 179.5t-99.5 251h-112q-22 0 -30 20q-8 19 7 35l224 224q10 9 23 9q12 0 23 -9l224 -224 q15 -16 7 -35q-8 -20 -30 -20h-112q0 -85 112.5 -162.5t287.5 -100.5v647h-192q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h192v163q-58 34 -93 92.5t-35 128.5q0 106 75 181t181 75t181 -75t75 -181q0 -70 -35 -128.5t-93 -92.5v-163h192q26 0 45 -19t19 -45v-128 q0 -26 -19 -45t-45 -19h-192v-647q175 23 287.5 100.5t112.5 162.5h-112q-22 0 -30 20q-8 19 7 35l224 224q11 9 23 9t23 -9l224 -224q15 -16 7 -35z" />
<glyph unicode="&#xf13e;" horiz-adv-x="1152" d="M1056 768q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v320q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45q0 106 -75 181t-181 75t-181 -75t-75 -181 v-320h736zM703 169l-69 229q32 17 51 47t19 67q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5q0 -37 19 -67t51 -47l-69 -229q-5 -15 5 -28t26 -13h192q16 0 26 13t5 28z" />
<glyph unicode="&#xf140;" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM1152 640q0 159 -112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM1280 640q0 -212 -150 -362t-362 -150t-362 150 t-150 362t150 362t362 150t362 -150t150 -362zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
<glyph unicode="&#xf141;" horiz-adv-x="1408" d="M384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM896 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM1408 800v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf142;" horiz-adv-x="384" d="M384 288v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 1312v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" />
<glyph unicode="&#xf143;" d="M512 256q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM863 162q-13 232 -177 396t-396 177q-14 1 -24 -9t-10 -23v-128q0 -13 8.5 -22t21.5 -10q154 -11 264 -121t121 -264q1 -13 10 -21.5t22 -8.5h128q13 0 23 10 t9 24zM1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf144;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1152 585q32 18 32 55t-32 55l-544 320q-31 19 -64 1q-32 -19 -32 -56v-640q0 -37 32 -56 q16 -8 32 -8q17 0 32 9z" />
<glyph unicode="&#xf145;" horiz-adv-x="1792" d="M1024 1084l316 -316l-572 -572l-316 316zM813 105l618 618q19 19 19 45t-19 45l-362 362q-18 18 -45 18t-45 -18l-618 -618q-19 -19 -19 -45t19 -45l362 -362q18 -18 45 -18t45 18zM1702 742l-907 -908q-37 -37 -90.5 -37t-90.5 37l-126 126q56 56 56 136t-56 136 t-136 56t-136 -56l-125 126q-37 37 -37 90.5t37 90.5l907 906q37 37 90.5 37t90.5 -37l125 -125q-56 -56 -56 -136t56 -136t136 -56t136 56l126 -125q37 -37 37 -90.5t-37 -90.5z" />
<glyph unicode="&#xf146;" d="M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
<glyph unicode="&#xf147;" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h832q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5 t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf148;" horiz-adv-x="1024" d="M1018 933q-18 -37 -58 -37h-192v-864q0 -14 -9 -23t-23 -9h-704q-21 0 -29 18q-8 20 4 35l160 192q9 11 25 11h320v640h-192q-40 0 -58 37q-17 37 9 68l320 384q18 22 49 22t49 -22l320 -384q27 -32 9 -68z" />
<glyph unicode="&#xf149;" horiz-adv-x="1024" d="M32 1280h704q13 0 22.5 -9.5t9.5 -23.5v-863h192q40 0 58 -37t-9 -69l-320 -384q-18 -22 -49 -22t-49 22l-320 384q-26 31 -9 69q18 37 58 37h192v640h-320q-14 0 -25 11l-160 192q-13 14 -4 34q9 19 29 19z" />
<glyph unicode="&#xf14a;" d="M685 237l614 614q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-467 -467l-211 211q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l358 -358q19 -19 45 -19t45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5 t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf14b;" d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818q14 -13 -3 -30l-291 -291q-17 -17 -30 -3q-14 13 3 30l291 291q17 17 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92q28 28 28 68t-28 68l-152 152q-28 28 -68 28t-68 -28l-92 -92zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf14c;" d="M1280 608v480q0 26 -19 45t-45 19h-480q-42 0 -59 -39q-17 -41 14 -70l144 -144l-534 -534q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l534 534l144 -144q18 -19 45 -19q12 0 25 5q39 17 39 59zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf14d;" d="M1005 435l352 352q19 19 19 45t-19 45l-352 352q-30 31 -69 14q-40 -17 -40 -59v-160q-119 0 -216 -19.5t-162.5 -51t-114 -79t-76.5 -95.5t-44.5 -109t-21.5 -111.5t-5 -110.5q0 -181 167 -404q10 -12 25 -12q7 0 13 3q22 9 19 33q-44 354 62 473q46 52 130 75.5 t224 23.5v-160q0 -42 40 -59q12 -5 24 -5q26 0 45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
<glyph unicode="&#xf14e;" horiz-adv-x="1792" />
<glyph unicode="&#xf500;" horiz-adv-x="1792" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -140,11 +140,6 @@ $(document).ready(function() {
$('.new-course-button').bind('click', addNewCourse);
// section name editing
$('.section-name').bind('click', editSectionName);
$('.edit-section-name-cancel').bind('click', cancelEditSectionName);
// $('.edit-section-name-save').bind('click', saveEditSectionName);
// section date setting
$('.set-publish-date').bind('click', setSectionScheduleDate);
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
@@ -209,8 +204,8 @@ function toggleSections(e) {
$section = $('.courseware-section');
sectionCount = $section.length;
$button = $(this);
$labelCollapsed = $('<i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span>');
$labelExpanded = $('<i class="ss-icon ss-symbolicons-block">down</i> <span class="label">Expand All Sections</span>');
$labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span>');
$labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span>');
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
$button.toggleClass('is-activated').html(buttonLabel);
@@ -416,8 +411,12 @@ function showFileSelectionMenu(e) {
}
function startUpload(e) {
var files = $('.file-input').get(0).files;
if (files.length === 0)
return;
$('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html($('.file-input').val().replace('C:\\fakepath\\', ''));
$('.upload-modal .file-name').html(files[0].name);
$('.upload-modal .file-chooser').ajaxSubmit({
beforeSend: resetUploadBar,
uploadProgress: showUploadFeedback,
@@ -763,72 +762,6 @@ function cancelNewSubsection(e) {
$(this).parents('li.branch').remove();
}
function editSectionName(e) {
e.preventDefault();
$(this).unbind('click', editSectionName);
$(this).children('.section-name-edit').show();
$(this).find('.edit-section-name').focus();
$(this).children('.section-name-span').hide();
$(this).find('.section-name-edit').bind('submit', saveEditSectionName);
$(this).find('.edit-section-name-cancel').bind('click', cancelNewSection);
$body.bind('keyup', {
$cancelButton: $(this).find('.edit-section-name-cancel')
}, checkForCancel);
}
function cancelEditSectionName(e) {
e.preventDefault();
$(this).parent().hide();
$(this).parent().siblings('.section-name-span').show();
$(this).closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
}
function saveEditSectionName(e) {
e.preventDefault();
$(this).closest('.section-name').unbind('click', editSectionName);
var id = $(this).closest('.courseware-section').data('id');
var display_name = $.trim($(this).find('.edit-section-name').val());
$(this).closest('.courseware-section .section-name').append($spinner);
$spinner.show();
if (display_name == '') {
alert("You must specify a name before saving.");
return;
}
analytics.track('Edited Section Name', {
'course': course_location_analytics,
'display_name': display_name,
'id': id
});
var $_this = $(this);
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': {
'display_name': display_name
}
})
}).success(function() {
$spinner.delay(250).fadeOut(250);
$_this.closest('h3').find('.section-name-span').html(display_name).show();
$_this.hide();
$_this.closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
});
}
function setSectionScheduleDate(e) {
e.preventDefault();
$(this).closest("h4").hide();

View File

@@ -0,0 +1,49 @@
CMS.Models.SystemFeedback = Backbone.Model.extend({
defaults: {
"intent": null, // "warning", "confirmation", "error", "announcement", "step-required", etc
"title": "",
"message": ""
/* could also have an "actions" hash: here is an example demonstrating
the expected structure
"actions": {
"primary": {
"text": "Save",
"class": "action-save",
"click": function() {
// do something when Save is clicked
// `this` refers to the model
}
},
"secondary": [
{
"text": "Cancel",
"class": "action-cancel",
"click": function() {}
}, {
"text": "Discard Changes",
"class": "action-discard",
"click": function() {}
}
]
}
*/
}
});
CMS.Models.WarningMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "warning"
})
});
CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "error"
})
});
CMS.Models.ConfirmationMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "confirmation"
})
});

View File

@@ -0,0 +1,113 @@
/**
* Model used for metadata setting editors. This model does not do its own saving,
* as that is done by module_edit.coffee.
*/
CMS.Models.Metadata = Backbone.Model.extend({
defaults: {
"field_name": null,
"display_name": null,
"value" : null,
"explicitly_set": null,
"default_value" : null,
"options" : null,
"type" : null
},
initialize: function() {
this.original_value = this.get('value');
this.original_explicitly_set = this.get('explicitly_set');
},
/**
* Returns true if the stored value is different, or if the "explicitly_set"
* property has changed.
*/
isModified : function() {
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
return false;
}
if (this.get('explicitly_set') && this.original_explicitly_set) {
return this.get('value') !== this.original_value;
}
return true;
},
/**
* Returns true if a non-default/non-inherited value has been set.
*/
isExplicitlySet: function() {
return this.get('explicitly_set');
},
/**
* The value, as shown in the UI. This may be an inherited or default value.
*/
getDisplayValue : function () {
return this.get('value');
},
/**
* The value, as should be returned to the server. if 'isExplicitlySet'
* returns false, this method returns null to indicate that the value
* is not set at this level.
*/
getValue: function() {
return this.get('explicitly_set') ? this.get('value') : null;
},
/**
* Sets the displayed value.
*/
setValue: function (value) {
this.set({
explicitly_set: true,
value: value
});
},
/**
* Returns the field name, which should be used for persisting the metadata
* field to the server.
*/
getFieldName: function () {
return this.get('field_name');
},
/**
* Returns the options. This may be a array of possible values, or an object
* with properties like "max", "min" and "step".
*/
getOptions: function () {
return this.get('options');
},
/**
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
*/
getType: function() {
return this.get('type');
},
/**
* Reverts the value to the default_value specified at construction, and updates the
* explicitly_set property.
*/
clear: function() {
this.set({
explicitly_set: false,
value: this.get('default_value')
});
}
});
CMS.Models.MetadataCollection = Backbone.Collection.extend({
model : CMS.Models.Metadata,
comparator: "display_name"
});
CMS.Models.Metadata.SELECT_TYPE = "Select";
CMS.Models.Metadata.INTEGER_TYPE = "Integer";
CMS.Models.Metadata.FLOAT_TYPE = "Float";
CMS.Models.Metadata.GENERIC_TYPE = "Generic";

View File

@@ -0,0 +1,43 @@
CMS.Models.Section = Backbone.Model.extend({
defaults: {
"name": ""
},
validate: function(attrs, options) {
if (!attrs.name) {
return gettext("You must specify a name");
}
},
url: "/save_item",
toJSON: function() {
return {
id: this.get("id"),
metadata: {
display_name: this.get("name")
}
};
},
initialize: function() {
this.listenTo(this, "request", this.showNotification);
this.listenTo(this, "sync", this.hideNotification);
},
showNotification: function() {
if(!this.msg) {
this.msg = new CMS.Models.SystemFeedback({
intent: "saving",
title: gettext("Saving&hellip;")
});
}
if(!this.msgView) {
this.msgView = new CMS.Views.Notification({
model: this.msg,
closeIcon: false,
minShown: 1250
});
}
this.msgView.show();
},
hideNotification: function() {
if(!this.msgView) { return; }
this.msgView.hide();
}
});

View File

@@ -40,7 +40,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
// data
data : JSON.stringify({ deleteKeys : self.deleteKeys})
})
.fail(function(hdr, status, error) { CMS.ServerError(self, "Deleting keys:" + status); })
.done(function(data, status, error) {
// clear deleteKeys on success
self.deleteKeys = [];

View File

@@ -22,7 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({
}
);
},
error: CMS.ServerError
reset: true
}
);
},
@@ -89,8 +89,7 @@ CMS.Views.Checklists = Backbone.View.extend({
'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked
});
},
error : CMS.ServerError
}
});
}
});

View File

@@ -105,7 +105,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError});
targetModel.save({});
this.closeEditor(this);
analytics.track('Saved Course Update', {
@@ -160,11 +160,15 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event);
this.modelDom(event).remove();
var cacheThis = this;
targetModel.destroy({success : function (model, response) {
cacheThis.collection.fetch({success : function() {cacheThis.render();},
error : CMS.ServerError});
},
error : CMS.ServerError
targetModel.destroy({
success: function (model, response) {
cacheThis.collection.fetch({
success: function() {
cacheThis.render();
},
reset: true
});
}
});
},
@@ -238,20 +242,18 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
initialize: function() {
var self = this;
this.model.fetch(
{
complete: function() {
window.templateLoader.loadRemoteTemplate("course_info_handouts",
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
error : CMS.ServerError
}
);
this.model.fetch({
complete: function() {
window.templateLoader.loadRemoteTemplate("course_info_handouts",
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
reset: true
});
},
render: function () {
@@ -291,7 +293,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue());
this.render();
this.model.save({}, {error: CMS.ServerError});
this.model.save({});
this.$form.hide();
this.closeEditor(this);

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