Commit 79e1ad7bc95de7c8b5c312b57d904181b45f7b6f

Authored by Rodrigo Souto
2 parents b1d5a85f 181fc268

Merge branch 'master' into stoa

Conflicts:
	app/controllers/application_controller.rb
	app/controllers/my_profile/memberships_controller.rb
	app/controllers/public/account_controller.rb
	app/helpers/application_helper.rb
	app/helpers/layout_helper.rb
	app/helpers/token_helper.rb
	app/views/account/signup.html.erb
	app/views/layouts/_javascript.html.erb
	db/schema.rb
	test/functional/application_controller_test.rb
Showing 61 changed files with 3943 additions and 303 deletions   Show diff stats
app/controllers/admin/categories_controller.rb
@@ -45,9 +45,11 @@ class CategoriesController < AdminController @@ -45,9 +45,11 @@ class CategoriesController < AdminController
45 if request.post? 45 if request.post?
46 @category.update_attributes!(params[:category]) 46 @category.update_attributes!(params[:category])
47 @saved = true 47 @saved = true
  48 + session[:notice] = _("Category %s saved." % @category.name)
48 redirect_to :action => 'index' 49 redirect_to :action => 'index'
49 end 50 end
50 rescue Exception => e 51 rescue Exception => e
  52 + session[:notice] = _('Could not save category.')
51 render :action => 'edit' 53 render :action => 'edit'
52 end 54 end
53 end 55 end
app/controllers/admin/environment_design_controller.rb
@@ -3,9 +3,7 @@ class EnvironmentDesignController < BoxOrganizerController @@ -3,9 +3,7 @@ class EnvironmentDesignController < BoxOrganizerController
3 protect 'edit_environment_design', :environment 3 protect 'edit_environment_design', :environment
4 4
5 def available_blocks 5 def available_blocks
6 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
7 - # the Noosfero core soon, see ActionItem3045  
8 - @available_blocks ||= [ ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ] 6 + @available_blocks ||= [ ArticleBlock, LoginBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
9 @available_blocks += plugins.dispatch(:extra_blocks, :type => Environment) 7 @available_blocks += plugins.dispatch(:extra_blocks, :type => Environment)
10 end 8 end
11 9
app/controllers/admin/features_controller.rb
@@ -51,4 +51,10 @@ class FeaturesController < AdminController @@ -51,4 +51,10 @@ class FeaturesController < AdminController
51 redirect_to :action => 'manage_fields' 51 redirect_to :action => 'manage_fields'
52 end 52 end
53 53
  54 + def search_members
  55 + arg = params[:q].downcase
  56 + result = environment.people.find(:all, :conditions => ['LOWER(name) LIKE ? OR identifier LIKE ?', "%#{arg}%", "%#{arg}%"])
  57 + render :text => prepare_to_token_input(result).to_json
  58 + end
  59 +
54 end 60 end
app/controllers/application_controller.rb
@@ -7,6 +7,12 @@ class ApplicationController < ActionController::Base @@ -7,6 +7,12 @@ class ApplicationController < ActionController::Base
7 before_filter :detect_stuff_by_domain 7 before_filter :detect_stuff_by_domain
8 before_filter :init_noosfero_plugins 8 before_filter :init_noosfero_plugins
9 before_filter :allow_cross_domain_access 9 before_filter :allow_cross_domain_access
  10 + before_filter :login_required, :if => :private_environment?
  11 + before_filter :verify_members_whitelist, :if => [:private_environment?, :user]
  12 +
  13 + def verify_members_whitelist
  14 + render_access_denied unless user.is_admin? || environment.in_whitelist?(user)
  15 + end
10 16
11 after_filter :set_csrf_cookie 17 after_filter :set_csrf_cookie
12 18
@@ -185,4 +191,8 @@ class ApplicationController < ActionController::Base @@ -185,4 +191,8 @@ class ApplicationController < ActionController::Base
185 def find_suggestions(query, context, asset, options={}) 191 def find_suggestions(query, context, asset, options={})
186 plugins.dispatch_first(:find_suggestions, query, context, asset, options) 192 plugins.dispatch_first(:find_suggestions, query, context, asset, options)
187 end 193 end
  194 +
  195 + def private_environment?
  196 + @environment.enabled?(:restrict_to_members)
  197 + end
188 end 198 end
app/controllers/my_profile/cms_controller.rb
@@ -4,6 +4,12 @@ class CmsController < MyProfileController @@ -4,6 +4,12 @@ class CmsController < MyProfileController
4 4
5 include ArticleHelper 5 include ArticleHelper
6 6
  7 + def search_tags
  8 + arg = params[:term].downcase
  9 + result = ActsAsTaggableOn::Tag.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"])
  10 + render :text => prepare_to_token_input_by_label(result).to_json, :content_type => 'application/json'
  11 + end
  12 +
7 def self.protect_if(*args) 13 def self.protect_if(*args)
8 before_filter(*args) do |c| 14 before_filter(*args) do |c|
9 user, profile = c.send(:user), c.send(:profile) 15 user, profile = c.send(:user), c.send(:profile)
app/controllers/my_profile/memberships_controller.rb
@@ -27,7 +27,7 @@ class MembershipsController < MyProfileController @@ -27,7 +27,7 @@ class MembershipsController < MyProfileController
27 redirect_to :action => 'welcome', :community_id => @community.id, :back_to => @back_to 27 redirect_to :action => 'welcome', :community_id => @community.id, :back_to => @back_to
28 rescue ActiveRecord::RecordNotFound 28 rescue ActiveRecord::RecordNotFound
29 # Community pending approval 29 # Community pending approval
30 - session[:notice] = _('Your community creation request is waiting approval of the administrator.') 30 + session[:notice] = _('Your new community creation request will be evaluated by an administrator. You will be notified.')
31 redirect_to @back_to 31 redirect_to @back_to
32 end 32 end
33 return 33 return
app/controllers/my_profile/tasks_controller.rb
@@ -4,6 +4,7 @@ class TasksController < MyProfileController @@ -4,6 +4,7 @@ class TasksController < MyProfileController
4 4
5 def index 5 def index
6 @filter = params[:filter_type].blank? ? nil : params[:filter_type] 6 @filter = params[:filter_type].blank? ? nil : params[:filter_type]
  7 + @task_types = Task.pending_types_for(profile)
7 @tasks = Task.to(profile).without_spam.pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page]) 8 @tasks = Task.to(profile).without_spam.pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page])
8 @failed = params ? params[:failed] : {} 9 @failed = params ? params[:failed] : {}
9 end 10 end
app/controllers/public/account_controller.rb
@@ -15,11 +15,23 @@ class AccountController < ApplicationController @@ -15,11 +15,23 @@ class AccountController < ApplicationController
15 15
16 def activate 16 def activate
17 @user = User.find_by_activation_code(params[:activation_code]) if params[:activation_code] 17 @user = User.find_by_activation_code(params[:activation_code]) if params[:activation_code]
18 - if @user and @user.activate  
19 - @message = _("Your account has been activated, now you can log in!")  
20 - check_redirection  
21 - session[:join] = params[:join] unless params[:join].blank?  
22 - render :action => 'login', :userlogin => @user.login 18 + if @user
  19 + unless @user.environment.enabled?('admin_must_approve_new_users')
  20 + if @user.activate
  21 + @message = _("Your account has been activated, now you can log in!")
  22 + check_redirection
  23 + session[:join] = params[:join] unless params[:join].blank?
  24 + render :action => 'login', :userlogin => @user.login
  25 + end
  26 + else
  27 + if @user.create_moderate_task
  28 + session[:notice] = _('Thanks for registering. The administrators were notified.')
  29 + @register_pending = true
  30 + @user.activation_code = nil
  31 + @user.save!
  32 + redirect_to :controller => :home
  33 + end
  34 + end
23 else 35 else
24 session[:notice] = _("It looks like you're trying to activate an account. Perhaps have already activated this account?") 36 session[:notice] = _("It looks like you're trying to activate an account. Perhaps have already activated this account?")
25 redirect_to :controller => :home 37 redirect_to :controller => :home
@@ -111,6 +123,7 @@ class AccountController < ApplicationController @@ -111,6 +123,7 @@ class AccountController < ApplicationController
111 go_to_signup_initial_page 123 go_to_signup_initial_page
112 else 124 else
113 redirect_to :controller => :home, :action => :welcome, :template_id => @user.person.template.id 125 redirect_to :controller => :home, :action => :welcome, :template_id => @user.person.template.id
  126 + session[:notice] = _('Thanks for registering!')
114 end 127 end
115 end 128 end
116 end 129 end
app/helpers/application_helper.rb
@@ -1438,4 +1438,15 @@ module ApplicationHelper @@ -1438,4 +1438,15 @@ module ApplicationHelper
1438 '' 1438 ''
1439 end 1439 end
1440 end 1440 end
  1441 +
  1442 + def labelled_colorpicker_field(human_name, object_name, method, options = {})
  1443 + options[:id] ||= 'text-field-' + FormsHelper.next_id_number
  1444 + content_tag('label', human_name, :for => options[:id], :class => 'formlabel') +
  1445 + colorpicker_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1446 + end
  1447 +
  1448 + def colorpicker_field(object_name, method, options = {})
  1449 + text_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1450 + end
  1451 +
1441 end 1452 end
app/helpers/article_helper.rb
@@ -83,6 +83,10 @@ module ArticleHelper @@ -83,6 +83,10 @@ module ArticleHelper
83 array.map { |object| {:id => object.id, :name => object.name} } 83 array.map { |object| {:id => object.id, :name => object.name} }
84 end 84 end
85 85
  86 + def prepare_to_token_input_by_label(array)
  87 + array.map { |object| {:label => object.name, :value => object.name} }
  88 + end
  89 +
86 def cms_label_for_new_children 90 def cms_label_for_new_children
87 _('New article') 91 _('New article')
88 end 92 end
app/helpers/categories_helper.rb
1 module CategoriesHelper 1 module CategoriesHelper
2 2
3 -  
4 - COLORS = [  
5 - [ N_('Do not display at the menu'), nil ],  
6 - [ N_('Orange'), 1],  
7 - [ N_('Green'), 2],  
8 - [ N_('Purple'), 3],  
9 - [ N_('Red'), 4],  
10 - [ N_('Dark Green'), 5],  
11 - [ N_('Blue Oil'), 6],  
12 - [ N_('Blue'), 7],  
13 - [ N_('Brown'), 8],  
14 - [ N_('Light Green'), 9],  
15 - [ N_('Light Blue'), 10],  
16 - [ N_('Dark Blue'), 11],  
17 - [ N_('Blue Pool'), 12],  
18 - [ N_('Beige'), 13],  
19 - [ N_('Yellow'), 14],  
20 - [ N_('Light Brown'), 15]  
21 - ]  
22 -  
23 TYPES = [ 3 TYPES = [
24 [ _('General Category'), Category.to_s ], 4 [ _('General Category'), Category.to_s ],
25 [ _('Product Category'), ProductCategory.to_s ], 5 [ _('Product Category'), ProductCategory.to_s ],
26 [ _('Region'), Region.to_s ], 6 [ _('Region'), Region.to_s ],
27 ] 7 ]
28 8
29 - def select_color_for_category  
30 - if @category.top_level?  
31 - labelled_form_field(_('Display at the menu?'), select('category', 'display_color', CategoriesHelper::COLORS.map {|item| [gettext(item[0]), item[1]] }))  
32 - else  
33 - ""  
34 - end  
35 - end  
36 -  
37 - def display_color_for_category(category)  
38 - color = category.display_color  
39 - if color.nil?  
40 - ""  
41 - else  
42 - "[" + gettext(CategoriesHelper::COLORS.find {|item| item[1] == color}.first) + "]"  
43 - end  
44 - end  
45 -  
46 def select_category_type(field) 9 def select_category_type(field)
47 value = params[field] 10 value = params[field]
48 labelled_form_field(_('Type of category'), select_tag('type', options_for_select(TYPES, value))) 11 labelled_form_field(_('Type of category'), select_tag('type', options_for_select(TYPES, value)))
49 end 12 end
50 13
  14 + def category_color_style(category)
  15 + return '' if category.nil? or category.display_color.blank?
  16 + 'background-color: #'+category.display_color+';'
  17 + end
  18 +
51 #FIXME make this test 19 #FIXME make this test
52 def selected_category_link(cat) 20 def selected_category_link(cat)
53 js_remove = "jQuery('#selected-category-#{cat.id}').remove();" 21 js_remove = "jQuery('#selected-category-#{cat.id}').remove();"
app/helpers/layout_helper.rb
@@ -28,6 +28,7 @@ module LayoutHelper @@ -28,6 +28,7 @@ module LayoutHelper
28 'lightbox', 28 'lightbox',
29 'colorbox', 29 'colorbox',
30 'selectordie', 30 'selectordie',
  31 + 'inputosaurus',
31 pngfix_stylesheet_path, 32 pngfix_stylesheet_path,
32 ] + tokeninput_stylesheets 33 ] + tokeninput_stylesheets
33 plugins_stylesheets = @plugins.select(&:stylesheet?).map { |plugin| plugin.class.public_path('style.css') } 34 plugins_stylesheets = @plugins.select(&:stylesheet?).map { |plugin| plugin.class.public_path('style.css') }
app/helpers/token_helper.rb
@@ -19,6 +19,7 @@ module TokenHelper @@ -19,6 +19,7 @@ module TokenHelper
19 options[:on_add] ||= 'null' 19 options[:on_add] ||= 'null'
20 options[:on_delete] ||= 'null' 20 options[:on_delete] ||= 'null'
21 options[:on_ready] ||= 'null' 21 options[:on_ready] ||= 'null'
  22 + options[:query_param] ||= 'q'
22 23
23 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id}))) 24 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
24 result += javascript_tag("jQuery('##{element_id}') 25 result += javascript_tag("jQuery('##{element_id}')
@@ -32,7 +33,7 @@ module TokenHelper @@ -32,7 +33,7 @@ module TokenHelper
32 preventDuplicates: #{options[:prevent_duplicates].to_json}, 33 preventDuplicates: #{options[:prevent_duplicates].to_json},
33 backspaceDeleteItem: #{options[:backspace_delete_item].to_json}, 34 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
34 zindex: #{options[:zindex].to_json}, 35 zindex: #{options[:zindex].to_json},
35 - queryParam: #{name.to_json}, 36 + queryParam: #{options[:query_param].to_json},
36 tokenLimit: #{options[:token_limit].to_json}, 37 tokenLimit: #{options[:token_limit].to_json},
37 onResult: #{options[:on_result]}, 38 onResult: #{options[:on_result]},
38 onAdd: #{options[:on_add]}, 39 onAdd: #{options[:on_add]},
app/models/box.rb
@@ -28,9 +28,6 @@ class Box < ActiveRecord::Base @@ -28,9 +28,6 @@ class Box < ActiveRecord::Base
28 CategoriesBlock, 28 CategoriesBlock,
29 CommunitiesBlock, 29 CommunitiesBlock,
30 EnterprisesBlock, 30 EnterprisesBlock,
31 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
32 - # the Noosfero core soon, see ActionItem3045  
33 - EnvironmentStatisticsBlock,  
34 FansBlock, 31 FansBlock,
35 FavoriteEnterprisesBlock, 32 FavoriteEnterprisesBlock,
36 FeedReaderBlock, 33 FeedReaderBlock,
@@ -53,9 +50,6 @@ class Box < ActiveRecord::Base @@ -53,9 +50,6 @@ class Box < ActiveRecord::Base
53 CommunitiesBlock, 50 CommunitiesBlock,
54 DisabledEnterpriseMessageBlock, 51 DisabledEnterpriseMessageBlock,
55 EnterprisesBlock, 52 EnterprisesBlock,
56 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
57 - # the Noosfero core soon, see ActionItem3045  
58 - EnvironmentStatisticsBlock,  
59 FansBlock, 53 FansBlock,
60 FavoriteEnterprisesBlock, 54 FavoriteEnterprisesBlock,
61 FeaturedProductsBlock, 55 FeaturedProductsBlock,
app/models/category.rb
@@ -14,9 +14,6 @@ class Category < ActiveRecord::Base @@ -14,9 +14,6 @@ class Category < ActiveRecord::Base
14 validates_uniqueness_of :slug,:scope => [ :environment_id, :parent_id ], :message => N_('{fn} is already being used by another category.').fix_i18n 14 validates_uniqueness_of :slug,:scope => [ :environment_id, :parent_id ], :message => N_('{fn} is already being used by another category.').fix_i18n
15 belongs_to :environment 15 belongs_to :environment
16 16
17 - validates_inclusion_of :display_color, :in => 1..15, :allow_nil => true  
18 - validates_uniqueness_of :display_color, :scope => :environment_id, :if => (lambda { |cat| ! cat.display_color.nil? }), :message => N_('{fn} was already assigned to another category.').fix_i18n  
19 -  
20 # Finds all top level categories for a given environment. 17 # Finds all top level categories for a given environment.
21 scope :top_level_for, lambda { |environment| 18 scope :top_level_for, lambda { |environment|
22 {:conditions => ['parent_id is null and environment_id = ?', environment.id ]} 19 {:conditions => ['parent_id is null and environment_id = ?', environment.id ]}
@@ -42,6 +39,13 @@ class Category < ActiveRecord::Base @@ -42,6 +39,13 @@ class Category < ActiveRecord::Base
42 39
43 acts_as_having_image 40 acts_as_having_image
44 41
  42 + before_save :normalize_display_color
  43 +
  44 + def normalize_display_color
  45 + display_color.gsub!('#', '') if display_color
  46 + display_color = nil if display_color.blank?
  47 + end
  48 +
45 scope :from_types, lambda { |types| 49 scope :from_types, lambda { |types|
46 types.select{ |t| t.blank? }.empty? ? 50 types.select{ |t| t.blank? }.empty? ?
47 { :conditions => { :type => types } } : 51 { :conditions => { :type => types } } :
@@ -101,4 +105,12 @@ class Category < ActiveRecord::Base @@ -101,4 +105,12 @@ class Category < ActiveRecord::Base
101 self.children.find(:all, :conditions => {:display_in_menu => true}).empty? 105 self.children.find(:all, :conditions => {:display_in_menu => true}).empty?
102 end 106 end
103 107
  108 + def with_color
  109 + if display_color.blank?
  110 + parent.nil? ? nil : parent.with_color
  111 + else
  112 + self
  113 + end
  114 + end
  115 +
104 end 116 end
app/models/environment.rb
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 # domains. 3 # domains.
4 class Environment < ActiveRecord::Base 4 class Environment < ActiveRecord::Base
5 5
6 - attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body 6 + attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body, :members_whitelist_enabled, :members_whitelist
7 7
8 has_many :users 8 has_many :users
9 9
@@ -125,6 +125,7 @@ class Environment &lt; ActiveRecord::Base @@ -125,6 +125,7 @@ class Environment &lt; ActiveRecord::Base
125 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"), 125 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"),
126 'enable_organization_url_change' => _("Allow organizations to change their URL"), 126 'enable_organization_url_change' => _("Allow organizations to change their URL"),
127 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"), 127 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"),
  128 + 'admin_must_approve_new_users' => _("Admin must approve registration of new users"),
128 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'), 129 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'),
129 'xmpp_chat' => _('XMPP/Jabber based chat'), 130 'xmpp_chat' => _('XMPP/Jabber based chat'),
130 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'), 131 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'),
@@ -133,7 +134,8 @@ class Environment &lt; ActiveRecord::Base @@ -133,7 +134,8 @@ class Environment &lt; ActiveRecord::Base
133 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'), 134 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'),
134 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'), 135 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'),
135 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'), 136 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'),
136 - 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage') 137 + 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage'),
  138 + 'restrict_to_members' => _('Show content only to members')
137 } 139 }
138 end 140 end
139 141
@@ -177,9 +179,6 @@ class Environment &lt; ActiveRecord::Base @@ -177,9 +179,6 @@ class Environment &lt; ActiveRecord::Base
177 179
178 # "left" area 180 # "left" area
179 env.boxes[1].blocks << LoginBlock.new 181 env.boxes[1].blocks << LoginBlock.new
180 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
181 - # the Noosfero core soon, see ActionItem3045  
182 - env.boxes[1].blocks << EnvironmentStatisticsBlock.new  
183 env.boxes[1].blocks << RecentDocumentsBlock.new 182 env.boxes[1].blocks << RecentDocumentsBlock.new
184 183
185 # "right" area 184 # "right" area
@@ -305,6 +304,17 @@ class Environment &lt; ActiveRecord::Base @@ -305,6 +304,17 @@ class Environment &lt; ActiveRecord::Base
305 settings[:signup_welcome_screen_body].present? 304 settings[:signup_welcome_screen_body].present?
306 end 305 end
307 306
  307 + settings_items :members_whitelist_enabled, :type => :boolean, :default => false
  308 + settings_items :members_whitelist, :type => Array, :default => []
  309 +
  310 + def in_whitelist?(person)
  311 + !members_whitelist_enabled || members_whitelist.include?(person.id)
  312 + end
  313 +
  314 + def members_whitelist=(members)
  315 + settings[:members_whitelist] = members.split(',').map(&:to_i)
  316 + end
  317 +
308 def news_amount_by_folder=(amount) 318 def news_amount_by_folder=(amount)
309 settings[:news_amount_by_folder] = amount.to_i 319 settings[:news_amount_by_folder] = amount.to_i
310 end 320 end
app/models/environment_statistics_block.rb
@@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
1 -# TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
2 -# the Noosfero core soon, see ActionItem3045  
3 -  
4 -class EnvironmentStatisticsBlock < Block  
5 -  
6 - def self.description  
7 - _('Environment stastistics (DEPRECATED)')  
8 - end  
9 -  
10 - def default_title  
11 - _('Statistics for %s') % owner.name  
12 - end  
13 -  
14 - def help  
15 - _('This block presents some statistics about your environment.')  
16 - end  
17 -  
18 - def content(args={})  
19 - users = owner.people.visible.count  
20 - enterprises = owner.enterprises.visible.count  
21 - communities = owner.communities.visible.count  
22 -  
23 - info = []  
24 - info << (n_('One user', '%{num} users', users) % { :num => users })  
25 - unless owner.enabled?('disable_asset_enterprises')  
26 - info << (n_('One enterprise', '%{num} enterprises', enterprises) % { :num => enterprises })  
27 - end  
28 - info << (n_('One community', '%{num} communities', communities) % { :num => communities })  
29 -  
30 - block_title(title) + content_tag('ul', info.map {|item| content_tag('li', item) }.join("\n"))  
31 - end  
32 -  
33 -end  
app/models/moderate_user_registration.rb 0 → 100644
@@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
  1 +class ModerateUserRegistration < Task
  2 +
  3 + settings_items :user_id, :type => String
  4 + settings_items :name, :type => String
  5 + settings_items :author_name, :type => String
  6 + settings_items :email, :type => String
  7 +
  8 + after_create :schedule_spam_checking
  9 +
  10 + alias :environment :target
  11 + alias :environment= :target=
  12 +
  13 + def schedule_spam_checking
  14 + self.delay.check_for_spam
  15 + end
  16 +
  17 + include Noosfero::Plugin::HotSpot
  18 +
  19 + def sender
  20 + "#{name} (#{email})"
  21 + end
  22 +
  23 + def perform
  24 + user=environment.users.find_by_id(user_id)
  25 + user.activate
  26 + end
  27 +
  28 + def title
  29 + _("New user")
  30 + end
  31 +
  32 + def subject
  33 + name
  34 + end
  35 +
  36 + def information
  37 + { :message => _('%{sender} wants to register.'),
  38 + :variables => {:sender => sender} }
  39 + end
  40 +
  41 + def icon
  42 + result = {:type => :defined_image, :src => '/images/icons-app/person-minor.png', :name => name}
  43 + end
  44 +
  45 + def target_notification_description
  46 + _('%{sender} tried to register.') %
  47 + {:sender => sender}
  48 + end
  49 +
  50 + def target_notification_message
  51 + target_notification_description + "\n\n" +
  52 + _('You need to login on %{system} in order to approve or reject this user.') % { :environment => self.environment }
  53 + end
  54 +
  55 + def target_notification_message
  56 + _("User \"%{user}\" just requested to register. You have to approve or reject it through the \"Pending Validations\" section in your control panel.\n") % { :user => self.name }
  57 + end
  58 +
  59 +end
0 \ No newline at end of file 60 \ No newline at end of file
app/models/task.rb
@@ -73,10 +73,6 @@ class Task &lt; ActiveRecord::Base @@ -73,10 +73,6 @@ class Task &lt; ActiveRecord::Base
73 end 73 end
74 end 74 end
75 75
76 - def self.all_types  
77 - %w[Invitation EnterpriseActivation AddMember Ticket SuggestArticle AddFriend CreateCommunity AbuseComplaint ApproveComment ApproveArticle CreateEnterprise ChangePassword EmailActivation InviteFriend InviteMember]  
78 - end  
79 -  
80 # this method finished the task. It calls #perform, which must be overriden 76 # this method finished the task. It calls #perform, which must be overriden
81 # by subclasses. At the end a message (as returned by #finish_message) is 77 # by subclasses. At the end a message (as returned by #finish_message) is
82 # sent to the requestor with #notify_requestor. 78 # sent to the requestor with #notify_requestor.
@@ -254,6 +250,10 @@ class Task &lt; ActiveRecord::Base @@ -254,6 +250,10 @@ class Task &lt; ActiveRecord::Base
254 { :conditions => [environment_condition, profile_condition].compact.join(' OR ') } 250 { :conditions => [environment_condition, profile_condition].compact.join(' OR ') }
255 } 251 }
256 252
  253 + def self.pending_types_for(profile)
  254 + Task.to(profile).pending.select('distinct type').map { |t| [t.class.name, t.title] }
  255 + end
  256 +
257 def opened? 257 def opened?
258 status == Task::Status::ACTIVE || status == Task::Status::HIDDEN 258 status == Task::Status::ACTIVE || status == Task::Status::HIDDEN
259 end 259 end
app/models/user.rb
@@ -51,8 +51,12 @@ class User &lt; ActiveRecord::Base @@ -51,8 +51,12 @@ class User &lt; ActiveRecord::Base
51 51
52 user.person = p 52 user.person = p
53 end 53 end
54 - if user.environment.enabled?('skip_new_user_email_confirmation')  
55 - user.activate 54 + if user.environment.enabled?('skip_new_user_email_confirmation')
  55 + if user.environment.enabled?('admin_must_approve_new_users')
  56 + create_moderate_task
  57 + else
  58 + user.activate
  59 + end
56 end 60 end
57 end 61 end
58 after_create :deliver_activation_code 62 after_create :deliver_activation_code
@@ -141,6 +145,15 @@ class User &lt; ActiveRecord::Base @@ -141,6 +145,15 @@ class User &lt; ActiveRecord::Base
141 end 145 end
142 end 146 end
143 147
  148 + def create_moderate_task
  149 + @task = ModerateUserRegistration.new
  150 + @task.user_id = self.id
  151 + @task.name = self.name
  152 + @task.email = self.email
  153 + @task.target = self.environment
  154 + @task.save
  155 + end
  156 +
144 def activated? 157 def activated?
145 self.activation_code.nil? && !self.activated_at.nil? 158 self.activation_code.nil? && !self.activated_at.nil?
146 end 159 end
app/sweepers/profile_sweeper.rb
@@ -8,9 +8,6 @@ class ProfileSweeper # &lt; ActiveRecord::Observer @@ -8,9 +8,6 @@ class ProfileSweeper # &lt; ActiveRecord::Observer
8 end 8 end
9 9
10 def after_create(profile) 10 def after_create(profile)
11 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
12 - # the Noosfero core soon, see ActionItem3045  
13 - expire_statistics_block_cache(profile)  
14 end 11 end
15 12
16 protected 13 protected
@@ -31,13 +28,6 @@ protected @@ -31,13 +28,6 @@ protected
31 expire_blogs(profile) if profile.organization? 28 expire_blogs(profile) if profile.organization?
32 end 29 end
33 30
34 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
35 - # the Noosfero core soon, see ActionItem3045  
36 - def expire_statistics_block_cache(profile)  
37 - blocks = profile.environment.blocks.select { |b| b.kind_of?(EnvironmentStatisticsBlock) }  
38 - BlockSweeper.expire_blocks(blocks)  
39 - end  
40 -  
41 def expire_blogs(profile) 31 def expire_blogs(profile)
42 profile.blogs.select{|b| !b.empty?}.each do |blog| 32 profile.blogs.select{|b| !b.empty?}.each do |blog|
43 pages = blog.posts.count / blog.posts_per_page + 1 33 pages = blog.posts.count / blog.posts_per_page + 1
app/views/categories/_category.html.erb
1 <li> 1 <li>
2 <div class='treeitem'> 2 <div class='treeitem'>
3 - <%= display_color_for_category(category) %>  
4 - <%= category.name %> 3 + <% unless category_color_style(category).empty? %>
  4 + <span class="color_marker" style="<%= category_color_style(category) %>" ></span>
  5 + <% end %>
  6 + <span><%= category.name %></span>
  7 +
5 <% if category.children.count > 0 %> 8 <% if category.children.count > 0 %>
6 <div class='button' id="category-loading-<%= category.id %>" style="position: relative;"> 9 <div class='button' id="category-loading-<%= category.id %>" style="position: relative;">
7 <a href="#" id="show-button-<%= category.id %>" class="show-button" onclick="return false;" data-category="<%= category.id %>"><%= _('Show') %></a> 10 <a href="#" id="show-button-<%= category.id %>" class="show-button" onclick="return false;" data-category="<%= category.id %>"><%= _('Show') %></a>
app/views/categories/_form.html.erb
  1 +<%= stylesheet_link_tag 'spectrum.css' %>
  2 +<%= javascript_include_tag "spectrum.js" %>
  3 +<%= javascript_include_tag "colorpicker-noosfero.js" %>
  4 +
1 <%= error_messages_for 'category' %> 5 <%= error_messages_for 'category' %>
2 6
3 <%= labelled_form_for 'category', :html => { :multipart => true} do |f| %> 7 <%= labelled_form_for 'category', :html => { :multipart => true} do |f| %>
@@ -13,12 +17,13 @@ @@ -13,12 +17,13 @@
13 <% end %> 17 <% end %>
14 <% end %> 18 <% end %>
15 19
16 - <%= select_color_for_category if !environment.enabled?('disable_categories_menu') %>  
17 -  
18 <%= required f.text_field('name') %> 20 <%= required f.text_field('name') %>
19 21
20 <%= labelled_check_box(_('Display in the menu'), 'category[display_in_menu]', '1', @category.display_in_menu) %> 22 <%= labelled_check_box(_('Display in the menu'), 'category[display_in_menu]', '1', @category.display_in_menu) %>
21 23
  24 + <%= labelled_colorpicker_field(_('Pick a color'), :category, 'display_color' ) unless environment.enabled?('disable_categories_menu')%>
  25 + <span id="color_preview" class = "color_marker" style="<%= category_color_style(@category) %>" ></span>
  26 +
22 <%= f.fields_for :image_builder, @category.image do |i| %> 27 <%= f.fields_for :image_builder, @category.image do |i| %>
23 <%= file_field_or_thumbnail(_('Image:'), @category.image, i) %> 28 <%= file_field_or_thumbnail(_('Image:'), @category.image, i) %>
24 <% end %> 29 <% end %>
app/views/cms/edit.html.erb
@@ -31,9 +31,18 @@ @@ -31,9 +31,18 @@
31 31
32 <%= select_categories(:article, _('Categorize your article')) %> 32 <%= select_categories(:article, _('Categorize your article')) %>
33 33
  34 + <br />
  35 +
34 <%= f.text_field('tag_list', :size => 64) %> 36 <%= f.text_field('tag_list', :size => 64) %>
35 <%= content_tag( 'small', _('Separate tags with commas') ) %> 37 <%= content_tag( 'small', _('Separate tags with commas') ) %>
36 38
  39 + <script>
  40 + jQuery('#article_tag_list').inputosaurus({
  41 + autoCompleteSource: <%= "'/myprofile/#{profile.identifier}/cms/search_tags'," %>
  42 + activateFinalResult : true
  43 + })
  44 + </script>
  45 +
37 <div id='edit-article-options'> 46 <div id='edit-article-options'>
38 <%= options_for_article(@article, @tokenized_children) %> 47 <%= options_for_article(@article, @tokenized_children) %>
39 </div> 48 </div>
app/views/features/index.html.erb
@@ -37,6 +37,18 @@ Check all the features you want to enable for your environment, uncheck all the @@ -37,6 +37,18 @@ Check all the features you want to enable for your environment, uncheck all the
37 <%= select_organization_approval_method('environment', 'organization_approval_method') %> 37 <%= select_organization_approval_method('environment', 'organization_approval_method') %>
38 <hr/> 38 <hr/>
39 39
  40 +<h3><%= _('Members Whitelist') %></h3>
  41 + <div class="option">
  42 + <%= check_box :environment, :members_whitelist_enabled %>
  43 + <label><%= _('Enable whitelist') %></label>
  44 + </div>
  45 + <div class="input">
  46 + <div class="info"><%= _('Allow these people to access this environment:') %></div>
  47 + <% tokenized_members = prepare_to_token_input(environment.people.find(:all, :conditions => {:id => environment.members_whitelist})) %>
  48 + <%= token_input_field_tag('environment[members_whitelist]', 'search-members', {:action => 'search_members'}, {:focus => false, :hint_text => _('Type in a search term for a user'), :pre_populate => tokenized_members}) %>
  49 + </div>
  50 +<hr/>
  51 +
40 <div> 52 <div>
41 <% button_bar do %> 53 <% button_bar do %>
42 <%= submit_button('save', _('Save changes')) %> 54 <%= submit_button('save', _('Save changes')) %>
app/views/home/welcome.html.erb
@@ -6,11 +6,15 @@ @@ -6,11 +6,15 @@
6 <%= environment.settings[:signup_welcome_screen_body].html_safe %> 6 <%= environment.settings[:signup_welcome_screen_body].html_safe %>
7 <% else %> 7 <% else %>
8 <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3> 8 <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
9 - <p><%= _("Firstly, some tips for getting started:") %></p>  
10 <% if @display_confirmation_tips %> 9 <% if @display_confirmation_tips %>
  10 + <p><%= _("Firstly, some tips for getting started:") %></p>
11 <h4><%= _("Confirm your account!") %></h4> 11 <h4><%= _("Confirm your account!") %></h4>
12 <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p> 12 <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
13 <p><%= _("You won't appear as %s until your account is confirmed.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p> 13 <p><%= _("You won't appear as %s until your account is confirmed.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
  14 + <% else %>
  15 + <h4><%= _("Wait for admin approvement!") %></h4>
  16 + <p><%= _("The administrators will evaluate your signup request for approvement.") %></p>
  17 + <p><%= _("You won't appear as %s until your account is approved.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
14 <% end %> 18 <% end %>
15 <h4><%= _("What to do next?") %></h4> 19 <h4><%= _("What to do next?") %></h4>
16 <p><%= _("Access your %s and see your face on the network!") % link_to(_('Profile'), {:controller => 'profile', :profile => user.identifier}, :target => '_blank') %></p> 20 <p><%= _("Access your %s and see your face on the network!") % link_to(_('Profile'), {:controller => 'profile', :profile => user.identifier}, :target => '_blank') %></p>
app/views/layouts/_javascript.html.erb
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 'jquery-ui-1.10.4/js/jquery-ui-1.10.4.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate', 4 'jquery-ui-1.10.4/js/jquery-ui-1.10.4.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate',
5 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'jquery.typewatch', 'jquery.textchange', 5 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'jquery.typewatch', 'jquery.textchange',
6 'add-and-join', 'report-abuse', 'catalog', 'manage-products', 'autogrow', 'select-or-die/_src/selectordie', 6 'add-and-join', 'report-abuse', 'catalog', 'manage-products', 'autogrow', 'select-or-die/_src/selectordie',
7 -'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', :cache => 'cache/application' %> 7 +'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', 'inputosaurus.js', :cache => 'cache/application' %>
8 8
9 <% language = FastGettext.locale %> 9 <% language = FastGettext.locale %>
10 <% %w{messages methods}.each do |type| %> 10 <% %w{messages methods}.each do |type| %>
app/views/tasks/index.html.erb
@@ -3,10 +3,9 @@ @@ -3,10 +3,9 @@
3 <h1><%= _("%s's pending tasks") % profile.name %></h1> 3 <h1><%= _("%s's pending tasks") % profile.name %></h1>
4 <p> 4 <p>
5 5
6 -<% type_collection = [[nil, _('All')]] %>  
7 -<% type_collection += Task.all_types.sort_by {|klass| klass.constantize.new.title}.map{|s| [s, s.constantize.new.title] } %>  
8 -  
9 - 6 +<%
  7 + type_collection = [[nil, _('All')]] + @task_types
  8 +%>
10 9
11 <% if !@failed.blank? %> 10 <% if !@failed.blank? %>
12 <div id="errorExplanation"> 11 <div id="errorExplanation">
@@ -39,7 +38,7 @@ @@ -39,7 +38,7 @@
39 38
40 <ul class='task-list'> 39 <ul class='task-list'>
41 <p> 40 <p>
42 - <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :onchange => 'document.location.href = "?filter_type="+this.value')%> 41 + <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :onchange => "document.location.href = '?filter_type='+this.value") %>
43 </p> 42 </p>
44 <p> 43 <p>
45 <%= labelled_select(_("Set all to: "), 'set-decisions', 'first', 'last', nil, [['',""],['accept',_("Accept")],['reject',_("Reject")],['skip',_("Skip")]], :id => "up-set-all-tasks-to") %> 44 <%= labelled_select(_("Set all to: "), 'set-decisions', 'first', 'last', nil, [['',""],['accept',_("Accept")],['reject',_("Reject")],['skip',_("Skip")]], :id => "up-set-all-tasks-to") %>
baseplugins/statistics 0 → 120000
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +../plugins/statistics
0 \ No newline at end of file 2 \ No newline at end of file
config/initializers/passenger.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +if defined? PhusionPassenger
  2 +
  3 + # from http://russbrooks.com/2010/10/20/rails-cache-memcache-on-passenger-with-smart-spawning
  4 + PhusionPassenger.on_event :starting_worker_process do |forked|
  5 + if forked
  6 + Rails.cache.instance_variable_get(:@data).reset if Rails.cache.class == ActiveSupport::Cache::MemCacheStore
  7 + end
  8 + end
  9 +end
db/migrate/20140724134601_fix_yaml_encoding.rb
@@ -6,6 +6,7 @@ class FixYamlEncoding &lt; ActiveRecord::Migration @@ -6,6 +6,7 @@ class FixYamlEncoding &lt; ActiveRecord::Migration
6 fix_encoding(Profile, 'data') 6 fix_encoding(Profile, 'data')
7 fix_encoding(ActionTracker::Record, 'params') 7 fix_encoding(ActionTracker::Record, 'params')
8 fix_encoding(Article, 'setting') 8 fix_encoding(Article, 'setting')
  9 + fix_encoding(Task, 'data')
9 end 10 end
10 11
11 def self.down 12 def self.down
db/migrate/20140807134625_change_category_display_color_to_string.rb 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +class ChangeCategoryDisplayColorToString < ActiveRecord::Migration
  2 +
  3 + COLORS = ['ffa500', '00FF00', 'a020f0', 'ff0000', '006400', '191970', '0000ff', 'a52a2a', '32cd32', 'add8e6', '483d8b', 'b8e9ee', 'f5f5dc', 'ffff00', 'f4a460']
  4 +
  5 + def self.up
  6 + change_table :categories do |t|
  7 + t.string :display_color_tmp, :limit => 6
  8 + end
  9 +
  10 + COLORS.each_with_index do |color, i|
  11 + Category.update_all({:display_color_tmp => color}, {:display_color => i+1})
  12 + end
  13 +
  14 + change_table :categories do |t|
  15 + t.remove :display_color
  16 + t.rename :display_color_tmp, :display_color
  17 + end
  18 + end
  19 +
  20 + def self.down
  21 + puts "WARNING: only old defined colors will be reverted"
  22 +
  23 + change_table :categories do |t|
  24 + t.integer :display_color_tmp
  25 + end
  26 +
  27 + COLORS.each_with_index do |color, i|
  28 + Category.update_all({:display_color_tmp => i+1}, {:display_color => color})
  29 + end
  30 +
  31 + change_table :categories do |t|
  32 + t.remove :display_color
  33 + t.rename :display_color_tmp, :display_color
  34 + end
  35 + end
  36 +end
db/migrate/20140827191326_remove_environment_statistics_block.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +class RemoveEnvironmentStatisticsBlock < ActiveRecord::Migration
  2 + def self.up
  3 + update("UPDATE blocks SET type = 'StatisticsBlock' WHERE type = 'EnvironmentStatisticsBlock'")
  4 + end
  5 +
  6 + def self.down
  7 + say("Nothing to undo (cannot recover the data)")
  8 + end
  9 +end
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended to check this file into your version control system. 12 # It's strongly recommended to check this file into your version control system.
13 13
14 -ActiveRecord::Schema.define(:version => 20140811141211) do 14 +ActiveRecord::Schema.define(:version => 20140827191326) do
15 15
16 create_table "abuse_reports", :force => true do |t| 16 create_table "abuse_reports", :force => true do |t|
17 t.integer "reporter_id" 17 t.integer "reporter_id"
@@ -194,19 +194,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140811141211) do @@ -194,19 +194,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140811141211) do
194 create_table "categories", :force => true do |t| 194 create_table "categories", :force => true do |t|
195 t.string "name" 195 t.string "name"
196 t.string "slug" 196 t.string "slug"
197 - t.text "path", :default => ""  
198 - t.integer "display_color" 197 + t.text "path", :default => ""
199 t.integer "environment_id" 198 t.integer "environment_id"
200 t.integer "parent_id" 199 t.integer "parent_id"
201 t.string "type" 200 t.string "type"
202 t.float "lat" 201 t.float "lat"
203 t.float "lng" 202 t.float "lng"
204 - t.boolean "display_in_menu", :default => false  
205 - t.integer "children_count", :default => 0  
206 - t.boolean "accept_products", :default => true 203 + t.boolean "display_in_menu", :default => false
  204 + t.integer "children_count", :default => 0
  205 + t.boolean "accept_products", :default => true
207 t.integer "image_id" 206 t.integer "image_id"
208 t.string "acronym" 207 t.string "acronym"
209 t.string "abbreviation" 208 t.string "abbreviation"
  209 + t.string "display_color", :limit => 6
210 end 210 end
211 211
212 create_table "categories_profiles", :id => false, :force => true do |t| 212 create_table "categories_profiles", :id => false, :force => true do |t|
features/signup.feature
@@ -313,3 +313,55 @@ Feature: signup @@ -313,3 +313,55 @@ Feature: signup
313 And wait for the captcha signup time 313 And wait for the captcha signup time
314 And I press "Create my account" 314 And I press "Create my account"
315 Then "José da Silva" should be a member of "Free Software" 315 Then "José da Silva" should be a member of "Free Software"
  316 +
  317 + @selenium
  318 + Scenario: user registration is moderated by admin
  319 + Given feature "admin_must_approve_new_users" is enabled on environment
  320 + And feature "skip_new_user_email_confirmation" is disabled on environment
  321 + And I go to /account/signup
  322 + And I fill in "Username" with "teste"
  323 + And I fill in "Password" with "123456"
  324 + And I fill in "Password confirmation" with "123456"
  325 + And I fill in "e-Mail" with "teste@teste.com"
  326 + And I fill in "Full name" with "Teste da Silva"
  327 + And wait for the captcha signup time
  328 + And I press "Create my account"
  329 + And I go to teste's confirmation URL
  330 + And I am logged in as admin
  331 + And I follow "Control panel"
  332 + And I follow "Tasks"
  333 + And I choose "Accept"
  334 + And I press "Apply!"
  335 + And I follow "Logout"
  336 + And Teste da Silva's account is activated
  337 + And I follow "Login"
  338 + And I fill in "Username / Email" with "teste"
  339 + And I fill in "Password" with "123456"
  340 + And I press "Log in"
  341 + Then I should see "teste"
  342 +
  343 +
  344 + @selenium
  345 + Scenario: user registration is not accepted by the admin
  346 + Given feature "admin_must_approve_new_users" is enabled on environment
  347 + And feature "skip_new_user_email_confirmation" is disabled on environment
  348 + And I go to /account/signup
  349 + And I fill in "Username" with "teste"
  350 + And I fill in "Password" with "123456"
  351 + And I fill in "Password confirmation" with "123456"
  352 + And I fill in "e-Mail" with "teste@teste.com"
  353 + And I fill in "Full name" with "Teste da Silva"
  354 + And wait for the captcha signup time
  355 + And I press "Create my account"
  356 + And I go to teste's confirmation URL
  357 + And I am logged in as admin
  358 + And I follow "Control panel"
  359 + And I follow "Tasks"
  360 + And I choose "Reject"
  361 + And I press "Apply!"
  362 + And I follow "Logout"
  363 + And I follow "Login"
  364 + And I fill in "Username / Email" with "teste"
  365 + And I fill in "Password" with "123456"
  366 + And I press "Log in"
  367 + Then I should not see "teste"
316 \ No newline at end of file 368 \ No newline at end of file
features/step_definitions/web_steps.rb
@@ -11,8 +11,14 @@ require File.expand_path(File.join(File.dirname(__FILE__), &quot;..&quot;, &quot;support&quot;, &quot;pat @@ -11,8 +11,14 @@ require File.expand_path(File.join(File.dirname(__FILE__), &quot;..&quot;, &quot;support&quot;, &quot;pat
11 11
12 module WithinHelpers 12 module WithinHelpers
13 def with_scope(locator) 13 def with_scope(locator)
14 - locator = locator ? first(locator) : locator  
15 - locator ? within(locator) { yield } : yield 14 + if locator
  15 + locator = first(locator) || locator
  16 + within(locator) do
  17 + yield
  18 + end
  19 + else
  20 + yield
  21 + end
16 end 22 end
17 end 23 end
18 World(WithinHelpers) 24 World(WithinHelpers)
lib/tasks/plugins.rake
@@ -7,7 +7,11 @@ namespace :noosfero do @@ -7,7 +7,11 @@ namespace :noosfero do
7 plugin_migration_dirs = Dir.glob(Rails.root.join('{baseplugins,config/plugins}', '*', 'db', 'migrate')) 7 plugin_migration_dirs = Dir.glob(Rails.root.join('{baseplugins,config/plugins}', '*', 'db', 'migrate'))
8 8
9 task :load_config do 9 task :load_config do
10 - dirs = Dir.glob("{baseplugins,config/plugins}/*/db/migrate") 10 + dirs = Dir.glob("{baseplugins,config/plugins}/*").uniq do |dir|
  11 + File.basename(dir)
  12 + end.map do |dir|
  13 + File.join(dir, 'db/migrate')
  14 + end
11 dirs.each do |dir| 15 dirs.each do |dir|
12 ActiveRecord::Migrator.migrations_paths << dir 16 ActiveRecord::Migrator.migrations_paths << dir
13 end 17 end
plugins/community_track/lib/community_track_plugin/track_helper.rb
1 module CommunityTrackPlugin::TrackHelper 1 module CommunityTrackPlugin::TrackHelper
2 2
  3 + include CategoriesHelper
  4 +
3 def category_class(track) 5 def category_class(track)
4 'category_' + (track.categories.empty? ? 'not_defined' : track.categories.first.name.to_slug) 6 'category_' + (track.categories.empty? ? 'not_defined' : track.categories.first.name.to_slug)
5 end 7 end
@@ -9,4 +11,13 @@ module CommunityTrackPlugin::TrackHelper @@ -9,4 +11,13 @@ module CommunityTrackPlugin::TrackHelper
9 excerpt(lead_stripped, lead_stripped.first(3), track.image ? 180 : 300) 11 excerpt(lead_stripped, lead_stripped.first(3), track.image ? 180 : 300)
10 end 12 end
11 13
  14 + def track_color_style(track)
  15 + category_color_style(track.categories.first.with_color) if !track.categories.empty?
  16 + end
  17 +
  18 + def track_name_color_style(track)
  19 + category = track.categories.empty? ? nil : track.categories.first.with_color
  20 + category ? "color: ##{category.display_color};" : ''
  21 + end
  22 +
12 end 23 end
plugins/community_track/test/unit/community_track_plugin/track_helper_test.rb
@@ -52,4 +52,16 @@ class TrackHelperTest &lt; ActiveSupport::TestCase @@ -52,4 +52,16 @@ class TrackHelperTest &lt; ActiveSupport::TestCase
52 assert_equal 186, track_card_lead(@track).length 52 assert_equal 186, track_card_lead(@track).length
53 end 53 end
54 54
  55 + should 'return category color if its defined' do
  56 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  57 + @track.categories << category1
  58 + assert_equal 'background-color: #fbfbfb;', track_color_style(@track)
  59 + end
  60 +
  61 + should 'return category color for track name' do
  62 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  63 + @track.categories << category1
  64 + assert_equal 'color: #fbfbfb;', track_name_color_style(@track)
  65 + end
  66 +
55 end 67 end
plugins/community_track/views/blocks/_track_card.html.erb
@@ -2,13 +2,13 @@ @@ -2,13 +2,13 @@
2 <div class="item_card <%= category_class(track_card) %>"> 2 <div class="item_card <%= category_class(track_card) %>">
3 <a href="<%= url_for track_card.url %>"> 3 <a href="<%= url_for track_card.url %>">
4 <div class="track_content"> 4 <div class="track_content">
5 - <div class="title"> 5 + <div class="title" style="<%= track_color_style(track_card) %>">
6 <%= track_card.category_name %> 6 <%= track_card.category_name %>
7 </div> 7 </div>
8 <div class="image"> 8 <div class="image">
9 <%= image_tag track_card.image.public_filename if track_card.image %> 9 <%= image_tag track_card.image.public_filename if track_card.image %>
10 </div> 10 </div>
11 - <div class="name"> 11 + <div class="name" style="<%= track_name_color_style(track_card) %>">
12 <%= track_card.name %> 12 <%= track_card.name %>
13 </div> 13 </div>
14 <div class="lead"> 14 <div class="lead">
po/pt/noosfero.po
@@ -13,7 +13,7 @@ msgid &quot;&quot; @@ -13,7 +13,7 @@ msgid &quot;&quot;
13 msgstr "" 13 msgstr ""
14 "Project-Id-Version: noosfero 0.47.1\n" 14 "Project-Id-Version: noosfero 0.47.1\n"
15 "POT-Creation-Date: 2014-06-05 20:27-0000\n" 15 "POT-Creation-Date: 2014-06-05 20:27-0000\n"
16 -"PO-Revision-Date: 2013-07-30 12:53-0300\n" 16 +"PO-Revision-Date: 2014-08-22 11:19-0300\n"
17 "Last-Translator: Rodrigo Souto <rodrigo@colivre.coop.br>\n" 17 "Last-Translator: Rodrigo Souto <rodrigo@colivre.coop.br>\n"
18 "Language-Team: Noosfero Develeopers <noosfero-dev@listas.softwarelivre.org>\n" 18 "Language-Team: Noosfero Develeopers <noosfero-dev@listas.softwarelivre.org>\n"
19 "Language: pt\n" 19 "Language: pt\n"
@@ -469,7 +469,7 @@ msgstr &quot;Local: &quot; @@ -469,7 +469,7 @@ msgstr &quot;Local: &quot;
469 #: app/helpers/boxes_helper.rb:37 app/helpers/boxes_helper.rb:63 469 #: app/helpers/boxes_helper.rb:37 app/helpers/boxes_helper.rb:63
470 #: app/models/main_block.rb:4 470 #: app/models/main_block.rb:4
471 msgid "Main content" 471 msgid "Main content"
472 -msgstr "Gerenciar conteúdo" 472 +msgstr "Conteúdo principal"
473 473
474 #: app/helpers/boxes_helper.rb:100 474 #: app/helpers/boxes_helper.rb:100
475 msgid "This block is invisible. Your visitors will not see it." 475 msgid "This block is invisible. Your visitors will not see it."
@@ -8925,7 +8925,7 @@ msgstr &quot;Boas vindas ao ambiente %s&quot; @@ -8925,7 +8925,7 @@ msgstr &quot;Boas vindas ao ambiente %s&quot;
8925 msgid "" 8925 msgid ""
8926 "Thanks for signing up, we're thrilled to have you on our social network!" 8926 "Thanks for signing up, we're thrilled to have you on our social network!"
8927 msgstr "" 8927 msgstr ""
8928 -"Obrigado por se registrar! Agora verifique seu e-mail para ativar sua conta!" 8928 +"Obrigado por se registrar, estamos empolgados de ter você na nossa rede social!"
8929 8929
8930 #: app/views/account/signup.rhtml:5 8930 #: app/views/account/signup.rhtml:5
8931 msgid "Firstly, some tips for getting started:" 8931 msgid "Firstly, some tips for getting started:"
public/javascripts/colorpicker-noosfero.js 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +jQuery(document).ready(function($) {
  2 + $(".colorpicker_field").spectrum({
  3 + showInput: true,
  4 + showInitial: false,
  5 + preferredFormat: "hex",
  6 + allowEmpty: true,
  7 + showPalette: true,
  8 + palette: [
  9 + ["rgb(0, 0, 0)", "rgb(67, 67, 67)", "rgb(102, 102, 102)",
  10 + "rgb(204, 204, 204)", "rgb(217, 217, 217)","rgb(255, 255, 255)"],
  11 + ["rgb(152, 0, 0)", "rgb(255, 0, 0)", "rgb(255, 153, 0)", "rgb(255, 255, 0)", "rgb(0, 255, 0)",
  12 + "rgb(0, 255, 255)", "rgb(74, 134, 232)", "rgb(0, 0, 255)", "rgb(153, 0, 255)", "rgb(255, 0, 255)"],
  13 + ["rgb(230, 184, 175)", "rgb(244, 204, 204)", "rgb(252, 229, 205)", "rgb(255, 242, 204)", "rgb(217, 234, 211)",
  14 + "rgb(208, 224, 227)", "rgb(201, 218, 248)", "rgb(207, 226, 243)", "rgb(217, 210, 233)", "rgb(234, 209, 220)",
  15 + "rgb(221, 126, 107)", "rgb(234, 153, 153)", "rgb(249, 203, 156)", "rgb(255, 229, 153)", "rgb(182, 215, 168)",
  16 + "rgb(162, 196, 201)", "rgb(164, 194, 244)", "rgb(159, 197, 232)", "rgb(180, 167, 214)", "rgb(213, 166, 189)",
  17 + "rgb(204, 65, 37)", "rgb(224, 102, 102)", "rgb(246, 178, 107)", "rgb(255, 217, 102)", "rgb(147, 196, 125)",
  18 + "rgb(118, 165, 175)", "rgb(109, 158, 235)", "rgb(111, 168, 220)", "rgb(142, 124, 195)", "rgb(194, 123, 160)",
  19 + "rgb(166, 28, 0)", "rgb(204, 0, 0)", "rgb(230, 145, 56)", "rgb(241, 194, 50)", "rgb(106, 168, 79)",
  20 + "rgb(69, 129, 142)", "rgb(60, 120, 216)", "rgb(61, 133, 198)", "rgb(103, 78, 167)", "rgb(166, 77, 121)",
  21 + "rgb(91, 15, 0)", "rgb(102, 0, 0)", "rgb(120, 63, 4)", "rgb(127, 96, 0)", "rgb(39, 78, 19)",
  22 + "rgb(12, 52, 61)", "rgb(28, 69, 135)", "rgb(7, 55, 99)", "rgb(32, 18, 77)", "rgb(76, 17, 48)"]
  23 + ]
  24 + });
  25 +});
public/javascripts/inputosaurus.js 0 → 100644
@@ -0,0 +1,523 @@ @@ -0,0 +1,523 @@
  1 +/**
  2 + * Inputosaurus Text
  3 + *
  4 + * Must be instantiated on an <input> element
  5 + * Allows multiple input items. Each item is represented with a removable tag that appears to be inside the input area.
  6 + *
  7 + * @requires:
  8 + *
  9 + * jQuery 1.7+
  10 + * jQueryUI 1.8+ Core
  11 + *
  12 + * @version 0.1.6
  13 + * @author Dan Kielp <dan@sproutsocial.com>
  14 + * @created October 3,2012
  15 + *
  16 + */
  17 +
  18 +
  19 +(function($) {
  20 +
  21 + var inputosaurustext = {
  22 +
  23 + version: "0.1.6",
  24 +
  25 + eventprefix: "inputosaurus",
  26 +
  27 + options: {
  28 +
  29 + // bindable events
  30 + //
  31 + // 'change' - triggered whenever a tag is added or removed (should be similar to binding the the change event of the instantiated input
  32 + // 'keyup' - keyup event on the newly created input
  33 +
  34 + // while typing, the user can separate values using these delimiters
  35 + // the value tags are created on the fly when an inputDelimiter is detected
  36 + inputDelimiters : [',', ';'],
  37 +
  38 + // this separator is used to rejoin all input items back to the value of the original <input>
  39 + outputDelimiter : ',',
  40 +
  41 + allowDuplicates : false,
  42 +
  43 + parseOnBlur : false,
  44 +
  45 + // optional wrapper for widget
  46 + wrapperElement : null,
  47 +
  48 + width : null,
  49 +
  50 + // simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality
  51 + autoCompleteSource : '',
  52 +
  53 + // When forcing users to select from the autocomplete list, allow them to press 'Enter' to select an item if it's the only option left.
  54 + activateFinalResult : false,
  55 +
  56 + // manipulate and return the input value after parseInput() parsing
  57 + // the array of tag names is passed and expected to be returned as an array after manipulation
  58 + parseHook : null,
  59 +
  60 + // define a placeholder to display when the input is empty
  61 + placeholder: null,
  62 +
  63 + // when you check for duplicates it check for the case
  64 + caseSensitiveDuplicates: false
  65 + },
  66 +
  67 + _create: function() {
  68 + var widget = this,
  69 + els = {},
  70 + o = widget.options,
  71 + placeholder = o.placeholder || this.element.attr('placeholder') || null;
  72 +
  73 + this._chosenValues = [];
  74 +
  75 + // Create the elements
  76 + els.ul = $('<ul class="inputosaurus-container">');
  77 + els.input = $('<input type="text" />');
  78 + els.inputCont = $('<li class="inputosaurus-input inputosaurus-required"></li>');
  79 + els.origInputCont = $('<li class="inputosaurus-input-hidden inputosaurus-required">');
  80 +
  81 + // define starting placeholder
  82 + if (placeholder) {
  83 + o.placeholder = placeholder;
  84 + els.input.attr('placeholder', o.placeholder);
  85 + if (o.width) {
  86 + els.input.css('min-width', o.width - 50);
  87 + }
  88 + }
  89 +
  90 + o.wrapperElement && o.wrapperElement.append(els.ul);
  91 + this.element.replaceWith(o.wrapperElement || els.ul);
  92 + els.origInputCont.append(this.element).hide();
  93 +
  94 + els.inputCont.append(els.input);
  95 + els.ul.append(els.inputCont);
  96 + els.ul.append(els.origInputCont);
  97 +
  98 + o.width && els.ul.css('width', o.width);
  99 +
  100 + this.elements = els;
  101 +
  102 + widget._attachEvents();
  103 +
  104 + // if instantiated input already contains a value, parse that junk
  105 + if($.trim(this.element.val())){
  106 + els.input.val( this.element.val() );
  107 + this.parseInput();
  108 + }
  109 +
  110 + this._instAutocomplete();
  111 + },
  112 +
  113 + _instAutocomplete : function() {
  114 + if(this.options.autoCompleteSource){
  115 + var widget = this;
  116 +
  117 + this.elements.input.autocomplete({
  118 + position : {
  119 + of : this.elements.ul
  120 + },
  121 + source : this.options.autoCompleteSource,
  122 + minLength : 1,
  123 + select : function(ev, ui){
  124 + ev.preventDefault();
  125 + widget.elements.input.val(ui.item.value);
  126 + widget.parseInput();
  127 + },
  128 + open : function() {
  129 + // Older versions of jQueryUI have a different namespace
  130 + var auto = $(this).data('ui-autocomplete') || $(this).data('autocomplete');
  131 + var menu = auto.menu,
  132 + $menuItems;
  133 +
  134 +
  135 + // zIndex will force the element on top of anything (like a dialog it's in)
  136 + menu.element.zIndex && menu.element.zIndex($(this).zIndex() + 1);
  137 + menu.element.width(widget.elements.ul.outerWidth());
  138 +
  139 + // auto-activate the result if it's the only one
  140 + if(widget.options.activateFinalResult){
  141 + $menuItems = menu.element.find('li');
  142 +
  143 + // activate single item to allow selection upon pressing 'Enter'
  144 + if($menuItems.size() === 1){
  145 + menu[menu.activate ? 'activate' : 'focus']($.Event('click'), $menuItems);
  146 + }
  147 + }
  148 + }
  149 + });
  150 + }
  151 + },
  152 +
  153 + _autoCompleteMenuPosition : function() {
  154 + var widget;
  155 + if(this.options.autoCompleteSource){
  156 + widget = this.elements.input.data('ui-autocomplete') || this.elements.input.data('autocomplete');
  157 + widget && widget.menu.element.position({
  158 + of: this.elements.ul,
  159 + my: 'left top',
  160 + at: 'left bottom',
  161 + collision: 'none'
  162 + });
  163 + }
  164 + },
  165 +
  166 + /*_closeAutoCompleteMenu : function() {
  167 + if(this.options.autoCompleteSource){
  168 + this.elements.input.autocomplete('close');
  169 + }
  170 + },*/
  171 +
  172 + parseInput : function(ev) {
  173 + var widget = (ev && ev.data.widget) || this,
  174 + val,
  175 + delimiterFound = false,
  176 + values = [];
  177 +
  178 + val = widget.elements.input.val();
  179 +
  180 + val && (delimiterFound = widget._containsDelimiter(val));
  181 +
  182 + if(delimiterFound !== false){
  183 + values = val.split(delimiterFound);
  184 + } else if(!ev || ev.which === $.ui.keyCode.ENTER && !$('.ui-menu-item.ui-state-focus').size() && !$('.ui-menu-item .ui-state-focus').size() && !$('#ui-active-menuitem').size()){
  185 + values.push(val);
  186 + ev && ev.preventDefault();
  187 +
  188 + // prevent autoComplete menu click from causing a false 'blur'
  189 + } else if(ev.type === 'blur' && !$('#ui-active-menuitem').size()){
  190 + values.push(val);
  191 + }
  192 +
  193 + $.isFunction(widget.options.parseHook) && (values = widget.options.parseHook(values));
  194 +
  195 + if(values.length){
  196 + widget._setChosen(values);
  197 + widget.elements.input.val('');
  198 + widget._resizeInput();
  199 + }
  200 +
  201 + widget._resetPlaceholder();
  202 + },
  203 +
  204 + _inputFocus : function(ev) {
  205 + var widget = ev.data.widget || this;
  206 +
  207 + widget.elements.input.value || (widget.options.autoCompleteSource.length && widget.elements.input.autocomplete('search', ''));
  208 + },
  209 +
  210 + _inputKeypress : function(ev) {
  211 + var widget = ev.data.widget || this;
  212 +
  213 + ev.type === 'keyup' && widget._trigger('keyup', ev, widget);
  214 +
  215 + switch(ev.which){
  216 + case $.ui.keyCode.BACKSPACE:
  217 + ev.type === 'keydown' && widget._inputBackspace(ev);
  218 + break;
  219 +
  220 + case $.ui.keyCode.LEFT:
  221 + ev.type === 'keydown' && widget._inputBackspace(ev);
  222 + break;
  223 +
  224 + default :
  225 + widget.parseInput(ev);
  226 + widget._resizeInput(ev);
  227 + }
  228 +
  229 + // reposition autoComplete menu as <ul> grows and shrinks vertically
  230 + if(widget.options.autoCompleteSource){
  231 + setTimeout(function(){widget._autoCompleteMenuPosition.call(widget);}, 200);
  232 + }
  233 + },
  234 +
  235 + // the input dynamically resizes based on the length of its value
  236 + _resizeInput : function(ev) {
  237 + var widget = (ev && ev.data.widget) || this,
  238 + maxWidth = widget.elements.ul.width(),
  239 + val = widget.elements.input.val(),
  240 + txtWidth = 25 + val.length * 8;
  241 +
  242 + widget.elements.input.width(txtWidth < maxWidth ? txtWidth : maxWidth);
  243 + },
  244 +
  245 + // resets placeholder on representative input
  246 + _resetPlaceholder: function () {
  247 + var placeholder = this.options.placeholder,
  248 + input = this.elements.input,
  249 + width = this.options.width || 'inherit';
  250 + if (placeholder && this.element.val().length === 0) {
  251 + input.attr('placeholder', placeholder).css('min-width', width - 50)
  252 + }else {
  253 + input.attr('placeholder', '').css('min-width', 'inherit')
  254 + }
  255 + },
  256 +
  257 + // if our input contains no value and backspace has been pressed, select the last tag
  258 + _inputBackspace : function(ev) {
  259 + var widget = (ev && ev.data.widget) || this;
  260 + lastTag = widget.elements.ul.find('li:not(.inputosaurus-required):last');
  261 +
  262 + // IE goes back in history if the event isn't stopped
  263 + ev.stopPropagation();
  264 +
  265 + if((!$(ev.currentTarget).val() || (('selectionStart' in ev.currentTarget) && ev.currentTarget.selectionStart === 0 && ev.currentTarget.selectionEnd === 0)) && lastTag.size()){
  266 + ev.preventDefault();
  267 + lastTag.find('a').focus();
  268 + }
  269 +
  270 + },
  271 +
  272 + _editTag : function(ev) {
  273 + var widget = (ev && ev.data.widget) || this,
  274 + tagName = '',
  275 + $closest = $(ev.currentTarget).closest('li'),
  276 + tagKey = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus');
  277 +
  278 + if(!tagKey){
  279 + return true;
  280 + }
  281 +
  282 + ev.preventDefault();
  283 +
  284 + $.each(widget._chosenValues, function(i,v) {
  285 + v.key === tagKey && (tagName = v.value);
  286 + });
  287 +
  288 + widget.elements.input.val(tagName);
  289 +
  290 + widget._removeTag(ev);
  291 + widget._resizeInput(ev);
  292 + },
  293 +
  294 + _tagKeypress : function(ev) {
  295 + var widget = ev.data.widget;
  296 + switch(ev.which){
  297 +
  298 + case $.ui.keyCode.BACKSPACE:
  299 + ev && ev.preventDefault();
  300 + ev && ev.stopPropagation();
  301 + $(ev.currentTarget).trigger('click');
  302 + break;
  303 +
  304 + // 'e' - edit tag (removes tag and places value into visible input
  305 + case 69:
  306 + widget._editTag(ev);
  307 + break;
  308 +
  309 + case $.ui.keyCode.LEFT:
  310 + ev.type === 'keydown' && widget._prevTag(ev);
  311 + break;
  312 +
  313 + case $.ui.keyCode.RIGHT:
  314 + ev.type === 'keydown' && widget._nextTag(ev);
  315 + break;
  316 +
  317 + case $.ui.keyCode.DOWN:
  318 + ev.type === 'keydown' && widget._focus(ev);
  319 + break;
  320 + }
  321 + },
  322 +
  323 + // select the previous tag or input if no more tags exist
  324 + _prevTag : function(ev) {
  325 + var widget = (ev && ev.data.widget) || this,
  326 + tag = $(ev.currentTarget).closest('li'),
  327 + previous = tag.prev();
  328 +
  329 + if(previous.is('li')){
  330 + previous.find('a').focus();
  331 + } else {
  332 + widget._focus();
  333 + }
  334 + },
  335 +
  336 + // select the next tag or input if no more tags exist
  337 + _nextTag : function(ev) {
  338 + var widget = (ev && ev.data.widget) || this,
  339 + tag = $(ev.currentTarget).closest('li'),
  340 + next = tag.next();
  341 +
  342 + if(next.is('li:not(.inputosaurus-input)')){
  343 + next.find('a').focus();
  344 + } else {
  345 + widget._focus();
  346 + }
  347 + },
  348 +
  349 + // return the inputDelimiter that was detected or false if none were found
  350 + _containsDelimiter : function(tagStr) {
  351 +
  352 + var found = false;
  353 +
  354 + $.each(this.options.inputDelimiters, function(k,v) {
  355 + if(tagStr.indexOf(v) !== -1){
  356 + found = v;
  357 + }
  358 + });
  359 +
  360 + return found;
  361 + },
  362 +
  363 + _setChosen : function(valArr) {
  364 + var self = this;
  365 +
  366 + if(!$.isArray(valArr)){
  367 + return false;
  368 + }
  369 +
  370 + $.each(valArr, function(k,v) {
  371 + var exists = false,
  372 + obj = {
  373 + key : '',
  374 + value : ''
  375 + };
  376 +
  377 + v = $.trim(v);
  378 +
  379 + $.each(self._chosenValues, function(kk,vv) {
  380 + if(!self.options.caseSensitiveDuplicates){
  381 + vv.value.toLowerCase() === v.toLowerCase() && (exists = true);
  382 + }
  383 + else{
  384 + vv.value === v && (exists = true);
  385 + }
  386 + });
  387 +
  388 + if(v !== '' && (!exists || self.options.allowDuplicates)){
  389 +
  390 + obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
  391 + obj.value = v;
  392 + self._chosenValues.push(obj);
  393 +
  394 + self._renderTags();
  395 + }
  396 + });
  397 + self._setValue(self._buildValue());
  398 + },
  399 +
  400 + _buildValue : function() {
  401 + var widget = this,
  402 + value = '';
  403 +
  404 + $.each(this._chosenValues, function(k,v) {
  405 + value += value.length ? widget.options.outputDelimiter + v.value : v.value;
  406 + });
  407 +
  408 + return value;
  409 + },
  410 +
  411 + _setValue : function(value) {
  412 + var val = this.element.val();
  413 +
  414 + if(val !== value){
  415 + this.element.val(value);
  416 + this._trigger('change');
  417 + }
  418 + },
  419 +
  420 + // @name text for tag
  421 + // @className optional className for <li>
  422 + _createTag : function(name, key, className) {
  423 + className = className ? ' class="' + className + '"' : '';
  424 +
  425 + if(name !== undefined){
  426 + return $('<li' + className + ' data-inputosaurus="' + key + '"><span>' + name + '</span> <a href="javascript:void(0);" class="ficon">&#x2716;</a></li>');
  427 + }
  428 + },
  429 +
  430 + _renderTags : function() {
  431 + var self = this;
  432 +
  433 + this.elements.ul.find('li:not(.inputosaurus-required)').remove();
  434 +
  435 + $.each(this._chosenValues, function(k,v) {
  436 + var el = self._createTag(v.value, v.key);
  437 + self.elements.ul.find('li.inputosaurus-input').before(el);
  438 + });
  439 + },
  440 +
  441 + _removeTag : function(ev) {
  442 + var $closest = $(ev.currentTarget).closest('li'),
  443 + key = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus'),
  444 + indexFound = false,
  445 + widget = (ev && ev.data.widget) || this;
  446 +
  447 +
  448 + $.each(widget._chosenValues, function(k,v) {
  449 + if(key === v.key){
  450 + indexFound = k;
  451 + }
  452 + });
  453 +
  454 + indexFound !== false && widget._chosenValues.splice(indexFound, 1);
  455 +
  456 + widget._setValue(widget._buildValue());
  457 +
  458 + $(ev.currentTarget).closest('li').remove();
  459 + widget.elements.input.focus();
  460 + },
  461 +
  462 + _focus : function(ev) {
  463 + var widget = (ev && ev.data.widget) || this,
  464 + $closest = $(ev.target).closest('li'),
  465 + $data = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus');
  466 +
  467 + if(!ev || !$data){
  468 + widget.elements.input.focus();
  469 + }
  470 + },
  471 +
  472 + _tagFocus : function(ev) {
  473 + $(ev.currentTarget).parent()[ev.type === 'focusout' ? 'removeClass' : 'addClass']('inputosaurus-selected');
  474 + },
  475 +
  476 + refresh : function() {
  477 + var delim = this.options.outputDelimiter,
  478 + val = this.element.val(),
  479 + values = [];
  480 +
  481 + values.push(val);
  482 + delim && (values = val.split(delim));
  483 +
  484 + if(values.length){
  485 + this._chosenValues = [];
  486 +
  487 + $.isFunction(this.options.parseHook) && (values = this.options.parseHook(values));
  488 +
  489 + this._setChosen(values);
  490 + this._renderTags();
  491 + this.elements.input.val('');
  492 + this._resizeInput();
  493 + }
  494 + },
  495 +
  496 + _attachEvents : function() {
  497 + var widget = this;
  498 +
  499 + this.elements.input.on('keyup.inputosaurus', {widget : widget}, this._inputKeypress);
  500 + this.elements.input.on('keydown.inputosaurus', {widget : widget}, this._inputKeypress);
  501 + this.elements.input.on('change.inputosaurus', {widget : widget}, this._inputKeypress);
  502 + this.elements.input.on('focus.inputosaurus', {widget : widget}, this._inputFocus);
  503 + this.options.parseOnBlur && this.elements.input.on('blur.inputosaurus', {widget : widget}, this.parseInput);
  504 +
  505 + this.elements.ul.on('click.inputosaurus', {widget : widget}, this._focus);
  506 + this.elements.ul.on('click.inputosaurus', 'a', {widget : widget}, this._removeTag);
  507 + this.elements.ul.on('dblclick.inputosaurus', 'li', {widget : widget}, this._editTag);
  508 + this.elements.ul.on('focus.inputosaurus', 'a', {widget : widget}, this._tagFocus);
  509 + this.elements.ul.on('blur.inputosaurus', 'a', {widget : widget}, this._tagFocus);
  510 + this.elements.ul.on('keydown.inputosaurus', 'a', {widget : widget}, this._tagKeypress);
  511 + },
  512 +
  513 + _destroy: function() {
  514 + this.elements.input.unbind('.inputosaurus');
  515 +
  516 + this.elements.ul.replaceWith(this.element);
  517 +
  518 + }
  519 + };
  520 +
  521 + $.widget("ui.inputosaurus", inputosaurustext);
  522 +})(jQuery);
  523 +
public/javascripts/spectrum.js 0 → 100644
@@ -0,0 +1,2259 @@ @@ -0,0 +1,2259 @@
  1 +// Spectrum Colorpicker v1.4.1
  2 +// https://github.com/bgrins/spectrum
  3 +// Author: Brian Grinstead
  4 +// License: MIT
  5 +
  6 +(function (window, $, undefined) {
  7 + "use strict";
  8 +
  9 + var defaultOpts = {
  10 +
  11 + // Callbacks
  12 + beforeShow: noop,
  13 + move: noop,
  14 + change: noop,
  15 + show: noop,
  16 + hide: noop,
  17 +
  18 + // Options
  19 + color: false,
  20 + flat: false,
  21 + showInput: false,
  22 + allowEmpty: false,
  23 + showButtons: true,
  24 + clickoutFiresChange: false,
  25 + showInitial: false,
  26 + showPalette: false,
  27 + showPaletteOnly: false,
  28 + hideAfterPaletteSelect: false,
  29 + togglePaletteOnly: false,
  30 + showSelectionPalette: true,
  31 + localStorageKey: false,
  32 + appendTo: "body",
  33 + maxSelectionSize: 7,
  34 + cancelText: "cancel",
  35 + chooseText: "choose",
  36 + togglePaletteMoreText: "more",
  37 + togglePaletteLessText: "less",
  38 + clearText: "Clear Color Selection",
  39 + noColorSelectedText: "No Color Selected",
  40 + preferredFormat: false,
  41 + className: "", // Deprecated - use containerClassName and replacerClassName instead.
  42 + containerClassName: "",
  43 + replacerClassName: "",
  44 + showAlpha: false,
  45 + theme: "sp-light",
  46 + palette: [["#ffffff", "#000000", "#ff0000", "#ff8000", "#ffff00", "#008000", "#0000ff", "#4b0082", "#9400d3"]],
  47 + selectionPalette: [],
  48 + disabled: false
  49 + },
  50 + spectrums = [],
  51 + IE = !!/msie/i.exec( window.navigator.userAgent ),
  52 + rgbaSupport = (function() {
  53 + function contains( str, substr ) {
  54 + return !!~('' + str).indexOf(substr);
  55 + }
  56 +
  57 + var elem = document.createElement('div');
  58 + var style = elem.style;
  59 + style.cssText = 'background-color:rgba(0,0,0,.5)';
  60 + return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla');
  61 + })(),
  62 + inputTypeColorSupport = (function() {
  63 + var colorInput = $("<input type='color' value='!' />")[0];
  64 + return colorInput.type === "color" && colorInput.value !== "!";
  65 + })(),
  66 + replaceInput = [
  67 + "<div class='sp-replacer'>",
  68 + "<div class='sp-preview'><div class='sp-preview-inner'></div></div>",
  69 + "<div class='sp-dd'>&#9660;</div>",
  70 + "</div>"
  71 + ].join(''),
  72 + markup = (function () {
  73 +
  74 + // IE does not support gradients with multiple stops, so we need to simulate
  75 + // that for the rainbow slider with 8 divs that each have a single gradient
  76 + var gradientFix = "";
  77 + if (IE) {
  78 + for (var i = 1; i <= 6; i++) {
  79 + gradientFix += "<div class='sp-" + i + "'></div>";
  80 + }
  81 + }
  82 +
  83 + return [
  84 + "<div class='sp-container sp-hidden'>",
  85 + "<div class='sp-palette-container'>",
  86 + "<div class='sp-palette sp-thumb sp-cf'></div>",
  87 + "<div class='sp-palette-button-container sp-cf'>",
  88 + "<button type='button' class='sp-palette-toggle'></button>",
  89 + "</div>",
  90 + "</div>",
  91 + "<div class='sp-picker-container'>",
  92 + "<div class='sp-top sp-cf'>",
  93 + "<div class='sp-fill'></div>",
  94 + "<div class='sp-top-inner'>",
  95 + "<div class='sp-color'>",
  96 + "<div class='sp-sat'>",
  97 + "<div class='sp-val'>",
  98 + "<div class='sp-dragger'></div>",
  99 + "</div>",
  100 + "</div>",
  101 + "</div>",
  102 + "<div class='sp-clear sp-clear-display'>",
  103 + "</div>",
  104 + "<div class='sp-hue'>",
  105 + "<div class='sp-slider'></div>",
  106 + gradientFix,
  107 + "</div>",
  108 + "</div>",
  109 + "<div class='sp-alpha'><div class='sp-alpha-inner'><div class='sp-alpha-handle'></div></div></div>",
  110 + "</div>",
  111 + "<div class='sp-input-container sp-cf'>",
  112 + "<input class='sp-input' type='text' spellcheck='false' />",
  113 + "</div>",
  114 + "<div class='sp-initial sp-thumb sp-cf'></div>",
  115 + "<div class='sp-button-container sp-cf'>",
  116 + "<a class='sp-cancel' href='#'></a>",
  117 + "<button type='button' class='sp-choose'></button>",
  118 + "</div>",
  119 + "</div>",
  120 + "</div>"
  121 + ].join("");
  122 + })();
  123 +
  124 + function paletteTemplate (p, color, className, opts) {
  125 + var html = [];
  126 + for (var i = 0; i < p.length; i++) {
  127 + var current = p[i];
  128 + if(current) {
  129 + var tiny = tinycolor(current);
  130 + var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
  131 + c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : "";
  132 + var formattedString = tiny.toString(opts.preferredFormat || "rgb");
  133 + var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
  134 + html.push('<span title="' + formattedString + '" data-color="' + tiny.toRgbString() + '" class="' + c + '"><span class="sp-thumb-inner" style="' + swatchStyle + ';" /></span>');
  135 + } else {
  136 + var cls = 'sp-clear-display';
  137 + html.push($('<div />')
  138 + .append($('<span data-color="" style="background-color:transparent;" class="' + cls + '"></span>')
  139 + .attr('title', opts.noColorSelectedText)
  140 + )
  141 + .html()
  142 + );
  143 + }
  144 + }
  145 + return "<div class='sp-cf " + className + "'>" + html.join('') + "</div>";
  146 + }
  147 +
  148 + function hideAll() {
  149 + for (var i = 0; i < spectrums.length; i++) {
  150 + if (spectrums[i]) {
  151 + spectrums[i].hide();
  152 + }
  153 + }
  154 + }
  155 +
  156 + function instanceOptions(o, callbackContext) {
  157 + var opts = $.extend({}, defaultOpts, o);
  158 + opts.callbacks = {
  159 + 'move': bind(opts.move, callbackContext),
  160 + 'change': bind(opts.change, callbackContext),
  161 + 'show': bind(opts.show, callbackContext),
  162 + 'hide': bind(opts.hide, callbackContext),
  163 + 'beforeShow': bind(opts.beforeShow, callbackContext)
  164 + };
  165 +
  166 + return opts;
  167 + }
  168 +
  169 + function spectrum(element, o) {
  170 +
  171 + var opts = instanceOptions(o, element),
  172 + flat = opts.flat,
  173 + showSelectionPalette = opts.showSelectionPalette,
  174 + localStorageKey = opts.localStorageKey,
  175 + theme = opts.theme,
  176 + callbacks = opts.callbacks,
  177 + resize = throttle(reflow, 10),
  178 + visible = false,
  179 + dragWidth = 0,
  180 + dragHeight = 0,
  181 + dragHelperHeight = 0,
  182 + slideHeight = 0,
  183 + slideWidth = 0,
  184 + alphaWidth = 0,
  185 + alphaSlideHelperWidth = 0,
  186 + slideHelperHeight = 0,
  187 + currentHue = 0,
  188 + currentSaturation = 0,
  189 + currentValue = 0,
  190 + currentAlpha = 1,
  191 + palette = [],
  192 + paletteArray = [],
  193 + paletteLookup = {},
  194 + selectionPalette = opts.selectionPalette.slice(0),
  195 + maxSelectionSize = opts.maxSelectionSize,
  196 + draggingClass = "sp-dragging",
  197 + shiftMovementDirection = null;
  198 +
  199 + var doc = element.ownerDocument,
  200 + body = doc.body,
  201 + boundElement = $(element),
  202 + disabled = false,
  203 + container = $(markup, doc).addClass(theme),
  204 + pickerContainer = container.find(".sp-picker-container"),
  205 + dragger = container.find(".sp-color"),
  206 + dragHelper = container.find(".sp-dragger"),
  207 + slider = container.find(".sp-hue"),
  208 + slideHelper = container.find(".sp-slider"),
  209 + alphaSliderInner = container.find(".sp-alpha-inner"),
  210 + alphaSlider = container.find(".sp-alpha"),
  211 + alphaSlideHelper = container.find(".sp-alpha-handle"),
  212 + textInput = container.find(".sp-input"),
  213 + paletteContainer = container.find(".sp-palette"),
  214 + initialColorContainer = container.find(".sp-initial"),
  215 + cancelButton = container.find(".sp-cancel"),
  216 + clearButton = container.find(".sp-clear"),
  217 + chooseButton = container.find(".sp-choose"),
  218 + toggleButton = container.find(".sp-palette-toggle"),
  219 + isInput = boundElement.is("input"),
  220 + isInputTypeColor = isInput && inputTypeColorSupport && boundElement.attr("type") === "color",
  221 + shouldReplace = isInput && !flat,
  222 + replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className).addClass(opts.replacerClassName) : $([]),
  223 + offsetElement = (shouldReplace) ? replacer : boundElement,
  224 + previewElement = replacer.find(".sp-preview-inner"),
  225 + initialColor = opts.color || (isInput && boundElement.val()),
  226 + colorOnShow = false,
  227 + preferredFormat = opts.preferredFormat,
  228 + currentPreferredFormat = preferredFormat,
  229 + clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange,
  230 + isEmpty = !initialColor,
  231 + allowEmpty = opts.allowEmpty && !isInputTypeColor;
  232 +
  233 + function applyOptions() {
  234 +
  235 + if (opts.showPaletteOnly) {
  236 + opts.showPalette = true;
  237 + }
  238 +
  239 + toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
  240 +
  241 + if (opts.palette) {
  242 + palette = opts.palette.slice(0);
  243 + paletteArray = $.isArray(palette[0]) ? palette : [palette];
  244 + paletteLookup = {};
  245 + for (var i = 0; i < paletteArray.length; i++) {
  246 + for (var j = 0; j < paletteArray[i].length; j++) {
  247 + var rgb = tinycolor(paletteArray[i][j]).toRgbString();
  248 + paletteLookup[rgb] = true;
  249 + }
  250 + }
  251 + }
  252 +
  253 + container.toggleClass("sp-flat", flat);
  254 + container.toggleClass("sp-input-disabled", !opts.showInput);
  255 + container.toggleClass("sp-alpha-enabled", opts.showAlpha);
  256 + container.toggleClass("sp-clear-enabled", allowEmpty);
  257 + container.toggleClass("sp-buttons-disabled", !opts.showButtons);
  258 + container.toggleClass("sp-palette-buttons-disabled", !opts.togglePaletteOnly);
  259 + container.toggleClass("sp-palette-disabled", !opts.showPalette);
  260 + container.toggleClass("sp-palette-only", opts.showPaletteOnly);
  261 + container.toggleClass("sp-initial-disabled", !opts.showInitial);
  262 + container.addClass(opts.className).addClass(opts.containerClassName);
  263 +
  264 + reflow();
  265 + }
  266 +
  267 + function initialize() {
  268 +
  269 + if (IE) {
  270 + container.find("*:not(input)").attr("unselectable", "on");
  271 + }
  272 +
  273 + applyOptions();
  274 +
  275 + if (shouldReplace) {
  276 + boundElement.after(replacer).hide();
  277 + }
  278 +
  279 + if (!allowEmpty) {
  280 + clearButton.hide();
  281 + }
  282 +
  283 + if (flat) {
  284 + boundElement.after(container).hide();
  285 + }
  286 + else {
  287 +
  288 + var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
  289 + if (appendTo.length !== 1) {
  290 + appendTo = $("body");
  291 + }
  292 +
  293 + appendTo.append(container);
  294 + }
  295 +
  296 + updateSelectionPaletteFromStorage();
  297 +
  298 + offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
  299 + if (!disabled) {
  300 + toggle();
  301 + }
  302 +
  303 + e.stopPropagation();
  304 +
  305 + if (!$(e.target).is("input")) {
  306 + e.preventDefault();
  307 + }
  308 + });
  309 +
  310 + if(boundElement.is(":disabled") || (opts.disabled === true)) {
  311 + disable();
  312 + }
  313 +
  314 + // Prevent clicks from bubbling up to document. This would cause it to be hidden.
  315 + container.click(stopPropagation);
  316 +
  317 + // Handle user typed input
  318 + textInput.change(setFromTextInput);
  319 + textInput.bind("paste", function () {
  320 + setTimeout(setFromTextInput, 1);
  321 + });
  322 + textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
  323 +
  324 + cancelButton.text(opts.cancelText);
  325 + cancelButton.bind("click.spectrum", function (e) {
  326 + e.stopPropagation();
  327 + e.preventDefault();
  328 + hide("cancel");
  329 + });
  330 +
  331 + clearButton.attr("title", opts.clearText);
  332 + clearButton.bind("click.spectrum", function (e) {
  333 + e.stopPropagation();
  334 + e.preventDefault();
  335 + isEmpty = true;
  336 + move();
  337 +
  338 + if(flat) {
  339 + //for the flat style, this is a change event
  340 + updateOriginalInput(true);
  341 + }
  342 + });
  343 +
  344 + chooseButton.text(opts.chooseText);
  345 + chooseButton.bind("click.spectrum", function (e) {
  346 + e.stopPropagation();
  347 + e.preventDefault();
  348 +
  349 + if (isValid()) {
  350 + updateOriginalInput(true);
  351 + hide();
  352 + }
  353 + });
  354 +
  355 + toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
  356 + toggleButton.bind("click.spectrum", function (e) {
  357 + e.stopPropagation();
  358 + e.preventDefault();
  359 +
  360 + opts.showPaletteOnly = !opts.showPaletteOnly;
  361 +
  362 + // To make sure the Picker area is drawn on the right, next to the
  363 + // Palette area (and not below the palette), first move the Palette
  364 + // to the left to make space for the picker, plus 5px extra.
  365 + // The 'applyOptions' function puts the whole container back into place
  366 + // and takes care of the button-text and the sp-palette-only CSS class.
  367 + if (!opts.showPaletteOnly && !flat) {
  368 + container.css('left', '-=' + (pickerContainer.outerWidth(true) + 5));
  369 + }
  370 + applyOptions();
  371 + });
  372 +
  373 + draggable(alphaSlider, function (dragX, dragY, e) {
  374 + currentAlpha = (dragX / alphaWidth);
  375 + isEmpty = false;
  376 + if (e.shiftKey) {
  377 + currentAlpha = Math.round(currentAlpha * 10) / 10;
  378 + }
  379 +
  380 + move();
  381 + }, dragStart, dragStop);
  382 +
  383 + draggable(slider, function (dragX, dragY) {
  384 + currentHue = parseFloat(dragY / slideHeight);
  385 + isEmpty = false;
  386 + if (!opts.showAlpha) {
  387 + currentAlpha = 1;
  388 + }
  389 + move();
  390 + }, dragStart, dragStop);
  391 +
  392 + draggable(dragger, function (dragX, dragY, e) {
  393 +
  394 + // shift+drag should snap the movement to either the x or y axis.
  395 + if (!e.shiftKey) {
  396 + shiftMovementDirection = null;
  397 + }
  398 + else if (!shiftMovementDirection) {
  399 + var oldDragX = currentSaturation * dragWidth;
  400 + var oldDragY = dragHeight - (currentValue * dragHeight);
  401 + var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
  402 +
  403 + shiftMovementDirection = furtherFromX ? "x" : "y";
  404 + }
  405 +
  406 + var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
  407 + var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
  408 +
  409 + if (setSaturation) {
  410 + currentSaturation = parseFloat(dragX / dragWidth);
  411 + }
  412 + if (setValue) {
  413 + currentValue = parseFloat((dragHeight - dragY) / dragHeight);
  414 + }
  415 +
  416 + isEmpty = false;
  417 + if (!opts.showAlpha) {
  418 + currentAlpha = 1;
  419 + }
  420 +
  421 + move();
  422 +
  423 + }, dragStart, dragStop);
  424 +
  425 + if (!!initialColor) {
  426 + set(initialColor);
  427 +
  428 + // In case color was black - update the preview UI and set the format
  429 + // since the set function will not run (default color is black).
  430 + updateUI();
  431 + currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
  432 +
  433 + addColorToSelectionPalette(initialColor);
  434 + }
  435 + else {
  436 + updateUI();
  437 + }
  438 +
  439 + if (flat) {
  440 + show();
  441 + }
  442 +
  443 + function paletteElementClick(e) {
  444 + if (e.data && e.data.ignore) {
  445 + set($(e.target).closest(".sp-thumb-el").data("color"));
  446 + move();
  447 + }
  448 + else {
  449 + set($(e.target).closest(".sp-thumb-el").data("color"));
  450 + move();
  451 + updateOriginalInput(true);
  452 + if (opts.hideAfterPaletteSelect) {
  453 + hide();
  454 + }
  455 + }
  456 +
  457 + return false;
  458 + }
  459 +
  460 + var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
  461 + paletteContainer.delegate(".sp-thumb-el", paletteEvent, paletteElementClick);
  462 + initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, paletteElementClick);
  463 + }
  464 +
  465 + function updateSelectionPaletteFromStorage() {
  466 +
  467 + if (localStorageKey && window.localStorage) {
  468 +
  469 + // Migrate old palettes over to new format. May want to remove this eventually.
  470 + try {
  471 + var oldPalette = window.localStorage[localStorageKey].split(",#");
  472 + if (oldPalette.length > 1) {
  473 + delete window.localStorage[localStorageKey];
  474 + $.each(oldPalette, function(i, c) {
  475 + addColorToSelectionPalette(c);
  476 + });
  477 + }
  478 + }
  479 + catch(e) { }
  480 +
  481 + try {
  482 + selectionPalette = window.localStorage[localStorageKey].split(";");
  483 + }
  484 + catch (e) { }
  485 + }
  486 + }
  487 +
  488 + function addColorToSelectionPalette(color) {
  489 + if (showSelectionPalette) {
  490 + var rgb = tinycolor(color).toRgbString();
  491 + if (!paletteLookup[rgb] && $.inArray(rgb, selectionPalette) === -1) {
  492 + selectionPalette.push(rgb);
  493 + while(selectionPalette.length > maxSelectionSize) {
  494 + selectionPalette.shift();
  495 + }
  496 + }
  497 +
  498 + if (localStorageKey && window.localStorage) {
  499 + try {
  500 + window.localStorage[localStorageKey] = selectionPalette.join(";");
  501 + }
  502 + catch(e) { }
  503 + }
  504 + }
  505 + }
  506 +
  507 + function getUniqueSelectionPalette() {
  508 + var unique = [];
  509 + if (opts.showPalette) {
  510 + for (var i = 0; i < selectionPalette.length; i++) {
  511 + var rgb = tinycolor(selectionPalette[i]).toRgbString();
  512 +
  513 + if (!paletteLookup[rgb]) {
  514 + unique.push(selectionPalette[i]);
  515 + }
  516 + }
  517 + }
  518 +
  519 + return unique.reverse().slice(0, opts.maxSelectionSize);
  520 + }
  521 +
  522 + function drawPalette() {
  523 +
  524 + var currentColor = get();
  525 +
  526 + var html = $.map(paletteArray, function (palette, i) {
  527 + return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i, opts);
  528 + });
  529 +
  530 + updateSelectionPaletteFromStorage();
  531 +
  532 + if (selectionPalette) {
  533 + html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection", opts));
  534 + }
  535 +
  536 + paletteContainer.html(html.join(""));
  537 + }
  538 +
  539 + function drawInitial() {
  540 + if (opts.showInitial) {
  541 + var initial = colorOnShow;
  542 + var current = get();
  543 + initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial", opts));
  544 + }
  545 + }
  546 +
  547 + function dragStart() {
  548 + if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
  549 + reflow();
  550 + }
  551 + container.addClass(draggingClass);
  552 + shiftMovementDirection = null;
  553 + boundElement.trigger('dragstart.spectrum', [ get() ]);
  554 + }
  555 +
  556 + function dragStop() {
  557 + container.removeClass(draggingClass);
  558 + boundElement.trigger('dragstop.spectrum', [ get() ]);
  559 + }
  560 +
  561 + function setFromTextInput() {
  562 +
  563 + var value = textInput.val();
  564 +
  565 + if ((value === null || value === "") && allowEmpty) {
  566 + set(null);
  567 + updateOriginalInput(true);
  568 + }
  569 + else {
  570 + var tiny = tinycolor(value);
  571 + if (tiny.isValid()) {
  572 + set(tiny);
  573 + updateOriginalInput(true);
  574 + }
  575 + else {
  576 + textInput.addClass("sp-validation-error");
  577 + }
  578 + }
  579 + }
  580 +
  581 + function toggle() {
  582 + if (visible) {
  583 + hide();
  584 + }
  585 + else {
  586 + show();
  587 + }
  588 + }
  589 +
  590 + function show() {
  591 + var event = $.Event('beforeShow.spectrum');
  592 +
  593 + if (visible) {
  594 + reflow();
  595 + return;
  596 + }
  597 +
  598 + boundElement.trigger(event, [ get() ]);
  599 +
  600 + if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
  601 + return;
  602 + }
  603 +
  604 + hideAll();
  605 + visible = true;
  606 +
  607 + $(doc).bind("click.spectrum", hide);
  608 + $(window).bind("resize.spectrum", resize);
  609 + replacer.addClass("sp-active");
  610 + container.removeClass("sp-hidden");
  611 +
  612 + reflow();
  613 + updateUI();
  614 +
  615 + colorOnShow = get();
  616 +
  617 + drawInitial();
  618 + callbacks.show(colorOnShow);
  619 + boundElement.trigger('show.spectrum', [ colorOnShow ]);
  620 + }
  621 +
  622 + function hide(e) {
  623 +
  624 + // Return on right click
  625 + if (e && e.type == "click" && e.button == 2) { return; }
  626 +
  627 + // Return if hiding is unnecessary
  628 + if (!visible || flat) { return; }
  629 + visible = false;
  630 +
  631 + $(doc).unbind("click.spectrum", hide);
  632 + $(window).unbind("resize.spectrum", resize);
  633 +
  634 + replacer.removeClass("sp-active");
  635 + container.addClass("sp-hidden");
  636 +
  637 + var colorHasChanged = !tinycolor.equals(get(), colorOnShow);
  638 +
  639 + if (colorHasChanged) {
  640 + if (clickoutFiresChange && e !== "cancel") {
  641 + updateOriginalInput(true);
  642 + }
  643 + else {
  644 + revert();
  645 + }
  646 + }
  647 +
  648 + callbacks.hide(get());
  649 + boundElement.trigger('hide.spectrum', [ get() ]);
  650 + }
  651 +
  652 + function revert() {
  653 + set(colorOnShow, true);
  654 + }
  655 +
  656 + function set(color, ignoreFormatChange) {
  657 + if (tinycolor.equals(color, get())) {
  658 + // Update UI just in case a validation error needs
  659 + // to be cleared.
  660 + updateUI();
  661 + return;
  662 + }
  663 +
  664 + var newColor, newHsv;
  665 + if (!color && allowEmpty) {
  666 + isEmpty = true;
  667 + } else {
  668 + isEmpty = false;
  669 + newColor = tinycolor(color);
  670 + newHsv = newColor.toHsv();
  671 +
  672 + currentHue = (newHsv.h % 360) / 360;
  673 + currentSaturation = newHsv.s;
  674 + currentValue = newHsv.v;
  675 + currentAlpha = newHsv.a;
  676 + }
  677 + updateUI();
  678 +
  679 + if (newColor && newColor.isValid() && !ignoreFormatChange) {
  680 + currentPreferredFormat = preferredFormat || newColor.getFormat();
  681 + }
  682 + }
  683 +
  684 + function get(opts) {
  685 + opts = opts || { };
  686 +
  687 + if (allowEmpty && isEmpty) {
  688 + return null;
  689 + }
  690 +
  691 + return tinycolor.fromRatio({
  692 + h: currentHue,
  693 + s: currentSaturation,
  694 + v: currentValue,
  695 + a: Math.round(currentAlpha * 100) / 100
  696 + }, { format: opts.format || currentPreferredFormat });
  697 + }
  698 +
  699 + function isValid() {
  700 + return !textInput.hasClass("sp-validation-error");
  701 + }
  702 +
  703 + function move() {
  704 + updateUI();
  705 +
  706 + callbacks.move(get());
  707 + boundElement.trigger('move.spectrum', [ get() ]);
  708 + }
  709 +
  710 + function updateUI() {
  711 +
  712 + textInput.removeClass("sp-validation-error");
  713 +
  714 + updateHelperLocations();
  715 +
  716 + // Update dragger background color (gradients take care of saturation and value).
  717 + var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
  718 + dragger.css("background-color", flatColor.toHexString());
  719 +
  720 + // Get a format that alpha will be included in (hex and names ignore alpha)
  721 + var format = currentPreferredFormat;
  722 + if (currentAlpha < 1 && !(currentAlpha === 0 && format === "name")) {
  723 + if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
  724 + format = "rgb";
  725 + }
  726 + }
  727 +
  728 + var realColor = get({ format: format }),
  729 + displayColor = '';
  730 +
  731 + //reset background info for preview element
  732 + previewElement.removeClass("sp-clear-display");
  733 + previewElement.css('background-color', 'transparent');
  734 +
  735 + if (!realColor && allowEmpty) {
  736 + // Update the replaced elements background with icon indicating no color selection
  737 + previewElement.addClass("sp-clear-display");
  738 + }
  739 + else {
  740 + var realHex = realColor.toHexString(),
  741 + realRgb = realColor.toRgbString();
  742 +
  743 + // Update the replaced elements background color (with actual selected color)
  744 + if (rgbaSupport || realColor.alpha === 1) {
  745 + previewElement.css("background-color", realRgb);
  746 + }
  747 + else {
  748 + previewElement.css("background-color", "transparent");
  749 + previewElement.css("filter", realColor.toFilter());
  750 + }
  751 +
  752 + if (opts.showAlpha) {
  753 + var rgb = realColor.toRgb();
  754 + rgb.a = 0;
  755 + var realAlpha = tinycolor(rgb).toRgbString();
  756 + var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
  757 +
  758 + if (IE) {
  759 + alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
  760 + }
  761 + else {
  762 + alphaSliderInner.css("background", "-webkit-" + gradient);
  763 + alphaSliderInner.css("background", "-moz-" + gradient);
  764 + alphaSliderInner.css("background", "-ms-" + gradient);
  765 + // Use current syntax gradient on unprefixed property.
  766 + alphaSliderInner.css("background",
  767 + "linear-gradient(to right, " + realAlpha + ", " + realHex + ")");
  768 + }
  769 + }
  770 +
  771 + displayColor = realColor.toString(format);
  772 + }
  773 +
  774 + // Update the text entry input as it changes happen
  775 + if (opts.showInput) {
  776 + textInput.val(displayColor);
  777 + }
  778 +
  779 + if (opts.showPalette) {
  780 + drawPalette();
  781 + }
  782 +
  783 + drawInitial();
  784 + }
  785 +
  786 + function updateHelperLocations() {
  787 + var s = currentSaturation;
  788 + var v = currentValue;
  789 +
  790 + if(allowEmpty && isEmpty) {
  791 + //if selected color is empty, hide the helpers
  792 + alphaSlideHelper.hide();
  793 + slideHelper.hide();
  794 + dragHelper.hide();
  795 + }
  796 + else {
  797 + //make sure helpers are visible
  798 + alphaSlideHelper.show();
  799 + slideHelper.show();
  800 + dragHelper.show();
  801 +
  802 + // Where to show the little circle in that displays your current selected color
  803 + var dragX = s * dragWidth;
  804 + var dragY = dragHeight - (v * dragHeight);
  805 + dragX = Math.max(
  806 + -dragHelperHeight,
  807 + Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
  808 + );
  809 + dragY = Math.max(
  810 + -dragHelperHeight,
  811 + Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
  812 + );
  813 + dragHelper.css({
  814 + "top": dragY + "px",
  815 + "left": dragX + "px"
  816 + });
  817 +
  818 + var alphaX = currentAlpha * alphaWidth;
  819 + alphaSlideHelper.css({
  820 + "left": (alphaX - (alphaSlideHelperWidth / 2)) + "px"
  821 + });
  822 +
  823 + // Where to show the bar that displays your current selected hue
  824 + var slideY = (currentHue) * slideHeight;
  825 + slideHelper.css({
  826 + "top": (slideY - slideHelperHeight) + "px"
  827 + });
  828 + }
  829 + }
  830 +
  831 + function updateOriginalInput(fireCallback) {
  832 + var color = get(),
  833 + displayColor = '',
  834 + hasChanged = !tinycolor.equals(color, colorOnShow);
  835 +
  836 + if (color) {
  837 + displayColor = color.toString(currentPreferredFormat);
  838 + // Update the selection palette with the current color
  839 + addColorToSelectionPalette(color);
  840 + }
  841 +
  842 + if (isInput) {
  843 + boundElement.val(displayColor);
  844 + }
  845 +
  846 + colorOnShow = color;
  847 +
  848 + if (fireCallback && hasChanged) {
  849 + callbacks.change(color);
  850 + boundElement.trigger('change', [ color ]);
  851 + }
  852 + }
  853 +
  854 + function reflow() {
  855 + dragWidth = dragger.width();
  856 + dragHeight = dragger.height();
  857 + dragHelperHeight = dragHelper.height();
  858 + slideWidth = slider.width();
  859 + slideHeight = slider.height();
  860 + slideHelperHeight = slideHelper.height();
  861 + alphaWidth = alphaSlider.width();
  862 + alphaSlideHelperWidth = alphaSlideHelper.width();
  863 +
  864 + if (!flat) {
  865 + container.css("position", "absolute");
  866 + container.offset(getOffset(container, offsetElement));
  867 + }
  868 +
  869 + updateHelperLocations();
  870 +
  871 + if (opts.showPalette) {
  872 + drawPalette();
  873 + }
  874 +
  875 + boundElement.trigger('reflow.spectrum');
  876 + }
  877 +
  878 + function destroy() {
  879 + boundElement.show();
  880 + offsetElement.unbind("click.spectrum touchstart.spectrum");
  881 + container.remove();
  882 + replacer.remove();
  883 + spectrums[spect.id] = null;
  884 + }
  885 +
  886 + function option(optionName, optionValue) {
  887 + if (optionName === undefined) {
  888 + return $.extend({}, opts);
  889 + }
  890 + if (optionValue === undefined) {
  891 + return opts[optionName];
  892 + }
  893 +
  894 + opts[optionName] = optionValue;
  895 + applyOptions();
  896 + }
  897 +
  898 + function enable() {
  899 + disabled = false;
  900 + boundElement.attr("disabled", false);
  901 + offsetElement.removeClass("sp-disabled");
  902 + }
  903 +
  904 + function disable() {
  905 + hide();
  906 + disabled = true;
  907 + boundElement.attr("disabled", true);
  908 + offsetElement.addClass("sp-disabled");
  909 + }
  910 +
  911 + initialize();
  912 +
  913 + var spect = {
  914 + show: show,
  915 + hide: hide,
  916 + toggle: toggle,
  917 + reflow: reflow,
  918 + option: option,
  919 + enable: enable,
  920 + disable: disable,
  921 + set: function (c) {
  922 + set(c);
  923 + updateOriginalInput();
  924 + },
  925 + get: get,
  926 + destroy: destroy,
  927 + container: container
  928 + };
  929 +
  930 + spect.id = spectrums.push(spect) - 1;
  931 +
  932 + return spect;
  933 + }
  934 +
  935 + /**
  936 + * checkOffset - get the offset below/above and left/right element depending on screen position
  937 + * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
  938 + */
  939 + function getOffset(picker, input) {
  940 + var extraY = 0;
  941 + var dpWidth = picker.outerWidth();
  942 + var dpHeight = picker.outerHeight();
  943 + var inputHeight = input.outerHeight();
  944 + var doc = picker[0].ownerDocument;
  945 + var docElem = doc.documentElement;
  946 + var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
  947 + var viewHeight = docElem.clientHeight + $(doc).scrollTop();
  948 + var offset = input.offset();
  949 + offset.top += inputHeight;
  950 +
  951 + offset.left -=
  952 + Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
  953 + Math.abs(offset.left + dpWidth - viewWidth) : 0);
  954 +
  955 + offset.top -=
  956 + Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
  957 + Math.abs(dpHeight + inputHeight - extraY) : extraY));
  958 +
  959 + return offset;
  960 + }
  961 +
  962 + /**
  963 + * noop - do nothing
  964 + */
  965 + function noop() {
  966 +
  967 + }
  968 +
  969 + /**
  970 + * stopPropagation - makes the code only doing this a little easier to read in line
  971 + */
  972 + function stopPropagation(e) {
  973 + e.stopPropagation();
  974 + }
  975 +
  976 + /**
  977 + * Create a function bound to a given object
  978 + * Thanks to underscore.js
  979 + */
  980 + function bind(func, obj) {
  981 + var slice = Array.prototype.slice;
  982 + var args = slice.call(arguments, 2);
  983 + return function () {
  984 + return func.apply(obj, args.concat(slice.call(arguments)));
  985 + };
  986 + }
  987 +
  988 + /**
  989 + * Lightweight drag helper. Handles containment within the element, so that
  990 + * when dragging, the x is within [0,element.width] and y is within [0,element.height]
  991 + */
  992 + function draggable(element, onmove, onstart, onstop) {
  993 + onmove = onmove || function () { };
  994 + onstart = onstart || function () { };
  995 + onstop = onstop || function () { };
  996 + var doc = element.ownerDocument || document;
  997 + var dragging = false;
  998 + var offset = {};
  999 + var maxHeight = 0;
  1000 + var maxWidth = 0;
  1001 + var hasTouch = ('ontouchstart' in window);
  1002 +
  1003 + var duringDragEvents = {};
  1004 + duringDragEvents["selectstart"] = prevent;
  1005 + duringDragEvents["dragstart"] = prevent;
  1006 + duringDragEvents["touchmove mousemove"] = move;
  1007 + duringDragEvents["touchend mouseup"] = stop;
  1008 +
  1009 + function prevent(e) {
  1010 + if (e.stopPropagation) {
  1011 + e.stopPropagation();
  1012 + }
  1013 + if (e.preventDefault) {
  1014 + e.preventDefault();
  1015 + }
  1016 + e.returnValue = false;
  1017 + }
  1018 +
  1019 + function move(e) {
  1020 + if (dragging) {
  1021 + // Mouseup happened outside of window
  1022 + if (IE && document.documentMode < 9 && !e.button) {
  1023 + return stop();
  1024 + }
  1025 +
  1026 + var touches = e.originalEvent.touches;
  1027 + var pageX = touches ? touches[0].pageX : e.pageX;
  1028 + var pageY = touches ? touches[0].pageY : e.pageY;
  1029 +
  1030 + var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
  1031 + var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
  1032 +
  1033 + if (hasTouch) {
  1034 + // Stop scrolling in iOS
  1035 + prevent(e);
  1036 + }
  1037 +
  1038 + onmove.apply(element, [dragX, dragY, e]);
  1039 + }
  1040 + }
  1041 +
  1042 + function start(e) {
  1043 + var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
  1044 + var touches = e.originalEvent.touches;
  1045 +
  1046 + if (!rightclick && !dragging) {
  1047 + if (onstart.apply(element, arguments) !== false) {
  1048 + dragging = true;
  1049 + maxHeight = $(element).height();
  1050 + maxWidth = $(element).width();
  1051 + offset = $(element).offset();
  1052 +
  1053 + $(doc).bind(duringDragEvents);
  1054 + $(doc.body).addClass("sp-dragging");
  1055 +
  1056 + if (!hasTouch) {
  1057 + move(e);
  1058 + }
  1059 +
  1060 + prevent(e);
  1061 + }
  1062 + }
  1063 + }
  1064 +
  1065 + function stop() {
  1066 + if (dragging) {
  1067 + $(doc).unbind(duringDragEvents);
  1068 + $(doc.body).removeClass("sp-dragging");
  1069 + onstop.apply(element, arguments);
  1070 + }
  1071 + dragging = false;
  1072 + }
  1073 +
  1074 + $(element).bind("touchstart mousedown", start);
  1075 + }
  1076 +
  1077 + function throttle(func, wait, debounce) {
  1078 + var timeout;
  1079 + return function () {
  1080 + var context = this, args = arguments;
  1081 + var throttler = function () {
  1082 + timeout = null;
  1083 + func.apply(context, args);
  1084 + };
  1085 + if (debounce) clearTimeout(timeout);
  1086 + if (debounce || !timeout) timeout = setTimeout(throttler, wait);
  1087 + };
  1088 + }
  1089 +
  1090 + /**
  1091 + * Define a jQuery plugin
  1092 + */
  1093 + var dataID = "spectrum.id";
  1094 + $.fn.spectrum = function (opts, extra) {
  1095 +
  1096 + if (typeof opts == "string") {
  1097 +
  1098 + var returnValue = this;
  1099 + var args = Array.prototype.slice.call( arguments, 1 );
  1100 +
  1101 + this.each(function () {
  1102 + var spect = spectrums[$(this).data(dataID)];
  1103 + if (spect) {
  1104 + var method = spect[opts];
  1105 + if (!method) {
  1106 + throw new Error( "Spectrum: no such method: '" + opts + "'" );
  1107 + }
  1108 +
  1109 + if (opts == "get") {
  1110 + returnValue = spect.get();
  1111 + }
  1112 + else if (opts == "container") {
  1113 + returnValue = spect.container;
  1114 + }
  1115 + else if (opts == "option") {
  1116 + returnValue = spect.option.apply(spect, args);
  1117 + }
  1118 + else if (opts == "destroy") {
  1119 + spect.destroy();
  1120 + $(this).removeData(dataID);
  1121 + }
  1122 + else {
  1123 + method.apply(spect, args);
  1124 + }
  1125 + }
  1126 + });
  1127 +
  1128 + return returnValue;
  1129 + }
  1130 +
  1131 + // Initializing a new instance of spectrum
  1132 + return this.spectrum("destroy").each(function () {
  1133 + var options = $.extend({}, opts, $(this).data());
  1134 + var spect = spectrum(this, options);
  1135 + $(this).data(dataID, spect.id);
  1136 + });
  1137 + };
  1138 +
  1139 + $.fn.spectrum.load = true;
  1140 + $.fn.spectrum.loadOpts = {};
  1141 + $.fn.spectrum.draggable = draggable;
  1142 + $.fn.spectrum.defaults = defaultOpts;
  1143 +
  1144 + $.spectrum = { };
  1145 + $.spectrum.localization = { };
  1146 + $.spectrum.palettes = { };
  1147 +
  1148 + $.fn.spectrum.processNativeColorInputs = function () {
  1149 + if (!inputTypeColorSupport) {
  1150 + $("input[type=color]").spectrum({
  1151 + preferredFormat: "hex6"
  1152 + });
  1153 + }
  1154 + };
  1155 +
  1156 + // TinyColor v1.0.0
  1157 + // https://github.com/bgrins/TinyColor
  1158 + // Brian Grinstead, MIT License
  1159 +
  1160 + (function() {
  1161 +
  1162 + var trimLeft = /^[\s,#]+/,
  1163 + trimRight = /\s+$/,
  1164 + tinyCounter = 0,
  1165 + math = Math,
  1166 + mathRound = math.round,
  1167 + mathMin = math.min,
  1168 + mathMax = math.max,
  1169 + mathRandom = math.random;
  1170 +
  1171 + var tinycolor = function tinycolor (color, opts) {
  1172 +
  1173 + color = (color) ? color : '';
  1174 + opts = opts || { };
  1175 +
  1176 + // If input is already a tinycolor, return itself
  1177 + if (color instanceof tinycolor) {
  1178 + return color;
  1179 + }
  1180 + // If we are called as a function, call using new instead
  1181 + if (!(this instanceof tinycolor)) {
  1182 + return new tinycolor(color, opts);
  1183 + }
  1184 +
  1185 + var rgb = inputToRGB(color);
  1186 + this._r = rgb.r,
  1187 + this._g = rgb.g,
  1188 + this._b = rgb.b,
  1189 + this._a = rgb.a,
  1190 + this._roundA = mathRound(100*this._a) / 100,
  1191 + this._format = opts.format || rgb.format;
  1192 + this._gradientType = opts.gradientType;
  1193 +
  1194 + // Don't let the range of [0,255] come back in [0,1].
  1195 + // Potentially lose a little bit of precision here, but will fix issues where
  1196 + // .5 gets interpreted as half of the total, instead of half of 1
  1197 + // If it was supposed to be 128, this was already taken care of by `inputToRgb`
  1198 + if (this._r < 1) { this._r = mathRound(this._r); }
  1199 + if (this._g < 1) { this._g = mathRound(this._g); }
  1200 + if (this._b < 1) { this._b = mathRound(this._b); }
  1201 +
  1202 + this._ok = rgb.ok;
  1203 + this._tc_id = tinyCounter++;
  1204 + };
  1205 +
  1206 + tinycolor.prototype = {
  1207 + isDark: function() {
  1208 + return this.getBrightness() < 128;
  1209 + },
  1210 + isLight: function() {
  1211 + return !this.isDark();
  1212 + },
  1213 + isValid: function() {
  1214 + return this._ok;
  1215 + },
  1216 + getFormat: function() {
  1217 + return this._format;
  1218 + },
  1219 + getAlpha: function() {
  1220 + return this._a;
  1221 + },
  1222 + getBrightness: function() {
  1223 + var rgb = this.toRgb();
  1224 + return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
  1225 + },
  1226 + setAlpha: function(value) {
  1227 + this._a = boundAlpha(value);
  1228 + this._roundA = mathRound(100*this._a) / 100;
  1229 + return this;
  1230 + },
  1231 + toHsv: function() {
  1232 + var hsv = rgbToHsv(this._r, this._g, this._b);
  1233 + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
  1234 + },
  1235 + toHsvString: function() {
  1236 + var hsv = rgbToHsv(this._r, this._g, this._b);
  1237 + var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
  1238 + return (this._a == 1) ?
  1239 + "hsv(" + h + ", " + s + "%, " + v + "%)" :
  1240 + "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
  1241 + },
  1242 + toHsl: function() {
  1243 + var hsl = rgbToHsl(this._r, this._g, this._b);
  1244 + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
  1245 + },
  1246 + toHslString: function() {
  1247 + var hsl = rgbToHsl(this._r, this._g, this._b);
  1248 + var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
  1249 + return (this._a == 1) ?
  1250 + "hsl(" + h + ", " + s + "%, " + l + "%)" :
  1251 + "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
  1252 + },
  1253 + toHex: function(allow3Char) {
  1254 + return rgbToHex(this._r, this._g, this._b, allow3Char);
  1255 + },
  1256 + toHexString: function(allow3Char) {
  1257 + return '#' + this.toHex(allow3Char);
  1258 + },
  1259 + toHex8: function() {
  1260 + return rgbaToHex(this._r, this._g, this._b, this._a);
  1261 + },
  1262 + toHex8String: function() {
  1263 + return '#' + this.toHex8();
  1264 + },
  1265 + toRgb: function() {
  1266 + return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
  1267 + },
  1268 + toRgbString: function() {
  1269 + return (this._a == 1) ?
  1270 + "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
  1271 + "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
  1272 + },
  1273 + toPercentageRgb: function() {
  1274 + return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
  1275 + },
  1276 + toPercentageRgbString: function() {
  1277 + return (this._a == 1) ?
  1278 + "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
  1279 + "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
  1280 + },
  1281 + toName: function() {
  1282 + if (this._a === 0) {
  1283 + return "transparent";
  1284 + }
  1285 +
  1286 + if (this._a < 1) {
  1287 + return false;
  1288 + }
  1289 +
  1290 + return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
  1291 + },
  1292 + toFilter: function(secondColor) {
  1293 + var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a);
  1294 + var secondHex8String = hex8String;
  1295 + var gradientType = this._gradientType ? "GradientType = 1, " : "";
  1296 +
  1297 + if (secondColor) {
  1298 + var s = tinycolor(secondColor);
  1299 + secondHex8String = s.toHex8String();
  1300 + }
  1301 +
  1302 + return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
  1303 + },
  1304 + toString: function(format) {
  1305 + var formatSet = !!format;
  1306 + format = format || this._format;
  1307 +
  1308 + var formattedString = false;
  1309 + var hasAlpha = this._a < 1 && this._a >= 0;
  1310 + var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
  1311 +
  1312 + if (needsAlphaFormat) {
  1313 + // Special case for "transparent", all other non-alpha formats
  1314 + // will return rgba when there is transparency.
  1315 + if (format === "name" && this._a === 0) {
  1316 + return this.toName();
  1317 + }
  1318 + return this.toRgbString();
  1319 + }
  1320 + if (format === "rgb") {
  1321 + formattedString = this.toRgbString();
  1322 + }
  1323 + if (format === "prgb") {
  1324 + formattedString = this.toPercentageRgbString();
  1325 + }
  1326 + if (format === "hex" || format === "hex6") {
  1327 + formattedString = this.toHexString();
  1328 + }
  1329 + if (format === "hex3") {
  1330 + formattedString = this.toHexString(true);
  1331 + }
  1332 + if (format === "hex8") {
  1333 + formattedString = this.toHex8String();
  1334 + }
  1335 + if (format === "name") {
  1336 + formattedString = this.toName();
  1337 + }
  1338 + if (format === "hsl") {
  1339 + formattedString = this.toHslString();
  1340 + }
  1341 + if (format === "hsv") {
  1342 + formattedString = this.toHsvString();
  1343 + }
  1344 +
  1345 + return formattedString || this.toHexString();
  1346 + },
  1347 +
  1348 + _applyModification: function(fn, args) {
  1349 + var color = fn.apply(null, [this].concat([].slice.call(args)));
  1350 + this._r = color._r;
  1351 + this._g = color._g;
  1352 + this._b = color._b;
  1353 + this.setAlpha(color._a);
  1354 + return this;
  1355 + },
  1356 + lighten: function() {
  1357 + return this._applyModification(lighten, arguments);
  1358 + },
  1359 + brighten: function() {
  1360 + return this._applyModification(brighten, arguments);
  1361 + },
  1362 + darken: function() {
  1363 + return this._applyModification(darken, arguments);
  1364 + },
  1365 + desaturate: function() {
  1366 + return this._applyModification(desaturate, arguments);
  1367 + },
  1368 + saturate: function() {
  1369 + return this._applyModification(saturate, arguments);
  1370 + },
  1371 + greyscale: function() {
  1372 + return this._applyModification(greyscale, arguments);
  1373 + },
  1374 + spin: function() {
  1375 + return this._applyModification(spin, arguments);
  1376 + },
  1377 +
  1378 + _applyCombination: function(fn, args) {
  1379 + return fn.apply(null, [this].concat([].slice.call(args)));
  1380 + },
  1381 + analogous: function() {
  1382 + return this._applyCombination(analogous, arguments);
  1383 + },
  1384 + complement: function() {
  1385 + return this._applyCombination(complement, arguments);
  1386 + },
  1387 + monochromatic: function() {
  1388 + return this._applyCombination(monochromatic, arguments);
  1389 + },
  1390 + splitcomplement: function() {
  1391 + return this._applyCombination(splitcomplement, arguments);
  1392 + },
  1393 + triad: function() {
  1394 + return this._applyCombination(triad, arguments);
  1395 + },
  1396 + tetrad: function() {
  1397 + return this._applyCombination(tetrad, arguments);
  1398 + }
  1399 + };
  1400 +
  1401 + // If input is an object, force 1 into "1.0" to handle ratios properly
  1402 + // String input requires "1.0" as input, so 1 will be treated as 1
  1403 + tinycolor.fromRatio = function(color, opts) {
  1404 + if (typeof color == "object") {
  1405 + var newColor = {};
  1406 + for (var i in color) {
  1407 + if (color.hasOwnProperty(i)) {
  1408 + if (i === "a") {
  1409 + newColor[i] = color[i];
  1410 + }
  1411 + else {
  1412 + newColor[i] = convertToPercentage(color[i]);
  1413 + }
  1414 + }
  1415 + }
  1416 + color = newColor;
  1417 + }
  1418 +
  1419 + return tinycolor(color, opts);
  1420 + };
  1421 +
  1422 + // Given a string or object, convert that input to RGB
  1423 + // Possible string inputs:
  1424 + //
  1425 + // "red"
  1426 + // "#f00" or "f00"
  1427 + // "#ff0000" or "ff0000"
  1428 + // "#ff000000" or "ff000000"
  1429 + // "rgb 255 0 0" or "rgb (255, 0, 0)"
  1430 + // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
  1431 + // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
  1432 + // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
  1433 + // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
  1434 + // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
  1435 + // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
  1436 + //
  1437 + function inputToRGB(color) {
  1438 +
  1439 + var rgb = { r: 0, g: 0, b: 0 };
  1440 + var a = 1;
  1441 + var ok = false;
  1442 + var format = false;
  1443 +
  1444 + if (typeof color == "string") {
  1445 + color = stringInputToObject(color);
  1446 + }
  1447 +
  1448 + if (typeof color == "object") {
  1449 + if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
  1450 + rgb = rgbToRgb(color.r, color.g, color.b);
  1451 + ok = true;
  1452 + format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
  1453 + }
  1454 + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
  1455 + color.s = convertToPercentage(color.s);
  1456 + color.v = convertToPercentage(color.v);
  1457 + rgb = hsvToRgb(color.h, color.s, color.v);
  1458 + ok = true;
  1459 + format = "hsv";
  1460 + }
  1461 + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
  1462 + color.s = convertToPercentage(color.s);
  1463 + color.l = convertToPercentage(color.l);
  1464 + rgb = hslToRgb(color.h, color.s, color.l);
  1465 + ok = true;
  1466 + format = "hsl";
  1467 + }
  1468 +
  1469 + if (color.hasOwnProperty("a")) {
  1470 + a = color.a;
  1471 + }
  1472 + }
  1473 +
  1474 + a = boundAlpha(a);
  1475 +
  1476 + return {
  1477 + ok: ok,
  1478 + format: color.format || format,
  1479 + r: mathMin(255, mathMax(rgb.r, 0)),
  1480 + g: mathMin(255, mathMax(rgb.g, 0)),
  1481 + b: mathMin(255, mathMax(rgb.b, 0)),
  1482 + a: a
  1483 + };
  1484 + }
  1485 +
  1486 +
  1487 + // Conversion Functions
  1488 + // --------------------
  1489 +
  1490 + // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
  1491 + // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
  1492 +
  1493 + // `rgbToRgb`
  1494 + // Handle bounds / percentage checking to conform to CSS color spec
  1495 + // <http://www.w3.org/TR/css3-color/>
  1496 + // *Assumes:* r, g, b in [0, 255] or [0, 1]
  1497 + // *Returns:* { r, g, b } in [0, 255]
  1498 + function rgbToRgb(r, g, b){
  1499 + return {
  1500 + r: bound01(r, 255) * 255,
  1501 + g: bound01(g, 255) * 255,
  1502 + b: bound01(b, 255) * 255
  1503 + };
  1504 + }
  1505 +
  1506 + // `rgbToHsl`
  1507 + // Converts an RGB color value to HSL.
  1508 + // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
  1509 + // *Returns:* { h, s, l } in [0,1]
  1510 + function rgbToHsl(r, g, b) {
  1511 +
  1512 + r = bound01(r, 255);
  1513 + g = bound01(g, 255);
  1514 + b = bound01(b, 255);
  1515 +
  1516 + var max = mathMax(r, g, b), min = mathMin(r, g, b);
  1517 + var h, s, l = (max + min) / 2;
  1518 +
  1519 + if(max == min) {
  1520 + h = s = 0; // achromatic
  1521 + }
  1522 + else {
  1523 + var d = max - min;
  1524 + s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  1525 + switch(max) {
  1526 + case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  1527 + case g: h = (b - r) / d + 2; break;
  1528 + case b: h = (r - g) / d + 4; break;
  1529 + }
  1530 +
  1531 + h /= 6;
  1532 + }
  1533 +
  1534 + return { h: h, s: s, l: l };
  1535 + }
  1536 +
  1537 + // `hslToRgb`
  1538 + // Converts an HSL color value to RGB.
  1539 + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
  1540 + // *Returns:* { r, g, b } in the set [0, 255]
  1541 + function hslToRgb(h, s, l) {
  1542 + var r, g, b;
  1543 +
  1544 + h = bound01(h, 360);
  1545 + s = bound01(s, 100);
  1546 + l = bound01(l, 100);
  1547 +
  1548 + function hue2rgb(p, q, t) {
  1549 + if(t < 0) t += 1;
  1550 + if(t > 1) t -= 1;
  1551 + if(t < 1/6) return p + (q - p) * 6 * t;
  1552 + if(t < 1/2) return q;
  1553 + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  1554 + return p;
  1555 + }
  1556 +
  1557 + if(s === 0) {
  1558 + r = g = b = l; // achromatic
  1559 + }
  1560 + else {
  1561 + var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  1562 + var p = 2 * l - q;
  1563 + r = hue2rgb(p, q, h + 1/3);
  1564 + g = hue2rgb(p, q, h);
  1565 + b = hue2rgb(p, q, h - 1/3);
  1566 + }
  1567 +
  1568 + return { r: r * 255, g: g * 255, b: b * 255 };
  1569 + }
  1570 +
  1571 + // `rgbToHsv`
  1572 + // Converts an RGB color value to HSV
  1573 + // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
  1574 + // *Returns:* { h, s, v } in [0,1]
  1575 + function rgbToHsv(r, g, b) {
  1576 +
  1577 + r = bound01(r, 255);
  1578 + g = bound01(g, 255);
  1579 + b = bound01(b, 255);
  1580 +
  1581 + var max = mathMax(r, g, b), min = mathMin(r, g, b);
  1582 + var h, s, v = max;
  1583 +
  1584 + var d = max - min;
  1585 + s = max === 0 ? 0 : d / max;
  1586 +
  1587 + if(max == min) {
  1588 + h = 0; // achromatic
  1589 + }
  1590 + else {
  1591 + switch(max) {
  1592 + case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  1593 + case g: h = (b - r) / d + 2; break;
  1594 + case b: h = (r - g) / d + 4; break;
  1595 + }
  1596 + h /= 6;
  1597 + }
  1598 + return { h: h, s: s, v: v };
  1599 + }
  1600 +
  1601 + // `hsvToRgb`
  1602 + // Converts an HSV color value to RGB.
  1603 + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
  1604 + // *Returns:* { r, g, b } in the set [0, 255]
  1605 + function hsvToRgb(h, s, v) {
  1606 +
  1607 + h = bound01(h, 360) * 6;
  1608 + s = bound01(s, 100);
  1609 + v = bound01(v, 100);
  1610 +
  1611 + var i = math.floor(h),
  1612 + f = h - i,
  1613 + p = v * (1 - s),
  1614 + q = v * (1 - f * s),
  1615 + t = v * (1 - (1 - f) * s),
  1616 + mod = i % 6,
  1617 + r = [v, q, p, p, t, v][mod],
  1618 + g = [t, v, v, q, p, p][mod],
  1619 + b = [p, p, t, v, v, q][mod];
  1620 +
  1621 + return { r: r * 255, g: g * 255, b: b * 255 };
  1622 + }
  1623 +
  1624 + // `rgbToHex`
  1625 + // Converts an RGB color to hex
  1626 + // Assumes r, g, and b are contained in the set [0, 255]
  1627 + // Returns a 3 or 6 character hex
  1628 + function rgbToHex(r, g, b, allow3Char) {
  1629 +
  1630 + var hex = [
  1631 + pad2(mathRound(r).toString(16)),
  1632 + pad2(mathRound(g).toString(16)),
  1633 + pad2(mathRound(b).toString(16))
  1634 + ];
  1635 +
  1636 + // Return a 3 character hex if possible
  1637 + if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
  1638 + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
  1639 + }
  1640 +
  1641 + return hex.join("");
  1642 + }
  1643 + // `rgbaToHex`
  1644 + // Converts an RGBA color plus alpha transparency to hex
  1645 + // Assumes r, g, b and a are contained in the set [0, 255]
  1646 + // Returns an 8 character hex
  1647 + function rgbaToHex(r, g, b, a) {
  1648 +
  1649 + var hex = [
  1650 + pad2(convertDecimalToHex(a)),
  1651 + pad2(mathRound(r).toString(16)),
  1652 + pad2(mathRound(g).toString(16)),
  1653 + pad2(mathRound(b).toString(16))
  1654 + ];
  1655 +
  1656 + return hex.join("");
  1657 + }
  1658 +
  1659 + // `equals`
  1660 + // Can be called with any tinycolor input
  1661 + tinycolor.equals = function (color1, color2) {
  1662 + if (!color1 || !color2) { return false; }
  1663 + return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
  1664 + };
  1665 + tinycolor.random = function() {
  1666 + return tinycolor.fromRatio({
  1667 + r: mathRandom(),
  1668 + g: mathRandom(),
  1669 + b: mathRandom()
  1670 + });
  1671 + };
  1672 +
  1673 +
  1674 + // Modification Functions
  1675 + // ----------------------
  1676 + // Thanks to less.js for some of the basics here
  1677 + // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
  1678 +
  1679 + function desaturate(color, amount) {
  1680 + amount = (amount === 0) ? 0 : (amount || 10);
  1681 + var hsl = tinycolor(color).toHsl();
  1682 + hsl.s -= amount / 100;
  1683 + hsl.s = clamp01(hsl.s);
  1684 + return tinycolor(hsl);
  1685 + }
  1686 +
  1687 + function saturate(color, amount) {
  1688 + amount = (amount === 0) ? 0 : (amount || 10);
  1689 + var hsl = tinycolor(color).toHsl();
  1690 + hsl.s += amount / 100;
  1691 + hsl.s = clamp01(hsl.s);
  1692 + return tinycolor(hsl);
  1693 + }
  1694 +
  1695 + function greyscale(color) {
  1696 + return tinycolor(color).desaturate(100);
  1697 + }
  1698 +
  1699 + function lighten (color, amount) {
  1700 + amount = (amount === 0) ? 0 : (amount || 10);
  1701 + var hsl = tinycolor(color).toHsl();
  1702 + hsl.l += amount / 100;
  1703 + hsl.l = clamp01(hsl.l);
  1704 + return tinycolor(hsl);
  1705 + }
  1706 +
  1707 + function brighten(color, amount) {
  1708 + amount = (amount === 0) ? 0 : (amount || 10);
  1709 + var rgb = tinycolor(color).toRgb();
  1710 + rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
  1711 + rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
  1712 + rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
  1713 + return tinycolor(rgb);
  1714 + }
  1715 +
  1716 + function darken (color, amount) {
  1717 + amount = (amount === 0) ? 0 : (amount || 10);
  1718 + var hsl = tinycolor(color).toHsl();
  1719 + hsl.l -= amount / 100;
  1720 + hsl.l = clamp01(hsl.l);
  1721 + return tinycolor(hsl);
  1722 + }
  1723 +
  1724 + // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
  1725 + // Values outside of this range will be wrapped into this range.
  1726 + function spin(color, amount) {
  1727 + var hsl = tinycolor(color).toHsl();
  1728 + var hue = (mathRound(hsl.h) + amount) % 360;
  1729 + hsl.h = hue < 0 ? 360 + hue : hue;
  1730 + return tinycolor(hsl);
  1731 + }
  1732 +
  1733 + // Combination Functions
  1734 + // ---------------------
  1735 + // Thanks to jQuery xColor for some of the ideas behind these
  1736 + // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
  1737 +
  1738 + function complement(color) {
  1739 + var hsl = tinycolor(color).toHsl();
  1740 + hsl.h = (hsl.h + 180) % 360;
  1741 + return tinycolor(hsl);
  1742 + }
  1743 +
  1744 + function triad(color) {
  1745 + var hsl = tinycolor(color).toHsl();
  1746 + var h = hsl.h;
  1747 + return [
  1748 + tinycolor(color),
  1749 + tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
  1750 + tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
  1751 + ];
  1752 + }
  1753 +
  1754 + function tetrad(color) {
  1755 + var hsl = tinycolor(color).toHsl();
  1756 + var h = hsl.h;
  1757 + return [
  1758 + tinycolor(color),
  1759 + tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
  1760 + tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
  1761 + tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
  1762 + ];
  1763 + }
  1764 +
  1765 + function splitcomplement(color) {
  1766 + var hsl = tinycolor(color).toHsl();
  1767 + var h = hsl.h;
  1768 + return [
  1769 + tinycolor(color),
  1770 + tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
  1771 + tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
  1772 + ];
  1773 + }
  1774 +
  1775 + function analogous(color, results, slices) {
  1776 + results = results || 6;
  1777 + slices = slices || 30;
  1778 +
  1779 + var hsl = tinycolor(color).toHsl();
  1780 + var part = 360 / slices;
  1781 + var ret = [tinycolor(color)];
  1782 +
  1783 + for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
  1784 + hsl.h = (hsl.h + part) % 360;
  1785 + ret.push(tinycolor(hsl));
  1786 + }
  1787 + return ret;
  1788 + }
  1789 +
  1790 + function monochromatic(color, results) {
  1791 + results = results || 6;
  1792 + var hsv = tinycolor(color).toHsv();
  1793 + var h = hsv.h, s = hsv.s, v = hsv.v;
  1794 + var ret = [];
  1795 + var modification = 1 / results;
  1796 +
  1797 + while (results--) {
  1798 + ret.push(tinycolor({ h: h, s: s, v: v}));
  1799 + v = (v + modification) % 1;
  1800 + }
  1801 +
  1802 + return ret;
  1803 + }
  1804 +
  1805 + // Utility Functions
  1806 + // ---------------------
  1807 +
  1808 + tinycolor.mix = function(color1, color2, amount) {
  1809 + amount = (amount === 0) ? 0 : (amount || 50);
  1810 +
  1811 + var rgb1 = tinycolor(color1).toRgb();
  1812 + var rgb2 = tinycolor(color2).toRgb();
  1813 +
  1814 + var p = amount / 100;
  1815 + var w = p * 2 - 1;
  1816 + var a = rgb2.a - rgb1.a;
  1817 +
  1818 + var w1;
  1819 +
  1820 + if (w * a == -1) {
  1821 + w1 = w;
  1822 + } else {
  1823 + w1 = (w + a) / (1 + w * a);
  1824 + }
  1825 +
  1826 + w1 = (w1 + 1) / 2;
  1827 +
  1828 + var w2 = 1 - w1;
  1829 +
  1830 + var rgba = {
  1831 + r: rgb2.r * w1 + rgb1.r * w2,
  1832 + g: rgb2.g * w1 + rgb1.g * w2,
  1833 + b: rgb2.b * w1 + rgb1.b * w2,
  1834 + a: rgb2.a * p + rgb1.a * (1 - p)
  1835 + };
  1836 +
  1837 + return tinycolor(rgba);
  1838 + };
  1839 +
  1840 +
  1841 + // Readability Functions
  1842 + // ---------------------
  1843 + // <http://www.w3.org/TR/AERT#color-contrast>
  1844 +
  1845 + // `readability`
  1846 + // Analyze the 2 colors and returns an object with the following properties:
  1847 + // `brightness`: difference in brightness between the two colors
  1848 + // `color`: difference in color/hue between the two colors
  1849 + tinycolor.readability = function(color1, color2) {
  1850 + var c1 = tinycolor(color1);
  1851 + var c2 = tinycolor(color2);
  1852 + var rgb1 = c1.toRgb();
  1853 + var rgb2 = c2.toRgb();
  1854 + var brightnessA = c1.getBrightness();
  1855 + var brightnessB = c2.getBrightness();
  1856 + var colorDiff = (
  1857 + Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
  1858 + Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
  1859 + Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
  1860 + );
  1861 +
  1862 + return {
  1863 + brightness: Math.abs(brightnessA - brightnessB),
  1864 + color: colorDiff
  1865 + };
  1866 + };
  1867 +
  1868 + // `readable`
  1869 + // http://www.w3.org/TR/AERT#color-contrast
  1870 + // Ensure that foreground and background color combinations provide sufficient contrast.
  1871 + // *Example*
  1872 + // tinycolor.isReadable("#000", "#111") => false
  1873 + tinycolor.isReadable = function(color1, color2) {
  1874 + var readability = tinycolor.readability(color1, color2);
  1875 + return readability.brightness > 125 && readability.color > 500;
  1876 + };
  1877 +
  1878 + // `mostReadable`
  1879 + // Given a base color and a list of possible foreground or background
  1880 + // colors for that base, returns the most readable color.
  1881 + // *Example*
  1882 + // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
  1883 + tinycolor.mostReadable = function(baseColor, colorList) {
  1884 + var bestColor = null;
  1885 + var bestScore = 0;
  1886 + var bestIsReadable = false;
  1887 + for (var i=0; i < colorList.length; i++) {
  1888 +
  1889 + // We normalize both around the "acceptable" breaking point,
  1890 + // but rank brightness constrast higher than hue.
  1891 +
  1892 + var readability = tinycolor.readability(baseColor, colorList[i]);
  1893 + var readable = readability.brightness > 125 && readability.color > 500;
  1894 + var score = 3 * (readability.brightness / 125) + (readability.color / 500);
  1895 +
  1896 + if ((readable && ! bestIsReadable) ||
  1897 + (readable && bestIsReadable && score > bestScore) ||
  1898 + ((! readable) && (! bestIsReadable) && score > bestScore)) {
  1899 + bestIsReadable = readable;
  1900 + bestScore = score;
  1901 + bestColor = tinycolor(colorList[i]);
  1902 + }
  1903 + }
  1904 + return bestColor;
  1905 + };
  1906 +
  1907 +
  1908 + // Big List of Colors
  1909 + // ------------------
  1910 + // <http://www.w3.org/TR/css3-color/#svg-color>
  1911 + var names = tinycolor.names = {
  1912 + aliceblue: "f0f8ff",
  1913 + antiquewhite: "faebd7",
  1914 + aqua: "0ff",
  1915 + aquamarine: "7fffd4",
  1916 + azure: "f0ffff",
  1917 + beige: "f5f5dc",
  1918 + bisque: "ffe4c4",
  1919 + black: "000",
  1920 + blanchedalmond: "ffebcd",
  1921 + blue: "00f",
  1922 + blueviolet: "8a2be2",
  1923 + brown: "a52a2a",
  1924 + burlywood: "deb887",
  1925 + burntsienna: "ea7e5d",
  1926 + cadetblue: "5f9ea0",
  1927 + chartreuse: "7fff00",
  1928 + chocolate: "d2691e",
  1929 + coral: "ff7f50",
  1930 + cornflowerblue: "6495ed",
  1931 + cornsilk: "fff8dc",
  1932 + crimson: "dc143c",
  1933 + cyan: "0ff",
  1934 + darkblue: "00008b",
  1935 + darkcyan: "008b8b",
  1936 + darkgoldenrod: "b8860b",
  1937 + darkgray: "a9a9a9",
  1938 + darkgreen: "006400",
  1939 + darkgrey: "a9a9a9",
  1940 + darkkhaki: "bdb76b",
  1941 + darkmagenta: "8b008b",
  1942 + darkolivegreen: "556b2f",
  1943 + darkorange: "ff8c00",
  1944 + darkorchid: "9932cc",
  1945 + darkred: "8b0000",
  1946 + darksalmon: "e9967a",
  1947 + darkseagreen: "8fbc8f",
  1948 + darkslateblue: "483d8b",
  1949 + darkslategray: "2f4f4f",
  1950 + darkslategrey: "2f4f4f",
  1951 + darkturquoise: "00ced1",
  1952 + darkviolet: "9400d3",
  1953 + deeppink: "ff1493",
  1954 + deepskyblue: "00bfff",
  1955 + dimgray: "696969",
  1956 + dimgrey: "696969",
  1957 + dodgerblue: "1e90ff",
  1958 + firebrick: "b22222",
  1959 + floralwhite: "fffaf0",
  1960 + forestgreen: "228b22",
  1961 + fuchsia: "f0f",
  1962 + gainsboro: "dcdcdc",
  1963 + ghostwhite: "f8f8ff",
  1964 + gold: "ffd700",
  1965 + goldenrod: "daa520",
  1966 + gray: "808080",
  1967 + green: "008000",
  1968 + greenyellow: "adff2f",
  1969 + grey: "808080",
  1970 + honeydew: "f0fff0",
  1971 + hotpink: "ff69b4",
  1972 + indianred: "cd5c5c",
  1973 + indigo: "4b0082",
  1974 + ivory: "fffff0",
  1975 + khaki: "f0e68c",
  1976 + lavender: "e6e6fa",
  1977 + lavenderblush: "fff0f5",
  1978 + lawngreen: "7cfc00",
  1979 + lemonchiffon: "fffacd",
  1980 + lightblue: "add8e6",
  1981 + lightcoral: "f08080",
  1982 + lightcyan: "e0ffff",
  1983 + lightgoldenrodyellow: "fafad2",
  1984 + lightgray: "d3d3d3",
  1985 + lightgreen: "90ee90",
  1986 + lightgrey: "d3d3d3",
  1987 + lightpink: "ffb6c1",
  1988 + lightsalmon: "ffa07a",
  1989 + lightseagreen: "20b2aa",
  1990 + lightskyblue: "87cefa",
  1991 + lightslategray: "789",
  1992 + lightslategrey: "789",
  1993 + lightsteelblue: "b0c4de",
  1994 + lightyellow: "ffffe0",
  1995 + lime: "0f0",
  1996 + limegreen: "32cd32",
  1997 + linen: "faf0e6",
  1998 + magenta: "f0f",
  1999 + maroon: "800000",
  2000 + mediumaquamarine: "66cdaa",
  2001 + mediumblue: "0000cd",
  2002 + mediumorchid: "ba55d3",
  2003 + mediumpurple: "9370db",
  2004 + mediumseagreen: "3cb371",
  2005 + mediumslateblue: "7b68ee",
  2006 + mediumspringgreen: "00fa9a",
  2007 + mediumturquoise: "48d1cc",
  2008 + mediumvioletred: "c71585",
  2009 + midnightblue: "191970",
  2010 + mintcream: "f5fffa",
  2011 + mistyrose: "ffe4e1",
  2012 + moccasin: "ffe4b5",
  2013 + navajowhite: "ffdead",
  2014 + navy: "000080",
  2015 + oldlace: "fdf5e6",
  2016 + olive: "808000",
  2017 + olivedrab: "6b8e23",
  2018 + orange: "ffa500",
  2019 + orangered: "ff4500",
  2020 + orchid: "da70d6",
  2021 + palegoldenrod: "eee8aa",
  2022 + palegreen: "98fb98",
  2023 + paleturquoise: "afeeee",
  2024 + palevioletred: "db7093",
  2025 + papayawhip: "ffefd5",
  2026 + peachpuff: "ffdab9",
  2027 + peru: "cd853f",
  2028 + pink: "ffc0cb",
  2029 + plum: "dda0dd",
  2030 + powderblue: "b0e0e6",
  2031 + purple: "800080",
  2032 + red: "f00",
  2033 + rosybrown: "bc8f8f",
  2034 + royalblue: "4169e1",
  2035 + saddlebrown: "8b4513",
  2036 + salmon: "fa8072",
  2037 + sandybrown: "f4a460",
  2038 + seagreen: "2e8b57",
  2039 + seashell: "fff5ee",
  2040 + sienna: "a0522d",
  2041 + silver: "c0c0c0",
  2042 + skyblue: "87ceeb",
  2043 + slateblue: "6a5acd",
  2044 + slategray: "708090",
  2045 + slategrey: "708090",
  2046 + snow: "fffafa",
  2047 + springgreen: "00ff7f",
  2048 + steelblue: "4682b4",
  2049 + tan: "d2b48c",
  2050 + teal: "008080",
  2051 + thistle: "d8bfd8",
  2052 + tomato: "ff6347",
  2053 + turquoise: "40e0d0",
  2054 + violet: "ee82ee",
  2055 + wheat: "f5deb3",
  2056 + white: "fff",
  2057 + whitesmoke: "f5f5f5",
  2058 + yellow: "ff0",
  2059 + yellowgreen: "9acd32"
  2060 + };
  2061 +
  2062 + // Make it easy to access colors via `hexNames[hex]`
  2063 + var hexNames = tinycolor.hexNames = flip(names);
  2064 +
  2065 +
  2066 + // Utilities
  2067 + // ---------
  2068 +
  2069 + // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
  2070 + function flip(o) {
  2071 + var flipped = { };
  2072 + for (var i in o) {
  2073 + if (o.hasOwnProperty(i)) {
  2074 + flipped[o[i]] = i;
  2075 + }
  2076 + }
  2077 + return flipped;
  2078 + }
  2079 +
  2080 + // Return a valid alpha value [0,1] with all invalid values being set to 1
  2081 + function boundAlpha(a) {
  2082 + a = parseFloat(a);
  2083 +
  2084 + if (isNaN(a) || a < 0 || a > 1) {
  2085 + a = 1;
  2086 + }
  2087 +
  2088 + return a;
  2089 + }
  2090 +
  2091 + // Take input from [0, n] and return it as [0, 1]
  2092 + function bound01(n, max) {
  2093 + if (isOnePointZero(n)) { n = "100%"; }
  2094 +
  2095 + var processPercent = isPercentage(n);
  2096 + n = mathMin(max, mathMax(0, parseFloat(n)));
  2097 +
  2098 + // Automatically convert percentage into number
  2099 + if (processPercent) {
  2100 + n = parseInt(n * max, 10) / 100;
  2101 + }
  2102 +
  2103 + // Handle floating point rounding errors
  2104 + if ((math.abs(n - max) < 0.000001)) {
  2105 + return 1;
  2106 + }
  2107 +
  2108 + // Convert into [0, 1] range if it isn't already
  2109 + return (n % max) / parseFloat(max);
  2110 + }
  2111 +
  2112 + // Force a number between 0 and 1
  2113 + function clamp01(val) {
  2114 + return mathMin(1, mathMax(0, val));
  2115 + }
  2116 +
  2117 + // Parse a base-16 hex value into a base-10 integer
  2118 + function parseIntFromHex(val) {
  2119 + return parseInt(val, 16);
  2120 + }
  2121 +
  2122 + // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
  2123 + // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
  2124 + function isOnePointZero(n) {
  2125 + return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
  2126 + }
  2127 +
  2128 + // Check to see if string passed in is a percentage
  2129 + function isPercentage(n) {
  2130 + return typeof n === "string" && n.indexOf('%') != -1;
  2131 + }
  2132 +
  2133 + // Force a hex value to have 2 characters
  2134 + function pad2(c) {
  2135 + return c.length == 1 ? '0' + c : '' + c;
  2136 + }
  2137 +
  2138 + // Replace a decimal with it's percentage value
  2139 + function convertToPercentage(n) {
  2140 + if (n <= 1) {
  2141 + n = (n * 100) + "%";
  2142 + }
  2143 +
  2144 + return n;
  2145 + }
  2146 +
  2147 + // Converts a decimal to a hex value
  2148 + function convertDecimalToHex(d) {
  2149 + return Math.round(parseFloat(d) * 255).toString(16);
  2150 + }
  2151 + // Converts a hex value to a decimal
  2152 + function convertHexToDecimal(h) {
  2153 + return (parseIntFromHex(h) / 255);
  2154 + }
  2155 +
  2156 + var matchers = (function() {
  2157 +
  2158 + // <http://www.w3.org/TR/css3-values/#integers>
  2159 + var CSS_INTEGER = "[-\\+]?\\d+%?";
  2160 +
  2161 + // <http://www.w3.org/TR/css3-values/#number-value>
  2162 + var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
  2163 +
  2164 + // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
  2165 + var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
  2166 +
  2167 + // Actual matching.
  2168 + // Parentheses and commas are optional, but not required.
  2169 + // Whitespace can take the place of commas or opening paren
  2170 + var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  2171 + var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  2172 +
  2173 + return {
  2174 + rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
  2175 + rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
  2176 + hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
  2177 + hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
  2178 + hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
  2179 + hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
  2180 + hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
  2181 + hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
  2182 + };
  2183 + })();
  2184 +
  2185 + // `stringInputToObject`
  2186 + // Permissive string parsing. Take in a number of formats, and output an object
  2187 + // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
  2188 + function stringInputToObject(color) {
  2189 +
  2190 + color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
  2191 + var named = false;
  2192 + if (names[color]) {
  2193 + color = names[color];
  2194 + named = true;
  2195 + }
  2196 + else if (color == 'transparent') {
  2197 + return { r: 0, g: 0, b: 0, a: 0, format: "name" };
  2198 + }
  2199 +
  2200 + // Try to match string input using regular expressions.
  2201 + // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
  2202 + // Just return an object and let the conversion functions handle that.
  2203 + // This way the result will be the same whether the tinycolor is initialized with string or object.
  2204 + var match;
  2205 + if ((match = matchers.rgb.exec(color))) {
  2206 + return { r: match[1], g: match[2], b: match[3] };
  2207 + }
  2208 + if ((match = matchers.rgba.exec(color))) {
  2209 + return { r: match[1], g: match[2], b: match[3], a: match[4] };
  2210 + }
  2211 + if ((match = matchers.hsl.exec(color))) {
  2212 + return { h: match[1], s: match[2], l: match[3] };
  2213 + }
  2214 + if ((match = matchers.hsla.exec(color))) {
  2215 + return { h: match[1], s: match[2], l: match[3], a: match[4] };
  2216 + }
  2217 + if ((match = matchers.hsv.exec(color))) {
  2218 + return { h: match[1], s: match[2], v: match[3] };
  2219 + }
  2220 + if ((match = matchers.hex8.exec(color))) {
  2221 + return {
  2222 + a: convertHexToDecimal(match[1]),
  2223 + r: parseIntFromHex(match[2]),
  2224 + g: parseIntFromHex(match[3]),
  2225 + b: parseIntFromHex(match[4]),
  2226 + format: named ? "name" : "hex8"
  2227 + };
  2228 + }
  2229 + if ((match = matchers.hex6.exec(color))) {
  2230 + return {
  2231 + r: parseIntFromHex(match[1]),
  2232 + g: parseIntFromHex(match[2]),
  2233 + b: parseIntFromHex(match[3]),
  2234 + format: named ? "name" : "hex"
  2235 + };
  2236 + }
  2237 + if ((match = matchers.hex3.exec(color))) {
  2238 + return {
  2239 + r: parseIntFromHex(match[1] + '' + match[1]),
  2240 + g: parseIntFromHex(match[2] + '' + match[2]),
  2241 + b: parseIntFromHex(match[3] + '' + match[3]),
  2242 + format: named ? "name" : "hex"
  2243 + };
  2244 + }
  2245 +
  2246 + return false;
  2247 + }
  2248 +
  2249 + window.tinycolor = tinycolor;
  2250 + })();
  2251 +
  2252 +
  2253 + $(function () {
  2254 + if ($.fn.spectrum.load) {
  2255 + $.fn.spectrum.processNativeColorInputs();
  2256 + }
  2257 + });
  2258 +
  2259 +})(window, jQuery);
public/stylesheets/application.css
@@ -2649,6 +2649,15 @@ div#activation_enterprise label, div#activation_enterprise input, div#activation @@ -2649,6 +2649,15 @@ div#activation_enterprise label, div#activation_enterprise input, div#activation
2649 color: #585858; 2649 color: #585858;
2650 font-size: 11px; 2650 font-size: 11px;
2651 } 2651 }
  2652 +.colorpicker_field {
  2653 + display: none;
  2654 +}
  2655 +.color_marker {
  2656 + display: inline-block;
  2657 + width: 10px;
  2658 + height: 10px;
  2659 + border: 1px solid;
  2660 +}
2652 .formfield input { 2661 .formfield input {
2653 text-indent: 5px; 2662 text-indent: 5px;
2654 padding: 2px 0px; 2663 padding: 2px 0px;
public/stylesheets/inputosaurus.css 0 → 100644
@@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
  1 +.inputosaurus-container .ui-autocomplete-input {
  2 + background-color: #ffffff;
  3 +}
  4 +
  5 +.inputosaurus-container {
  6 + /*background-color: #fff;*/
  7 + /*border: 1px solid #bcbec0;*/
  8 + margin: 0;
  9 + padding: 0;
  10 + display: inline-block;
  11 + cursor: text;
  12 + /**font-size: 14px;**/
  13 + /**font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;**/
  14 + max-height: 300px;
  15 + overflow: hidden;
  16 + overflow-y: auto;
  17 +}
  18 +.inputosaurus-container li {
  19 + display: block;
  20 + float: left;
  21 + overflow: hidden;
  22 + margin: 2px 2px 0;
  23 + padding: 2px 3px;
  24 + white-space: nowrap;
  25 + overflow: hidden;
  26 + -o-text-overflow: ellipsis;
  27 + -ms-text-overflow: ellipsis;
  28 + text-overflow: ellipsis;
  29 + background-color: #e5eff7;
  30 + border: #a9cae4 solid 1px;
  31 + -webkit-border-radius: 2px;
  32 + -moz-border-radius: 2px;
  33 + border-radius: 2px;
  34 + color: #5b9bcd;
  35 + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  36 + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  37 + box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  38 + line-height: 20px;
  39 + cursor: default;
  40 +}
  41 +.inputosaurus-container li.inputosaurus-selected { background-color: #bdd6eb; }
  42 +.inputosaurus-container li a {
  43 + font-size: 16px;
  44 + color: #5b9bcd;
  45 + padding: 1px;
  46 + text-decoration: none;
  47 + outline: none;
  48 +}
  49 +.inputosaurus-container .inputosaurus-input {
  50 + border: none;
  51 + box-shadow: none;
  52 +}
  53 +.inputosaurus-container .inputosaurus-input input {
  54 + border: none;
  55 + height: 23px;
  56 + font-size: 14px;
  57 + line-height: 20px;
  58 + color: #555;
  59 + margin: 0;
  60 + outline: none;
  61 + padding: 0 0 1px 1px;
  62 + width: 25px;
  63 + -webkit-box-shadow: none;
  64 + -moz-box-shadow: none;
  65 + box-shadow: none;
  66 + background-image: none;
  67 + text-indent: 0px;
  68 +}
  69 +
  70 +.inputosaurus-container .inputosaurus-input input:hover {
  71 + -webkit-box-shadow: none;
  72 + -moz-box-shadow: none;
  73 + box-shadow: none;
  74 + background-color: #f0f0f0;
  75 +}
  76 +.inputosaurus-input-hidden { display: none; }
public/stylesheets/spectrum.css 0 → 100644
@@ -0,0 +1,506 @@ @@ -0,0 +1,506 @@
  1 +/***
  2 +Spectrum Colorpicker v1.4.1
  3 +https://github.com/bgrins/spectrum
  4 +Author: Brian Grinstead
  5 +License: MIT
  6 +***/
  7 +
  8 +.sp-container {
  9 + position:absolute;
  10 + top:0;
  11 + left:0;
  12 + display:inline-block;
  13 + *display: inline;
  14 + *zoom: 1;
  15 + /* https://github.com/bgrins/spectrum/issues/40 */
  16 + z-index: 9999994;
  17 + overflow: hidden;
  18 +}
  19 +.sp-container.sp-flat {
  20 + position: relative;
  21 +}
  22 +
  23 +/* Fix for * { box-sizing: border-box; } */
  24 +.sp-container,
  25 +.sp-container * {
  26 + -webkit-box-sizing: content-box;
  27 + -moz-box-sizing: content-box;
  28 + box-sizing: content-box;
  29 +}
  30 +
  31 +/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
  32 +.sp-top {
  33 + position:relative;
  34 + width: 100%;
  35 + display:inline-block;
  36 +}
  37 +.sp-top-inner {
  38 + position:absolute;
  39 + top:0;
  40 + left:0;
  41 + bottom:0;
  42 + right:0;
  43 +}
  44 +.sp-color {
  45 + position: absolute;
  46 + top:0;
  47 + left:0;
  48 + bottom:0;
  49 + right:20%;
  50 +}
  51 +.sp-hue {
  52 + position: absolute;
  53 + top:0;
  54 + right:0;
  55 + bottom:0;
  56 + left:84%;
  57 + height: 100%;
  58 +}
  59 +
  60 +.sp-clear-enabled .sp-hue {
  61 + top:33px;
  62 + height: 77.5%;
  63 +}
  64 +
  65 +.sp-fill {
  66 + padding-top: 80%;
  67 +}
  68 +.sp-sat, .sp-val {
  69 + position: absolute;
  70 + top:0;
  71 + left:0;
  72 + right:0;
  73 + bottom:0;
  74 +}
  75 +
  76 +.sp-alpha-enabled .sp-top {
  77 + margin-bottom: 18px;
  78 +}
  79 +.sp-alpha-enabled .sp-alpha {
  80 + display: block;
  81 +}
  82 +.sp-alpha-handle {
  83 + position:absolute;
  84 + top:-4px;
  85 + bottom: -4px;
  86 + width: 6px;
  87 + left: 50%;
  88 + cursor: pointer;
  89 + border: 1px solid black;
  90 + background: white;
  91 + opacity: .8;
  92 +}
  93 +.sp-alpha {
  94 + display: none;
  95 + position: absolute;
  96 + bottom: -14px;
  97 + right: 0;
  98 + left: 0;
  99 + height: 8px;
  100 +}
  101 +.sp-alpha-inner {
  102 + border: solid 1px #333;
  103 +}
  104 +
  105 +.sp-clear {
  106 + display: none;
  107 +}
  108 +
  109 +.sp-clear.sp-clear-display {
  110 + background-position: center;
  111 +}
  112 +
  113 +.sp-clear-enabled .sp-clear {
  114 + display: block;
  115 + position:absolute;
  116 + top:0px;
  117 + right:0;
  118 + bottom:0;
  119 + left:84%;
  120 + height: 28px;
  121 +}
  122 +
  123 +/* Don't allow text selection */
  124 +.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
  125 + -webkit-user-select:none;
  126 + -moz-user-select: -moz-none;
  127 + -o-user-select:none;
  128 + user-select: none;
  129 +}
  130 +
  131 +.sp-container.sp-input-disabled .sp-input-container {
  132 + display: none;
  133 +}
  134 +.sp-container.sp-buttons-disabled .sp-button-container {
  135 + display: none;
  136 +}
  137 +.sp-container.sp-palette-buttons-disabled .sp-palette-button-container {
  138 + display: none;
  139 +}
  140 +.sp-palette-only .sp-picker-container {
  141 + display: none;
  142 +}
  143 +.sp-palette-disabled .sp-palette-container {
  144 + display: none;
  145 +}
  146 +
  147 +.sp-initial-disabled .sp-initial {
  148 + display: none;
  149 +}
  150 +
  151 +
  152 +/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
  153 +.sp-sat {
  154 + background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
  155 + background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
  156 + background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  157 + background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  158 + background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  159 + background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
  160 + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
  161 + filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
  162 +}
  163 +.sp-val {
  164 + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
  165 + background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
  166 + background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  167 + background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  168 + background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  169 + background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
  170 + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
  171 + filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
  172 +}
  173 +
  174 +.sp-hue {
  175 + background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  176 + background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  177 + background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  178 + background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
  179 + background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  180 +}
  181 +
  182 +/* IE filters do not support multiple color stops.
  183 + Generate 6 divs, line them up, and do two color gradients for each.
  184 + Yes, really.
  185 + */
  186 +.sp-1 {
  187 + height:17%;
  188 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
  189 +}
  190 +.sp-2 {
  191 + height:16%;
  192 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
  193 +}
  194 +.sp-3 {
  195 + height:17%;
  196 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
  197 +}
  198 +.sp-4 {
  199 + height:17%;
  200 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
  201 +}
  202 +.sp-5 {
  203 + height:16%;
  204 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
  205 +}
  206 +.sp-6 {
  207 + height:17%;
  208 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
  209 +}
  210 +
  211 +.sp-hidden {
  212 + display: none !important;
  213 +}
  214 +
  215 +/* Clearfix hack */
  216 +.sp-cf:before, .sp-cf:after { content: ""; display: table; }
  217 +.sp-cf:after { clear: both; }
  218 +.sp-cf { *zoom: 1; }
  219 +
  220 +/* Mobile devices, make hue slider bigger so it is easier to slide */
  221 +@media (max-device-width: 480px) {
  222 + .sp-color { right: 40%; }
  223 + .sp-hue { left: 63%; }
  224 + .sp-fill { padding-top: 60%; }
  225 +}
  226 +.sp-dragger {
  227 + border-radius: 5px;
  228 + height: 5px;
  229 + width: 5px;
  230 + border: 1px solid #fff;
  231 + background: #000;
  232 + cursor: pointer;
  233 + position:absolute;
  234 + top:0;
  235 + left: 0;
  236 +}
  237 +.sp-slider {
  238 + position: absolute;
  239 + top:0;
  240 + cursor:pointer;
  241 + height: 3px;
  242 + left: -1px;
  243 + right: -1px;
  244 + border: 1px solid #000;
  245 + background: white;
  246 + opacity: .8;
  247 +}
  248 +
  249 +/*
  250 +Theme authors:
  251 +Here are the basic themeable display options (colors, fonts, global widths).
  252 +See http://bgrins.github.io/spectrum/themes/ for instructions.
  253 +*/
  254 +
  255 +.sp-container {
  256 + border-radius: 0;
  257 + background-color: #ECECEC;
  258 + border: solid 1px #f0c49B;
  259 + padding: 0;
  260 +}
  261 +.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear {
  262 + font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
  263 + -webkit-box-sizing: border-box;
  264 + -moz-box-sizing: border-box;
  265 + -ms-box-sizing: border-box;
  266 + box-sizing: border-box;
  267 +}
  268 +.sp-top {
  269 + margin-bottom: 3px;
  270 +}
  271 +.sp-color, .sp-hue, .sp-clear {
  272 + border: solid 1px #666;
  273 +}
  274 +
  275 +/* Input */
  276 +.sp-input-container {
  277 + float:right;
  278 + width: 100px;
  279 + margin-bottom: 4px;
  280 +}
  281 +.sp-initial-disabled .sp-input-container {
  282 + width: 100%;
  283 +}
  284 +.sp-input {
  285 + font-size: 12px !important;
  286 + border: 1px inset;
  287 + padding: 4px 5px;
  288 + margin: 0;
  289 + width: 100%;
  290 + background:transparent;
  291 + border-radius: 3px;
  292 + color: #222;
  293 +}
  294 +.sp-input:focus {
  295 + border: 1px solid orange;
  296 +}
  297 +.sp-input.sp-validation-error {
  298 + border: 1px solid red;
  299 + background: #fdd;
  300 +}
  301 +.sp-picker-container , .sp-palette-container {
  302 + float:left;
  303 + position: relative;
  304 + padding: 10px;
  305 + padding-bottom: 300px;
  306 + margin-bottom: -290px;
  307 +}
  308 +.sp-picker-container {
  309 + width: 172px;
  310 + border-left: solid 1px #fff;
  311 +}
  312 +
  313 +/* Palettes */
  314 +.sp-palette-container {
  315 + border-right: solid 1px #ccc;
  316 +}
  317 +
  318 +.sp-palette-only .sp-palette-container {
  319 + border: 0;
  320 +}
  321 +
  322 +.sp-palette .sp-thumb-el {
  323 + display: block;
  324 + position:relative;
  325 + float:left;
  326 + width: 24px;
  327 + height: 15px;
  328 + margin: 3px;
  329 + cursor: pointer;
  330 + border:solid 2px transparent;
  331 +}
  332 +.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
  333 + border-color: orange;
  334 +}
  335 +.sp-thumb-el {
  336 + position:relative;
  337 +}
  338 +
  339 +/* Initial */
  340 +.sp-initial {
  341 + float: left;
  342 + border: solid 1px #333;
  343 +}
  344 +.sp-initial span {
  345 + width: 30px;
  346 + height: 25px;
  347 + border:none;
  348 + display:block;
  349 + float:left;
  350 + margin:0;
  351 +}
  352 +
  353 +.sp-initial .sp-clear-display {
  354 + background-position: center;
  355 +}
  356 +
  357 +/* Buttons */
  358 +.sp-palette-button-container,
  359 +.sp-button-container {
  360 + float: right;
  361 +}
  362 +
  363 +/* Replacer (the little preview div that shows up instead of the <input>) */
  364 +.sp-replacer {
  365 + margin:0;
  366 + overflow:hidden;
  367 + cursor:pointer;
  368 + padding: 4px;
  369 + display:inline-block;
  370 + *zoom: 1;
  371 + *display: inline;
  372 + border: solid 1px #91765d;
  373 + background: #eee;
  374 + color: #333;
  375 + vertical-align: middle;
  376 +}
  377 +.sp-replacer:hover, .sp-replacer.sp-active {
  378 + border-color: #F0C49B;
  379 + color: #111;
  380 +}
  381 +.sp-replacer.sp-disabled {
  382 + cursor:default;
  383 + border-color: silver;
  384 + color: silver;
  385 +}
  386 +.sp-dd {
  387 + padding: 2px 0;
  388 + height: 16px;
  389 + line-height: 16px;
  390 + float:left;
  391 + font-size:10px;
  392 +}
  393 +.sp-preview {
  394 + position:relative;
  395 + width:25px;
  396 + height: 20px;
  397 + border: solid 1px #222;
  398 + margin-right: 5px;
  399 + float:left;
  400 + z-index: 0;
  401 +}
  402 +
  403 +.sp-palette {
  404 + *width: 220px;
  405 + max-width: 220px;
  406 +}
  407 +.sp-palette .sp-thumb-el {
  408 + width:16px;
  409 + height: 16px;
  410 + margin:2px 1px;
  411 + border: solid 1px #d0d0d0;
  412 +}
  413 +
  414 +.sp-container {
  415 + padding-bottom:0;
  416 +}
  417 +
  418 +
  419 +/* Buttons: http://hellohappy.org/css3-buttons/ */
  420 +.sp-container button {
  421 + background-color: #eeeeee;
  422 + background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
  423 + background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
  424 + background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
  425 + background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
  426 + background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
  427 + border: 1px solid #ccc;
  428 + border-bottom: 1px solid #bbb;
  429 + border-radius: 3px;
  430 + color: #333;
  431 + font-size: 14px;
  432 + line-height: 1;
  433 + padding: 5px 4px;
  434 + text-align: center;
  435 + text-shadow: 0 1px 0 #eee;
  436 + vertical-align: middle;
  437 +}
  438 +.sp-container button:hover {
  439 + background-color: #dddddd;
  440 + background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
  441 + background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
  442 + background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
  443 + background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
  444 + background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
  445 + border: 1px solid #bbb;
  446 + border-bottom: 1px solid #999;
  447 + cursor: pointer;
  448 + text-shadow: 0 1px 0 #ddd;
  449 +}
  450 +.sp-container button:active {
  451 + border: 1px solid #aaa;
  452 + border-bottom: 1px solid #888;
  453 + -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  454 + -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  455 + -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  456 + -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  457 + box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  458 +}
  459 +.sp-cancel {
  460 + font-size: 11px;
  461 + color: #d93f3f !important;
  462 + margin:0;
  463 + padding:2px;
  464 + margin-right: 5px;
  465 + vertical-align: middle;
  466 + text-decoration:none;
  467 +
  468 +}
  469 +.sp-cancel:hover {
  470 + color: #d93f3f !important;
  471 + text-decoration: underline;
  472 +}
  473 +
  474 +
  475 +.sp-palette span:hover, .sp-palette span.sp-thumb-active {
  476 + border-color: #000;
  477 +}
  478 +
  479 +.sp-preview, .sp-alpha, .sp-thumb-el {
  480 + position:relative;
  481 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
  482 +}
  483 +.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner {
  484 + display:block;
  485 + position:absolute;
  486 + top:0;left:0;bottom:0;right:0;
  487 +}
  488 +
  489 +.sp-palette .sp-thumb-inner {
  490 + background-position: 50% 50%;
  491 + background-repeat: no-repeat;
  492 +}
  493 +
  494 +.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner {
  495 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
  496 +}
  497 +
  498 +.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner {
  499 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
  500 +}
  501 +
  502 +.sp-clear-display {
  503 + background-repeat:no-repeat;
  504 + background-position: center;
  505 + background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
  506 +}
script/noosfero-plugins
@@ -77,8 +77,7 @@ run(){ @@ -77,8 +77,7 @@ run(){
77 77
78 _enable(){ 78 _enable(){
79 plugin="$1" 79 plugin="$1"
80 - cd $enabled_plugins_dir  
81 - source="../../plugins/$plugin" 80 + source="$available_plugins_dir/$plugin"
82 target="$enabled_plugins_dir/$plugin" 81 target="$enabled_plugins_dir/$plugin"
83 base="$base_plugins_dir/$plugin" 82 base="$base_plugins_dir/$plugin"
84 run "$source/before_enable.rb" 83 run "$source/before_enable.rb"
@@ -101,15 +100,11 @@ _enable(){ @@ -101,15 +100,11 @@ _enable(){
101 fi 100 fi
102 fi 101 fi
103 if [ "$installation_ok" = true ] && [ "$dependencies_ok" = true ]; then 102 if [ "$installation_ok" = true ] && [ "$dependencies_ok" = true ]; then
104 - ln -s "$source" "$plugin" 103 + ln -s "$source" "$target"
105 plugins_public_dir="$NOOSFERO_DIR/public/plugins" 104 plugins_public_dir="$NOOSFERO_DIR/public/plugins"
106 plugins_features_dir="$NOOSFERO_DIR/features/plugins" 105 plugins_features_dir="$NOOSFERO_DIR/features/plugins"
107 - cd $plugins_public_dir  
108 - test -d "$source/public" && ln -s "$source/public" "$plugin"  
109 - if [ -d "$NOOSFERO_DIR/features" ]; then  
110 - cd $plugins_features_dir  
111 - test -d "$source/features" && ln -s "$source/features" "$plugin"  
112 - fi 106 + test -d "$target/public" && ln -s "$target/public" "$plugins_public_dir/$plugin"
  107 + test -d "$NOOSFERO_DIR/features" && test -d "$target/features" && ln -s "$target/features" "$plugins_features_dir/$plugin"
113 _say "$plugin enabled" 108 _say "$plugin enabled"
114 run "$source/after_enable.rb" 109 run "$source/after_enable.rb"
115 needs_migrate=true 110 needs_migrate=true
script/quick-start
@@ -85,6 +85,7 @@ fi @@ -85,6 +85,7 @@ fi
85 run rake db:schema:load 85 run rake db:schema:load
86 run rake db:data:minimal 86 run rake db:data:minimal
87 run rake db:test:prepare 87 run rake db:test:prepare
  88 +rails runner 'Environment.default.enable("skip_new_user_email_confirmation")'
88 89
89 # FIXME compile translations depends on ruby-gettext-rails, please see debian/control 90 # FIXME compile translations depends on ruby-gettext-rails, please see debian/control
90 # run rake noosfero:translations:compile 91 # run rake noosfero:translations:compile
test/functional/application_controller_test.rb
@@ -166,7 +166,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase @@ -166,7 +166,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
166 166
167 should 'display only some categories in menu' do 167 should 'display only some categories in menu' do
168 @controller.stubs(:get_layout).returns('application') 168 @controller.stubs(:get_layout).returns('application')
169 - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true ) 169 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true )
170 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true ) 170 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true )
171 get :index 171 get :index
172 assert_tag :tag => 'a', :content => /Category 2/ 172 assert_tag :tag => 'a', :content => /Category 2/
@@ -174,7 +174,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase @@ -174,7 +174,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
174 174
175 should 'not display some categories in menu' do 175 should 'not display some categories in menu' do
176 @controller.stubs(:get_layout).returns('application') 176 @controller.stubs(:get_layout).returns('application')
177 - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true) 177 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true)
178 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1) 178 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1)
179 get :index 179 get :index
180 assert_no_tag :tag => 'a', :content => /Category 2/ 180 assert_no_tag :tag => 'a', :content => /Category 2/
@@ -239,7 +239,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase @@ -239,7 +239,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
239 239
240 should 'not display categories menu if categories feature disabled' do 240 should 'not display categories menu if categories feature disabled' do
241 Environment.any_instance.stubs(:enabled?).with(anything).returns(true) 241 Environment.any_instance.stubs(:enabled?).with(anything).returns(true)
242 - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true ) 242 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true )
243 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true ) 243 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true )
244 get :index 244 get :index
245 assert_no_tag :tag => 'a', :content => /Category 2/ 245 assert_no_tag :tag => 'a', :content => /Category 2/
@@ -579,4 +579,63 @@ class ApplicationControllerTest &lt; ActionController::TestCase @@ -579,4 +579,63 @@ class ApplicationControllerTest &lt; ActionController::TestCase
579 579
580 assert_equal ['a', 'b', 'c'], controller.send(:find_suggestions, 'random', Environment.default, 'random') 580 assert_equal ['a', 'b', 'c'], controller.send(:find_suggestions, 'random', Environment.default, 'random')
581 end 581 end
  582 +
  583 + should 'redirect to login if environment is restrict to members' do
  584 + Environment.default.enable(:restrict_to_members)
  585 + get :index
  586 + assert_redirected_to :controller => 'account', :action => 'login'
  587 + end
  588 +
  589 + should 'do not allow member not included in whitelist to access an restricted environment' do
  590 + user = create_user
  591 + e = Environment.default
  592 + e.enable(:restrict_to_members)
  593 + e.members_whitelist_enabled = true
  594 + e.save!
  595 + login_as(user.login)
  596 + get :index
  597 + assert_response :forbidden
  598 + end
  599 +
  600 + should 'allow member in whitelist to access an environment' do
  601 + user = create_user
  602 + e = Environment.default
  603 + e.members_whitelist_enabled = true
  604 + e.members_whitelist = "#{user.person.id}"
  605 + e.save!
  606 + login_as(user.login)
  607 + get :index
  608 + assert_response :success
  609 + end
  610 +
  611 + should 'allow members to access an environment if whitelist is disabled' do
  612 + user = create_user
  613 + e = Environment.default
  614 + e.members_whitelist_enabled = false
  615 + e.save!
  616 + login_as(user.login)
  617 + get :index
  618 + assert_response :success
  619 + end
  620 +
  621 + should 'allow admin to access an environment if whitelist is enabled' do
  622 + e = Environment.default
  623 + e.members_whitelist_enabled = true
  624 + e.save!
  625 + login_as(create_admin_user(e))
  626 + get :index
  627 + assert_response :success
  628 + end
  629 +
  630 + should 'not check whitelist members if the environment is not restrict to members' do
  631 + e = Environment.default
  632 + e.disable(:restrict_to_members)
  633 + e.members_whitelist_enabled = true
  634 + e.save!
  635 + @controller.expects(:verify_members_whitelist).never
  636 + login_as create_user.login
  637 + get :index
  638 + assert_response :success
  639 + end
  640 +
582 end 641 end
test/functional/categories_controller_test.rb
@@ -41,7 +41,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase @@ -41,7 +41,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
41 end 41 end
42 42
43 def test_edit_save 43 def test_edit_save
44 - post :edit, :id => cat1.id, :category => { :name => 'new name for category' } 44 + post :edit, :id => cat1.id, :category => { :name => 'new name for category', :display_color => nil }
45 assert_redirected_to :action => 'index' 45 assert_redirected_to :action => 'index'
46 assert_equal 'new name for category', Category.find(cat1.id).name 46 assert_equal 'new name for category', Category.find(cat1.id).name
47 end 47 end
@@ -134,7 +134,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase @@ -134,7 +134,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
134 env.save! 134 env.save!
135 get :new 135 get :new
136 136
137 - assert_no_tag :tag => 'select', :attributes => { :name => "category[display_color]" } 137 + assert_no_tag :tag => 'input', :attributes => { :name => "category[display_color]" }
138 end 138 end
139 139
140 should 'display color selection if environment.categories_menu is true' do 140 should 'display color selection if environment.categories_menu is true' do
@@ -142,7 +142,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase @@ -142,7 +142,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
142 env.save! 142 env.save!
143 get :new 143 get :new
144 144
145 - assert_tag :tag => 'select', :attributes => { :name => "category[display_color]" } 145 + assert_tag :tag => 'input', :attributes => { :name => "category[display_color]" }
146 end 146 end
147 147
148 should 'not list regions and product categories' do 148 should 'not list regions and product categories' do
test/functional/cms_controller_test.rb
@@ -1807,6 +1807,23 @@ class CmsControllerTest &lt; ActionController::TestCase @@ -1807,6 +1807,23 @@ class CmsControllerTest &lt; ActionController::TestCase
1807 assert_template 'cms/publish' 1807 assert_template 'cms/publish'
1808 end 1808 end
1809 1809
  1810 + should 'response of search_tags be json' do
  1811 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1812 + assert_equal 'application/json', @response.content_type
  1813 + end
  1814 +
  1815 + should 'return empty json if does not find tag' do
  1816 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1817 + assert_equal "[]", @response.body
  1818 + end
  1819 +
  1820 + should 'return tags found' do
  1821 + tag = mock; tag.stubs(:name).returns('linux')
  1822 + ActsAsTaggableOn::Tag.stubs(:find).returns([tag])
  1823 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1824 + assert_equal '[{"label":"linux","value":"linux"}]', @response.body
  1825 + end
  1826 +
1810 protected 1827 protected
1811 1828
1812 # FIXME this is to avoid adding an extra dependency for a proper JSON parser. 1829 # FIXME this is to avoid adding an extra dependency for a proper JSON parser.
test/functional/environment_design_controller_test.rb
@@ -6,9 +6,7 @@ class EnvironmentDesignController; def rescue_action(e) raise e end; end @@ -6,9 +6,7 @@ class EnvironmentDesignController; def rescue_action(e) raise e end; end
6 6
7 class EnvironmentDesignControllerTest < ActionController::TestCase 7 class EnvironmentDesignControllerTest < ActionController::TestCase
8 8
9 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
10 - # the Noosfero core soon, see ActionItem3045  
11 - ALL_BLOCKS = [ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ] 9 + ALL_BLOCKS = [ArticleBlock, LoginBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
12 10
13 def setup 11 def setup
14 @controller = EnvironmentDesignController.new 12 @controller = EnvironmentDesignController.new
@@ -77,18 +75,6 @@ class EnvironmentDesignControllerTest &lt; ActionController::TestCase @@ -77,18 +75,6 @@ class EnvironmentDesignControllerTest &lt; ActionController::TestCase
77 assert_tag :tag => 'p', :attributes => { :id => 'no_portal_community' } 75 assert_tag :tag => 'p', :attributes => { :id => 'no_portal_community' }
78 end 76 end
79 77
80 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
81 - # the Noosfero core soon, see ActionItem3045  
82 - should 'be able to edit EnvironmentStatisticsBlock' do  
83 - login_as(create_admin_user(Environment.default))  
84 - b = EnvironmentStatisticsBlock.create!  
85 - e = Environment.default  
86 - e.boxes.create!  
87 - e.boxes.first.blocks << b  
88 - get :edit, :id => b.id  
89 - assert_tag :tag => 'input', :attributes => { :id => 'block_title' }  
90 - end  
91 -  
92 should 'be able to edit EnterprisesBlock' do 78 should 'be able to edit EnterprisesBlock' do
93 login_as(create_admin_user(Environment.default)) 79 login_as(create_admin_user(Environment.default))
94 b = EnterprisesBlock.create! 80 b = EnterprisesBlock.create!
test/functional/features_controller_test.rb
@@ -146,4 +146,20 @@ class FeaturesControllerTest &lt; ActionController::TestCase @@ -146,4 +146,20 @@ class FeaturesControllerTest &lt; ActionController::TestCase
146 assert_equal true, e.custom_community_fields['contact_person']['required'] 146 assert_equal true, e.custom_community_fields['contact_person']['required']
147 end 147 end
148 148
  149 + should 'search members by name' do
  150 + uses_host 'anhetegua.net'
  151 + person = fast_create(Person, :environment_id => Environment.find(2).id)
  152 + xhr :get, :search_members, :q => person.name[0..2]
  153 + json_response = ActiveSupport::JSON.decode(@response.body)
  154 + assert_includes json_response, {"id"=>person.id, "name"=>person.name}
  155 + end
  156 +
  157 + should 'search members by identifier' do
  158 + uses_host 'anhetegua.net'
  159 + person = fast_create(Person, :name => 'Some Name', :identifier => 'person-identifier', :environment_id => Environment.find(2).id)
  160 + xhr :get, :search_members, :q => person.identifier
  161 + json_response = ActiveSupport::JSON.decode(@response.body)
  162 + assert_includes json_response, {"id"=>person.id, "name"=>person.name}
  163 + end
  164 +
149 end 165 end
test/unit/box_test.rb
@@ -33,9 +33,6 @@ class BoxTest &lt; ActiveSupport::TestCase @@ -33,9 +33,6 @@ class BoxTest &lt; ActiveSupport::TestCase
33 assert blocks.include?('categories-block') 33 assert blocks.include?('categories-block')
34 assert blocks.include?('communities-block') 34 assert blocks.include?('communities-block')
35 assert blocks.include?('enterprises-block') 35 assert blocks.include?('enterprises-block')
36 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
37 - # the Noosfero core soon, see ActionItem3045  
38 - assert blocks.include?('environment-statistics-block')  
39 assert blocks.include?('fans-block') 36 assert blocks.include?('fans-block')
40 assert blocks.include?('favorite-enterprises-block') 37 assert blocks.include?('favorite-enterprises-block')
41 assert blocks.include?('feed-reader-block') 38 assert blocks.include?('feed-reader-block')
@@ -64,9 +61,6 @@ class BoxTest &lt; ActiveSupport::TestCase @@ -64,9 +61,6 @@ class BoxTest &lt; ActiveSupport::TestCase
64 assert blocks.include?('communities-block') 61 assert blocks.include?('communities-block')
65 assert blocks.include?('disabled-enterprise-message-block') 62 assert blocks.include?('disabled-enterprise-message-block')
66 assert blocks.include?('enterprises-block') 63 assert blocks.include?('enterprises-block')
67 - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
68 - # the Noosfero core soon, see ActionItem3045  
69 - assert blocks.include?('environment-statistics-block')  
70 assert blocks.include?('fans-block') 64 assert blocks.include?('fans-block')
71 assert blocks.include?('favorite-enterprises-block') 65 assert blocks.include?('favorite-enterprises-block')
72 assert blocks.include?('featured-products-block') 66 assert blocks.include?('featured-products-block')
test/unit/categories_helper_test.rb
@@ -15,8 +15,26 @@ class CategoriesHelperTest &lt; ActiveSupport::TestCase @@ -15,8 +15,26 @@ class CategoriesHelperTest &lt; ActiveSupport::TestCase
15 expects(:options_for_select).with([['General Category', 'Category'],[ 'Product Category', 'ProductCategory'],[ 'Region', 'Region' ]], 'fieldvalue').returns('OPTIONS') 15 expects(:options_for_select).with([['General Category', 'Category'],[ 'Product Category', 'ProductCategory'],[ 'Region', 'Region' ]], 'fieldvalue').returns('OPTIONS')
16 expects(:select_tag).with('type', 'OPTIONS').returns('TAG') 16 expects(:select_tag).with('type', 'OPTIONS').returns('TAG')
17 expects(:labelled_form_field).with(anything, 'TAG').returns('RESULT') 17 expects(:labelled_form_field).with(anything, 'TAG').returns('RESULT')
18 - 18 +
19 assert_equal 'RESULT', select_category_type('fieldname') 19 assert_equal 'RESULT', select_category_type('fieldname')
20 end 20 end
21 21
  22 + should 'return category color if its defined' do
  23 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  24 + assert_equal 'background-color: #fbfbfb;', category_color_style(category1)
  25 + end
  26 +
  27 + should 'not return category parent color if category color is not defined' do
  28 + e = fast_create(Environment)
  29 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb', :environment_id => e.id)
  30 + category2 = fast_create(Category, :name => 'education', :display_color => nil, :parent_id => category1.id, :environment_id => e.id)
  31 + assert_equal '', category_color_style(category2)
  32 + end
  33 +
  34 + should 'not return category parent color if category is nil' do
  35 + assert_nothing_raised do
  36 + assert_equal '', category_color_style(nil)
  37 + end
  38 + end
  39 +
22 end 40 end
test/unit/category_test.rb
@@ -159,36 +159,6 @@ class CategoryTest &lt; ActiveSupport::TestCase @@ -159,36 +159,6 @@ class CategoryTest &lt; ActiveSupport::TestCase
159 159
160 end 160 end
161 161
162 - should "limit the possibile display colors" do  
163 - c = build(Category, :name => 'test category', :environment_id => @env.id)  
164 -  
165 - c.display_color = 16  
166 - c.valid?  
167 - assert c.errors[:display_color.to_s].present?  
168 -  
169 - valid = (1..15).map { |item| item.to_i }  
170 - valid.each do |item|  
171 - c.display_color = item  
172 - c.valid?  
173 - assert !c.errors[:display_color.to_s].present?  
174 - end  
175 -  
176 - end  
177 -  
178 - should 'avoid duplicated display colors' do  
179 - c1 = fast_create(Category, :name => 'test category', :environment_id => @env.id, :display_color => 1)  
180 -  
181 - c = build(Category, :name => 'lalala', :environment_id => @env.id)  
182 - c.display_color = 1  
183 - assert !c.valid?  
184 - assert c.errors[:display_color.to_s].present?  
185 -  
186 - c.display_color = 2  
187 - c.valid?  
188 - assert !c.errors[:display_color.to_s].present?  
189 -  
190 - end  
191 -  
192 should 'be able to get top ancestor' do 162 should 'be able to get top ancestor' do
193 c1 = fast_create(Category, :name => 'test category', :environment_id => @env.id) 163 c1 = fast_create(Category, :name => 'test category', :environment_id => @env.id)
194 c2 = fast_create(Category, :name => 'test category', :environment_id => @env.id, :parent_id => c1.id) 164 c2 = fast_create(Category, :name => 'test category', :environment_id => @env.id, :parent_id => c1.id)
@@ -535,4 +505,28 @@ class CategoryTest &lt; ActiveSupport::TestCase @@ -535,4 +505,28 @@ class CategoryTest &lt; ActiveSupport::TestCase
535 assert_includes Category.on_level(parent.id), category 505 assert_includes Category.on_level(parent.id), category
536 end 506 end
537 507
  508 + should 'return self if the category has display_color defined' do
  509 + c1 = fast_create(Category)
  510 + c2 = fast_create(Category, :parent_id => c1)
  511 + c3 = fast_create(Category, :parent_id => c2, :display_color => 'FFFFFF')
  512 + c4 = fast_create(Category, :parent_id => c3, :display_color => '000000')
  513 + assert_equal c4, c4.with_color
  514 + end
  515 +
  516 + should 'return first category on hierarchy with display_color defined' do
  517 + c1 = fast_create(Category, :display_color => '111111')
  518 + c2 = fast_create(Category, :parent_id => c1)
  519 + c3 = fast_create(Category, :parent_id => c2)
  520 + c4 = fast_create(Category, :parent_id => c3)
  521 + assert_equal c1, c4.with_color
  522 + end
  523 +
  524 + should 'return nil if no category on hierarchy has display_color defined' do
  525 + c1 = fast_create(Category)
  526 + c2 = fast_create(Category, :parent_id => c1)
  527 + c3 = fast_create(Category, :parent_id => c2)
  528 + c4 = fast_create(Category, :parent_id => c3)
  529 + assert_equal nil, c4.with_color
  530 + end
  531 +
538 end 532 end
test/unit/environment_statistics_block_test.rb
@@ -1,99 +0,0 @@ @@ -1,99 +0,0 @@
1 -# TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from  
2 -# the Noosfero core soon, see ActionItem3045  
3 -  
4 -require File.dirname(__FILE__) + '/../test_helper'  
5 -  
6 -class EnvironmentStatisticsBlockTest < ActiveSupport::TestCase  
7 -  
8 - should 'inherit from Block' do  
9 - assert_kind_of Block, EnvironmentStatisticsBlock.new  
10 - end  
11 -  
12 - should 'describe itself' do  
13 - assert_not_equal Block.description, EnvironmentStatisticsBlock.description  
14 - end  
15 -  
16 - should 'provide a default title' do  
17 - owner = mock  
18 - owner.expects(:name).returns('my environment')  
19 -  
20 - block = EnvironmentStatisticsBlock.new  
21 - block.expects(:owner).returns(owner)  
22 - assert_equal 'Statistics for my environment', block.title  
23 - end  
24 -  
25 - should 'generate statistics' do  
26 - env = create(Environment)  
27 - user1 = create_user('testuser1', :environment_id => env.id)  
28 - user2 = create_user('testuser2', :environment_id => env.id)  
29 -  
30 - fast_create(Enterprise, :environment_id => env.id)  
31 - fast_create(Community, :environment_id => env.id)  
32 -  
33 - block = EnvironmentStatisticsBlock.new  
34 - env.boxes.first.blocks << block  
35 -  
36 - content = block.content  
37 -  
38 - assert_match(/One enterprise/, content)  
39 - assert_match(/2 users/, content)  
40 - assert_match(/One community/, content)  
41 - end  
42 -  
43 - should 'generate statistics including private profiles' do  
44 - env = create(Environment)  
45 - user1 = create_user('testuser1', :environment_id => env.id)  
46 - user2 = create_user('testuser2', :environment_id => env.id)  
47 - user3 = create_user('testuser3', :environment_id => env.id)  
48 - p = user3.person; p.public_profile = false; p.save!  
49 -  
50 - fast_create(Enterprise, :environment_id => env.id)  
51 - fast_create(Enterprise, :environment_id => env.id, :public_profile => false)  
52 -  
53 - fast_create(Community, :environment_id => env.id)  
54 - fast_create(Community, :environment_id => env.id, :public_profile => false)  
55 -  
56 - block = EnvironmentStatisticsBlock.new  
57 - env.boxes.first.blocks << block  
58 -  
59 - content = block.content  
60 -  
61 - assert_match /2 enterprises/, content  
62 - assert_match /3 users/, content  
63 - assert_match /2 communities/, content  
64 - end  
65 -  
66 - should 'generate statistics but not for not visible profiles' do  
67 - env = create(Environment)  
68 - user1 = create_user('testuser1', :environment_id => env.id)  
69 - user2 = create_user('testuser2', :environment_id => env.id)  
70 - user3 = create_user('testuser3', :environment_id => env.id)  
71 - p = user3.person; p.visible = false; p.save!  
72 -  
73 - fast_create(Enterprise, :environment_id => env.id)  
74 - fast_create(Enterprise, :environment_id => env.id, :visible => false)  
75 -  
76 - fast_create(Community, :environment_id => env.id)  
77 - fast_create(Community, :environment_id => env.id, :visible => false)  
78 -  
79 - block = EnvironmentStatisticsBlock.new  
80 - env.boxes.first.blocks << block  
81 -  
82 - content = block.content  
83 -  
84 - assert_match /One enterprise/, content  
85 - assert_match /2 users/, content  
86 - assert_match /One community/, content  
87 - end  
88 -  
89 - should 'not display enterprises if disabled' do  
90 - env = fast_create(Environment)  
91 - env.enable('disable_asset_enterprises', false)  
92 -  
93 - block = EnvironmentStatisticsBlock.new  
94 - block.stubs(:owner).returns(env)  
95 -  
96 - assert_no_match /enterprises/i, block.content  
97 - end  
98 -  
99 -end  
test/unit/environment_test.rb
@@ -156,7 +156,7 @@ class EnvironmentTest &lt; ActiveSupport::TestCase @@ -156,7 +156,7 @@ class EnvironmentTest &lt; ActiveSupport::TestCase
156 156
157 should 'list displayable categories' do 157 should 'list displayable categories' do
158 env = fast_create(Environment) 158 env = fast_create(Environment)
159 - cat1 = create(Category, :environment => env, :name => 'category one', :display_color => 1) 159 + cat1 = create(Category, :environment => env, :name => 'category one', :display_color => 'ffa500')
160 assert ! cat1.new_record? 160 assert ! cat1.new_record?
161 161
162 # subcategories should be ignored 162 # subcategories should be ignored
test/unit/moderate_user_registration_test.rb 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +# encoding: UTF-8
  2 +require File.dirname(__FILE__) + '/../test_helper'
  3 +
  4 +class ModerateUserRegistrationTest < ActiveSupport::TestCase
  5 + fixtures :users, :environments
  6 +
  7 + def test_should_on_perform_activate_user
  8 + user = User.new(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test')
  9 + user.save!
  10 + environment = Environment.default
  11 + t= ModerateUserRegistration.new
  12 + t.user_id = user.id
  13 + t.name = user.name
  14 + t.author_name = user.name
  15 + t.email = user.email
  16 + t.target= environment
  17 + t.save!
  18 + assert !user.activated?
  19 + t.perform
  20 + assert environment.users.find_by_id(user.id).activated?
  21 + end
  22 +end
vendor/plugins/delayed_job/lib/delayed/command.rb
@@ -97,7 +97,7 @@ module Delayed @@ -97,7 +97,7 @@ module Delayed
97 Dir.chdir(Rails.root) 97 Dir.chdir(Rails.root)
98 98
99 Delayed::Worker.after_fork 99 Delayed::Worker.after_fork
100 - Delayed::Worker.logger ||= Logger.new(File.join(Rails.root, 'log', 'delayed_job.log')) 100 + Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', "#{Rails.env}_delayed_job.log"))
101 101
102 worker = Delayed::Worker.new(@options) 102 worker = Delayed::Worker.new(@options)
103 worker.name_prefix = "#{worker_name} " 103 worker.name_prefix = "#{worker_name} "