Add ability to customize css and header for mobile

This commit is contained in:
Neil Lalonde 2013-09-16 12:21:49 -04:00
parent c9ebf23561
commit 13f17b2a5c
10 changed files with 189 additions and 72 deletions

View File

@ -7,7 +7,7 @@
@module Discourse @module Discourse
**/ **/
Discourse.SiteCustomization = Discourse.Model.extend({ Discourse.SiteCustomization = Discourse.Model.extend({
trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'override_default_style'], trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'mobile_stylesheet', 'mobile_header', 'override_default_style'],
init: function() { init: function() {
this._super(); this._super();
@ -33,7 +33,7 @@ Discourse.SiteCustomization = Discourse.Model.extend({
return changed; return changed;
}.property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'), }.property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'mobile_stylesheet', 'mobile_header', 'originals'),
startTrackingChanges: function() { startTrackingChanges: function() {
var _this = this; var _this = this;
@ -62,6 +62,8 @@ Discourse.SiteCustomization = Discourse.Model.extend({
enabled: this.enabled, enabled: this.enabled,
stylesheet: this.stylesheet, stylesheet: this.stylesheet,
header: this.header, header: this.header,
mobile_stylesheet: this.mobile_stylesheet,
mobile_header: this.mobile_header,
override_default_style: this.override_default_style override_default_style: this.override_default_style
}; };

View File

@ -12,6 +12,9 @@
{{#if selectedItem}} {{#if selectedItem}}
<div class='current-style'> <div class='current-style'>
{{#with selectedItem}}
{{textField class="style-name" value=name}}
<div class='admin-controls'> <div class='admin-controls'>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li {{bindAttr class="view.stylesheetActive:active"}}> <li {{bindAttr class="view.stylesheetActive:active"}}>
@ -20,17 +23,27 @@
<li {{bindAttr class="view.headerActive:active"}}> <li {{bindAttr class="view.headerActive:active"}}>
<a {{action selectHeader href="true" target="view"}}>{{i18n admin.customize.header}}</a> <a {{action selectHeader href="true" target="view"}}>{{i18n admin.customize.header}}</a>
</li> </li>
<li {{bindAttr class="view.mobileStylesheetActive:active"}}>
<a {{action selectMobileStylesheet href="true" target="view"}}>{{i18n admin.customize.mobile_css}}</a>
</li>
<li {{bindAttr class="view.mobileHeaderActive:active"}}>
<a {{action selectMobileHeader href="true" target="view"}}>{{i18n admin.customize.mobile_header}}</a>
</li>
</ul> </ul>
</div> </div>
{{#with selectedItem}}
{{textField class="style-name" value=name}}
{{#if view.headerActive}} {{#if view.headerActive}}
{{aceEditor content=header mode="html"}} {{aceEditor content=header mode="html"}}
{{/if}} {{/if}}
{{#if view.stylesheetActive}} {{#if view.stylesheetActive}}
{{aceEditor content=stylesheet mode="scss"}} {{aceEditor content=stylesheet mode="scss"}}
{{/if}} {{/if}}
{{#if view.mobileHeaderActive}}
{{aceEditor content=mobile_header mode="html"}}
{{/if}}
{{#if view.mobileStylesheetActive}}
{{aceEditor content=mobile_stylesheet mode="scss"}}
{{/if}}
{{/with}} {{/with}}
<br> <br>
<div class='status-actions'> <div class='status-actions'>

View File

@ -11,20 +11,16 @@
Discourse.AdminCustomizeView = Discourse.View.extend({ Discourse.AdminCustomizeView = Discourse.View.extend({
templateName: 'admin/templates/customize', templateName: 'admin/templates/customize',
classNames: ['customize'], classNames: ['customize'],
headerActive: Ember.computed.equal('selected', 'header'),
stylesheetActive: Ember.computed.equal('selected', 'stylesheet'),
mobileHeaderActive: Ember.computed.equal('selected', 'mobileHeader'),
mobileStylesheetActive: Ember.computed.equal('selected', 'mobileStylesheet'),
init: function() { init: function() {
this._super(); this._super();
this.set('selected', 'stylesheet'); this.set('selected', 'stylesheet');
}, },
headerActive: (function() {
return this.get('selected') === 'header';
}).property('selected'),
stylesheetActive: (function() {
return this.get('selected') === 'stylesheet';
}).property('selected'),
selectHeader: function() { selectHeader: function() {
this.set('selected', 'header'); this.set('selected', 'header');
}, },
@ -33,6 +29,14 @@ Discourse.AdminCustomizeView = Discourse.View.extend({
this.set('selected', 'stylesheet'); this.set('selected', 'stylesheet');
}, },
selectMobileHeader: function() {
this.set('selected', 'mobileHeader');
},
selectMobileStylesheet: function() {
this.set('selected', 'mobileStylesheet');
},
didInsertElement: function() { didInsertElement: function() {
var controller = this.get('controller'); var controller = this.get('controller');
return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() { return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() {

View File

@ -49,7 +49,7 @@ class Admin::SiteCustomizationsController < Admin::AdminController
private private
def site_customization_params def site_customization_params
params.require(:site_customization).permit(:name, :stylesheet, :header, :position, :enabled, :key, :override_default_style, :stylesheet_baked) params.require(:site_customization).permit(:name, :stylesheet, :header, :mobile_stylesheet, :mobile_header, :position, :enabled, :key, :override_default_style, :stylesheet_baked)
end end
def log_site_customization_change(old_record, new_params) def log_site_customization_change(old_record, new_params)

View File

@ -12,31 +12,35 @@ class SiteCustomization < ActiveRecord::Base
end end
before_save do before_save do
if stylesheet_changed? ['stylesheet', 'mobile_stylesheet'].each do |stylesheet_attr|
if self.send("#{stylesheet_attr}_changed?")
begin begin
self.stylesheet_baked = Sass.compile stylesheet self.send("#{stylesheet_attr}_baked=", Sass.compile(self.send(stylesheet_attr)))
rescue Sass::SyntaxError => e rescue Sass::SyntaxError => e
error = e.sass_backtrace_str("custom stylesheet") error = e.sass_backtrace_str("custom stylesheet")
error.gsub!("\n", '\A ') error.gsub!("\n", '\A ')
error.gsub!("'", '\27 ') error.gsub!("'", '\27 ')
self.stylesheet_baked = self.send("#{stylesheet_attr}_baked=",
"#main {display: none;} "#main {display: none;}
footer {white-space: pre; margin-left: 100px;} footer {white-space: pre; margin-left: 100px;}
footer:after{ content: '#{error}' }" footer:after{ content: '#{error}' }")
end
end end
end end
end end
after_save do after_save do
if stylesheet_changed? if stylesheet_changed?
if File.exists?(stylesheet_fullpath) File.delete(stylesheet_fullpath) if File.exists?(stylesheet_fullpath)
File.delete stylesheet_fullpath
end end
if mobile_stylesheet_changed?
File.delete(stylesheet_fullpath(:mobile)) if File.exists?(stylesheet_fullpath(:mobile))
end end
remove_from_cache! remove_from_cache!
if stylesheet_changed? if stylesheet_changed? or mobile_stylesheet_changed?
ensure_stylesheet_on_disk! ensure_stylesheets_on_disk!
# TODO: this is broken now because there's mobile stuff too
MessageBus.publish "/file-change/#{key}", stylesheet_hash MessageBus.publish "/file-change/#{key}", stylesheet_hash
end end
MessageBus.publish "/header-change/#{key}", header if header_changed? MessageBus.publish "/header-change/#{key}", header if header_changed?
@ -47,6 +51,9 @@ footer:after{ content: '#{error}' }"
if File.exists?(stylesheet_fullpath) if File.exists?(stylesheet_fullpath)
File.delete stylesheet_fullpath File.delete stylesheet_fullpath
end end
if File.exists?(stylesheet_fullpath(:mobile))
File.delete stylesheet_fullpath(:mobile)
end
self.remove_from_cache! self.remove_from_cache!
end end
@ -71,17 +78,17 @@ footer:after{ content: '#{error}' }"
end end
end end
def self.custom_stylesheet(preview_style) def self.custom_stylesheet(preview_style, target=:desktop)
preview_style ||= enabled_style_key preview_style ||= enabled_style_key
style = lookup_style(preview_style) style = lookup_style(preview_style)
style.stylesheet_link_tag.html_safe if style style.stylesheet_link_tag(target).html_safe if style
end end
def self.custom_header(preview_style) def self.custom_header(preview_style, target=:desktop)
preview_style ||= enabled_style_key preview_style ||= enabled_style_key
style = lookup_style(preview_style) style = lookup_style(preview_style)
if style && style.header if style && ((target == :mobile && style.mobile_header) || style.header)
style.header.html_safe target == :mobile ? style.mobile_header.html_safe : style.header.html_safe
else else
"" ""
end end
@ -104,7 +111,7 @@ footer:after{ content: '#{error}' }"
@lock.synchronize do @lock.synchronize do
style = where(key: key).first style = where(key: key).first
style.ensure_stylesheet_on_disk! if style style.ensure_stylesheets_on_disk! if style
@cache[key] = style @cache[key] = style
end end
end end
@ -135,39 +142,49 @@ footer:after{ content: '#{error}' }"
self.class.remove_from_cache!(key) self.class.remove_from_cache!(key)
end end
def stylesheet_hash def stylesheet_hash(target=:desktop)
Digest::MD5.hexdigest(stylesheet) Digest::MD5.hexdigest( target == :mobile ? mobile_stylesheet : stylesheet )
end end
def cache_fullpath def cache_fullpath
"#{Rails.root}/public/#{CACHE_PATH}" "#{Rails.root}/public/#{CACHE_PATH}"
end end
def ensure_stylesheet_on_disk! def ensure_stylesheets_on_disk!
path = stylesheet_fullpath [[:desktop, 'stylesheet_baked'], [:mobile, 'mobile_stylesheet_baked']].each do |target, baked_attr|
path = stylesheet_fullpath(target)
dir = cache_fullpath dir = cache_fullpath
FileUtils.mkdir_p(dir) FileUtils.mkdir_p(dir)
unless File.exists?(path) unless File.exists?(path)
File.open(path, "w") do |f| File.open(path, "w") do |f|
f.puts stylesheet_baked f.puts self.send(baked_attr)
end
end end
end end
end end
def stylesheet_filename def stylesheet_filename(target=:desktop)
"/#{self.key}.css" target == :desktop ? "/#{self.key}.css" : "/#{target}_#{self.key}.css"
end end
def stylesheet_fullpath def stylesheet_fullpath(target=:desktop)
"#{cache_fullpath}#{stylesheet_filename}" "#{cache_fullpath}#{stylesheet_filename(target)}"
end end
def stylesheet_link_tag def stylesheet_link_tag(target=:desktop)
return mobile_stylesheet_link_tag if target == :mobile
return "" unless stylesheet.present? return "" unless stylesheet.present?
return @stylesheet_link_tag if @stylesheet_link_tag return @stylesheet_link_tag if @stylesheet_link_tag
ensure_stylesheet_on_disk! ensure_stylesheets_on_disk!
@stylesheet_link_tag = "<link class=\"custom-css\" rel=\"stylesheet\" href=\"/#{CACHE_PATH}#{stylesheet_filename}?#{stylesheet_hash}\" type=\"text/css\" media=\"screen\">" @stylesheet_link_tag = "<link class=\"custom-css\" rel=\"stylesheet\" href=\"/#{CACHE_PATH}#{stylesheet_filename}?#{stylesheet_hash}\" type=\"text/css\" media=\"screen\">"
end end
def mobile_stylesheet_link_tag
return "" unless mobile_stylesheet.present?
return @mobile_stylesheet_link_tag if @mobile_stylesheet_link_tag
ensure_stylesheets_on_disk!
@mobile_stylesheet_link_tag = "<link class=\"custom-css\" rel=\"stylesheet\" href=\"/#{CACHE_PATH}#{stylesheet_filename(:mobile)}?#{stylesheet_hash(:mobile)}\" type=\"text/css\" media=\"screen\">"
end
end end
# == Schema Information # == Schema Information

View File

@ -10,6 +10,4 @@
<%= stylesheet_link_tag "admin"%> <%= stylesheet_link_tag "admin"%>
<%-end%> <%-end%>
<% unless mobile_view? %> <%= SiteCustomization.custom_stylesheet(session[:preview_style], mobile_view? ? :mobile : :desktop) %>
<%= SiteCustomization.custom_stylesheet(session[:preview_style]) %>
<% end %>

View File

@ -30,9 +30,7 @@
<body> <body>
<!--[if IE 9]><script type="text/javascript">ie = "new";</script><![endif]--> <!--[if IE 9]><script type="text/javascript">ie = "new";</script><![endif]-->
<% unless mobile_view? %> <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %>
<%= SiteCustomization.custom_header(session[:preview_style]) %>
<% end %>
<section id='main'> <section id='main'>
</section> </section>

View File

@ -1166,6 +1166,8 @@ en:
long_title: "Site Customizations" long_title: "Site Customizations"
header: "Header" header: "Header"
css: "Stylesheet" css: "Stylesheet"
mobile_header: "Mobile Header"
mobile_css: "Mobile Stylesheet"
override_default: "Do not include standard style sheet" override_default: "Do not include standard style sheet"
enabled: "Enabled?" enabled: "Enabled?"
preview: "preview" preview: "preview"

View File

@ -0,0 +1,7 @@
class AddMobileToSiteCustomizations < ActiveRecord::Migration
def change
add_column :site_customizations, :mobile_stylesheet, :text
add_column :site_customizations, :mobile_header, :text
add_column :site_customizations, :mobile_stylesheet_baked, :text
end
end

View File

@ -6,8 +6,16 @@ describe SiteCustomization do
Fabricate(:user) Fabricate(:user)
end end
let :customization_params do
{name: 'my name', user_id: user.id, header: "my awesome header", stylesheet: "my awesome css", mobile_stylesheet: '', mobile_header: ''}
end
let :customization do let :customization do
SiteCustomization.create!(name: 'my name', user_id: user.id, header: "my awesome header", stylesheet: "my awesome css") SiteCustomization.create!(customization_params)
end
let :customization_with_mobile do
SiteCustomization.create!(customization_params.merge(mobile_stylesheet: ".mobile {better: true;}", mobile_header: "fancy mobile stuff"))
end end
it 'should set default key when creating a new customization' do it 'should set default key when creating a new customization' do
@ -50,17 +58,48 @@ describe SiteCustomization do
c = customization c = customization
c.remove_from_cache! c.remove_from_cache!
File.delete(c.stylesheet_fullpath) File.delete(c.stylesheet_fullpath)
File.delete(c.stylesheet_fullpath(:mobile))
SiteCustomization.custom_stylesheet(c.key) SiteCustomization.custom_stylesheet(c.key)
File.exists?(c.stylesheet_fullpath).should == true File.exists?(c.stylesheet_fullpath).should == true
File.exists?(c.stylesheet_fullpath(:mobile)).should == true
end end
context '#custom_stylesheet' do
it 'should allow me to lookup a filename containing my preview stylesheet' do it 'should allow me to lookup a filename containing my preview stylesheet' do
SiteCustomization.custom_stylesheet(customization.key).should == SiteCustomization.custom_stylesheet(customization.key).should ==
"<link class=\"custom-css\" rel=\"stylesheet\" href=\"/uploads/stylesheet-cache/#{customization.key}.css?#{customization.stylesheet_hash}\" type=\"text/css\" media=\"screen\">" "<link class=\"custom-css\" rel=\"stylesheet\" href=\"/uploads/stylesheet-cache/#{customization.key}.css?#{customization.stylesheet_hash}\" type=\"text/css\" media=\"screen\">"
end end
it "should return blank link tag for mobile if mobile_stylesheet is blank" do
SiteCustomization.custom_stylesheet(customization.key, :mobile).should == ""
end
it "should return link tag for mobile custom stylesheet" do
SiteCustomization.custom_stylesheet(customization_with_mobile.key, :mobile).should ==
"<link class=\"custom-css\" rel=\"stylesheet\" href=\"/uploads/stylesheet-cache/mobile_#{customization_with_mobile.key}.css?#{customization_with_mobile.stylesheet_hash(:mobile)}\" type=\"text/css\" media=\"screen\">"
end
end
context '#custom_header' do
it "returns empty string when there is no custom header" do
c = SiteCustomization.create!(customization_params.merge(header: ''))
SiteCustomization.custom_header(c.key).should == ''
end
it "can return the custom header html" do
SiteCustomization.custom_header(customization.key).should == customization_params[:header]
end
it "returns empty string for mobile header when there's no custom mobile header" do
SiteCustomization.custom_header(customization.key, :mobile).should == ''
end
it "can return the custom mobile header html" do
SiteCustomization.custom_header(customization_with_mobile.key, :mobile).should == customization_with_mobile.mobile_header
end
end
it 'should fix stylesheet files after changing the stylesheet' do it 'should fix stylesheet files after changing the stylesheet' do
old_file = customization.stylesheet_fullpath old_file = customization.stylesheet_fullpath
original = SiteCustomization.custom_stylesheet(customization.key) original = SiteCustomization.custom_stylesheet(customization.key)
@ -72,13 +111,31 @@ describe SiteCustomization do
SiteCustomization.custom_stylesheet(customization.key).should_not == original SiteCustomization.custom_stylesheet(customization.key).should_not == original
end end
it 'should fix mobile stylesheet files after changing the mobile_stylesheet' do
old_file = customization_with_mobile.stylesheet_fullpath(:mobile)
original = SiteCustomization.custom_stylesheet(customization_with_mobile.key, :mobile)
File.exists?(old_file).should == true
customization_with_mobile.mobile_stylesheet = "div { clear:both; }"
customization_with_mobile.save
SiteCustomization.custom_stylesheet(customization_with_mobile.key).should_not == original
end
it 'should delete old stylesheet files after deleting' do it 'should delete old stylesheet files after deleting' do
old_file = customization.stylesheet_fullpath old_file = customization.stylesheet_fullpath
customization.ensure_stylesheet_on_disk! customization.ensure_stylesheets_on_disk!
customization.destroy customization.destroy
File.exists?(old_file).should == false File.exists?(old_file).should == false
end end
it 'should delete old mobile stylesheet files after deleting' do
old_file = customization_with_mobile.stylesheet_fullpath(:mobile)
customization_with_mobile.ensure_stylesheets_on_disk!
customization_with_mobile.destroy
File.exists?(old_file).should == false
end
it 'should nuke old revs out of the cache' do it 'should nuke old revs out of the cache' do
old_style = SiteCustomization.custom_stylesheet(customization.key) old_style = SiteCustomization.custom_stylesheet(customization.key)
@ -87,16 +144,35 @@ describe SiteCustomization do
SiteCustomization.custom_stylesheet(customization.key).should_not == old_style SiteCustomization.custom_stylesheet(customization.key).should_not == old_style
end end
it 'should nuke old revs out of the cache for mobile too' do
old_style = SiteCustomization.custom_stylesheet(customization_with_mobile.key)
customization_with_mobile.mobile_stylesheet = "hello worldz"
customization_with_mobile.save
SiteCustomization.custom_stylesheet(customization.key, :mobile).should_not == old_style
end
it 'should compile scss' do it 'should compile scss' do
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '$black: #000; #a { color: $black; }', header: '') c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '$black: #000; #a { color: $black; }', header: '')
c.stylesheet_baked.should == "#a {\n color: black; }\n" c.stylesheet_baked.should == "#a {\n color: black; }\n"
end end
it 'should compile mobile scss' do
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: '$black: #000; #a { color: $black; }', mobile_header: '')
c.mobile_stylesheet_baked.should == "#a {\n color: black; }\n"
end
it 'should provide an awesome error on failure' do it 'should provide an awesome error on failure' do
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", header: '') c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", header: '')
c.stylesheet_baked.should =~ /Syntax error/ c.stylesheet_baked.should =~ /Syntax error/
c.mobile_stylesheet_baked.should_not be_present
end
it 'should provide an awesome error on failure for mobile too' do
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", mobile_header: '')
c.mobile_stylesheet_baked.should =~ /Syntax error/
c.stylesheet_baked.should_not be_present
end end
end end