Skip to content

Commit

Permalink
Generate JSON file for brand configs
Browse files Browse the repository at this point in the history
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 <[email protected]>
Tested-by: Jenkins
QA-Review: August Thornton <[email protected]>
Product-Review: Simon Williams <[email protected]>
  • Loading branch information
brentropy committed May 4, 2016
1 parent 1e6a5a6 commit 2134a9b
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 14 deletions.
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def js_env(hash = {})
@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),
Expand Down
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@ def active_brand_config(opts={})
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)

Expand Down
52 changes: 45 additions & 7 deletions app/models/brand_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def self.md5_for(brand_config)
end

def get_value(variable_name)
self.variables[variable_name]
effective_variables[variable_name]
end

def overrides?
Expand Down Expand Up @@ -96,24 +96,62 @@ def scss_file
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)
Expand Down Expand Up @@ -143,7 +181,7 @@ def sync_to_s3_and_save_to_account!(progress, account_id)

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
Expand All @@ -163,7 +201,7 @@ def self.destroy_if_unused(md5)
first
if unused_brand_config
unused_brand_config.destroy
unused_brand_config.remove_scss_file!
unused_brand_config.remove_brand_dir!
end
end

Expand Down
66 changes: 63 additions & 3 deletions lib/brandable_css.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# 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
Expand Down Expand Up @@ -85,24 +87,82 @@ def variables_map
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') %>
default = default.sub(/^\/images\//, '') if config['type'] == 'image'
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
Expand Down
4 changes: 4 additions & 0 deletions lib/canvas/cdn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def push_to_s3!(*args, &block)
uploader = Canvas::Cdn::S3Uploader.new(*args)
uploader.upload!(&block)
end

def enabled?
!!config.bucket
end
end
end
end
2 changes: 1 addition & 1 deletion lib/canvas/cdn/s3_uploader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def upload!
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)
Expand Down
5 changes: 3 additions & 2 deletions lib/tasks/brand_configs.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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.
Expand Down
102 changes: 102 additions & 0 deletions spec/lib/brandable_css_spec.rb
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions spec/lib/canvas/cdn_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 2134a9b

Please sign in to comment.