Generate JSON file for brand configs

Refs CNVS-28275, closes CNVS-28885

Generate a json file to go along with the scss file for each brand config.
The intention is that the json file for each brand config will be pushed
to the cdn. One difference from the scss file is that it includes all
variables, even if they are not specified in the brand config. Variable
that have not been customized will use the default value.

In addition to generating a json file for each brand, a json file for that
inclues all default values is generated so other services don't need to
know the defaults if no brand config is available.

To allow for long term caching the filename of the json file includes a
hash of the current defaults (including fingerprinted urls for default
images). This way when the defaults change (or a default image) it will
point to a new file even if the brand config didn't change.

Test plan:

- Save a new brand config.
- Look in public/dist/brandable_css/[brand config hash]/
- There should be a [hash of defaults].json file
  - Should include custom values from brand config
  - Should include default values not specified in the brand config
- Run rake brand_configs:clean && rake brand_configs:write
- Should generate json file for all brand configs
- Open console in browser
  - ENV.active_brand_config_json_url should be path the current brand json file
- Go back to the default brand
  - ENV.active_brand_config_json_url should be path to default json file
- Test with a real s3 bucket for the CDN
  - JSON files should be uploaded to the CDN
  - ENV.active_brand_config_json should work when used with ENV.ASSET_HOST

Change-Id: Ibcaf54a2bff324f419a7614a8d3906c0c49aed9e
Reviewed-on: https://gerrit.instructure.com/77427
Reviewed-by: Ryan Shaw <ryan@instructure.com>
Tested-by: Jenkins
QA-Review: August Thornton <august@instructure.com>
Product-Review: Simon Williams <simon@instructure.com>
This commit is contained in:
Brent Burgoyne 2016-04-19 14:55:21 -06:00
parent 1e6a5a66bf
commit 2134a9b430
10 changed files with 340 additions and 14 deletions

View File

@ -107,6 +107,7 @@ class ApplicationController < ActionController::Base
@js_env = {
ASSET_HOST: Canvas::Cdn.config.host,
active_brand_config: active_brand_config.try(:md5),
active_brand_config_json_url: active_brand_config_json_url,
url_to_what_gets_loaded_inside_the_tinymce_editor_css: editor_css,
current_user_id: @current_user.try(:id),
current_user: user_display_json(@current_user, :profile),

View File

@ -677,6 +677,12 @@ module ApplicationHelper
end
end
def active_brand_config_json_url(opts={})
path = active_brand_config(opts).try(:public_json_path)
path ||= BrandableCSS.public_default_json_path
"/#{path}"
end
def brand_config_for_account(opts={})
account = Context.get_account(@context)

View File

@ -61,7 +61,7 @@ class BrandConfig < ActiveRecord::Base
end
def get_value(variable_name)
self.variables[variable_name]
effective_variables[variable_name]
end
def overrides?
@ -96,24 +96,62 @@ class BrandConfig < ActiveRecord::Base
scss_dir.join('_brand_variables.scss')
end
def to_json
BrandableCSS.all_brand_variable_values(self).to_json
end
def json_file
public_brand_dir.join("variables-#{BrandableCSS.default_variables_md5}.json")
end
def scss_dir
BrandableCSS.branded_scss_folder.join(md5)
end
def public_brand_dir
BrandableCSS.public_brandable_css_folder.join(md5)
end
def public_folder
"dist/brandable_css/#{md5}"
end
def public_json_path
"#{public_folder}/variables-#{BrandableCSS.default_variables_md5}.json"
end
def save_scss_file!
logger.info "saving brand variables file: #{scss_file}"
scss_dir.mkpath
scss_file.write(to_scss)
end
def remove_scss_file!
return unless scss_dir.exist?
logger.info "removing: #{scss_dir}"
scss_dir.rmtree
def save_json_file!
logger.info "saving brand variables file: #{json_file}"
public_brand_dir.mkpath
json_file.write(to_json)
move_json_to_s3_if_enabled!
end
def move_json_to_s3_if_enabled!
return unless Canvas::Cdn.enabled?
s3_uploader.upload_file(public_json_path)
File.delete(json_file)
end
def s3_uploader
@s3_uploaderer ||= Canvas::Cdn::S3Uploader.new
end
def save_all_files!
save_scss_file!
save_json_file!
end
def remove_scss_dir!
return unless brand_dir.exist?
logger.info "removing: #{brand_dir}"
brand_dir.rmtree
end
def compile_css!(opts=nil)
@ -143,7 +181,7 @@ class BrandConfig < ActiveRecord::Base
def save_and_sync_to_s3!(progress=nil)
progress.update_completion!(5) if progress
save_scss_file!
save_all_files!
progress.update_completion!(10) if progress
compile_css! on_progress: -> (percent_complete) {
# send at most 1 UPDATE query per 2 seconds
@ -163,7 +201,7 @@ class BrandConfig < ActiveRecord::Base
first
if unused_brand_config
unused_brand_config.destroy
unused_brand_config.remove_scss_file!
unused_brand_config.remove_brand_dir!
end
end

View File

@ -8,6 +8,8 @@ require 'open3'
# changes here if they happen may need to be mirrored in that file.
module BrandableCSS
extend ActionView::Helpers::AssetTagHelper
APP_ROOT = defined?(Rails) && Rails.root || Pathname.pwd
CONFIG = YAML.load_file(APP_ROOT.join('config/brandable_css.yml')).freeze
BRANDABLE_VARIABLES = JSON.parse(File.read(APP_ROOT.join(CONFIG['paths']['brandable_variables_json']))).freeze
@ -85,13 +87,29 @@ module BrandableCSS
end.freeze
end
def variables_map_with_image_urls
@variables_map_with_image_urls ||= variables_map.each_with_object({}) do |(key, config), memo|
if config['type'] == 'image'
memo[key] = config.merge('default' => image_url(config['default']))
else
memo[key] = config
end
end.freeze
end
def default_variables_md5
@default_variables_md5 ||= Digest::MD5.hexdigest(variables_map_with_image_urls.to_json)
end
# gets the *effective* value for a brandable variable
def brand_variable_value(variable_name, active_brand_config=nil)
def brand_variable_value(variable_name, active_brand_config=nil, config_map=variables_map)
explicit_value = active_brand_config && active_brand_config.get_value(variable_name).presence
return explicit_value if explicit_value
config = variables_map[variable_name]
config = config_map[variable_name]
default = config['default']
return brand_variable_value(default[1..-1], active_brand_config) if default && default.starts_with?('$')
if default && default.starts_with?('$')
return brand_variable_value(default[1..-1], active_brand_config, config_map)
end
# 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') %>
@ -99,10 +117,52 @@ module BrandableCSS
default
end
def all_brand_variable_values(active_brand_config=nil)
variables_map.each_with_object({}) do |(key, _), memo|
memo[key] = brand_variable_value(key, active_brand_config, variables_map_with_image_urls)
end
end
def branded_scss_folder
Pathname.new(CONFIG['paths']['branded_scss_folder'])
end
def public_brandable_css_folder
Pathname.new('public/dist/brandable_css')
end
def default_brand_folder
public_brandable_css_folder.join('default')
end
def default_brand_json_file
default_brand_folder.join("variables-#{default_variables_md5}.json")
end
def default_json
all_brand_variable_values.to_json
end
def save_default_json!
default_brand_folder.mkpath
default_brand_json_file.write(default_json)
move_default_json_to_s3_if_enabled!
end
def move_default_json_to_s3_if_enabled!
return unless Canvas::Cdn.enabled?
s3_uploader.upload_file(public_default_json_path)
File.delete(default_brand_json_file)
end
def s3_uploader
@s3_uploaderer ||= Canvas::Cdn::S3Uploader.new
end
def public_default_json_path
"dist/brandable_css/default/variables-#{default_variables_md5}.json"
end
def variants
@variants ||= CONFIG['variants'].map{|(k)| k }.freeze
end

View File

@ -27,6 +27,10 @@ module Canvas
uploader = Canvas::Cdn::S3Uploader.new(*args)
uploader.upload!(&block)
end
def enabled?
!!config.bucket
end
end
end
end

View File

@ -28,7 +28,7 @@ module Canvas
end
def fingerprinted?(path)
/-[0-9a-fA-F]{10}$/.match(path.basename(path.extname).to_s)
/-[0-9a-fA-F]{10,32}$/.match(path.basename(path.extname).to_s)
end
def font?(path)

View File

@ -5,10 +5,10 @@ namespace :brand_configs do
"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_scss_file!
BrandConfig.find(md5).save_all_files!
else
BrandConfig.clean_unused_from_db!
BrandConfig.find_each(&:save_scss_file!)
BrandConfig.find_each(&:save_all_files!)
end
end
Switchman::Rake.shardify_task('brand_configs:write')
@ -25,6 +25,7 @@ namespace :brand_configs do
desc "generate all brands and upload everything to s3"
task :generate_and_upload_all do
Rake::Task['brand_configs:clean'].invoke
BrandableCSS.save_default_json!
Rake::Task['brand_configs:write'].invoke
# This'll pick up on all those written brand_configs and compile their css.

View File

@ -0,0 +1,102 @@
#
# Copyright (C) 2016 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
describe BrandableCSS do
describe "all_brand_variable_values" do
it "returns defaults if called without a brand config" do
expect(BrandableCSS.all_brand_variable_values["ic-link-color"]).to eq '#0081bd'
end
it "includes image_url asset path for default images" do
# un-memoize so it calls image_url stub
BrandableCSS.remove_instance_variable(:@variables_map_with_image_urls)
image_name = "image.png"
BrandableCSS.stubs(:image_url).returns(image_name)
tile_wide = BrandableCSS.all_brand_variable_values["ic-brand-msapplication-tile-wide"]
expect(tile_wide).to eq image_name
end
describe "when called with a brand config" do
before :once do
parent_account = Account.default
parent_account.enable_feature!(:use_new_styles)
parent_config = BrandConfig.create(variables: {"ic-brand-primary" => "#321"})
subaccount_bc = BrandConfig.for(
variables: {"ic-brand-global-nav-bgd" => "#123"},
parent_md5: parent_config.md5,
js_overrides: nil,
css_overrides: nil,
mobile_js_overrides: nil,
mobile_css_overrides: nil
)
subaccount_bc.save!
@brand_variables = BrandableCSS.all_brand_variable_values(subaccount_bc)
end
it "includes custom variables from brand config" do
expect(@brand_variables["ic-brand-global-nav-bgd"]).to eq '#123'
end
it "includes custom variables from parent brand config" do
expect(@brand_variables["ic-brand-primary"]).to eq '#321'
end
it "includes default variables not found in brand config" do
expect(@brand_variables["ic-link-color"]).to eq '#0081bd'
end
end
end
describe "default_json" do
it "includes default variables not found in brand config" do
brand_variables = JSON.parse(BrandableCSS.default_json)
expect(brand_variables["ic-link-color"]).to eq '#0081bd'
end
end
describe "save_default_file!" do
it "writes the default json represendation to the default json file" do
Canvas::Cdn.stubs(:enabled?).returns(false)
file = StringIO.new
BrandableCSS.stubs(:default_brand_json_file).returns(file)
BrandableCSS.save_default_json!
expect(file.string).to eq BrandableCSS.default_json
end
it 'uploads file to s3 if cdn is enabled' do
Canvas::Cdn.stubs(:enabled?).returns(true)
file = StringIO.new
BrandableCSS.stubs(:default_brand_json_file).returns(file)
File.stubs(:delete)
BrandableCSS.s3_uploader.expects(:upload_file).with(BrandableCSS.public_default_json_path)
BrandableCSS.save_default_json!
end
it 'delete the local file if cdn is enabled' do
Canvas::Cdn.stubs(:enabled?).returns(true)
file = StringIO.new
BrandableCSS.stubs(:default_brand_json_file).returns(file)
File.expects(:delete).with(BrandableCSS.default_brand_json_file)
BrandableCSS.s3_uploader.expects(:upload_file)
BrandableCSS.save_default_json!
end
end
end

View File

@ -0,0 +1,41 @@
#
# Copyright (C) 2016 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
describe Canvas::Cdn do
describe '.enabled?' do
before :each do
@original_config = Canvas::Cdn.config.dup
end
after :each do
Canvas::Cdn.config.replace(@original_config)
end
it 'returns true when the cdn config has a bucket' do
Canvas::Cdn.config.merge! enabled: true, bucket: 'bucket_name'
expect(Canvas::Cdn.enabled?).to eq true
end
it 'returns false when the cdn config does not have a bucket' do
Canvas::Cdn.config.merge! enabled: true, bucket: nil
expect(Canvas::Cdn.enabled?).to eq false
end
end
end

View File

@ -86,4 +86,77 @@ describe BrandConfig do
expect(@parent_config.chain_of_ancestor_configs.length).to eq 1
end
end
end
describe "to_json" do
before :once do
setup_subaccount_with_config
@brand_variables = JSON.parse(@subaccount_bc.to_json)
end
it "includes custom variables from brand config" do
expect(@brand_variables["ic-brand-global-nav-bgd"]).to eq '#123'
end
it "includes custom variables from parent brand config" do
expect(@brand_variables["ic-brand-primary"]).to eq '#321'
end
it "includes default variables not found in brand config" do
expect(@brand_variables["ic-link-color"]).to eq '#0081bd'
end
end
describe "save_all_files!" do
before :once do
setup_subaccount_with_config
end
before :each do
@json_file = StringIO.new
@scss_file = StringIO.new
@subaccount_bc.stubs(:json_file).returns(@json_file)
@subaccount_bc.stubs(:scss_file).returns(@scss_file)
end
describe "with cdn disabled" do
before :each do
Canvas::Cdn.expects(:enabled?).returns(false)
@subaccount_bc.s3_uploader.expects(:upload_file).never
File.expects(:delete).never
end
it "writes the json represendation to the json file" do
@subaccount_bc.save_all_files!
expect(@json_file.string).to eq @subaccount_bc.to_json
end
it "writes the scss represendation to scss file" do
@subaccount_bc.save_all_files!
expect(@scss_file.string).to eq @subaccount_bc.to_scss
end
end
describe "with cdn enabled" do
before :each do
Canvas::Cdn.expects(:enabled?).returns(true)
@upload_expectation = @subaccount_bc.s3_uploader.expects(:upload_file).once
@delete_expectation = File.expects(:delete).once
end
it "writes the json represendation to the json file" do
@subaccount_bc.save_all_files!
expect(@json_file.string).to eq @subaccount_bc.to_json
end
it 'uploads json file to s3 if cdn enabled' do
@upload_expectation.with(@subaccount_bc.public_json_path)
@subaccount_bc.save_all_files!
end
it 'deletes local json file if cdn enabled' do
@delete_expectation.with(@subaccount_bc.json_file)
@subaccount_bc.save_all_files!
end
end
end
end