A new way of doing css/sass & New Canvas Theme Editor

what this does:
* Changes the way we generate css so we are able to generate custom
  css for people that use the theme editor.
* Sets everything up so we can push all of our static assets
  (js, fonts, css, images, etc) to s3 pre-deploy and serve them
  from cloudfront. Yay! faster canvas for everyone!
* as part of that, this enables the rails asset pipeline just so we
  can use it to put md5s in our urls.  we don't use it for any of the
  coffeescript/sass/sprockets transformer stuff.
* adds a new "Theme editor" functionality (only for people that have
  have the use-new-styles feature flag turned on) where an admin for
  an account can pick their own colors/images for all the users
  at their account/school.
* when the user is done saving things in theme editor, it will,
  in a delayed job, generate all the css with against the variables
  that user specified and push it to s3 so it will be available to
  anyone else that requests it.  (the delayed job will shell
  out to a node.js executable called `brandable_css`).
* ability to pick an existing shared theme and to reset to
  blank theme. closes: CNVS-19685
* gets rid of jammit.

test plan:
(this is exaustive, so not every person has to do every step
but we should make sure at least someone does each of these things.
maybe as part of the review add a comment if you have done one of these
bulletpoints)

* before you check this out, compile all css and copy the
  public/stylsheets_compiled directory somewhere. after you check out
  this code and regenerate all the css. make sure there are no
  significant changes to the css output. (we updated the versions of
  node-sass and autoprefixer that we use so we want to make sure they
  don't change things in a way we weren't expecting)

* make sure the way we load css for handlebars templates still works.
  eg: if there is a handlebars template at
  app/views/jst/some/template.handlebars
  if there is also a scss file at
  app/stylesheets/jst/some/template.scss
  then that stylesheet should get loaded when that template is rendered

* check out the code and run migrations. browse around canvas,
  make sure css and js files load correctly as before.

* cody, jacob, or someone on queso: look at the db migrations and
  make sure everything looks good and that I am handling sharding
  correctly.
* verify that both rake canvas:compile_assets and guard, works as well
  as `node_modules/.bin/brandable_css` (note: if you have
  "node_modules/.bin" in your PATH (which you should), it will also
  work with just `brandable_css`)

* verify that passing the --watch option to
  `.bin/node_modules/brandable_css` works and picks up changes to
  sass files, images, fonts, or any other resource that goes into
  a css file. and that it only recompiles the css files that actually
  depend on that file.

* go to https://github.com/ryankshaw/brandable_css and check out the
  code there. that is what is actually doing the sass compiling

* create a config/canvas_cdn.yml file and add aws access creds and
  an s3 bucket and cdn hostname (for testing, you can use the credentials
  for instructure_uploads_engineering from
  https://gollum.instructure.com/OtherServiceTestAccounts ). for a test
  cdn hostname you can use https://diu0rq5m1weh1.cloudfront.net. that
  is a cloudfront bucket I set up on my personal account that points to
  instructure_uploads_engineering

* run rake canvas:compile_assets again, this time, at the end, you
  should see it run the assets:precompile task that puts md5s in filenames
  and, gzipps them, and copys them to public/assets.
  then you should see it run canvas:cdn:upload_to_s3
  (look at log/development.log for progress),
  which pushes everything to s3.
  closes: CNVS-17333 CNVS-17430 CNVS-17337

* try out the theme editor: turn on new styles, go to accounts/x
  (where x is the @domain root acount you are testing from) and click
  the "theme editor" button on the right side of the page.
  that should take you to a page that has the ability to pick colors/images
  on the left side and preview your changes in an iframe on the right
  closes: CNVS-19360 CNVS-20551

* test the "preview", "save", "reset", and "choose existing" functionality
  closes: CNVS-17339 CNVS-17338 CNVS-19685

* make sure that the themeeditor works both if you have
  config/canvas_cdn.yml set up and enabled as well as if you don't.
  if it is enabled, you should see it push the css for just that new
  brand config to s3 when you hit preview, and the css
  should be accessible from the cdn you configured.

Change-Id: Ie0a812d04f5eeb40e7df7e71941ff63ea51a4d22
Reviewed-on: https://gerrit.instructure.com/53873
Tested-by: Jenkins
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Product-Review: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
Ryan Shaw 2015-02-11 12:51:05 -07:00
parent e8eefad376
commit 84a7192a36
71 changed files with 1734 additions and 1169 deletions

6
.gitignore vendored
View File

@ -14,8 +14,9 @@
/.yardoc/
/app/coffeescripts/ember/*/main.coffee
/app/views/info/styleguide.html.erb
app/stylesheets/_brandable_variables_defaults_autogenerated.scss
app/stylesheets/brandable_css_brands
/config/*.yml
/config/brand_variables.scss
/config/build.js
/config/environments/*-local.rb
/config/locales/generated/
@ -31,14 +32,13 @@ Gemfile.lock3
Gemfile.lock4
/log/
/node_modules
/public/assets/
/public/dist/
/public/doc/api/
/public/javascripts/compiled/
/public/javascripts/jst/
/public/javascripts/jsx/
/public/javascripts/translations/
/public/optimized/
/public/stylesheets_compiled/
/spec/javascripts/compiled/
/spec/javascripts/requirejs_config.js
/spec/javascripts/runner.html

View File

@ -1,5 +1,6 @@
group :development do
gem 'guard', '1.8.0'
gem 'guard-gulp'
gem 'listen', '~>1.3' # pinned to fix guard error
gem 'rb-inotify', '~>0.9.0', require: false
gem 'rb-fsevent', require: false

View File

@ -49,9 +49,6 @@ gem 'i18nema19', '0.0.8', platform: :ruby_19
gem 'i18nliner', '0.0.11'
gem 'icalendar', '1.5.4', require: false
gem 'ims-lti', '2.0.0.beta.26', require: false
gem 'jammit', github: 'documentcloud/jammit', ref: '98b50a67029c2860717485a72a2ff0ae8ec37840'
gem 'cssmin', '1.0.3', require: false
gem 'jsmin', '1.0.1', require: false
gem 'json', '1.8.2'
gem 'oj', '2.5.5'
gem 'jwt', '1.2.1', require: false

View File

@ -2,6 +2,7 @@ $LOAD_PATH << File.dirname(__FILE__)
ignore! Listen::DirectoryRecord::DEFAULT_IGNORED_DIRECTORIES - ['vendor'] + [%r{vendor/(?!plugins)}]
guard :brandable_css
guard 'coffeescript', :input => 'app/coffeescripts', :output => 'public/javascripts/compiled'
guard 'coffeescript', :input => 'spec/coffeescripts', :output => 'spec/javascripts/compiled'
guard 'coffeescript', :input => 'spec_canvas/coffeescripts', :output => 'spec_canvas/javascripts'
@ -11,5 +12,8 @@ guard :ember_templates
guard :ember_bundles
guard :styleguide
guard :js_extensions
guard :compass
guard :jsx
guard 'gulp' do
watch(%r{^gulpfile.babel.js$})
end

View File

@ -0,0 +1,10 @@
require [
'react'
'jsx/theme_editor/ThemeEditor'
], (React, ThemeEditor) ->
React.render(React.createElement(ThemeEditor, {
brandConfig: window.ENV.brandConfig,
variableSchema: window.ENV.variableSchema
sharedBrandConfigs: window.ENV.sharedBrandConfigs
}), document.body)

View File

@ -0,0 +1,46 @@
define ->
loadedStylesheets = {}
brandableCss =
getCssVariant: ->
variant = if window.ENV.k12
'k12'
else if window.ENV.use_new_styles
'new_styles'
else
'legacy'
contrast = if window.ENV.use_high_contrast
'_high_contrast'
else
'_normal_contrast'
variant + contrast
urlFor: (bundleName) ->
brandPart = if window.ENV.active_brand_config
'/' + window.ENV.active_brand_config
else
''
return [
window.ENV.ASSET_HOST || '',
'dist'
'brandable_css' + brandPart,
brandableCss.getCssVariant(),
bundleName + '.css'
].join('/')
# bundleName needs to include the 'combinedChecksum'
# eg: 'jst/foo-65ed0284f8f911179a6d5655ebbb8498'
# 'jst/foo' will not work.
loadStylesheet: (bundleName) ->
return if bundleName of loadedStylesheets
linkElement = document.createElement("link")
linkElement.rel = "stylesheet"
linkElement.href = brandableCss.urlFor(bundleName)
# give the person trying to track down a bug a hint on how
# this link tag got on the page
linkElement.setAttribute('data-loaded-by-brandableCss', true)
document.head.appendChild(linkElement)

View File

@ -1,39 +0,0 @@
define ->
doc = window.document
templatesWithStyles = {}
registerTemplateCss = (templateId, css) ->
templatesWithStyles[templateId] = css
render()
registerTemplateCss.clear = ->
templatesWithStyles = {}
render()
render = ->
strings = []
for templateId, css of templatesWithStyles
strings.push "/* From: #{templateId} */"
strings.push css
combined = strings.join '\n'
styleNode = cleanStyleNode()
if 'cssText' of styleNode
styleNode.cssText = combined
else
styleNode.appendChild doc.createTextNode(combined)
_styleNode = null
cleanStyleNode = ->
if _styleNode
_styleNode.removeChild child while child = _styleNode.firstChild
return _styleNode
if doc.createStyleSheet
_styleNode = doc.createStyleSheet()
else
head = doc.head || doc.getElementsByTagName('head')[0]
_styleNode = doc.createElement('style')
head.appendChild(_styleNode)
return registerTemplateCss

View File

@ -454,21 +454,6 @@ class AccountsController < ApplicationController
params[:account].delete :services
end
if @account.grants_right?(@current_user, :manage_site_settings)
# handle branding stuff
if @account.root_account? && params[:account][:settings]
(Account::BRANDING_SETTINGS - [:msapplication_tile_color]).each do |setting|
if params[:account][:settings]["#{setting}_remove"] == "1"
params[:account][:settings][setting] = nil
elsif params[:account][:settings][setting].present?
attachment = Attachment.create(uploaded_data: params[:account][:settings][setting], context: @account)
params[:account][:settings][setting] = attachment.authenticated_s3_url(:expires => 15.years)
end
end
end
# If the setting is present (update is called from 2 different settings forms, one for notifications)
if params[:account][:settings] && params[:account][:settings][:outgoing_email_default_name_option].present?
# If set to default, remove the custom name so it doesn't get saved
@ -491,7 +476,7 @@ class AccountsController < ApplicationController
end
else
# must have :manage_site_settings to update these
([ :admins_can_change_passwords,
[ :admins_can_change_passwords,
:admins_can_view_notifications,
:enable_alerts,
:enable_eportfolios,
@ -499,7 +484,7 @@ class AccountsController < ApplicationController
:show_scheduler,
:global_includes,
:gmail_domain
] + Account::BRANDING_SETTINGS).each do |key|
].each do |key|
params[:account][:settings].try(:delete, key)
end
end

View File

@ -1658,16 +1658,35 @@ class ApplicationController < ActionController::Base
super
end
def jammit_css_bundles; @jammit_css_bundles ||= []; end
helper_method :jammit_css_bundles
def active_brand_config
@active_brand_config ||= begin
if !use_new_styles? || (@current_user && @current_user.prefers_high_contrast?)
nil
elsif session.key?(:brand_config_md5)
BrandConfig.find(session[:brand_config_md5]) if session[:brand_config_md5]
else
@domain_root_account.brand_config
end
end
end
helper_method :active_brand_config
def jammit_css(*args)
def css_bundles
@css_bundles ||= []
end
helper_method :css_bundles
def css_bundle(*args)
opts = (args.last.is_a?(Hash) ? args.pop : {})
Array(args).flatten.each do |bundle|
jammit_css_bundles << [bundle, opts[:plugin]] unless jammit_css_bundles.include? [bundle, opts[:plugin]]
css_bundles << [bundle, opts[:plugin]] unless css_bundles.include? [bundle, opts[:plugin]]
end
nil
end
helper_method :css_bundle
alias_method :jammit_css, :css_bundle
deprecate :jammit_css
helper_method :jammit_css
def js_bundles; @js_bundles ||= []; end

View File

@ -0,0 +1,73 @@
class BrandConfigsController < ApplicationController
before_filter :require_user
before_filter :require_manage_site_settings, except: [:destroy]
def new
brand_config = active_brand_config || BrandConfig.new
@page_title = t('Canvas Theme Editor')
css_bundle :common, :theme_editor
js_bundle :theme_editor
js_env brandConfig: brand_config.variables,
variableSchema: BrandableCSS::BRANDABLE_VARIABLES,
sharedBrandConfigs: BrandConfig.select('md5, name').where(share: true).as_json(include_root: false)
render text: '', layout: 'layouts/bare'
end
def create
session[:brand_config_md5] = begin
if params[:brand_config] == ''
false
elsif params[:brand_config][:md5]
BrandConfig.find(params[:brand_config][:md5]).md5
elsif (variables = params[:brand_config][:variables])
create_brand_config(variables).md5
end
end
redirect_to brand_configs_new_path
end
def save_to_account
@domain_root_account.update_attributes!(brand_config_md5: session[:brand_config_md5].presence)
BrandConfig.clean_up_unused
session.delete(:brand_config_md5)
redirect_to :back, notice: t('Success! All users on this domain will now see this branding.')
end
def destroy
session.delete(:brand_config_md5)
BrandConfig.clean_up_unused
redirect_to :back, notice: t('Theme editor changes have been cancelled.')
end
protected
def require_manage_site_settings
return false unless authorized_action(@domain_root_account, @current_user, :manage_site_settings) && use_new_styles?
end
def create_brand_config(variables)
variables_to_save = variables.each_with_object({}) do |(key, value), memo|
next unless value.present? && (config = BrandableCSS.variables_map[key])
value = upload_image(value) if config['type'] == 'image' && value.is_a?(ActionDispatch::Http::UploadedFile)
memo[key] = value
end
brand_config = BrandConfig.create!(variables: variables_to_save)
# TODO, show user progress of this
brand_config.send_later_if_production(:save_and_sync_to_s3!)
brand_config
end
def upload_image(image)
attachment = Attachment.create(uploaded_data: image, context: @domain_root_account)
expires_in = 15.years
attachment.authenticated_s3_url({
# this is how long the s3 verifier token will work
expires: expires_in,
# these are the http cache headers that will be set on the response
response_expires: expires_in,
response_cache_control: "Cache-Control:max-age=#{expires_in}, public"
})
end
end

View File

@ -222,7 +222,6 @@ module ApplicationHelper
def use_optimized_js?
if ENV['USE_OPTIMIZED_JS'] == 'true'
# allows overriding by adding ?debug_assets=1 or ?debug_js=1 to the url
# (debug_assets is also used by jammit => you'll get unpackaged css AND js)
!(params[:debug_assets] || params[:debug_js])
else
# allows overriding by adding ?optimized_js=1 to the url
@ -247,7 +246,7 @@ module ApplicationHelper
# Returns a <script> tag for each registered js_bundle
def include_js_bundles
paths = js_bundles.inject([]) do |ary, (bundle, plugin)|
base_url = js_base_url
base_url = "#{js_base_url}"
base_url += "/plugins/#{plugin}" if plugin
ary.concat(Canvas::RequireJs.extensions_for(bundle, 'plugins/')) unless use_optimized_js?
ary << "#{base_url}/compiled/bundles/#{bundle}.js"
@ -256,32 +255,50 @@ module ApplicationHelper
end
def include_css_bundles
unless jammit_css_bundles.empty?
bundles = jammit_css_bundles.map do |(bundle,plugin)|
bundle = variant_name_for(bundle)
plugin ? "plugins_#{plugin}_#{bundle}" : bundle
unless css_bundles.empty?
bundles = css_bundles.map do |(bundle,plugin)|
css_url_for(bundle, plugin)
end
bundles << {:media => 'all'}
include_stylesheets(*bundles)
stylesheet_link_tag(*bundles)
end
end
def variant_name_for(bundle_name)
def css_variant
if k12?
variant = '_k12'
variant = 'k12'
elsif use_new_styles?
variant = '_new_styles'
variant = 'new_styles'
else
variant = '_legacy'
variant = 'legacy'
end
use_high_contrast = @current_user && @current_user.prefers_high_contrast?
variant += use_high_contrast ? '_high_contrast' : '_normal_contrast'
"#{bundle_name}#{variant}"
variant + (use_high_contrast ? '_high_contrast' : '_normal_contrast')
end
def css_url_for(bundle_name, plugin=false)
bundle_path = "#{plugin ? "plugins/#{plugin}" : 'bundles'}/#{bundle_name}"
content_md5 = BrandableCSS.fingerprint_for(bundle_path, css_variant)
File.join('/dist', 'brandable_css', active_brand_config.try(:md5).to_s,
css_variant, "#{bundle_path}-#{content_md5}.css")
end
def brand_variable(variable_name)
BrandableCSS.brand_variable_value(variable_name, active_brand_config)
end
def favicon
possibly_customized_favicon = brand_variable('ic-brand-favicon')
default_favicon = BrandableCSS.brand_variable_value('ic-brand-favicon')
if possibly_customized_favicon == default_favicon
return "favicon-green.ico" if Rails.env.development?
return "favicon-yellow.ico" if Rails.env.test?
end
possibly_customized_favicon
end
def include_common_stylesheets
include_stylesheets variant_name_for(:vendor), variant_name_for(:common), media: "all"
stylesheet_link_tag css_url_for(:vendor), css_url_for(:common), media: "all"
end
def sortable_tabs

View File

@ -0,0 +1,34 @@
/** @jsx React.DOM */
define([
'react',
'i18n!theme_editor',
'str/htmlEscape'
], (React, I18n, htmlEscape) => {
return React.createClass({
displayName: 'sharedBrandConfigPicker',
selectBrandConfig(md5) {
$(`
<form hidden method="POST" action="/brand_configs" method="POST">
<input name="authenticity_token" type="hidden" value="${htmlEscape($.cookie('_csrf_token'))}" />
<input name="brand_config[md5]" value="${htmlEscape(md5)}" />
</form>
`).appendTo('body').submit()
},
render() {
return (
<select onChange={event => this.selectBrandConfig(event.target.value)}>
<option value="" disabled>{I18n.t('Start from a template')}</option>
{this.props.sharedBrandConfigs.map(brandConfig =>
<option key={brandConfig.md5} value={brandConfig.md5}>
{brandConfig.name}
</option>
)}
</select>
)
}
})
});

View File

@ -0,0 +1,188 @@
/** @jsx React.DOM */
define([
'i18n!theme_editor',
'react',
'str/htmlEscape',
'compiled/fn/preventDefault',
'./ThemeEditorAccordion',
'./SharedBrandConfigPicker'
], (I18n, React, htmlEscape, preventDefault, ThemeEditorAccordion, SharedBrandConfigPicker) => {
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
function findVarDef (variableSchema, variableName) {
for (var i = 0; i < variableSchema.length; i++) {
for (var j = 0; j < variableSchema[i].variables.length; j++) {
var varDef = variableSchema[i].variables[j]
if (varDef.variable_name === variableName){
return varDef
}
}
}
}
function submitHtmlForm(action, method) {
$(`
<form hidden action="${htmlEscape(action)}" method="POST">
<input name="_method" type="hidden" value="${htmlEscape(method)}" />
<input name="authenticity_token" type="hidden" value="${htmlEscape($.cookie('_csrf_token'))}" />
<input name="brand_config" value="" />
</form>
`).appendTo('body').submit()
}
return React.createClass({
displayName: 'ThemeEditor',
getInitialState() {
return {
somethingChanged: false,
changedValues: {}
}
},
somethingChanged(variableName, newValue) {
this.state.changedValues[variableName] = newValue
this.setState({
somethingChanged: true,
changedValues: this.state.changedValues
})
},
getDefault(variableName) {
var val = this.state.changedValues[variableName]
if (val) return val
if (val !== '') {
val = this.props.brandConfig[variableName]
if (val) return val
}
val = findVarDef(this.props.variableSchema, variableName).default
if (val && val[0] === '$') return this.getDefault(val.slice(1))
return val
},
resetToCanvasDefaults() {
submitHtmlForm('/brand_configs', 'POST')
},
cancelChanges() {
var msg = I18n.t('This will just cancel the unsaved changes you ' +
'have made in the Theme Editor. It will not affect ' +
'anyone else and you will now see canvas as everyone ' +
'else at your accout does.')
if(confirm(msg)) submitHtmlForm('/brand_configs', 'DELETE')
},
saveToAccount() {
var msg = I18n.t('This will apply the changes that you have made in ' +
'the Theme Editor so everyone else at your accout will ' +
'see Canvas as you are seeing it now. ' +
'Are you sure you want to do this?')
if (confirm(msg)) submitHtmlForm('/brand_configs/save_to_account', 'POST')
},
render() {
return (
<form encType="multipart/form-data" acceptCharset="UTF-8" action="/brand_configs" method="POST" className="Theme__container">
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden" value={$.cookie('_csrf_token')} />
<div className="Theme__editor">
<div className="Theme__editor-header">
<h1 className="Theme__editor-header_title">Theme Editor</h1>
<div className="Theme__editor-header_actions">
<div className="al-dropdown__container">
<a className="al-trigger Button" role="button" href="#">
<i className="icon-more"></i>
<span className="screenreader-only">{I18n.t('Settings')}</span>
</a>
<ul
className="al-options"
role="menu"
tabIndex="0"
aria-hidden="true"
aria-expanded="false">
<li role="presentation" tabIndex="-1" role="menuitem">
<a
href="#"
className="icon-reset"
onClick={preventDefault(this.resetToCanvasDefaults)}
>
{I18n.t('Reset all to Canvas defaults')}
</a>
</li>
<li role="presentation" tabIndex="-1" role="menuitem">
<a
href="#"
className="icon-end"
onClick={preventDefault(this.cancelChanges)}
>
{I18n.t('Cancel Changes')}
</a>
</li>
</ul>
</div>
<button
type="button"
className="Theme__editor-header_button Button Button--success"
disabled={this.state.somethingChanged}
title={this.state.somethingChanged ?
I18n.t('"Preview Your Changes" before applying to everyone') :
null
}
onClick={this.saveToAccount}
>
{I18n.t('Save')}
</button>
</div>
</div>
{this.props.sharedBrandConfigs.length ?
<SharedBrandConfigPicker sharedBrandConfigs={this.props.sharedBrandConfigs} />
:
null
}
<div id="Theme__editor-tabs">
<div id="te-editor">
<div className="Theme__editor-tabs_panel">
<ThemeEditorAccordion
variableSchema={this.props.variableSchema}
brandConfig={this.props.brandConfig}
getDefault={this.getDefault}
changedValues={this.state.changedValues}
somethingChanged={this.somethingChanged}
/>
</div>
</div>
</div>
</div>
<div className="Theme__preview">
<div
hidden={!this.state.somethingChanged}
className="Theme__preview-overlay"
>
<div
style={{
width: 700,
textAlign: 'center',
height: '100vh',
margin: '0 auto'
}}
>
<button type="submit" className="Button Button--primary" style={{margin: '40% auto'}}>
<i className="icon-refresh" />
{I18n.t('Preview Your Changes')}
</button>
</div>
</div>
<iframe src="/" style={{border: 'none', width: '100%', height: '100vh'}} />
</div>
</form>
)
}
})
});

View File

@ -0,0 +1,63 @@
/** @jsx React.DOM */
define([
'react',
'./ThemeEditorColorRow',
'./ThemeEditorImageRow',
'jquery',
'jqueryui/accordion'
], (React, ThemeEditorColorRow, ThemeEditorImageRow, $) => {
return React.createClass({
displayName: 'ThemeEditorAccordion',
propTypes: {
variableSchema: React.PropTypes.array.isRequired,
brandConfig: React.PropTypes.object.isRequired,
changedValues: React.PropTypes.object.isRequired,
somethingChanged: React.PropTypes.func.isRequired,
getDefault: React.PropTypes.func.isRequired
},
componentDidMount() {
$(this.getDOMNode()).accordion({
header: "h3",
heightStyle: "content"
})
},
renderRow(varDef) {
var props = {
currentValue: this.props.brandConfig[varDef.variable_name],
chosenValue: this.props.changedValues[varDef.variable_name],
onChange: this.props.somethingChanged.bind(null, varDef.variable_name),
placeholder: this.props.getDefault(varDef.variable_name),
varDef: varDef
}
return varDef.type === 'color' ? ThemeEditorColorRow(props) : ThemeEditorImageRow(props)
},
render() {
return (
<div className="accordion ui-accordion--mini Theme__editor-accordion">
{this.props.variableSchema.map(variableGroup =>
[
<h3>
<a href="#">
<div className="te-Flex">
<span className="te-Flex__block">{variableGroup.group_name}</span>
<i className="Theme__editor-accordion-icon icon-mini-arrow-right" />
</div>
</a>
</h3>
,
<div>
{variableGroup.variables.map(this.renderRow)}
</div>
]
)}
</div>
)
}
})
});

View File

@ -0,0 +1,54 @@
/** @jsx React.DOM */
define(['react'], (React) => {
return React.createClass({
displayName: 'ThemeEditorColorRow',
propTypes: {
varDef: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
currentValue: React.PropTypes.string,
placeholder: React.PropTypes.string.isRequired
},
render() {
return (
<section className="Theme__editor-accordion_element Theme__editor-color">
<div className="Theme__editor-form--color">
<label
htmlFor={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
className="Theme__editor-color_title"
>
{this.props.varDef.human_name}
</label>
<div className="Theme__editor-color-block">
<input
type="text"
className="Theme__editor-color-block_input-text Theme__editor-color-block_input"
placeholder={this.props.placeholder}
name={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
value={this.props.chosenValue != null ? this.props.chosenValue : this.props.currentValue}
onChange={event => this.props.onChange(event.target.value) }
/>
<label className="Theme__editor-color-label Theme__editor-color-block_label-sample" style={{backgroundColor: this.props.placeholder}}
/* this <label> and <input type=color> are here so if you click the 'sample',
it will pop up a color picker on browsers that support it */
>
<input
className="Theme__editor-color-block_input-sample Theme__editor-color-block_input"
type="color"
ref="colorpicker"
value={this.props.placeholder}
role="presentation-only"
onChange={event => this.props.onChange(event.target.value) }
/>
</label>
</div>
</div>
</section>
)
}
})
});

View File

@ -0,0 +1,89 @@
/** @jsx React.DOM */
define([
'react',
'i18n!theme_editor'
], (React, I18n) => {
return React.createClass({
displayName: 'ThemeEditorImageRow',
propTypes: {
varDef: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
currentValue: React.PropTypes.string,
placeholder: React.PropTypes.string
},
setValue(inputElement) {
var chosenValue = inputElement
if (!chosenValue) {
// if they hit the "remove" button, we want to also clear out the value of the <input type=file>
// but we don't want to mess with its value otherwise
this.refs.fileInput.getDOMNode().value = ''
} else {
chosenValue = window.URL.createObjectURL(inputElement.files[0])
}
this.props.onChange(chosenValue)
},
render() {
return (
<section className="Theme__editor-accordion_element Theme__editor-upload">
<div className="te-Flex">
<div className="Theme__editor-form--upload">
<label
htmlFor={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
className="Theme__editor-upload_title"
>
{this.props.varDef.human_name}
<span className="Theme__editor-upload_restrictions">
{this.props.varDef.helper_text}
</span>
</label>
<div className="Theme__editor_preview-img-container">
<img
src={this.props.chosenValue || this.props.placeholder}
className="Theme__editor-placeholder--main"
/>
</div>
<input
type="hidden"
name={this.props.chosenValue == null ? '' : 'brand_config[variables]['+ this.props.varDef.variable_name +']'}
value={this.props.currentValue}
/>
<div className="Theme__editor-image_upload">
<label className="Theme__editor-image_upload-label">
<span className="Theme__editor-button_upload Button Button--primary">Upload Image</span>
<input
type="file"
className="Theme__editor-input_upload"
name={this.props.chosenValue ? 'brand_config[variables]['+ this.props.varDef.variable_name +']' : ''}
accept={this.props.varDef.accept}
onChange={event => this.setValue(event.target)}
ref="fileInput"
/>
<div className="Theme__editor-input_resets">
{this.props.chosenValue || (this.props.currentValue && this.props.chosenValue !== '') ? (
<button
type="button"
className="Button Button--secondary"
onClick={() => this.setValue(this.props.chosenValue ? null : '')}
>
{this.props.chosenValue && this.props.currentValue ? I18n.t('Undo') : I18n.t('Clear')}
</button>
) : (
null
)}
</div>
</label>
</div>
</div>
</div>
</section>
)
}
})
});

View File

@ -24,7 +24,7 @@ class Account < ActiveRecord::Base
:turnitin_host, :turnitin_comments, :turnitin_pledge,
:default_time_zone, :parent_account, :settings, :default_storage_quota,
:default_storage_quota_mb, :storage_quota, :ip_filters, :default_locale,
:default_user_storage_quota_mb, :default_group_storage_quota_mb, :integration_id
:default_user_storage_quota_mb, :default_group_storage_quota_mb, :integration_id, :brand_config_md5
EXPORTABLE_ATTRIBUTES = [:id, :name, :created_at, :updated_at, :workflow_state, :deleted_at,
:default_time_zone, :external_status, :storage_quota,
@ -118,6 +118,7 @@ class Account < ActiveRecord::Base
has_many :user_account_associations
has_many :report_snapshots
has_many :external_integration_keys, :as => :context, :dependent => :destroy
belongs_to :brand_config, foreign_key: "brand_config_md5"
before_validation :verify_unique_sis_source_id
before_save :ensure_defaults
@ -223,12 +224,6 @@ class Account < ActiveRecord::Base
add_setting :include_students_in_global_survey, boolean: true, root_only: true, default: false
add_setting :trusted_referers, root_only: true
BRANDING_SETTINGS = [:header_image, :favicon, :apple_touch_icon,
:msapplication_tile_color, :msapplication_tile_square,
:msapplication_tile_wide].freeze
BRANDING_SETTINGS.each { |setting| add_setting(setting, root_only: true) }
def settings=(hash)
@invalidate_settings_cache = true
if hash.is_a?(Hash)

View File

@ -0,0 +1,73 @@
class BrandConfig < ActiveRecord::Base
include BrandableCSS
self.primary_key = 'md5'
serialize :variables, Hash
attr_accessible :variables
validates :variables, presence: true
validates :md5, length: {is: 32}
before_validation :generate_md5
before_update do
raise 'BrandConfigs are a key-value mapping of config variables and an md5 digest '\
'of those variables, so they are immutable. You do not update them, you just '\
'save a new one and it will generate the new md5 for you'
end
has_many :accounts, foreign_key: 'brand_config_md5'
def generate_md5
self.id = Digest::MD5.hexdigest(self.variables.to_s)
end
def get_value(variable_name)
self.variables[variable_name]
end
def to_scss
"// This file is autogenerated by brand_config.rb as a result of running `rake brand_configs:write`\n" +
variables.map do |name, value|
next unless (config = BrandableCSS.variables_map[name])
value = %{url("#{value}")} if config['type'] == 'image'
"$#{name}: #{value};"
end.compact.join("\n")
end
def filename
File.join(CONFIG['paths']['branded_scss_folder'], md5, '_brand_variables.scss')
end
def public_folder
"dist/brandable_css/#{md5}"
end
def save_file!
logger.info "saving BrandConfig: #{filename}"
FileUtils.mkdir_p(File.dirname(filename))
File.write(filename, to_scss)
end
def compile_css!
BrandableCSS.compile_brand!(md5)
end
def sync_to_s3!
Canvas::CDN.push_to_s3!(public_folder) if Canvas::CDN.enabled?
end
def save_and_sync_to_s3!
save_file!
compile_css!
sync_to_s3!
end
def self.clean_up_unused
BrandConfig.
where('NOT EXISTS (SELECT 1 FROM accounts WHERE brand_config_md5=brand_configs.md5)').
where('NOT share').
destroy_all
end
end

View File

@ -20,13 +20,22 @@
//================
// Custom Branding
//================
// Pulls in variables in 'config/brand_variables.scss', if it exists.
// See docs in config/brand_variables.scss.example.
// In the future, this will be how we'll pull in the variables that
// a school sets in their "Theme Editor" interface.
// Only variables here with a "!default" at the end are able to be overrideable by the school.
// see: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#variable_defaults_
@import "brand_variables";
// First, pull in the defaults for the brandable varables, which are auto-generated
// from app/stylesheets/brandable_variables.json (so if you are looking to set the
// default for any of the $ic-brand-* variables, go there)
@import "brandable_variables_defaults_autogenerated";
// Now, pull in the account's custom BrandConfig variables, if there is one.
// run `rake brand_configs:write && ./node_modules/.bin/brandable_css`
// Which adds that folder to the top of the sass 'includePaths', so it will
// get picked up here.
// But if not (for the css used when there is no custom BrandConfig),
// this just pulls in 'app/stylesheets/_brand_variables.scss'
// which is a blank file and does nothing.
@if $use_new_styles and not $use_high_contrast {
@import "brand_variables";
}
//=======================
// Canvas LMS Color Sheet
@ -36,19 +45,11 @@
// When you need to use a color, please create a functional variable name
// and use the color variable name of your choosing. See examples below.
//=================================
// Canvas Theme Color Variables
//=================================
// These are the new colors that we would like to start rebranding
// all the canvas pages in. All of the app variables that denote a
// color should use or be based off of one of these variables
$ic-brand-primary: #0096db !default; // light blue
@if $use_high_contrast { $ic-brand-primary: #0073ac; }
$ic-brand-secondary: #5b6c79 !default; // dark blue
@if $use_new_styles { $ic-brand-secondary: #384a58; }
@if $use_high_contrast { $ic-brand-primary: #0073ac; }
@if $use_high_contrast { $ic-brand-secondary: #343c44; }
$ic-color-success: #008a14; // green
@if $use_high_contrast { $ic-color-success: #007a12; }
@ -205,7 +206,7 @@ $ic-dim-helper-text: darken($ic-color-neutral, 33);
$ic-dim-helper-text: darken($ic-color-neutral, 50);
}
// We don't use any glyph icons like Bootstrap. This is the overwrite.
// We don't use any of the Bootstrap glyph icons, But bootstrap's scss expects this variable to be defined.
$iconSpritePath: "canvas_does_not_use_boostraps_default_sprit_based_icons_so_this_is_meaningless" !default;
$iconWhiteSpritePath: $iconSpritePath !default;

View File

@ -0,0 +1,83 @@
[{
"group_name": "Global Branding",
"variables": [{
"human_name": "Primary Color",
"variable_name": "ic-brand-primary",
"type": "color",
"default": "#0096db"
},{
"human_name": "Secondary Color",
"variable_name": "ic-brand-secondary",
"type": "color",
"default": "#5b6c79"
},{
"human_name": "Primary Button Color",
"variable_name": "ic-brand-button--primary-bgd",
"type": "color",
"default": "$ic-brand-primary"
},{
"human_name": "Primary Button Text Color",
"variable_name": "ic-brand-button--primary-text",
"type": "color",
"default": "#ffffff"
},{
"human_name": "Secondary Button",
"variable_name": "ic-brand-button--secondary-bgd",
"type": "color",
"default": "$ic-brand-secondary"
},{
"human_name": "Secondary Button Text Color",
"variable_name": "ic-brand-button--secondary-text",
"type": "color",
"default": "#ffffff"
},{
"human_name": "Link Color",
"variable_name": "ic-link-color",
"type": "color",
"default": "$ic-brand-primary"
}
]
},{
"group_name": "Logos & Watermarks",
"variables": [{
"human_name": "Main Logo",
"helper_text": "400x400, svg, png, jpg, gif",
"variable_name": "ic-brand-header-image",
"type": "image",
"accept": "image/png,image/gif,image/jpeg,image/svg",
"default": "/images/k-12-logo-placeholder.png"
},{
"human_name": "Favicon",
"helper_text": "use a single 16x16, 32x32, 48x48 ico file",
"variable_name": "ic-brand-favicon",
"type": "image",
"accept": "image/vnd.microsoft.icon,image/x-icon,image/png,image/gif",
"default": "/images/favicon.ico"
},{
"human_name": "Mobile Homescreen Icon",
"helper_text": "The shortcut icon for iOS/Android devices. 180x180 svg, png, jpg, gif",
"variable_name": "ic-brand-apple-touch-icon",
"type": "image",
"accept": "image/png,image/svg",
"default": "/images/apple-touch-icon.png"
},{
"human_name": "Windows Tile: Square",
"helper_text": "558x558 svg, png, jpg, gif (1.8x the standard tile size, so it can be scaled up or down as needed)",
"variable_name": "ic-brand-msapplication-tile-square",
"type": "image",
"accept": "image/png,image/gif,image/jpeg",
"default": "/images/windows-tile.png"
},{
"human_name": "Windows Tile: Wide",
"helper_text": "558x270 svg, png, jpg, gif",
"variable_name": "ic-brand-msapplication-tile-wide",
"type": "image",
"accept": "image/png,image/gif,image/jpeg",
"default": "/images/windows-tile-wide.png"
},{
"human_name": "Windows Tile Color",
"variable_name": "ic-brand-msapplication-tile-color",
"type": "color",
"default": "$ic-brand-primary"
}]
}]

View File

@ -0,0 +1,357 @@
@import "base/environment";
// TODO
// - make theme editor panel content (if exceeding browser height) scroll independently from preview area
//// Vertical content alignment on elements in Theme editor
///////
.te-Flex {
display: flex;
.te-Flex__block {flex: 1;}
.te-Flex__end {
align-self: flex-end;
}
&.te-Flex--v-middle {
align-items: center;
}
}
//// Make sure we're using border-box
///////
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
//// Override for Canvas full-width (may be taken out with responsive redesign)
///////
body, html {height: 100vh;margin: 0; padding: 0;}
#main {
max-width: 100% !important;
margin-bottom: 0;
padding-bottom: 0;
}
.grid-row--fix {
overflow: hidden;
margin: 0;
}
//// Variables for theme editor
///////
$theme-editor-bgd: $ic-color-dark;
$theme-editor-content-bgd: lighten($ic-color-dark, 10);
$theme-editor-border-color: darken($ic-color-neutral, 30);
//// Layout for Theme editor
///////
.Theme__container {
height: 100vh;
display: flex;
}
.Theme__editor {
height: 100vh;
width: 300px;
box-shadow: 0 0 8px $ic-color-dark;
background: $theme-editor-bgd;
}
.Theme__preview {
flex: 1;
width: 100%;
height: 100vh;
position: relative;
.grid-row {margin: 0;}
}
.Theme__preview-overlay {
background: rgba(255, 255, 255, 0.7);
height: 100vh;
width: 100%;
z-index: 8000;
position: absolute;
left: 0;
top: 0;
}
//// Theme Editor content
///////
.Theme__editor-header {
background: $ic-color-light;
padding: 10px 0;
display: flex;
align-items: center;
.Theme__editor-header_title {
padding: 0 0 0 $ic-sp;
margin: 0;
line-height: normal;
font-size: 18px;
font-weight: 500;
flex: 1;
}
.Theme__editor-header_actions {
display: flex;
justify-content: flex-end;
}
.Theme__editor-Button--trigger {
}
.Theme__editor-header_button {
margin: 0 $ic-sp;
}
}
//// Tabs for Theme Editor
///////
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
.Theme__container {
opacity: 0.01;
animation: fadeIn ease-in 1;
animation-fill-mode:forwards;
animation-duration: 0.2s;
}
//// TE Panel Header
///////
// TODO: get tabs working properly
.Theme__editor-tabs_list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.Theme__editor-tabs_panel {
color: $ic-color-light;
}
.Theme__editor-tabs_list {
background: $ic-color-neutral;
}
.Theme__editor-tabs_list-item {
font-size: 16px;
font-weight: 300;
padding: $ic-sp;
flex: 1;
text-align: center;
&:hover {
background: darken($ic-color-neutral, 10);
text-decoration: none;
@if $use_high_contrast {text-decoration: underline;}
}
}
.Theme__editor-tabs_list-item_link {
display: inline-block;
color: $ic-color-dark;
text-decoration: none;
@if $use_high_contrast {text-decoration: underline;}
}
//// Accordion styles for Theme Editor
///////
// There is a lot of overriding here to support our jquery
// accordion ui, but the theme for theme editor
@mixin animate-icon {
transform: rotate(90deg);
}
@mixin animate-hover {
transition: padding 0.2s;
padding-left: 5px;
padding-right: 5px;
}
.Theme__editor-accordion.ui-accordion {
.ui-accordion-header {
border: none;
background: $theme-editor-bgd;
font-weight: 300;
border-radius: 0;
&:not(:first-child) {
border-top: 1px solid $theme-editor-border-color;
}
// Default accordion state
&.ui-state-default {
background: $theme-editor-bgd;
box-shadow: none;
border: none;
border-bottom: 1px solid $theme-editor-border-color;
border-radius: 0;
padding: 8px $ic-sp;
margin-top: 0;
.Theme__editor-accordion-icon {
transform: rotate(-90deg);
}
&:last-child {
border-bottom: none;
}
// Link, Link hover & Focus accordion state
a {
color: $ic-color-light;
transition: color 0.2s;
border-radius: 0;
transition: padding 0.2s;
padding: $ic-sp/2 0;
font-weight: 500;
font-size: 14px;
}
&.ui-state-hover {
background: $ic-brand-primary;
.Theme__editor-accordion-icon {
@include animate-icon;
}
}
&.ui-state-focus {
background: $ic-brand-primary;
border: none;
outline: none;
a {
box-shadow: inset 0 0 0 2px $ic-brand-primary;
}
.Theme__editor-accordion-icon {
@include animate-icon;
}
}
// Active & Focus accordion state
&.ui-state-active {
border: none;
background: $theme-editor-bgd;
border-color: $theme-editor-content-bgd;
&.ui-state-focus {
a {
box-shadow: inset 0 0 0 2px $theme-editor-content-bgd;
}
}
.Theme__editor-accordion-icon {
@include animate-icon;
}
}
// Takes out un-needed jquery icon
> span {display: none;}
}
}
.ui-accordion-content {
background: $theme-editor-content-bgd;
padding: 0 $ic-sp;
color: $ic-color-light;
border: none;
border-radius: 0;
&.ui-accordion-content-active {
border-radius: 0;
}
}
}
//// Panel Content Styles
//////
.Theme__editor-accordion_element {
padding: $ic-sp 0;
margin: 0;
}
.Theme__editor-form--color {
display: flex;
align-items: center;
.Theme__editor-color-label {
padding: 0;
margin: 0;
}
}
////
// Styles for color block
////
.Theme__editor-color_title {
flex: 1 80px;
padding: 0;
margin: 0;
font-size: 13px;
}
.Theme__editor-color-block {
display: flex;
align-items: center;
justify-content: flex-end;
.Theme__editor-color-block_input {
padding: 0;
margin: 0;
box-shadow: none;
border: none;
outline: none;
}
$te-input-height: 30px;
.Theme__editor-color-block_input-text {
width: 75px;
height: $te-input-height;
margin: 0 $ic-sp 0 0;
padding: 0 $ic-sp/2;
color: $ic-color-dark;
border: 1px solid $ic-color-neutral;
border-radius: 3px;
$te-placeholder-color: $ic-color-dark;
&::-webkit-input-placeholder {color: $te-placeholder-color;}
&::-moz-placeholder {color: $te-placeholder-color;}
&:-ms-input-placeholder {color: $te-placeholder-color;}
}
.Theme__editor-color-block_label-sample {
border: 1px solid $ic-color-light;
border-radius: 3px;
cursor: pointer;
}
.Theme__editor-color-block_input-sample {
visibility: hidden;
display: block;
width: $te-input-height;
height: $te-input-height;
box-shadow: none;
}
}
////
// Styles for image upload block
////
.Theme__editor-upload {
.Theme__editor-form--upload {
width: 100%; // needed for IE to size image previews based on width of parent
}
.Theme__editor-upload_title {
}
.Theme__editor-image_upload {
flex: 100%;
}
.Theme__editor-upload_restrictions {
color: $ic-color-neutral;
font-style: italic;
display: block;
font-size: 12px;
font-weight: 100;
padding-top: $ic-sp/2;
}
.Theme__editor_preview-img-container {
padding: $ic-sp;
width: 100%;
display: flex;
align-items: center;
border: 1px solid rgba($ic-color-neutral, 0.2);
background: rgba($ic-color-light, 0.1);
img {
width: 100%;
}
}
.Theme__editor-image_upload {
margin: $ic-sp 0;
position: relative;
.Theme__editor-image_upload-label {
display: flex;
align-items: center;
}
.Theme__editor-input_upload {
position: absolute;
top: 0;
left: 0;
visibility: hidden;
z-index: 1;
}
.Theme__editor-input_resets {
z-index: 2; // make sure these go over the hidden input box
display: flex;
align-items: flex-end;
}
}
.Theme__editor-placeholder {
}
}

View File

@ -101,7 +101,7 @@ Simply add the **.ui-accordion--mini** class to the parent **.accordion** elemen
background: lighten($ic-color-neutral, 5%);
box-shadow: none;
border-color: $ic-border-light;
border-bottom: 1px solid $ic-border-light !important;
border-bottom: 1px solid $ic-border-light;
&.ui-state-focus {
a { @include button-focus-light; }
}
@ -110,11 +110,11 @@ Simply add the **.ui-accordion--mini** class to the parent **.accordion** elemen
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-color: darken($ic-brand-primary, 5%);
border-bottom: 1px solid darken($ic-brand-primary, 5%) !important;
border-bottom: 1px solid darken($ic-brand-primary, 5%);
background: $ic-brand-primary;
&.ui-state-focus {
border-color: darken($ic-brand-primary, 10%);
border-bottom: 1px solid darken($ic-brand-primary, 10%) !important;
border-bottom: 1px solid darken($ic-brand-primary, 10%);
a { @include button-focus-dark; }
}
}

View File

@ -112,3 +112,15 @@
.ic-flash-error {
background-color: #b94a48;
}
.ic-flash--Theme-Editor {
background-color: $ic-color-dark;
}
// Buttons in Flash actions
a.Button--flash {
text-decoration: none;
@if $use_high_contrast {
text-decoration: underline;
}
}

View File

@ -223,13 +223,9 @@ input[type="button"] {
&.btn-block, &.Button--block { width: 100%; }
}
// Set the backgrounds
// -------------------------
// The colors for these come from app/stylesheets/brandable_variables.json
// Or the values the account sets in the Theme Editor
$ic-brand-button--primary-bgd: $ic-brand-primary !default;
$ic-brand-button--primary-text: $ic-color-light !default;
$ic-brand-button--secondary-bgd: $ic-brand-secondary !default;
$ic-brand-button--secondary-text: $ic-color-light !default;
// Primary appears as blue
.btn-primary, // <-- deprecated- do not use
@ -340,3 +336,7 @@ $ic-brand-button--secondary-text: $ic-color-light !default;
}
&.ui-state-disabled { @extend .Button.disabled; }
}
.Button--theme-editor-apply {
text-decoration: none;
}

View File

@ -244,134 +244,6 @@ TEXT
<% end %>
<% unless @account.site_admin? %>
<% if @account == @domain_root_account && (use_new_styles? || k12?) %>
<%= f.fields_for :settings do |settings| %>
<fieldset>
<legend>Branding</legend>
<table class="formtable Settings__Branding" style="100%">
<tr>
<td>
<%= settings.label :header_image, t('Logo') %>
</td>
<td>
<div class="branding_section">
<%= settings.file_field :header_image, accept: 'image/png,image/gif,image/jpeg,image/svg' %>
<p style="font-size: 0.9em;">
<%= t("The logo image in the top right Canvas header. 200x200 svg or png") %>
</p>
<% if @account.settings[:header_image] %>
<p><%= t("Current:") %></p>
<p><img src="<%= @account.settings[:header_image] %>" width="200" height="200" /></p>
<% end %>
</div>
<% if @account.settings[:header_image] %>
<%= settings.check_box :header_image_remove, "class" => "branding_section_toggler" %>
<%= settings.label :header_image_remove, :en => "Revert to default" %>
<% end %>
</td>
</tr>
<tr>
<td>
<%= settings.label :favicon, t('Favicon') %>
</td>
<td>
<div class="branding_section">
<%= settings.file_field :favicon, accept: 'image/vnd.microsoft.icon,image/x-icon,image/png,image/gif' %>
<p style="font-size: 0.9em;">
<%= t do %>
Use a <a href="http://realfavicongenerator.net/" target="_blank">single 16x16, 32x32, 48x48 ico file</a>.
<% end %>
</p>
<% if @account.settings[:favicon] %>
<p><%= t("Current:") %></p>
<p><img src="<%= @account.settings[:favicon] %>" width="16" height="16" /></p>
<% end %>
</div>
<% if @account.settings[:favicon] %>
<%= settings.check_box :favicon_remove, "class" => "branding_section_toggler" %>
<%= settings.label :favicon_remove, :en => "Revert to default" %>
<% end %>
</td>
</tr>
<tr>
<td>
<%= settings.label :apple_touch_icon, t('Apple Touch Icon') %>
</td>
<td>
<div class="branding_section">
<%= settings.file_field :apple_touch_icon, accept: 'image/png,image/svg' %>
<p style="font-size: 0.9em;">
<%= t("The shortcut icon for iOS/Android devices. 180x180 png or svg") %>
</p>
<% if @account.settings[:apple_touch_icon] %>
<p><%= t("Current:") %></p>
<p><img src="<%= @account.settings[:apple_touch_icon] %>" width="57" height="57" /></p>
<% end %>
</div>
<% if @account.settings[:apple_touch_icon] %>
<%= settings.check_box :apple_touch_icon_remove, "class" => "branding_section_toggler" %>
<%= settings.label :apple_touch_icon_remove, :en => "Revert to default" %>
<% end %>
</td>
</tr>
<tr>
<td><%= settings.label :msapplication_tile_square, t('Windows Square Tile') %></td>
<td>
<div class="branding_section">
<%= settings.file_field :msapplication_tile_square, accept: 'image/png,image/gif,image/jpeg' %>
<p style="font-size: 0.9em;">
<%= t("558x558 png (1.8x the standard tile size, so it can be scaled up or down
as needed)") %>
</p>
<% if @account.settings[:msapplication_tile_square] %>
<p><%= t("Current:") %></p>
<p><img src="<%= @account.settings[:msapplication_tile_square] %>" width="150" height="150" /></p>
<% end %>
</div>
<% if @account.settings[:msapplication_tile_square] %>
<%= settings.check_box :msapplication_tile_square_remove, "class" => "branding_section_toggler" %>
<%= settings.label :msapplication_tile_square_remove, :en => "Revert to default" %>
<% end %>
</td>
</tr>
<tr>
<td><%= settings.label :msapplication_tile_wide, t('Windows Wide Tile') %></td>
<td>
<div class="branding_section">
<%= settings.file_field :msapplication_tile_wide, accept: 'image/png,image/gif,image/jpeg' %>
<p style="font-size: 0.9em;">
<%= t("558x270 png") %>
</p>
<% if @account.settings[:msapplication_tile_wide] %>
<p><%= t("Current:") %></p>
<p><img src="<%= @account.settings[:msapplication_tile_wide] %>" width="150" height="150" /></p>
<% end %>
</div>
<% if @account.settings[:msapplication_tile_wide] %>
<%= settings.check_box :msapplication_tile_wide_remove, "class" => "branding_section_toggler" %>
<%= settings.label :msapplication_tile_wide_remove, :en => "Revert to default" %>
<% end %>
</td>
</tr>
<tr>
<td>
<%= settings.label :msapplication_tile_color, t('Windows Tile Color') %>
</td>
<td>
<%= settings.text_field :msapplication_tile_color, value: @account.settings[:msapplication_tile_color], placeholder: '#0099e0'
# #0099e0 is $canvas-primary from sass. If you change this, make sure to change it in app/views/info/browserconfig.xml.builder too.
%>
<p style="font-size:0.9em">
<a href="http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx" target="_blank">
<%= t("Read more about windows tiles") %>
</a>
</p>
</td>
</tr>
</table>
</fieldset>
<% end %>
<% end %>
<fieldset>
<legend><%= t(:quiz_ip_filters_title, "Quiz IP Address Filters")%>
<%= link_to("<i class='icon-question standalone-icon'></i>".html_safe, '#', :class => 'ip_help_link no-hover', :title => t(:quiz_ip_filters_help_tooltip, "What are Quiz IP Filters?")) %></legend>

View File

@ -1,13 +1,13 @@
# Please read: http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx
# read: http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx to learn more about browserconfig.xml
xml.instruct!
xml.browserconfig do
xml.msapplication do
xml.tile do
xml.square70x70logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
xml.square150x150logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
xml.wide310x150logo src: @domain_root_account.settings[:msapplication_tile_wide].presence || "/windows-tile-wide.png"
xml.square310x310logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
xml.TileColor @domain_root_account.settings[:msapplication_tile_color].presence || '#009900' # This is $canvas-primary from sass. If you change this, make sure to change it in the placeholder in app/views/accounts/settings.html.erb too.
xml.square70x70logo src: brand_variable('ic-brand-msapplication-tile-square')
xml.square150x150logo src: brand_variable('ic-brand-msapplication-tile-square')
xml.wide310x150logo src: brand_variable('ic-brand-msapplication-tile-wide')
xml.square310x310logo src: brand_variable('ic-brand-msapplication-tile-square')
xml.TileColor brand_variable('ic-brand-msapplication-tile-color')
end
end
end
end

View File

@ -5,6 +5,17 @@
// TODO: move this out when we have a single require call
require = {
translate: <%= include_js_translations? %>,
<%-
# Purposely not using action_controller.asset_host here, since
# any scripts require.js loads dynamically don't know about the
# md5 fingerprints to use. Since they don't have a fingerprint
# it would be unsafe to rely on serving them from an s3 bucket--
# deploys would overwrite what's there and you'd run into a case
# where a user talking to a server running old code gets a newer
# js file than they were expecting.
# By serving it from the same hostname they loaded the page from, it will
# have the same version of static assets as the code that host is running.
%>
baseUrl: '<%= js_base_url %>',
paths: <%= raw Canvas::RequireJs.paths(true) %>,
packages : <%= raw Canvas::RequireJs.packages %>,
@ -15,7 +26,8 @@
</script>
<%= javascript_include_tag "#{js_base_url}/vendor/require.js" %>
<%= javascript_include_tag "#{js_base_url}/compiled/bundles/common" if include_common_bundle %>
<% js_bundles.unshift([:common]) if include_common_bundle %>
<%= include_js_bundles %>
<%= include_account_js %>
<%= render_js_blocks %>

View File

@ -12,14 +12,8 @@
<title><%= (yield :page_title).presence || @page_title || t('default_page_title', "Canvas LMS") %></title>
<!--[if lte IE 8]> <meta http-equiv=refresh content="0; URL=/ie-8-is-not-supported.html" /> <![endif]-->
<link rel="icon" type="image/x-icon" href="<%=
@domain_root_account.settings[:favicon].presence ||
(Rails.env.development? && "/favicon-green.ico") ||
(Rails.env.test? && "/favicon-green.ico") ||
"/favicon.ico" %>"
/>
<link rel="apple-touch-icon" href="<%= @domain_root_account.settings[:apple_touch_icon].presence || "/apple-touch-icon.png" %>">
<%= favicon_link_tag(favicon) %>
<%= favicon_link_tag(brand_variable('ic-brand-apple-touch-icon'), rel: 'apple-touch-icon', type: nil) %>
<%= yield :auto_discovery %>
<%= yield :head %>
@ -48,4 +42,3 @@
document.addEventListener('click', _earlyClick);
</script>
</head>

View File

@ -36,15 +36,10 @@
<%= render :partial => "layouts/head" %>
<body class="<%= (@body_classes).uniq.join(" ") %>">
<%# Flash messages must be outside of #application or they won't work in screenreaders with modals open. %>
<%= render :partial => 'shared/static_notices' %>
<%= render :partial => 'shared/flash_notices' %>
<div id="application" <% if use_new_styles? %>class="ic-app"<% end %>>
<noscript><div role="alert" class="ic-flash-static ic-flash-error"><h1><%= t :javascript_required, "You need to have JavaScript enabled in order to access this site." %></h1></div></noscript>
<% if @current_user.try(:show_bouncing_channel_message?) %>
<div role="alert" class="ic-flash-static ic-flash-warning">
<%= t(:bouncing_communication_channels, "There appears to be a problem with one of your contact methods. Please check your *Settings Page*.", wrapper: link_to('\1', settings_profile_path)) %>
</div>
<% end %>
<%# Begin alternate markup for K-12 Canvas header %>
<% if k12? %>
<a href="#content" id="skip_navigation_link"><%= t 'links.skip_to_content', "Skip To Content" %></a>

View File

@ -1,53 +1,13 @@
<%-
content_for :head, include_common_stylesheets
<%= render :partial => "layouts/head" %>
<body class="Sg-only">
<%= render :partial => 'shared/static_notices' %>
<%= render :partial => 'shared/flash_notices' %>
-%>
<%-
@locale = raw I18n.locale.to_json
@body_classes ||= []
@body_classes << "context-#{@context.asset_string}" if @context
yield :pre_html
-%><!DOCTYPE html>
<!--[if gte IE 9 ]><html class="Sg-only ie ie9 scripts-not-loaded" lang=<%= @locale %>> <![endif]-->
<!--[if !(IE)]><!--> <html class="Sg-only not-ie scripts-not-loaded" lang=<%= @locale %>> <!--<![endif]-->
<head>
<meta charset="utf-8">
<title><%= (yield :page_title).presence || @page_title || t('default_page_title', "Canvas LMS") %></title>
<!--[if lte IE 8]> <meta http-equiv=refresh content="0; URL=/ie-8-is-not-supported.html" /> <![endif]-->
<%= yield :auto_discovery %>
<%= yield :head %>
<%= yield :meta_tags %>
<%= include_custom_meta_tags %>
<%= include_css_bundles %>
<%= yield :stylesheets %>
<%= include_account_css %>
<script>
// listen for any clicks on links that have href="#" and queue them to be fired on dom ready.
function _earlyClick(e){
var cur = e.target || e.srcElement;
while ( cur && cur.ownerDocument ) {
if ( cur.getAttribute('href') == '#' ) {
e.preventDefault();
_earlyClick.clicks = _earlyClick.clicks || [];
_earlyClick.clicks.push(cur);
break;
}
cur = cur.parentNode;
}
}
document.addEventListener('click', _earlyClick);
</script>
</head>
<body class="Sg-only">
<div id="application">
<div id="main" class="Sg-only">
<%= yield %>
</div>s
<%= render :partial => 'layouts/foot', :locals => { :include_common_bundle => true } %>
</div> <!-- #application -- this is important to keep since it kicks off some of our js examples -->
</body>
</html>
<div id="application">
<div id="main" class="Sg-only">
<%= yield %>
</div>s
<%= render :partial => 'layouts/foot', :locals => { :include_common_bundle => true } %>
</div> <!-- #application -- this is important to keep since it kicks off some of our js examples -->
</body>
</html>

View File

@ -9,3 +9,9 @@
<% end -%>
</div>
<% end %>
<% if @account == @domain_root_account && use_new_styles? && can_do(@account, @current_user, :manage_site_settings) %>
<div class="rs-margin-lr">
<%= link_to t("Open Theme Editor"), brand_configs_new_path, :class => 'btn button-sidebar-wide' %>
</div>
<% end %>

View File

@ -1,8 +1,4 @@
<%
notices = flash_notices()
js_env(:notices => notices)
%>
<% js_env notices: flash_notices() %>
<ul role="alert" aria-live="assertive" id="flash_message_holder"></ul>
<div role="alert" aria-live="assertive" id="flash_screenreader_holder" class="screenreader-only"></div>

View File

@ -0,0 +1,23 @@
<noscript>
<div role="alert" class="ic-flash-static ic-flash-error">
<h1><%= t :javascript_required, "You need to have JavaScript enabled in order to access this site." %></h1>
</div>
</noscript>
<% if session.has_key? :brand_config_md5 %>
<div role="alert" class="ic-flash-static ic-flash-info ic-flash--Theme-Editor">
<%= t("You're editing your Canvas Theme! The changes have not been applied yet.") %>
<% unless params[:editing_brand_config] %>
<%= link_to t("Apply Changes"), brand_configs_new_path, :class => "Button Button--success Button--flash" %>
or
<%= link_to t("Open Theme Editor"), brand_configs_new_path, :class => '' %>
<% end %>
</div>
<% end %>
<% if @current_user.try(:show_bouncing_channel_message?) %>
<div role="alert" class="ic-flash-static ic-flash-warning">
<%= t(:bouncing_communication_channels, "There appears to be a problem with one of your contact methods. Please check your *Settings Page*.", wrapper: link_to('\1', settings_profile_path)) %>
</div>
<% end %>

View File

@ -220,5 +220,9 @@ module CanvasRails
end
config.exceptions_app = ExceptionsApp.new
config.before_initialize do
config.action_controller.asset_host = Canvas::CDN.config.host if Canvas::CDN.config.host
end
end
end

View File

@ -1,86 +0,0 @@
<%=
=begin
WHAT HAPPEND TO assets.yml? AND WHAT ARE THE $use_new_styles, $is-k12 AND $use_high_contrast SASS VARIABLES?
-----------------------------------------------------------------------------
TL,DR: don't add things to this file. Put css bundles in 'assets_real.yml'
In order to allow accounts to opt-in to a feature flag to use the new styles
that UI/UX is working on, and in order to provide a k12-specific look-and-feel
and in order to allow individual users to turn on a
feature flag for high-contrast styles, we do some trickery in our sass.
### What you, the developer need to know.
In any sass file, you can do things like:
@include 'environment'; // or 'variables' or 'variant_variables' if you need to be more fine grained
@if $use_new_styles {
background-color: red;
} @else {
background-color: blue;
}
@if not $use_high_contrast{
font-size: 12px;
}
...and those styles will show up for users depending on their own
:use_high_contrast feature flag and the account's :use_new_styles feature flag.
Make sure, if you see stuff about $use_high_contrast, $is-k12 or $use_new_styles in a
sass file you are working on, that you test it with the feature flag both on and
off, so your changes don't mess up the other. At least having them in the same
file will make it easier than keeping track of some orverrides.css file floating
around somewhere that might be overriding or depending on the rules you're
changing though eh?
Common Gotcha With Variables:
variables in sass are block scoped, and @if is a block so you can't do:
@if $use_new_styles { $textColor: black } @else { $textColor: grey}
because $textColor wont be available anywere ouside that @if.
to get around that do:
$textColor: null;
@if $use_new_styles { $textColor: black } @else { $textColor: grey}
You *could* do:
$textColor: grey;
@if $use_new_styles { $textColor: black }
but DON'T, the explicit semantics of either being in the @if or the @else will help
us when we delete things after everyone has $use_new_styles.
A key goal is that, as we work toward the new styles, put anything that we won't
need anymore inside an '@if not $use_new_styles' block, SO WE CAN DELETE STUFF
SOMEDAY!! YAY!
for more info on the @if and @else syntax see:
http://sass-lang.com/documentation/file.SASS_REFERENCE.html#_8
The Nitty Gritty:
We can do this because Canvas::MultiVariantCompassCompiler#compile_all
(which is the thing that we use to compile all of our sass) compiles each sass
file 6 times, each time adding the path 'app/stylesheets/variants/<variant>'
to the load path so when we @import 'variant_variables', it is actually
grabbing, for example:
app/stylesheets/variants/new_styles_high_contrast/_variant_variables.scss
which sets the variables $use_new_styles and $use_high_contrast correspondingly.
It also outputs all the css files for each variant to a folder with the same
name. Here in assets.yml we manipulate the actual yml in assets_real.yml to make 4 bundles
out of each original bundle, and rename the file paths in each to point to the generated
css directory for that variant.
And finally we do stuff in ApplicationController#include_css_bundles, so when you say
`jammit_css :speedgrader`, it will look at the user's :use_high_contrast feature flag
and the account's :use_new_styles flag and (assuming they were both set to true) it will
tell jammit it wants the 'speedgrader_new_styles_high_contrast' bundle which will include
things like 'public/stylesheets_compiled/new_styles_high_contrast/speed_grader.css',
which was compiled with the $use_new_styles and $use_high_contrast variables set correctly.
Note:
If $is-k12 is turned on, then $use_new_styles will be true as well.
=end
require File.join(Jammit::ASSET_ROOT, 'lib', 'multi_variant_compass_compiler')
MultiVariantCompassCompiler.make_variants_from_real_assets_yml
%>

View File

@ -1,175 +0,0 @@
embed_assets: off
gzip_assets: off
# libsass does this for us
compress_assets: off
# if you want use IE in dev mode and want to get around the max of 30 stylesheets
# problem, uncomment the following lines and make sure you
# rm -rf public/assets after you make any changes to css
# package_assets: always
# compress_assets: off
<% require File.expand_path(Jammit::ASSET_ROOT + '/lib/canvas/plugins/plugin_assets') %>
<% plugin_assets = PluginAssets.new %>
<%= plugin_assets.bundle_yml %>
stylesheets:
saml_fields:
- public/stylesheets/compiled/bundles/saml_fields.css
course_wizard:
- public/stylesheets/compiled/bundles/course_wizard.css
react_files:
- public/stylesheets/compiled/bundles/react_files.css
instructure_eportfolio:
- public/stylesheets/compiled/bundles/instructure_eportfolio.css
course_show:
- public/stylesheets/compiled/bundles/course_show.css
course_list:
- public/stylesheets/compiled/bundles/course_list.css
content_migrations:
- public/stylesheets/compiled/bundles/content_migrations.css
vendor:
- public/stylesheets/compiled/bundles/vendor.css
common:
- public/stylesheets/compiled/bundles/common.css
context_modules:
- public/stylesheets/compiled/bundles/context_modules.css
context_modules2:
- public/stylesheets/compiled/bundles/context_modules2.css
content_next:
- public/stylesheets/compiled/bundles/content_next.css
modules_next:
- public/stylesheets/compiled/bundles/modules_next.css
context_module_progressions:
- public/stylesheets/compiled/bundles/context_module_progressions.css
dashboard:
- public/stylesheets/compiled/bundles/dashboard.css
registration:
- public/stylesheets/compiled/bundles/registration.css
profile_show:
- public/stylesheets/compiled/bundles/profile_show.css
profile_edit:
- public/stylesheets/compiled/bundles/profile_edit.css
facebook:
- public/stylesheets/compiled/bundles/facebook.css
speed_grader:
- public/stylesheets/compiled/bundles/speed_grader.css
conferences:
- public/stylesheets/compiled/bundles/conferences.css
gradebook_uploads:
- public/stylesheets/compiled/bundles/gradebook_uploads.css
calendar2:
- public/stylesheets/compiled/bundles/calendar2.css
agenda_view:
- public/stylesheets/compiled/bundles/agenda_view.css
course_settings:
- public/stylesheets/compiled/bundles/course_settings.css
discussions:
- public/stylesheets/compiled/bundles/discussions.css
discussions_list:
- public/stylesheets/compiled/bundles/discussions_list.css
full_files:
- public/stylesheets/compiled/bundles/full_files.css
datagrid:
- public/stylesheets/compiled/bundles/datagrid.css
gradebook_history:
- public/stylesheets/compiled/bundles/gradebook_history.css
gradebook2:
- public/stylesheets/compiled/bundles/gradebook2.css
screenreader_gradebook:
- public/stylesheets/compiled/bundles/screenreader_gradebook.css
attendance:
- public/stylesheets/compiled/bundles/attendance.css
quizzes:
- public/stylesheets/compiled/bundles/quizzes.css
quizzes_ember:
- public/stylesheets/compiled/bundles/quizzes_ember.css
moderate_quiz:
- public/stylesheets/compiled/bundles/moderate_quiz.css
assignments:
- public/stylesheets/compiled/bundles/assignments.css
assignments_edit:
- public/stylesheets/compiled/bundles/assignments_edit.css
new_assignments:
- public/stylesheets/compiled/bundles/new_assignments.css
grading_standards:
- public/stylesheets/compiled/bundles/grading_standards.css
grading_periods:
- public/stylesheets/compiled/bundles/grading_periods.css
login:
- public/stylesheets/compiled/bundles/login.css
otp_login:
- public/stylesheets/compiled/bundles/otp_login.css
roster:
- public/stylesheets/compiled/bundles/roster.css
roster_user:
- public/stylesheets/compiled/bundles/roster_user.css
learning_outcomes:
- public/stylesheets/compiled/bundles/learning_outcomes.css
grade_summary:
- public/stylesheets/compiled/bundles/grade_summary.css
context_list:
- public/stylesheets/compiled/bundles/context_list.css
page_views:
- public/stylesheets/compiled/bundles/page_views.css
prior_users:
- public/stylesheets/compiled/bundles/prior_users.css
reports:
- public/stylesheets/compiled/bundles/reports.css
statistics:
- public/stylesheets/compiled/bundles/statistics.css
slickgrid:
- public/stylesheets/compiled/bundles/slickgrid.css
sub_accounts:
- public/stylesheets/compiled/bundles/sub_accounts.css
user_grades:
- public/stylesheets/compiled/bundles/user_grades.css
user_logins:
- public/stylesheets/compiled/bundles/user_logins.css
account_settings:
- public/stylesheets/compiled/bundles/account_settings.css
account_admin_tools:
- public/stylesheets/compiled/bundles/account_admin_tools.css
select_content_dialog:
- public/stylesheets/compiled/bundles/select_content_dialog.css
conversations_new:
- public/stylesheets/compiled/bundles/conversations_new.css
alerts:
- public/stylesheets/compiled/bundles/alerts.css
developer_keys:
- public/stylesheets/compiled/bundles/developer_keys.css
edit_calendar_event_full:
- public/stylesheets/compiled/bundles/edit_calendar_event_full.css
notification_preferences:
- public/stylesheets/compiled/bundles/notification_preferences.css
tinymce:
- public/stylesheets/compiled/bundles/tinymce.css
messages:
- public/stylesheets/compiled/bundles/messages.css
user_notes:
- public/stylesheets/compiled/bundles/user_notes.css
imports:
- public/stylesheets/compiled/bundles/imports.css
styleguide:
- public/stylesheets/compiled/bundles/styleguide.css
locale:
- public/stylesheets/compiled/bundles/locale.css
mobile_auth:
- public/stylesheets/compiled/bundles/mobile_auth.css
all_courses:
- public/stylesheets/compiled/bundles/all_courses.css
external_tool_full_width:
- public/stylesheets/compiled/bundles/external_tool_full_width.css
dashboard_card:
- public/stylesheets/compiled/bundles/dashboard_card.css
terms:
- public/stylesheets/compiled/bundles/terms.css
wiki_page:
- public/stylesheets/compiled/bundles/wiki_page.css
# Client Apps:
canvas_quizzes:
- public/stylesheets/compiled/bundles/canvas_quizzes.css
<%= plugin_assets.anchors_yml %>

View File

@ -1,40 +0,0 @@
/*
This is an expirimental feature to preview how branding canvas with your own colors will work.
It's not ready yet, and should only be used on dev machines.
To use, copy this file to `config/brand_variables.scss` (remove the `.example`).
You can set values to whatever you want here and they will be used in the
'new_styles_normal_contrast' and 'k12_normal_contrast' variants when they are compiled on
your computer.
remember, anything that you want to be able to set in this file, needs to have a "!default"
next to it where it is declared in the real sass sheet.
As we decide upon new variables that we want to be able to set, remember to add them to
both "config/brand_variables.scss.example" as well as your "config/brand_variables.cass" locally.
*/
//// Canvas Primary Palette
////////////////////////////////////////////////
// $ic-brand-primary
// $ic-brand-secondary
// $linkColor
//// Instructure Canvas (IC) Global Variables
////////////////////////////////////////////////
// $ic-body-background-color
// $ic-content-background-color
// $ic-font-color-light
// $ic-font-color-dark
// $ic-font-color--subdued
// $ic-border-color
// $ic-list-item-background--hover
// $ic-list-item-background--selected
//// Buttons
////////////////////////////
// $ic-brand-button--primary-bgd
// $ic-brand-button--primary-text
// $ic-brand-button--secondary-bgd
// $ic-brand-button--secondary-text

23
config/brandable_css.yml Normal file
View File

@ -0,0 +1,23 @@
paths:
public_dir: public
sass_dir: app/stylesheets
all_sass_bundles: 'app/stylesheets/{,plugins/*/}**/[^_]*.s[ac]ss'
all_sass_files: 'app/stylesheets/{,plugins/*/}**/*.s[ac]ss'
brandable_variables_json: app/stylesheets/brandable_variables.json
brandable_variables_defaults_scss: app/stylesheets/_brandable_variables_defaults_autogenerated.scss
branded_scss_folder: app/stylesheets/brandable_css_brands
bundles_with_deps: public/dist/brandable_css/brandable_css_bundles_with_deps.json
file_checksums: public/dist/brandable_css/brandable_css_file_checksums.json
output_dir: public/dist/brandable_css
browsers_yml: config/browsers.yml
manifest_key_seperator: $$$$$$$$$$$
variants:
legacy_normal_contrast: {}
legacy_high_contrast: {}
new_styles_normal_contrast:
brandable: true
new_styles_high_contrast: {}
k12_normal_contrast: {}
k12_high_contrast: {}

View File

@ -0,0 +1,16 @@
defaults: &defaults
# host: 'the hostname to use for static asset cdn, eg: https://mydistribution.cloudfront.net'
# bucket: "name of the s3 bucket to push things to"
# aws_access_key_id: "secret aws_access_key_id that has write access to that bucket"
# aws_secret_access_key: "secret_access_key for that access_key_id"
# enabled: true #set false to not push anything to s3 and serve assets from same hostname as rails
development:
<<: *defaults
test:
<<: *defaults
enabled: false
production:
<<: *defaults

View File

@ -1,34 +1,31 @@
require 'fileutils'
require 'pathname'
# stolen and adapted from ./plugin_symlinks.rb
def maintain_client_app_symlinks
# remove bad symlinks first
Dir.glob("public/javascripts/client_apps/*").each do |app_symlink|
if File.symlink?(app_symlink) && !File.exist?(app_symlink)
File.unlink(app_symlink)
end
end
output_dir = Pathname.new "public/javascripts/client_apps"
# remove anything that was there first
output_dir.rmtree if output_dir.exist?
# create new ones
Dir.glob("client_apps/*").select { |f| File.directory?(f) }.each do |app_dir|
unless File.exist?("public/javascripts/client_apps")
FileUtils.makedirs("public/javascripts/client_apps")
Pathname.glob("client_apps/*").select(&:directory?).each do |app_dir|
app = app_dir.basename
dist = app_dir.join('dist')
next unless dist.exist?
files = Dir.chdir(dist) do
[Pathname.new("#{app}.js")] + Pathname.glob("#{app}/**/*").reject(&:directory?)
end
app = File.basename(app_dir)
[ "#{app}", "#{app}.js" ].each do |asset|
source = "public/javascripts/client_apps/#{asset}"
target = "../../../#{app_dir}/dist/#{asset}"
unless File.symlink?(source) && File.readlink(source) == target
File.unlink(source) if File.exist?(source)
File.symlink(target, source)
end
files.each do |asset|
original = dist.join(asset)
target = output_dir.join(asset)
FileUtils.mkdir_p(target.dirname)
File.symlink(original.relative_path_from(target.dirname), target)
end
end
end
File.open(__FILE__) do |f|
f.flock(File::LOCK_EX)
maintain_client_app_symlinks
end

View File

@ -0,0 +1,67 @@
module RevManifest
def self.manifest
# don't look this up every request in prduction
return @manifest if ActionController::Base.perform_caching && defined? @manifest
file = Rails.root.join('public', 'dist', 'rev-manifest.json')
if file.exist?
Rails.logger.debug "reading rev-manifest.json"
@manifest = JSON.parse(file.read).freeze
elsif Rails.env.production?
raise "you need to run `gulp rev` first"
else
@manifest = {}.freeze
end
end
def self.url_for(source)
# remove the leading slash if there is one
source = source.sub(/^\//, '')
fingerprinted = manifest[source]
"/dist/#{fingerprinted}" if fingerprinted
end
end
# This is where we monkeypatch rails to look at the rev-manifest.json file we make in `gulp rev`
# instead of doing it's normal cache busting stuff on the url.
# eg: instead of '/images/whatever.png?12345', we want '/dist/images/whatever-<md5 of file>.png'
# There is a different method that needs to be monkeypatched for rails 3 vs rails 4
if CANVAS_RAILS3
module ActionView
module Helpers
module AssetTagHelper
class AssetPaths
private
# Rails 3 expects us to override 'rewrite_asset_path' if we want to do something other than the
# default "/images/whatever.png?12345".
def rewrite_asset_path_with_gulp_assets(source, dir, options = nil)
# our brandable_css stylesheets are already fingerprinted, we don't need to do anything to them
return source if source =~ /^\/dist\/brandable_css/
key = (source[0] == '/') ? source : "#{dir}/#{source}"
RevManifest.url_for(key) || rewrite_asset_path_without_gulp_assets(source, dir, options)
end
alias_method_chain :rewrite_asset_path, :gulp_assets
end
end
end
end
else
require 'action_view/helpers/asset_url_helper'
module ActionView
module Helpers
module AssetUrlHelper
# Rails 4 leaves us 'compute_asset_path' to override instead.
def compute_asset_path_with_gulp_assets(source, options = {})
original_path = compute_asset_path_without_gulp_assets(source, options)
RevManifest.url_for(original_path) || original_path
end
alias_method_chain :compute_asset_path, :gulp_assets
end
end
end
end

View File

@ -808,6 +808,14 @@ CanvasRails::Application.routes.draw do
concerns :files
end
scope(controller: :brand_configs) do
get 'brand_configs/new', action: :new
post 'brand_configs', action: :create
delete 'brand_configs', action: :destroy
post 'brand_configs/save_to_account', action: :save_to_account
end
get 'courses/:course_id/outcome_rollups' => 'outcome_results#rollups', as: 'course_outcome_rollups'
get 'terms_of_use' => 'legal_information#terms_of_use', as: 'terms_of_use_redirect'

View File

@ -0,0 +1,29 @@
class CreateBrandConfigs < ActiveRecord::Migration
tag :predeploy
disable_ddl_transaction!
LENGTH_OF_AN_MD5_HASH = 32
def up
create_table :brand_configs, id: false do |t|
t.string :md5, limit: LENGTH_OF_AN_MD5_HASH, null: false, unique: true
t.column :variables, :text, null: false
t.boolean :share, default: false, null: false
t.string :name
t.datetime :created_at, null: false
end
# because we didn't use the rails default `id` int primary key, we have to add it manually
execute %{ ALTER TABLE brand_configs ADD PRIMARY KEY (md5); }
add_index :brand_configs, :share
add_column :accounts, :brand_config_md5, :string, limit: LENGTH_OF_AN_MD5_HASH
add_foreign_key :accounts, :brand_configs, column: 'brand_config_md5', primary_key: 'md5'
add_index :accounts, :brand_config_md5, where: 'brand_config_md5 IS NOT NULL', algorithm: :concurrently
end
def down
remove_column :accounts, :brand_config_md5
drop_table :brand_configs
end
end

View File

@ -65,10 +65,17 @@ module HandlebarsTasks
dependencies = ['compiled/handlebars_helpers']
if css = get_css(id)
dependencies << "compiled/util/registerTemplateCss"
# arguments[1] will be the registerTemplateCss function
css_registration = "\narguments[1]('#{id}', #{MultiJson.dump css});\n"
# if a scss file named exactly like this exists, load it when this is loaded
if Dir.glob("app/stylesheets/jst/#{id}.s[ac]ss").first
bundle = "jst/#{id}"
require 'lib/brandable_css'
fingerprints = MultiJson.dump BrandableCSS.all_fingerprints_for(bundle)
dependencies << "compiled/util/brandableCss"
# arguments[1] will be brandableCss
css_registration = "
var fingerprint = #{fingerprints}[arguments[1].getCssVariant()];
arguments[1].loadStylesheet('#{bundle}-' + fingerprint);
"
end
# take care of `require`ing partials
@ -88,23 +95,12 @@ define('#{plugin ? plugin + "/" : ""}jst/#{id}', #{MultiJson.dump dependencies},
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['#{id}'] = template(#{data["template"]});
#{partial_registration}
#{css_registration}
#{css_registration}
return templates['#{id}'];
});
JS
end
def get_css(file_path)
if sass_file = Dir.glob("app/stylesheets/jst/#{file_path}.s[ac]ss").first
# renders the sass file to disk, then returns the css it wrote
# note: for now, all jst stylesheets will be just in 'legacy_normal_contrast'
system({"CANVAS_SASS_STYLE" => "compressed"}, "node script/compile-sass.js #{sass_file}")
File.read sass_file
.sub(/^app\/stylesheets/, 'public/stylesheets_compiled/legacy_normal_contrast')
.sub(/.s[ac]ss$/, '.css')
end
end
protected
def find_partial_deps(template)

15
guard/brandable_css.rb Normal file
View File

@ -0,0 +1,15 @@
require 'guard'
require 'guard/guard'
module Guard
class BrandableCSS < Guard
def start
@pid = spawn("./node_modules/.bin/brandable_css --watch")
end
def stop
Process.kill(:INT, @pid)
end
end
end

View File

@ -1,25 +0,0 @@
require 'guard'
require 'guard/guard'
module Guard
class Compass < Guard
def initialize(watchers=[], options={})
super([::Guard::Watcher.new(/(app\/stylesheets.*)/)], {})
end
def run_on_change(paths)
# for now just recompile everything, we'll do this more optimized when we
# fix the TODO below
run_all
end
def run_all
::Guard::UI.info "Forcing recompilation of all SASS files"
# TODO: get rid of this guard and watch sass in our JS based frontend watcher.
# whatever that ends up being (gulp, broccoli, etc)
`npm run compile-sass`
end
end
end

20
gulpfile.babel.js Normal file
View File

@ -0,0 +1,20 @@
const gulp = require('gulp')
const gulpPlugins = require('gulp-load-plugins')()
const DIST = 'public/dist'
const PUBLIC_NOT_DIST = ['public/**/*', '!' + DIST + '/**/*']
gulp.task('rev', function() {
return gulp.src(PUBLIC_NOT_DIST)
.pipe(gulp.dest(DIST))
.pipe(gulpPlugins.rev())
.pipe(gulp.dest(DIST))
.pipe(gulpPlugins.rev.manifest())
.pipe(gulp.dest(DIST))
})
gulp.task('watch', function (){
gulp.watch(PUBLIC_NOT_DIST, ['rev'])
})
gulp.task('default', ['rev', 'watch'])

96
lib/brandable_css.rb Normal file
View File

@ -0,0 +1,96 @@
module BrandableCSS
require 'rails'
CONFIG = YAML.load_file(Rails.root.join('config/brandable_css.yml')).freeze
BRANDABLE_VARIABLES = JSON.parse(File.read(Rails.root.join(CONFIG['paths']['brandable_variables_json']))).freeze
SASS_STYLE = ENV['SASS_STYLE'] || (Rails.env.production? ? 'compressed' : 'nested')
class << self
def variables_map
@variables_map ||= BRANDABLE_VARIABLES.each_with_object({}) do |variable_group, memo|
variable_group['variables'].each { |variable| memo[variable['variable_name']] = variable }
end.freeze
end
# gets the *effective* value for a brandable variable
def brand_variable_value(variable_name, active_brand_config=nil)
explicit_value = active_brand_config && active_brand_config.get_value(variable_name).presence
return explicit_value if explicit_value
config = variables_map[variable_name]
default = config['default']
return brand_variable_value(default[1..-1], active_brand_config) if default && default.starts_with?('$')
# while in our sass, we want `url(/images/foo.png)`,
# the Rails Asset Helpers expect us to not have the '/images/', eg: <%= image_tag('foo.png') %>
default.sub!(/^\/images\//, '') if config['type'] == 'image'
default
end
def variants
@variants ||= CONFIG['variants'].map{|(k)| k }.freeze
end
def brandable_variants
@brandable_variants ||= CONFIG['variants'].select{|_, v| v['brandable']}.map{ |k,_| k }.freeze
end
def combined_checksums
return @combined_checksums if ActionController::Base.perform_caching && defined? @combined_checksums
file = Rails.root.join(CONFIG['paths']['bundles_with_deps'] + SASS_STYLE)
if file.exist?
@combined_checksums = JSON.parse(file.read).each_with_object({}) do |(k, v), memo|
memo[k] = v['combinedChecksum']
end.freeze
elsif Rails.env.production?
raise "you need to run #{cli} before you can serve css."
else
# for dev/test there might be cases where you don't want it to raise an exception
# if you haven't ran `brandable_css` and the manifest file doesn't exist yet.
# eg: you want to test a controller action and you don't care that it links
# to a css file that hasn't been created yet.
default_value = "Error: unknown css checksum. you need to run #{cli}"
@combined_checksums = Hash.new(default_value).freeze
end
end
# bundle path should be something like "bundles/speedgrader" or "plugins/analytics/something"
def fingerprint_for(bundle_path, variant)
key = ["#{bundle_path}.scss", variant].join(CONFIG['manifest_key_seperator'])
fingerprint = combined_checksums[key]
raise "Fingerprint not found. #{bundle_path} #{variant}" unless fingerprint
fingerprint
end
def all_fingerprints_for(bundle_path)
variants.each_with_object({}) do |variant, object|
object[variant] = fingerprint_for(bundle_path, variant)
end
end
def cli
'./node_modules/.bin/brandable_css'
end
def compile_all!
run_cli!
end
def compile_brand!(brand_id)
run_cli!('--brand-id', brand_id)
end
private
def run_cli!(*args)
# this makes sure the symlinks to app/stylesheets/plugins/analytics, etc exist
# so their scss files can be picked up and compiled with everything else
require 'config/initializers/plugin_symlinks'
command = [cli].push(*args).shelljoin + ' 2>&1'
msg = "running BrandableCSS CLI: #{command}"
Rails.logger.try(:debug, msg)
raise "Error: #{msg}" unless system(command)
end
end
end

25
lib/canvas/cdn.rb Normal file
View File

@ -0,0 +1,25 @@
module Canvas
module CDN
class << self
def config
@config ||= begin
config = ActiveSupport::OrderedOptions.new
config.enabled = false
yml = ConfigFile.load('canvas_cdn')
config.merge!(yml.symbolize_keys) if yml
config
end
end
def enabled?
config.enabled
end
def push_to_s3!(*args)
return unless enabled?
uploader = Canvas::CDN::S3Uploader.new(*args)
uploader.upload!
end
end
end
end

View File

@ -0,0 +1,74 @@
module Canvas
module CDN
class S3Uploader
attr_accessor :bucket, :config
def initialize(folder='dist')
require 'aws-sdk'
@folder = folder
@config = Canvas::CDN.config
@s3 = AWS::S3.new(access_key_id: config.aws_access_key_id,
secret_access_key: config.aws_secret_access_key)
@bucket = @s3.buckets[config.bucket]
end
def local_files
Dir.chdir(Rails.public_path) { Dir["#{@folder}/**/**"]}
end
def upload!
Parallel.each(local_files, :in_threads=>8) do |file|
upload_file(file)
end
end
def fingerprint?(path)
/-[0-9a-fA-F]{32}$/.match(path.basename(path.extname).to_s)
end
def font?(path)
%w{.ttf .ttc .otf .eot .woff .woff2}.include?(path.extname)
end
def mime_for(path)
Mime::Type.lookup_by_extension(path.extname[1..-1])
end
def options_for(path)
options = {acl: :public_read, content_type: mime_for(path)}
if fingerprint?(path)
options.merge!({
cache_control: "public, max-age=#{1.year}",
expires: 1.year.from_now.httpdate
})
end
# Set headers so font's work cross-orign. While you can also set a
# CORSConfig when you set up your s3 bucket to do the same thing, this
# will make sure it is always set for fonts.
options['Access-Control-Allow-Origin'] = '*' if font?(path)
options
end
def upload_file(remote_path)
local_path = Pathname.new("#{Rails.public_path}/#{remote_path}")
return if (local_path.extname == '.gz') || local_path.directory?
s3_object = bucket.objects[remote_path]
return log("skipping already existing #{remote_path}") if s3_object.exists?
options = options_for(local_path)
gzipped = Pathname.new("#{local_path}.gz")
if gzipped.exist?
local_path = gzipped
options[:content_encoding] = 'gzip'
end
log "uploading #{remote_path} #{options[:content_encoding]}"
s3_object.write(local_path, options)
end
def log(msg)
Rails.logger.debug "#{self.class} - #{msg}"
end
end
end
end

View File

@ -1,132 +0,0 @@
# pull in the bundles from the various plugins' config/assets.yml extension
# files and combine them under a plugins.<plugin> dictionary. so e.g. the
# stylesheets bundles from {gems,vendor}/plugins/myplugin/config/assets.yml
# will be added under
#
# plugins:
# myplugin:
# stylesheets:
# ...
#
# in the output. additionally, rescope bundle elements defined under public/
# (in the context of the plugin) as follows:
#
# * public/stylesheets/compiled -> public/stylesheets/compiled/plugins/<plugin>
# * public/* -> public/plugins/<plugin>/*
#
# i.e. public/x becomes public/plugins/myplugin/x with the exception of
# compiled stylesheets, where compass throws them in the more specific
# public/stylesheets/compiled/plugins/myplugin/
#
# to prevent this translation on a bundle element -- to request an element
# from canvas-lms in your bundle, for example -- prefix it with "~:". this
# prefix will be removed but no other changes made.
#
# similarly, a prefix of "otherplugin:" just as without a prefix, but the
# rescoping will target "otherplugin" rather than "myplugin". this is useful
# if myplugin can rely on otherplugin being installed and wishes to reuse
# some of the assets from otherplugin.
class PluginAssets
attr_reader :anchors, :asset_matcher, :plugin_matcher
def initialize( options = {} )
@anchors = { 'stylesheets' => {} }
@asset_matcher = options[:asset_matcher] || '{gems,vendor}/plugins/*/config/assets.yml'
@plugin_matcher = options[:plugin_matcher] || %r{^(?:gems|vendor)/plugins/(.*)/config/assets\.yml$}
end
# this is the yaml that can be dropped into the top of assets.yml
# to output the different plugin bundle definitions
def bundle_yml
subdoc = YAML.dump('plugins' => plugin_assets).gsub(/^---\s?\n/, '')
# add anchors to the various bundles in the imported plugin asset definitions.
# these bundles will be included in the known bundle types below with a
# namespaced bundle name. for instance, the bar stylesheet bundle in the foo
# plugin will be referred under stylesheets below as "plugins/foo/bar" and
# use the corresponding anchor.
#
# I'd add the anchors programmatically instead of through post-processing of
# the serialized document, but I couldn't figure out how
subdoc.gsub!(%r{^( {6})plugins/([^/]*)/([^/]*)/([^:]*): ?$}) do |match|
indent, plugin, type, bundle = $1, $2, $3, $4
namespaced_bundle = "plugins_#{plugin}_#{bundle}"
anchor = "#{type}_#{namespaced_bundle}"
anchors[type][namespaced_bundle] = anchor if anchors[type]
"#{indent}#{bundle}: &#{anchor}"
end
# for some reason the serialized document outputs as
#
# plugins:
# foo:
# stylesheets:
# bar: &anchor
# - value
#
# instead of
#
# plugins:
# foo:
# stylesheets:
# bar: &anchor
# - value
#
# both are equivalent without an anchor on bar, but with an anchor on bar,
# the first for adds value to plugins.foo.stylesheets.bar but *not* to
# *anchor. the second form adds value to both.
subdoc.gsub!(%r{(^ {6})- (.*)$}, '\\1 - \\2')
subdoc
end
def anchors_yml(options = {})
indent_depth = options[:indent_depth] || 2
indent_token = Array.new(indent_depth, ' ').join('')
bundle_yml if anchors['stylesheets'].empty?
anchors['stylesheets'].map { |(bundle, anchor)| "#{bundle}: *#{anchor}" }.join( "\n#{indent_token}" )
end
def plugin_assets
return @plugin_assets if @plugin_assets
@plugin_assets = {}
for_each_plugin do |name, assets|
assets.each do |type,bundles|
bundles.keys.each do |bundle,entries|
# the bundle is temporarily renamed to a fully namespaced bundle so
# that we can detect and translate that namespace into an anchor in
# post-processing.
bundles["plugins/#{name}/#{type}/#{bundle}"] = bundles.
delete(bundle).map { |entry| format_bundle_entry( entry, name ) }
end
end
@plugin_assets[name] = assets
end
@plugin_assets
end
def for_each_plugin
Dir.glob( asset_matcher ).sort.each do |asset_file|
yield plugin_name_for(asset_file), YAML.load(File.read(asset_file))
end
end
def format_bundle_entry(entry, plugin)
entry.gsub(%r{^public/stylesheets/compiled/}, "~:public/stylesheets/compiled/plugins/#{plugin}/").
gsub(%r{^public/}, "~:public/plugins/#{plugin}/").
gsub(%r{^(\w+):public/stylesheets/compiled/}, "~:public/stylesheets/compiled/plugins/\\1/").
gsub(%r{^(\w+):public/}, "~:public/plugins/\\1/").
gsub(%r{^~:}, '')
end
def plugin_name_for(path)
match = plugin_matcher.match(path)
raise ArgumentError, 'must provide a valid plugin asset.yml path' unless match
match[1]
end
end

View File

@ -1,33 +0,0 @@
##########################################################################
# See the docs at the top of assets.yml for more info on what this is for.
##########################################################################
module MultiVariantCompassCompiler
VARIANTS = %w{legacy_normal_contrast legacy_high_contrast new_styles_normal_contrast new_styles_high_contrast k12_normal_contrast k12_high_contrast}.freeze
def all_sass_files
require 'config/initializers/plugin_symlinks'
# build the list of files ourselves so that we get it to follow symlinks
sass_path = File.expand_path('app/stylesheets')
sass_files = Dir.glob("#{sass_path}/{,plugins/*/}**/[^_]*.s[ac]ss")
end
def self.make_variants_from_real_assets_yml
require 'erb'
require 'yaml'
require 'jammit'
original_yml = ERB.new(File.read(File.expand_path('../../config/assets_real.yml', __FILE__))).result(binding)
parsed = YAML.load(original_yml)
stylesheets_with_variants = {}
VARIANTS.each do |variant|
parsed['stylesheets'].each do |bundle_name, file_paths|
paths = file_paths.map{ |p| p.gsub(%r{/compiled/}, '_compiled/' + variant + '/')}
stylesheets_with_variants[bundle_name + '_' + variant] = paths
end
end
parsed['stylesheets'] = stylesheets_with_variants
YAML.dump(parsed)
end
end

View File

@ -0,0 +1,17 @@
namespace :brand_configs do
desc "Write _brand_variable.scss to disk so canvas_css can render stylesheets for that branding. " +
"Set BRAND_CONFIG_MD5=<whatever> to save just that one, otherwise writes a file for each BrandConfig in db."
task :write => :environment do
if md5 = ENV['BRAND_CONFIG_MD5']
BrandConfig.find(md5).save_file!
else
Rake::Task['brand_configs:clean'].invoke
BrandConfig.find_each(&:save_file!)
end
end
desc "Remove all Brand Variable scss files"
task :clean do
rm_rf BrandConfig::CONFIG['paths']['branded_scss_folder']
end
end

View File

@ -119,18 +119,12 @@ namespace :canvas do
tasks = Hash.new
if compile_css
tasks["Compile sass and make jammit css bundles"] = -> {
log_time('npm run compile-sass') do
half_of_avilable_cores = (processes / 2).ceil.to_s
raise unless system({"CANVAS_SASS_STYLE" => "compressed", "CANVAS_BUILD_CONCURRENCY" => half_of_avilable_cores}, "npm run compile-sass")
end
# "tmp/brandable_css_bundles_with_deps.json needs to exist before we run
# handlebars stuff, so we have to do this first
require 'lib/brandable_css'
log_time('compile css (including custom brands)') { BrandableCSS.compile_all! }
log_time("Jammit") do
require 'jammit'
Jammit.package!
end
}
if compile_css
tasks["css:styleguide"] = -> {
Rake::Task['css:styleguide'].invoke
}
@ -170,6 +164,7 @@ namespace :canvas do
end
combined_time = times.reduce(:+)
puts "Finished compiling assets in #{real_time}. parallelism saved #{combined_time - real_time} (#{real_time.to_f / combined_time.to_f * 100.0}%)"
raise "Error reving files" unless system('node_modules/.bin/gulp rev')
end
desc "Check static assets and generate api documentation."

View File

@ -0,0 +1,8 @@
namespace :canvas do
namespace :cdn do
desc 'Push static assets to s3'
task :upload_to_s3 => :environment do
Canvas::CDN.push_to_s3!
end
end
end

View File

@ -4,10 +4,4 @@ namespace :css do
puts "--> creating styleguide"
puts `dress_code config/styleguide.yml`
end
desc "Compile css assets."
task :generate do
raise 'the new way to compile sass is with `npm run compile-sass`. FYI, it uses libsass and is much faster'
end
end

View File

@ -2,21 +2,19 @@
"name": "canvas-lms",
"version": "0.0.0",
"dependencies": {
"autoprefixer": "^2.2.0",
"coffee-script": "1.6.2",
"compute-cluster": "0.0.9",
"glob": "~3.2.9",
"js-yaml": "^3.1.0",
"lodash": "^2.4.1",
"mkdirp": "^0.5.0",
"node-sass": "0.9.3",
"react-tools": "0.11.2",
"requirejs": "~2.1.10",
"uglify-js": "~2.4.12"
"brandable_css": "0.0.30"
},
"devDependencies": {
"babel": "^5.6.10",
"coffee-script": "coffee-script is pinned to exact version matching 'coffee-script-source' bundler gem so all output matches",
"coffee-script": "1.6.2",
"compute-cluster": "0.0.9",
"fleck": "~0.5.1",
"gglobby": "0.0.2",
"glob": "^5.0.6",
"gulp": "^3.9",
"gulp-load-plugins": "^0.10.0",
"gulp-rev": "^5.0.1",
"karma": "~0.10.9",
"karma-coffee-preprocessor": "0.1.3",
"karma-coverage": "~0.1.4",
@ -27,7 +25,13 @@
"karma-qunit": "~0.1.1",
"karma-requirejs": "^0.2.2",
"karma-safari-launcher": "~0.1.1",
"lodash": "^2.4.1",
"react-tools": "~0.11.2",
"requirejs": "~2.1.10",
"testem": "~0.7.1",
"uglify-js": "~2.4.12",
"vinyl-fs": "vinly-fs is not actually required by us but is required by glup. Pinning it to 0.3.7 here because 0.3.8 doesn't work with symlinks. We should really be using npm-shrinkwrap and when we do, remove this.",
"vinyl-fs": "0.3.7",
"xsslint": "0.1.0"
},
"repository": {
@ -37,7 +41,6 @@
"scripts": {
"test": "./node_modules/karma/bin/karma start --browsers Chrome,Firefox,Safari --single-run",
"compress": "node script/compress.js",
"compile-sass": "node script/compile-sass.js",
"preinstall": "script/gem_npm install",
"preupdate": "script/gem_npm update"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 B

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

1
public/images/favicon.ico Symbolic link
View File

@ -0,0 +1 @@
../favicon.ico

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -294,11 +294,6 @@ define([
$('#self_registration_type_radios').toggle(this.checked);
}).trigger('change');
$('.branding_section_toggler').on('change', function(){
$(this).prevAll('.branding_section').last().toggle(!this.checked)
})
});
});

View File

@ -1,5 +0,0 @@
This folder contains symlinks to every client app's built JS assets.
A client app is expected to store its built JS asset in:
/client_apps/NAME/dist/NAME.js

View File

@ -1,45 +0,0 @@
const computecluster = require('compute-cluster');
const glob = require('glob');
const path = require('path');
const mkdirp = require('mkdirp');
var clusterOpts = {
module: path.join(__dirname, "compile-sass_worker.js"),
max_backlog: 10000
}
// You can run this with with `node script/compile-sass.js app/stylesheets/jst/something.scss to compile a specific file.
var sassFileToConvert = process.argv[2];
var sassFiles = sassFileToConvert ? [sassFileToConvert] : glob.sync("app/stylesheets/{,plugins/*/}**/[^_]*.s[ac]ss");
// by default, we'll create a cluster as big as the number of cores on your machine,
// set the CANVAS_BUILD_CONCURRENCY environment variable if you want it to be something else
if (process.env.CANVAS_BUILD_CONCURRENCY) clusterOpts.max_processes = parseInt(process.env.CANVAS_BUILD_CONCURRENCY);
// if we're just doing one file, just spin up one worker
if (sassFileToConvert) clusterOpts.max_processes = 1;
var cc = new computecluster(clusterOpts);
cc.on('error', function(e) {
cc.exit()
process.exit(1);
});
const VARIANTS = 'legacy_normal_contrast legacy_high_contrast new_styles_normal_contrast new_styles_high_contrast k12_normal_contrast k12_high_contrast'.split(' ')
var toRun = 0;
VARIANTS.forEach(function(variant){
sassFiles.forEach(function(sassFile){
// TODO: figure out how to exclude app/stylesheets/jst from the glob when running everything.
if (sassFile.match(/^app\/stylesheets\/jst/) && (!sassFileToConvert || variant !== 'legacy_normal_contrast')) return;
toRun++
cc.enqueue([variant, sassFile], function(err,r){
if (err) return// console.log("an error occured compiling sass:", err);
if (--toRun === 0) {
cc.exit();
}
})
})
})

View File

@ -1,56 +0,0 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const sass = require('node-sass')
const path = require('path')
const yaml = require('js-yaml')
const _ = require('lodash')
const browserSupport = _.map(yaml.safeLoad(fs.readFileSync('config/browsers.yml')).minimums, function(version, browserName) {
return browserName.replace('Internet Explorer', 'Explorer') + ' >= ' + version
})
const autoprefixer = require('autoprefixer')(browserSupport)
// if you want compressed output (eg: in production), set the environment variable CANVAS_SASS_STYLE=compressed
const outputStyle = process.env.CANVAS_SASS_STYLE || 'nested'
process.on('message', function(variantAndSassFile){
const variant = variantAndSassFile[0]
const sassFile = variantAndSassFile[1]
const cssFolder = path.dirname(sassFile).replace(/^app\/stylesheets/, 'public/stylesheets_compiled/' + variant)
const cssFile = cssFolder + '/' + path.basename(sassFile).replace(/.s[ac]ss$/, '.css')
var includePaths = ['app/stylesheets', 'app/stylesheets/variants/' + variant]
// pull in 'config/brand_variables.scss' if we should
if ((variant === 'new_styles_normal_contrast' || variant === 'k12_normal_contrast') && fs.existsSync('config/brand_variables.scss')) {
includePaths.unshift('config')
}
// make sure the folder is there before we try to write the css file to it
mkdirp.sync(cssFolder)
sass.render({
file: sassFile,
success: function(css){
try {
css = autoprefixer.process(css, {cascade: false})
} catch (e) {
console.log("FAILED on: " + sassFile, e.message)
throw e
}
fs.writeFile(cssFile, css, function(err) {
if (err) return console.log(err)
process.send('complete')
})
},
error: function(errorMsg){
console.log('Error compiling sass:', errorMsg)
throw new Error(errorMsg)
},
includePaths: includePaths,
imagePath: '/images',
outputStyle: outputStyle,
sourceComments: (outputStyle === 'compressed' ? 'none' : 'normal'), // one of 'none', 'normal', 'map'
sourceMap: false
})
})

View File

@ -0,0 +1,38 @@
define [
'jquery'
'compiled/util/brandableCss'
], ($, brandableCss) ->
testBundleId = 'bundles/foo-asdf1234'
stubENV = ->
window.ENV ||= {}
window.ENV.active_brand_config = "brand_config_id"
window.ENV.ASSET_HOST = 'http://cdn.example.com'
window.ENV.use_new_styles = true
window.ENV.use_high_contrast = true
module 'brandableCss.loadStylesheet'
test 'should load correctly', ->
brandableCss.loadStylesheet(testBundleId)
ok $('head link[rel="stylesheet"]:last').attr('href').match(testBundleId)
module 'brandableCss.getCssVariant'
test 'should be legacy_normal_contrast by default', ->
equal brandableCss.getCssVariant(), 'legacy_normal_contrast'
test 'should pick up ENV settings', ->
stubENV()
equal brandableCss.getCssVariant(), 'new_styles_high_contrast'
module 'brandableCss.urlFor'
test 'should have right default', ->
window.ENV = {}
expected = "/dist/brandable_css/legacy_normal_contrast/#{testBundleId}.css"
equal brandableCss.urlFor(testBundleId), expected
test 'should pick up ENV settings', ->
stubENV()
window.ENV.use_high_contrast = false
expected = "http://cdn.example.com/dist/brandable_css/#{window.ENV.active_brand_config}/new_styles_normal_contrast/#{testBundleId}.css"
equal brandableCss.urlFor(testBundleId), expected

View File

@ -1,23 +0,0 @@
define [
'jquery'
'compiled/util/registerTemplateCss'
], ($, registerTemplateCss) ->
testColor = 'rgb(255, 0, 0)'
testRule = "#fixtures {color:#{testColor};}"
testTemplateId = templateId = 'test_template_id'
module 'registerTemplateCss'
test 'should render correctly', ->
registerTemplateCss testTemplateId, testRule
equal $('#fixtures').css('color'), testColor
test 'should append <style> node to bottom of <head>', ->
registerTemplateCss testTemplateId, testRule
ok $('head style:last').text().indexOf("/* From: #{testTemplateId} */\n#{testRule}") >= 0
test 'should remove all styles when you call clear()', ->
registerTemplateCss testTemplateId, testRule
registerTemplateCss.clear()
equal $('head style:last').text(), ''
ok $('#fixtures').css('color') != testColor

View File

@ -1,155 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/../../../../lib/canvas/plugins/plugin_assets')
describe PluginAssets do
let(:plugin_assets) { PluginAssets.new }
let(:fixture_assets) { PluginAssets.new( :asset_matcher => 'spec/fixtures/asset_files/*.yml', :plugin_matcher => %r{(\w+)\.yml$} ) }
describe '#initialize' do
describe 'with no options' do
subject { plugin_assets }
describe '#anchors' do
subject { super().anchors }
it { is_expected.to eq({ 'stylesheets' => {} }) }
end
describe '#asset_matcher' do
subject { super().asset_matcher }
it { is_expected.to eq '{gems,vendor}/plugins/*/config/assets.yml' }
end
describe '#plugin_matcher' do
subject { super().plugin_matcher }
it { is_expected.to eq %r{^(?:gems|vendor)/plugins/(.*)/config/assets\.yml$} }
end
end
describe 'with an options hash' do
let(:options) { {:asset_matcher => 'test/*/file.yml', :plugin_matcher => %r{(\w+)} } }
subject { PluginAssets.new options }
describe '#asset_matcher' do
subject { super().asset_matcher }
it { is_expected.to eq options[:asset_matcher] }
end
describe '#plugin_matcher' do
subject { super().plugin_matcher }
it { is_expected.to eq options[:plugin_matcher] }
end
end
end
describe '#bundle_yml' do
subject { fixture_assets.bundle_yml }
it { is_expected.to match %r{plugin_assets_1:\s+stylesheets:} }
it { is_expected.to match %r{plugin_assets_2:\s+stylesheets:} }
it { is_expected.to match %r{plugins/plugin_assets_1/first_plugin\.css} }
it { is_expected.to match %r{plugins/plugin_assets_1/first_plugin_alt\.css} }
it { is_expected.to match %r{plugins/plugin_assets_2/second_plugin\.css} }
it { is_expected.to match %r{plugins/plugin_assets_2/second_plugin_alt\.css} }
end
describe '#anchors_yml' do
subject { fixture_assets.anchors_yml }
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin: \*stylesheets_plugins_plugin_assets_1_first_plugin} }
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin_alt: \*stylesheets_plugins_plugin_assets_1_first_plugin_alt} }
it { is_expected.to match %r{plugins_plugin_assets_2_second_plugin: \*stylesheets_plugins_plugin_assets_2_second_plugin} }
it { is_expected.to match %r{plugins_plugin_assets_2_second_plugin_alt: \*stylesheets_plugins_plugin_assets_2_second_plugin_alt} }
#check indent depth
it { is_expected.to match %r{^ plugins} }
it { is_expected.not_to match %r{^ plugins} }
describe 'with overriden indent' do
subject { fixture_assets.anchors_yml(:indent_depth => 4) }
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin: \*stylesheets_plugins_plugin_assets_1_first_plugin} }
it { is_expected.to match %r{^ plugins} }
it { is_expected.not_to match %r{^ plugins} }
end
end
describe '#for_each_plugin' do
before do
@yield_values = []
fixture_assets.for_each_plugin { |name, yaml| @yield_values << [name, yaml] }
end
it 'pulls the plugin names correctly' do
expect(@yield_values[0][0]).to eq 'plugin_assets_1'
expect(@yield_values[1][0]).to eq 'plugin_assets_2'
end
it 'parses the yaml files' do
expect(@yield_values[0][1]["stylesheets"].keys.sort).to eq ['first_plugin', 'first_plugin_alt']
expect(@yield_values[1][1]["stylesheets"].keys.sort).to eq ['second_plugin', 'second_plugin_alt']
end
end
describe '#format_bundle_entry' do
let(:plugin) { "plugin_name" }
def formatted(path)
fixture_assets.format_bundle_entry( path, plugin )
end
it 'tacks a plugin path onto a compiled css path' do
expect(formatted('public/stylesheets/compiled/something.css')).to eq "public/stylesheets/compiled/plugins/#{plugin}/something.css"
end
it 'expands a simple path with the plugin name' do
expect(formatted( 'public/jellyfish/sting.txt')).to eq "public/plugins/#{plugin}/jellyfish/sting.txt"
end
it 'moves plugin_name from before the path into the middle of the path for a compiled css path' do
new_path = formatted( 'other_plugin_name:public/stylesheets/compiled/something.css' )
expect(new_path).to eq "public/stylesheets/compiled/plugins/other_plugin_name/something.css"
end
it 'moves the plugin_name forward on a simiple path' do
expect(formatted( 'other_plugin:public/simple/path.css' )).to eq "public/plugins/other_plugin/simple/path.css"
end
end
describe '#plugin_name_for' do
it 'pulls the plugin name out of well formed vendor/plugin paths' do
expect(plugin_assets.plugin_name_for('vendor/plugins/analytics/config/assets.yml')).to eq 'analytics'
end
it 'pulls the plugin name out of well formed gems/plugin paths' do
expect(plugin_assets.plugin_name_for('gems/plugins/analytics/config/assets.yml')).to eq 'analytics'
end
it 'errors on badly formed paths' do
expect { plugin_assets.plugin_name_for('bogus/path/blargh.yml') }.to raise_error(ArgumentError, 'must provide a valid plugin asset.yml path')
end
end
describe '#plugin_assets' do
it 'builds a hash for dumping to yaml' do
expect(fixture_assets.plugin_assets).to eq({
"plugin_assets_2" => {
"stylesheets" => {
"plugins/plugin_assets_2/stylesheets/second_plugin" => ["public/stylesheets/compiled/plugins/plugin_assets_2/second_plugin.css"],
"plugins/plugin_assets_2/stylesheets/second_plugin_alt" => ["public/stylesheets/compiled/plugins/plugin_assets_2/second_plugin_alt.css"]
}
},
"plugin_assets_1" => {
"stylesheets" => {
"plugins/plugin_assets_1/stylesheets/first_plugin_alt" => ["public/stylesheets/compiled/plugins/plugin_assets_1/first_plugin_alt.css"],
"plugins/plugin_assets_1/stylesheets/first_plugin" => ["public/stylesheets/compiled/plugins/plugin_assets_1/first_plugin.css"]
}
}
})
end
end
end