Commit 888d94d7a4011226b870715c392ee409c147c20a

Authored by Leandro Santos
2 parents dc75141d f9358611

Merge branches 'master' and 'AI3279-block_store' into AI3279-block_store

Showing 46 changed files with 3318 additions and 127 deletions   Show diff stats
@@ -17,6 +17,7 @@ gem 'nokogiri' @@ -17,6 +17,7 @@ gem 'nokogiri'
17 gem 'rake', :require => false 17 gem 'rake', :require => false
18 gem 'rest-client' 18 gem 'rest-client'
19 gem 'exception_notification' 19 gem 'exception_notification'
  20 +gem 'gettext_rails'
20 21
21 # FIXME list here all actual dependencies (i.e. the ones in debian/control), 22 # FIXME list here all actual dependencies (i.e. the ones in debian/control),
22 # with their GEM names (not the Debian package names) 23 # with their GEM names (not the Debian package names)
@@ -67,6 +67,15 @@ GEM @@ -67,6 +67,15 @@ GEM
67 activesupport (>= 3.0.4) 67 activesupport (>= 3.0.4)
68 fast_gettext (0.6.8) 68 fast_gettext (0.6.8)
69 ffi (1.0.11) 69 ffi (1.0.11)
  70 + gettext (2.2.1)
  71 + locale
  72 + gettext_activerecord (2.1.0)
  73 + activerecord (>= 2.3.2)
  74 + gettext (>= 2.1.0)
  75 + gettext_rails (2.1.0)
  76 + gettext_activerecord (>= 2.1.0)
  77 + locale_rails (>= 2.0.5)
  78 + rails (>= 2.3.2)
70 gherkin (2.4.21) 79 gherkin (2.4.21)
71 json (>= 1.4.6) 80 json (>= 1.4.6)
72 hike (1.2.1) 81 hike (1.2.1)
@@ -74,6 +83,9 @@ GEM @@ -74,6 +83,9 @@ GEM
74 i18n (0.6.0) 83 i18n (0.6.0)
75 journey (1.0.3) 84 journey (1.0.3)
76 json (1.7.3) 85 json (1.7.3)
  86 + locale (2.0.5)
  87 + locale_rails (2.0.5)
  88 + locale (>= 2.0.5)
77 mail (2.4.4) 89 mail (2.4.4)
78 i18n (>= 0.4.0) 90 i18n (>= 0.4.0)
79 mime-types (~> 1.16) 91 mime-types (~> 1.16)
@@ -172,6 +184,7 @@ DEPENDENCIES @@ -172,6 +184,7 @@ DEPENDENCIES
172 database_cleaner 184 database_cleaner
173 exception_notification 185 exception_notification
174 fast_gettext 186 fast_gettext
  187 + gettext_rails
175 hpricot 188 hpricot
176 mocha 189 mocha
177 nokogiri 190 nokogiri
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/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 => :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
@@ -187,4 +193,8 @@ class ApplicationController < ActionController::Base @@ -187,4 +193,8 @@ class ApplicationController < ActionController::Base
187 {:results => scope.paginate(paginate_options)} 193 {:results => scope.paginate(paginate_options)}
188 end 194 end
189 195
  196 + def private_environment?
  197 + @environment.enabled?(:restrict_to_members)
  198 + end
  199 +
190 end 200 end
app/controllers/my_profile/memberships_controller.rb
@@ -21,6 +21,9 @@ class MembershipsController < MyProfileController @@ -21,6 +21,9 @@ class MembershipsController < MyProfileController
21 @back_to = params[:back_to] || url_for(:action => 'index') 21 @back_to = params[:back_to] || url_for(:action => 'index')
22 if request.post? && @community.valid? 22 if request.post? && @community.valid?
23 @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment})) 23 @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment}))
  24 + if @community.new_record?
  25 + session[:notice] = _('Your new community creation request will be evaluated by an administrator. You will be notified.')
  26 + end
24 redirect_to @back_to 27 redirect_to @back_to
25 return 28 return
26 end 29 end
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
@@ -108,6 +120,7 @@ class AccountController < ApplicationController @@ -108,6 +120,7 @@ class AccountController < ApplicationController
108 check_join_in_community(@user) 120 check_join_in_community(@user)
109 go_to_signup_initial_page 121 go_to_signup_initial_page
110 else 122 else
  123 + session[:notice] = _('Thanks for registering!')
111 @register_pending = true 124 @register_pending = true
112 end 125 end
113 end 126 end
app/helpers/application_helper.rb
@@ -1402,4 +1402,14 @@ module ApplicationHelper @@ -1402,4 +1402,14 @@ module ApplicationHelper
1402 content_tag('ul', article.versions.map {|v| link_to("r#{v.version}", @page.url.merge(:version => v.version))}) 1402 content_tag('ul', article.versions.map {|v| link_to("r#{v.version}", @page.url.merge(:version => v.version))})
1403 end 1403 end
1404 1404
  1405 + def labelled_colorpicker_field(human_name, object_name, method, options = {})
  1406 + options[:id] ||= 'text-field-' + FormsHelper.next_id_number
  1407 + content_tag('label', human_name, :for => options[:id], :class => 'formlabel') +
  1408 + colorpicker_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1409 + end
  1410 +
  1411 + def colorpicker_field(object_name, method, options = {})
  1412 + text_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1413 + end
  1414 +
1405 end 1415 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/token_helper.rb
@@ -18,6 +18,7 @@ module TokenHelper @@ -18,6 +18,7 @@ module TokenHelper
18 options[:on_add] ||= 'null' 18 options[:on_add] ||= 'null'
19 options[:on_delete] ||= 'null' 19 options[:on_delete] ||= 'null'
20 options[:on_ready] ||= 'null' 20 options[:on_ready] ||= 'null'
  21 + options[:query_param] ||= 'q'
21 22
22 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id}))) 23 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
23 result += javascript_tag("jQuery('##{element_id}') 24 result += javascript_tag("jQuery('##{element_id}')
@@ -30,7 +31,7 @@ module TokenHelper @@ -30,7 +31,7 @@ module TokenHelper
30 searchDelay: #{options[:search_delay].to_json}, 31 searchDelay: #{options[:search_delay].to_json},
31 preventDuplicates: #{options[:prevent_duplicates].to_json}, 32 preventDuplicates: #{options[:prevent_duplicates].to_json},
32 backspaceDeleteItem: #{options[:backspace_delete_item].to_json}, 33 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
33 - queryParam: #{name.to_json}, 34 + queryParam: #{options[:query_param].to_json},
34 tokenLimit: #{options[:token_limit].to_json}, 35 tokenLimit: #{options[:token_limit].to_json},
35 onResult: #{options[:on_result]}, 36 onResult: #{options[:on_result]},
36 onAdd: #{options[:on_add]}, 37 onAdd: #{options[:on_add]},
@@ -48,4 +49,4 @@ module TokenHelper @@ -48,4 +49,4 @@ module TokenHelper
48 result 49 result
49 end 50 end
50 51
51 -end  
52 \ No newline at end of file 52 \ No newline at end of file
  53 +end
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
@@ -124,6 +124,7 @@ class Environment &lt; ActiveRecord::Base @@ -124,6 +124,7 @@ class Environment &lt; ActiveRecord::Base
124 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"), 124 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"),
125 'enable_organization_url_change' => _("Allow organizations to change their URL"), 125 'enable_organization_url_change' => _("Allow organizations to change their URL"),
126 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"), 126 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"),
  127 + 'admin_must_approve_new_users' => _("Admin must approve registration of new users"),
127 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'), 128 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'),
128 'xmpp_chat' => _('XMPP/Jabber based chat'), 129 'xmpp_chat' => _('XMPP/Jabber based chat'),
129 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'), 130 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'),
@@ -132,7 +133,8 @@ class Environment &lt; ActiveRecord::Base @@ -132,7 +133,8 @@ class Environment &lt; ActiveRecord::Base
132 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'), 133 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'),
133 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'), 134 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'),
134 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'), 135 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'),
135 - 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage') 136 + 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage'),
  137 + 'restrict_to_members' => _('Show content only to members')
136 } 138 }
137 end 139 end
138 140
@@ -303,6 +305,17 @@ class Environment &lt; ActiveRecord::Base @@ -303,6 +305,17 @@ class Environment &lt; ActiveRecord::Base
303 settings[:signup_welcome_screen_body].present? 305 settings[:signup_welcome_screen_body].present?
304 end 306 end
305 307
  308 + settings_items :members_whitelist_enabled, :type => :boolean, :default => false
  309 + settings_items :members_whitelist, :type => Array, :default => []
  310 +
  311 + def in_whitelist?(person)
  312 + !members_whitelist_enabled || members_whitelist.include?(person.id)
  313 + end
  314 +
  315 + def members_whitelist=(members)
  316 + settings[:members_whitelist] = members.split(',').map(&:to_i)
  317 + end
  318 +
306 def news_amount_by_folder=(amount) 319 def news_amount_by_folder=(amount)
307 settings[:news_amount_by_folder] = amount.to_i 320 settings[:news_amount_by_folder] = amount.to_i
308 end 321 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
@@ -47,8 +47,12 @@ class User &lt; ActiveRecord::Base @@ -47,8 +47,12 @@ class User &lt; ActiveRecord::Base
47 47
48 user.person = p 48 user.person = p
49 end 49 end
50 - if user.environment.enabled?('skip_new_user_email_confirmation')  
51 - user.activate 50 + if user.environment.enabled?('skip_new_user_email_confirmation')
  51 + if user.environment.enabled?('admin_must_approve_new_users')
  52 + create_moderate_task
  53 + else
  54 + user.activate
  55 + end
52 end 56 end
53 end 57 end
54 after_create :deliver_activation_code 58 after_create :deliver_activation_code
@@ -137,6 +141,15 @@ class User &lt; ActiveRecord::Base @@ -137,6 +141,15 @@ class User &lt; ActiveRecord::Base
137 end 141 end
138 end 142 end
139 143
  144 + def create_moderate_task
  145 + @task = ModerateUserRegistration.new
  146 + @task.user_id = self.id
  147 + @task.name = self.name
  148 + @task.email = self.email
  149 + @task.target = self.environment
  150 + @task.save
  151 + end
  152 +
140 def activated? 153 def activated?
141 self.activation_code.nil? && !self.activated_at.nil? 154 self.activation_code.nil? && !self.activated_at.nil?
142 end 155 end
app/views/account/signup.html.erb
@@ -2,18 +2,36 @@ @@ -2,18 +2,36 @@
2 <div id='thanks-for-signing'> 2 <div id='thanks-for-signing'>
3 <% if environment.has_custom_welcome_screen? %> 3 <% if environment.has_custom_welcome_screen? %>
4 <%= environment.settings[:signup_welcome_screen_body].html_safe %> 4 <%= environment.settings[:signup_welcome_screen_body].html_safe %>
5 - <% else %>  
6 - <h1><%= _("Welcome to %s!") % environment.name %></h1>  
7 - <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>  
8 - <p><%= _("Firstly, some tips for getting started:") %></p>  
9 - <h4><%= _("Confirm your account!") %></h4> 5 + <% elsif environment.enabled?('admin_must_approve_new_users')%>
  6 + <h1><%= _("Welcome to %s!") % environment.name %></h1>
  7 + <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
  8 + <p><%= _("Firstly, some tips for getting started:") %></p>
  9 + <% unless environment.enabled?('skip_new_user_email_confirmation') %>
  10 + <h4><%= _("Confirm your account and wait for admin approvement!") %></h4>
10 <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p> 11 <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
11 - <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>  
12 - <h4><%= _("What to do next?") %></h4>  
13 - <p><%= _("%s. Upload an avatar and let your friends find you easily :)") % link_to(_('Customize your profile'), {:controller => 'doc', :section => 'user', :topic => 'editing-person-info'}, :target => '_blank') %></p>  
14 - <p><%= _("Learn the guidelines. Read the %s for more details on how to use this social network!") % link_to(_('Documentation'), {:controller => 'doc'}, :target => '_blank') %></p>  
15 - <p><%= _("%s your Gmail, Yahoo and Hotmail contacts!") % link_to(_('Invite and find'), {:controller => 'doc', :section => 'user', :topic => 'invite-contacts'}, :target => '_blank') %></p>  
16 - <p><%= _("Start exploring and have fun!") %></p> 12 + <p><%= _("You won't appear as %s until your account is confirmed and approved.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
  13 + <% else %>
  14 + <h4><%= _("Wait for admin approvement!") %></h4>
  15 + <p><%= _("The administrators will evaluate your signup request for approvement.") %></p>
  16 + <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>
  17 + <% end %>
  18 + <h4><%= _("What to do next?") %></h4>
  19 + <p><%= _("%s. Upload an avatar and let your friends find you easily :)") % link_to(_('Customize your profile'), {:controller => 'doc', :section => 'user', :topic => 'editing-person-info'}, :target => '_blank') %></p>
  20 + <p><%= _("Learn the guidelines. Read the %s for more details on how to use this social network!") % link_to(_('Documentation'), {:controller => 'doc'}, :target => '_blank') %></p>
  21 + <p><%= _("%s your Gmail, Yahoo and Hotmail contacts!") % link_to(_('Invite and find'), {:controller => 'doc', :section => 'user', :topic => 'invite-contacts'}, :target => '_blank') %></p>
  22 + <p><%= _("Start exploring and have fun!") %></p>
  23 + <% else %>
  24 + <h1><%= _("Welcome to %s!") % environment.name %></h1>
  25 + <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
  26 + <p><%= _("Firstly, some tips for getting started:") %></p>
  27 + <h4><%= _("Confirm your account!") %></h4>
  28 + <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
  29 + <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>
  30 + <h4><%= _("What to do next?") %></h4>
  31 + <p><%= _("%s. Upload an avatar and let your friends find you easily :)") % link_to(_('Customize your profile'), {:controller => 'doc', :section => 'user', :topic => 'editing-person-info'}, :target => '_blank') %></p>
  32 + <p><%= _("Learn the guidelines. Read the %s for more details on how to use this social network!") % link_to(_('Documentation'), {:controller => 'doc'}, :target => '_blank') %></p>
  33 + <p><%= _("%s your Gmail, Yahoo and Hotmail contacts!") % link_to(_('Invite and find'), {:controller => 'doc', :section => 'user', :topic => 'invite-contacts'}, :target => '_blank') %></p>
  34 + <p><%= _("Start exploring and have fun!") %></p>
17 <% end %> 35 <% end %>
18 </div> 36 </div>
19 <% else %> 37 <% else %>
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/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/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") %>
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
@@ -194,19 +194,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140808185510) do @@ -194,19 +194,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140808185510) 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|
debian/control
@@ -7,6 +7,7 @@ Build-Depends: @@ -7,6 +7,7 @@ Build-Depends:
7 debhelper (>= 7.0.50~), 7 debhelper (>= 7.0.50~),
8 po4a, 8 po4a,
9 ruby-gettext, 9 ruby-gettext,
  10 + ruby-gettext-rails,
10 ruby-sqlite3, 11 ruby-sqlite3,
11 rake, 12 rake,
12 rails3 (>= 3.2.6-1~), 13 rails3 (>= 3.2.6-1~),
features/signup.feature
@@ -298,3 +298,55 @@ Feature: signup @@ -298,3 +298,55 @@ Feature: signup
298 And wait for the captcha signup time 298 And wait for the captcha signup time
299 And I press "Create my account" 299 And I press "Create my account"
300 Then "José da Silva" should be a member of "Free Software" 300 Then "José da Silva" should be a member of "Free Software"
  301 +
  302 + @selenium
  303 + Scenario: user registration is moderated by admin
  304 + Given feature "admin_must_approve_new_users" is enabled on environment
  305 + And feature "skip_new_user_email_confirmation" is disabled on environment
  306 + And I go to /account/signup
  307 + And I fill in "Username" with "teste"
  308 + And I fill in "Password" with "123456"
  309 + And I fill in "Password confirmation" with "123456"
  310 + And I fill in "e-Mail" with "teste@teste.com"
  311 + And I fill in "Full name" with "Teste da Silva"
  312 + And wait for the captcha signup time
  313 + And I press "Create my account"
  314 + And I go to teste's confirmation URL
  315 + And I am logged in as admin
  316 + And I follow "Control panel"
  317 + And I follow "Tasks"
  318 + And I choose "Accept"
  319 + And I press "Apply!"
  320 + And I follow "Logout"
  321 + And Teste da Silva's account is activated
  322 + And I follow "Login"
  323 + And I fill in "Username / Email" with "teste"
  324 + And I fill in "Password" with "123456"
  325 + And I press "Log in"
  326 + Then I should see "teste"
  327 +
  328 +
  329 + @selenium
  330 + Scenario: user registration is not accepted by the admin
  331 + Given feature "admin_must_approve_new_users" is enabled on environment
  332 + And feature "skip_new_user_email_confirmation" is disabled on environment
  333 + And I go to /account/signup
  334 + And I fill in "Username" with "teste"
  335 + And I fill in "Password" with "123456"
  336 + And I fill in "Password confirmation" with "123456"
  337 + And I fill in "e-Mail" with "teste@teste.com"
  338 + And I fill in "Full name" with "Teste da Silva"
  339 + And wait for the captcha signup time
  340 + And I press "Create my account"
  341 + And I go to teste's confirmation URL
  342 + And I am logged in as admin
  343 + And I follow "Control panel"
  344 + And I follow "Tasks"
  345 + And I choose "Reject"
  346 + And I press "Apply!"
  347 + And I follow "Logout"
  348 + And I follow "Login"
  349 + And I fill in "Username / Email" with "teste"
  350 + And I fill in "Password" with "123456"
  351 + And I press "Log in"
  352 + Then I should not see "teste"
301 \ No newline at end of file 353 \ 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/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
@@ -2565,6 +2565,15 @@ div#activation_enterprise label, div#activation_enterprise input, div#activation @@ -2565,6 +2565,15 @@ div#activation_enterprise label, div#activation_enterprise input, div#activation
2565 color: #585858; 2565 color: #585858;
2566 font-size: 11px; 2566 font-size: 11px;
2567 } 2567 }
  2568 +.colorpicker_field {
  2569 + display: none;
  2570 +}
  2571 +.color_marker {
  2572 + display: inline-block;
  2573 + width: 10px;
  2574 + height: 10px;
  2575 + border: 1px solid;
  2576 +}
2568 .formfield input { 2577 .formfield input {
2569 text-indent: 5px; 2578 text-indent: 5px;
2570 padding: 2px 0px; 2579 padding: 2px 0px;
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/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 +run 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/
@@ -557,4 +557,51 @@ class ApplicationControllerTest &lt; ActionController::TestCase @@ -557,4 +557,51 @@ class ApplicationControllerTest &lt; ActionController::TestCase
557 assert_no_tag :tag => 'meta', :attributes => { :property => 'article:published_time' } 557 assert_no_tag :tag => 'meta', :attributes => { :property => 'article:published_time' }
558 assert_no_tag :tag => 'meta', :attributes => { :property => 'og:image' } 558 assert_no_tag :tag => 'meta', :attributes => { :property => 'og:image' }
559 end 559 end
  560 +
  561 + should 'redirect to login if environment is restrict to members' do
  562 + Environment.default.enable(:restrict_to_members)
  563 + get :index
  564 + assert_redirected_to :controller => 'account', :action => 'login'
  565 + end
  566 +
  567 + should 'do not allow member not included in whitelist to access an environment' do
  568 + user = create_user
  569 + e = Environment.default
  570 + e.members_whitelist_enabled = true
  571 + e.save!
  572 + login_as(user.login)
  573 + get :index
  574 + assert_response :forbidden
  575 + end
  576 +
  577 + should 'allow member in whitelist to access an environment' do
  578 + user = create_user
  579 + e = Environment.default
  580 + e.members_whitelist_enabled = true
  581 + e.members_whitelist = "#{user.person.id}"
  582 + e.save!
  583 + login_as(user.login)
  584 + get :index
  585 + assert_response :success
  586 + end
  587 +
  588 + should 'allow members to access an environment if whitelist is disabled' do
  589 + user = create_user
  590 + e = Environment.default
  591 + e.members_whitelist_enabled = false
  592 + e.save!
  593 + login_as(user.login)
  594 + get :index
  595 + assert_response :success
  596 + end
  597 +
  598 + should 'allow admin to access an environment if whitelist is enabled' do
  599 + e = Environment.default
  600 + e.members_whitelist_enabled = true
  601 + e.save!
  602 + login_as(create_admin_user(e))
  603 + get :index
  604 + assert_response :success
  605 + end
  606 +
560 end 607 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/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/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_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} "