Merge pull request #230 from opf/feature/rails3-themes-refactoring
Refactoring of Themes Backendpull/241/merge
commit
456ce7ae47
@ -0,0 +1,112 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'singleton' |
||||
require 'open_project/themes/theme_finder' |
||||
|
||||
module OpenProject |
||||
module Themes |
||||
class Theme |
||||
class SubclassResponsibility < StandardError |
||||
end |
||||
|
||||
class << self |
||||
def inherited(subclass) |
||||
# make all theme classes singletons |
||||
subclass.send :include, Singleton |
||||
|
||||
# register the theme with the ThemeFinder |
||||
ThemeFinder.register_theme(subclass.instance) |
||||
end |
||||
|
||||
def new_theme(identifier = nil) |
||||
theme = Class.new(self).instance |
||||
theme.identifier = identifier if identifier |
||||
theme |
||||
end |
||||
|
||||
def abstract! |
||||
@abstract = true |
||||
|
||||
# tell ThemeFinder to forget the theme |
||||
ThemeFinder.forget_theme(instance) |
||||
|
||||
# undefine methods responsible for creating instances |
||||
singleton_class.send :remove_method, *[:new, :allocate, :instance] |
||||
end |
||||
|
||||
def abstract? |
||||
@abstract |
||||
end |
||||
end |
||||
|
||||
# 'OpenProject::Themes::GoofyTheme' => :'goofy' |
||||
def identifier |
||||
@identifier ||= self.class.to_s.gsub(/Theme$/, '').demodulize.underscore.dasherize.to_sym |
||||
end |
||||
attr_writer :identifier |
||||
|
||||
# 'OpenProject::Themes::GoofyTheme' => 'Goofy' |
||||
def name |
||||
@name ||= self.class.to_s.gsub(/Theme$/, '').demodulize.titleize |
||||
end |
||||
|
||||
def stylesheet_manifest |
||||
"#{identifier}.css" |
||||
end |
||||
|
||||
def assets_prefix |
||||
identifier.to_s |
||||
end |
||||
|
||||
def assets_path |
||||
raise SubclassResponsibility, "override this method to point to your theme's assets folder" |
||||
end |
||||
|
||||
def overridden_images_path |
||||
@overridden_images_path ||= File.join(assets_path, 'images', assets_prefix) |
||||
end |
||||
|
||||
def overridden_images |
||||
@overridden_images ||= \ |
||||
begin |
||||
Dir.chdir(overridden_images_path) { Dir.glob('**/*') } |
||||
rescue Errno::ENOENT # overridden_images_path missing |
||||
[] |
||||
end.to_set |
||||
end |
||||
|
||||
def image_overridden?(source) |
||||
source.in?(overridden_images) |
||||
end |
||||
|
||||
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//} |
||||
|
||||
def path_to_image(source) |
||||
return source if source =~ URI_REGEXP |
||||
return source if source[0] == ?/ |
||||
|
||||
if image_overridden?(source) |
||||
File.join(assets_prefix, source) |
||||
else |
||||
source |
||||
end |
||||
end |
||||
|
||||
include Comparable |
||||
delegate :'<=>', :abstract?, to: :'self.class' |
||||
|
||||
include Singleton |
||||
abstract! |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,62 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module OpenProject |
||||
module Themes |
||||
module ThemeFinder |
||||
class << self |
||||
def themes |
||||
@_themes ||= [] |
||||
end |
||||
alias_method :all, :themes |
||||
|
||||
def registered_themes |
||||
@_registered_themes ||= \ |
||||
themes.each_with_object({}) do |theme, themes| |
||||
themes[theme.identifier] = theme |
||||
end |
||||
end |
||||
delegate :fetch, to: :registered_themes |
||||
|
||||
def register_theme(theme) |
||||
self.themes << theme |
||||
clear_cache |
||||
|
||||
# register the theme's stylesheet manifest with rails' asset pipeline |
||||
# we need to wrap the call to #stylesheet_manifest in a Proc, |
||||
# because when this code is executed the theme instance (theme) hasn't had |
||||
# a chance to override the method yet |
||||
Rails.application.config.assets.precompile << Proc.new { |
||||
theme.stylesheet_manifest unless theme.abstract? |
||||
} |
||||
end |
||||
|
||||
def forget_theme(theme) |
||||
themes.delete(theme) |
||||
clear_cache |
||||
end |
||||
|
||||
def clear_themes |
||||
themes.clear |
||||
clear_cache |
||||
end |
||||
|
||||
def clear_cache |
||||
@_registered_themes = nil |
||||
end |
||||
|
||||
include Enumerable |
||||
delegate :each, to: :themes |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,148 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
require 'singleton' |
||||
require 'active_support/descendants_tracker' |
||||
|
||||
module Redmine |
||||
module Themes |
||||
class Theme |
||||
class SubclassResponsibility < StandardError |
||||
end |
||||
|
||||
class << self |
||||
include ActiveSupport::DescendantsTracker |
||||
|
||||
def inherited(base) |
||||
super # call to ActiveSupport::DescendantsTracker |
||||
base.send :include, Singleton # make all theme classes singletons |
||||
clear_cache # clear the themes cache |
||||
|
||||
# register the theme's stylesheet manifest with rails' asset pipeline |
||||
# we need to wrap the call to #stylesheet_manifest in a Proc, |
||||
# because when this code is executed the theme class (base) hasn't had |
||||
# a chance to override the method yet |
||||
Rails.application.config.assets.precompile << Proc.new { |
||||
base.instance.stylesheet_manifest unless base.abstract? |
||||
} |
||||
end |
||||
|
||||
def new_theme(identifier = nil) |
||||
theme = Class.new(self).instance |
||||
theme.identifier = identifier |
||||
theme |
||||
end |
||||
|
||||
def themes |
||||
@_themes ||= (descendants - abstract_themes).map(&:instance) |
||||
end |
||||
alias_method :all, :themes |
||||
|
||||
def registered_themes |
||||
@_registered_themes ||= \ |
||||
themes.each_with_object(Hash.new) do |theme, themes| |
||||
themes[theme.identifier] = theme |
||||
end |
||||
end |
||||
delegate :fetch, to: :registered_themes |
||||
|
||||
def clear |
||||
direct_descendants.clear && clear_cache |
||||
end |
||||
|
||||
def clear_cache |
||||
@_themes = @_registered_themes = nil |
||||
end |
||||
|
||||
def abstract! |
||||
Theme.abstract_themes << self |
||||
|
||||
# undefine methods responsible for creating instances |
||||
singleton_class.send :remove_method, *[:new, :allocate, :instance] |
||||
end |
||||
|
||||
def abstract? |
||||
self.in?(Theme.abstract_themes) |
||||
end |
||||
|
||||
def abstract_themes |
||||
@_abstract_themes ||= Array.new |
||||
end |
||||
|
||||
include Enumerable |
||||
delegate :each, to: :themes |
||||
end |
||||
|
||||
# "Redmine::Themes::AwesomeTheme".demodulize.underscore.dasherize.to_sym => :"awesome-theme" |
||||
def identifier |
||||
@identifier ||= self.class.to_s.demodulize.underscore.dasherize.to_sym |
||||
end |
||||
attr_writer :identifier |
||||
|
||||
# "Redmine::Themes::AwesomeTheme".demodulize.titleize => "Awesome Theme" |
||||
def name |
||||
@name ||= self.class.to_s.demodulize.titleize |
||||
end |
||||
|
||||
def stylesheet_manifest |
||||
"#{identifier}.css" |
||||
end |
||||
|
||||
def assets_prefix |
||||
identifier.to_s |
||||
end |
||||
|
||||
def assets_path |
||||
raise SubclassResponsibility, "override this method to point to your theme's assets folder" |
||||
end |
||||
|
||||
def overridden_images_path |
||||
@overridden_images_path ||= File.join(assets_path, 'images', assets_prefix) |
||||
end |
||||
|
||||
def overridden_images |
||||
@overridden_images ||= \ |
||||
begin |
||||
Dir.chdir(overridden_images_path) { Dir.glob('**/*') } |
||||
rescue Errno::ENOENT # overridden_images_path missing |
||||
[] |
||||
end.to_set |
||||
end |
||||
|
||||
def image_overridden?(source) |
||||
source.in?(overridden_images) |
||||
end |
||||
|
||||
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//} |
||||
|
||||
def path_to_image(source) |
||||
return source if source =~ URI_REGEXP |
||||
return source if source[0] == ?/ |
||||
|
||||
if image_overridden?(source) |
||||
File.join(assets_prefix, source) |
||||
else |
||||
source |
||||
end |
||||
end |
||||
|
||||
def default? |
||||
false |
||||
end |
||||
|
||||
include Comparable |
||||
delegate :'<=>', to: :'self.class' |
||||
|
||||
include Singleton |
||||
abstract! |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,137 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
module OpenProject |
||||
module Themes |
||||
describe ThemeFinder do |
||||
before { ThemeFinder.clear_themes } |
||||
|
||||
describe '.themes' do |
||||
it "returns all instances of descendants of themes" do |
||||
theme = Theme.new_theme |
||||
expect(ThemeFinder.themes).to include theme |
||||
end |
||||
|
||||
# the before filter above removes the default theme as well. to test |
||||
# the correct behaviour we just spec that the default theme class |
||||
# was loaded (by looking through all subclasses of BasicObject) |
||||
it "always includes the default theme" do |
||||
loaded_classes = Object.descendants |
||||
expect(loaded_classes).to include Themes::DefaultTheme |
||||
end |
||||
|
||||
# test through the theme instances classes because |
||||
# an abstract theme can't have an instance |
||||
it "filters out themes marked as abstract" do |
||||
theme_class = Class.new(Theme) { abstract! } |
||||
theme_classes = ThemeFinder.themes.map(&:class) |
||||
expect(theme_classes).to_not include theme_class |
||||
end |
||||
|
||||
it "subclasses of abstract themes aren't abstract by default" do |
||||
abstract_theme_class = Class.new(Theme) { abstract! } |
||||
theme = Class.new(abstract_theme_class).instance |
||||
expect(ThemeFinder.themes).to include theme |
||||
end |
||||
end |
||||
|
||||
describe '.registered_themes' do |
||||
it "returns a hash of themes with their identifiers as keys" do |
||||
theme = Theme.new_theme(:new_theme) |
||||
expect(ThemeFinder.registered_themes).to include :new_theme => theme |
||||
end |
||||
end |
||||
|
||||
describe '.register_theme' do |
||||
it "remembers whatever is passed in (this is called by #inherited hook)" do |
||||
theme = stub # do not invoke inherited callback |
||||
ThemeFinder.register_theme(theme) |
||||
expect(ThemeFinder.themes).to include theme |
||||
end |
||||
|
||||
# TODO: clean me up |
||||
it "registers the theme's stylesheet manifest for precompilation" do |
||||
Class.new(Theme) { def stylesheet_manifest; 'stylesheet_path.css'; end } |
||||
|
||||
# TODO: gives an error on the whole list |
||||
# TODO: remove themes from the list, when clear_themes is called |
||||
precompile_list = Rails.application.config.assets.precompile |
||||
precompile_list = Array(precompile_list.last) |
||||
precompile_list.map! { |element| element.respond_to?(:call) ? element.call : element } |
||||
|
||||
expect(precompile_list).to include 'stylesheet_path.css' |
||||
end |
||||
|
||||
it "clears the cache successfully" do |
||||
ThemeFinder.registered_themes # fill the cache |
||||
theme = Theme.new_theme(:new_theme) |
||||
expect(ThemeFinder.registered_themes).to include :new_theme => theme |
||||
end |
||||
end |
||||
|
||||
describe '.forget_theme' do |
||||
it "removes the theme from the themes list" do |
||||
theme = Theme.new_theme(:new_theme) |
||||
ThemeFinder.forget_theme(theme) |
||||
expect(ThemeFinder.themes).to_not include theme |
||||
end |
||||
end |
||||
|
||||
describe '.clear_cache' do |
||||
it "removes the theme from the registered themes list and clears the cache" do |
||||
theme = Theme.new_theme(:new_theme) |
||||
ThemeFinder.registered_themes # fill the cache |
||||
ThemeFinder.forget_theme(theme) |
||||
expect(ThemeFinder.registered_themes).to_not include :new_theme => theme |
||||
end |
||||
end |
||||
|
||||
describe '.abstract!' do |
||||
it "abstract themes won't show up in the themes llist" do |
||||
abstract_theme_class = Class.new(Theme) { abstract! } |
||||
theme_classes = ThemeFinder.themes.map(&:class) |
||||
expect(theme_classes).to_not include abstract_theme_class |
||||
end |
||||
|
||||
it "the basic theme class is abstract" do |
||||
theme_classes = ThemeFinder.themes.map(&:class) |
||||
expect(theme_classes).to_not include Theme |
||||
end |
||||
end |
||||
|
||||
describe '.clear_themes' do |
||||
it "it wipes out all registered themes" do |
||||
theme_class = Class.new(Theme) |
||||
ThemeFinder.clear_themes |
||||
expect(ThemeFinder.themes).to be_empty |
||||
end |
||||
|
||||
it "clears the registered themes cache" do |
||||
theme = Theme.new_theme(:new_theme) |
||||
ThemeFinder.registered_themes # fill the cache |
||||
ThemeFinder.clear_themes |
||||
expect(ThemeFinder.registered_themes).to_not include :new_theme => theme |
||||
end |
||||
end |
||||
|
||||
describe '.each' do |
||||
it "iterates over all themes" do |
||||
Theme.new_theme(:new_theme) |
||||
themes = [] |
||||
ThemeFinder.each { |theme| themes << theme.identifier } |
||||
expect(themes).to eq [:new_theme] |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue