Commit 5ca65170e5ee54ea9ab4b11fd6005de0ef124fea
Committed by
Joenio Costa
1 parent
5b1eb2d2
Exists in
master
and in
29 other branches
Add a block to display the categories as a website menu.
(ActionItem1543)
Showing
13 changed files
with
329 additions
and
6 deletions
Show diff stats
app/controllers/admin/environment_design_controller.rb
@@ -3,7 +3,7 @@ class EnvironmentDesignController < BoxOrganizerController | @@ -3,7 +3,7 @@ class EnvironmentDesignController < BoxOrganizerController | ||
3 | protect 'edit_environment_design', :environment | 3 | protect 'edit_environment_design', :environment |
4 | 4 | ||
5 | def available_blocks | 5 | def available_blocks |
6 | - @available_blocks ||= [ ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, PeopleBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock ] | 6 | + @available_blocks ||= [ ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, PeopleBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock ] |
7 | end | 7 | end |
8 | 8 | ||
9 | end | 9 | end |
app/helpers/application_helper.rb
@@ -985,4 +985,38 @@ module ApplicationHelper | @@ -985,4 +985,38 @@ module ApplicationHelper | ||
985 | number_to_currency(value, :unit => environment.currency_unit, :separator => environment.currency_separator, :delimiter => environment.currency_delimiter, :format => "%u %n") | 985 | number_to_currency(value, :unit => environment.currency_unit, :separator => environment.currency_separator, :delimiter => environment.currency_delimiter, :format => "%u %n") |
986 | end | 986 | end |
987 | 987 | ||
988 | + def collapsed_item_icon | ||
989 | + "<span class='ui-icon ui-icon-circlesmall-plus' style='float:left;'></span>" | ||
990 | + end | ||
991 | + def expanded_item_icon | ||
992 | + "<span class='ui-icon ui-icon-circlesmall-minus' style='float:left;'></span>" | ||
993 | + end | ||
994 | + def leaf_item_icon | ||
995 | + "<span class='ui-icon ui-icon-arrow-1-e' style='float:left;'></span>" | ||
996 | + end | ||
997 | + | ||
998 | + def display_category_menu(block, categories, root = true) | ||
999 | + categories = categories.sort{|x,y| x.name <=> y.name} | ||
1000 | + return "" if categories.blank? | ||
1001 | + content_tag(:ul, | ||
1002 | + categories.map do |category| | ||
1003 | + category_path = category.kind_of?(ProductCategory) ? {:controller => 'search', :action => 'assets', :asset => 'products', :product_category => category.id} : { :controller => 'search', :action => 'category_index', :category_path => category.explode_path } | ||
1004 | + category.display_in_menu? ? | ||
1005 | + content_tag(:li, | ||
1006 | + ( !category.is_leaf_displayable_in_menu? ? content_tag(:a, collapsed_item_icon, :href => "#", :id => "block_#{block.id}_category_#{category.id}", :class => 'category-link-expand ' + (root ? 'category-root' : 'category-no-root'), :onclick => "expandCategory(#{block.id}, #{category.id}); return false", :style => 'display: none') : leaf_item_icon) + | ||
1007 | + link_to(content_tag(:span, category.name, :class => 'category-name'), category_path, :class => ("category-leaf" if category.is_leaf_displayable_in_menu?)) + | ||
1008 | + content_tag(:div, display_category_menu(block, category.children, false), :id => "block_#{block.id}_category_content_#{category.id}", :class => 'child-category') | ||
1009 | + ) : '' | ||
1010 | + end | ||
1011 | + ) + | ||
1012 | + content_tag(:p) + | ||
1013 | + (root ? javascript_tag(" | ||
1014 | + jQuery('.child-category').hide(); | ||
1015 | + jQuery('.category-link-expand').show(); | ||
1016 | + var expanded_icon = \"#{ expanded_item_icon }\"; | ||
1017 | + var collapsed_icon = \"#{ collapsed_item_icon }\"; | ||
1018 | + var category_expanded = { 'block' : 0, 'category' : 0 }; | ||
1019 | + ") : '') | ||
1020 | + end | ||
1021 | + | ||
988 | end | 1022 | end |
@@ -0,0 +1,38 @@ | @@ -0,0 +1,38 @@ | ||
1 | +class CategoriesBlock < Block | ||
2 | + | ||
3 | + CATEGORY_TYPES = { | ||
4 | + _('Generic category') => nil, | ||
5 | + _('Region') => 'Region', | ||
6 | + _('Product') => 'ProductCategory' | ||
7 | + } | ||
8 | + | ||
9 | + settings_items :category_types, :type => Array, :default => [] | ||
10 | + | ||
11 | + def self.description | ||
12 | + _("Categories Menu") | ||
13 | + end | ||
14 | + | ||
15 | + def default_title | ||
16 | + _("Categories Menu") | ||
17 | + end | ||
18 | + | ||
19 | + def help | ||
20 | + _('This block presents the categories like a web site menu.') | ||
21 | + end | ||
22 | + | ||
23 | + def available_category_types | ||
24 | + CATEGORY_TYPES | ||
25 | + end | ||
26 | + | ||
27 | + def selected_categories | ||
28 | + Category.top_level_for(self.owner).from_types(self.category_types) | ||
29 | + end | ||
30 | + | ||
31 | + def content | ||
32 | + block = self | ||
33 | + lambda do | ||
34 | + render :file => 'blocks/categories', :locals => { :block => block } | ||
35 | + end | ||
36 | + end | ||
37 | + | ||
38 | +end |
app/models/category.rb
@@ -9,9 +9,9 @@ class Category < ActiveRecord::Base | @@ -9,9 +9,9 @@ class Category < ActiveRecord::Base | ||
9 | validates_uniqueness_of :display_color, :scope => :environment_id, :if => (lambda { |cat| ! cat.display_color.nil? }), :message => N_('%{fn} was already assigned to another category.') | 9 | validates_uniqueness_of :display_color, :scope => :environment_id, :if => (lambda { |cat| ! cat.display_color.nil? }), :message => N_('%{fn} was already assigned to another category.') |
10 | 10 | ||
11 | # Finds all top level categories for a given environment. | 11 | # Finds all top level categories for a given environment. |
12 | - def self.top_level_for(environment) | ||
13 | - self.find(:all, :conditions => ['parent_id is null and environment_id = ?', environment.id ]) | ||
14 | - end | 12 | + named_scope :top_level_for, lambda { |environment| |
13 | + {:conditions => ['parent_id is null and environment_id = ?', environment.id ]} | ||
14 | + } | ||
15 | 15 | ||
16 | acts_as_filesystem | 16 | acts_as_filesystem |
17 | 17 | ||
@@ -30,6 +30,12 @@ class Category < ActiveRecord::Base | @@ -30,6 +30,12 @@ class Category < ActiveRecord::Base | ||
30 | 30 | ||
31 | acts_as_having_image | 31 | acts_as_having_image |
32 | 32 | ||
33 | + named_scope :from_types, lambda { |types| | ||
34 | + types.select{ |t| t.blank? }.empty? ? | ||
35 | + { :conditions => { :type => types } } : | ||
36 | + { :conditions => [ "type IN (?) OR type IS NULL", types.reject{ |t| t.blank? } ] } | ||
37 | + } | ||
38 | + | ||
33 | def recent_articles(limit = 10) | 39 | def recent_articles(limit = 10) |
34 | self.articles.recent(limit) | 40 | self.articles.recent(limit) |
35 | end | 41 | end |
@@ -58,4 +64,9 @@ class Category < ActiveRecord::Base | @@ -58,4 +64,9 @@ class Category < ActiveRecord::Base | ||
58 | results | 64 | results |
59 | end | 65 | end |
60 | 66 | ||
67 | + def is_leaf_displayable_in_menu? | ||
68 | + return false if self.display_in_menu == false | ||
69 | + self.children.find(:all, :conditions => {:display_in_menu => true}).empty? | ||
70 | + end | ||
71 | + | ||
61 | end | 72 | end |
@@ -0,0 +1,5 @@ | @@ -0,0 +1,5 @@ | ||
1 | +<strong><%= _('Category types') %></strong> | ||
2 | + | ||
3 | +<% @block.available_category_types.each do |type_label, type_value| %> | ||
4 | + <p><%= labelled_check_box(type_label, 'block[category_types][]', type_value.to_s, @block.category_types.include?(type_value.to_s)) %></p> | ||
5 | +<% end %> |
@@ -0,0 +1,86 @@ | @@ -0,0 +1,86 @@ | ||
1 | +Feature: categories_block | ||
2 | + As an admin | ||
3 | + I want to manage the categories block | ||
4 | + | ||
5 | + Background: | ||
6 | + Given I am on the homepage | ||
7 | + And the following product_categories | ||
8 | + | name | display_in_menu | | ||
9 | + | Food | true | | ||
10 | + | Book | true | | ||
11 | + And the following product_categories | ||
12 | + | parent | name | display_in_menu | | ||
13 | + | Food | Vegetarian | true | | ||
14 | + | Food | Steak | true | | ||
15 | + | Book | Fiction | false | | ||
16 | + | Book | Literature | true | | ||
17 | + And the following categories | ||
18 | + | name | display_in_menu | | ||
19 | + | Wood | true | | ||
20 | + And the following regions | ||
21 | + | name | display_in_menu | | ||
22 | + | Bahia | true | | ||
23 | + And the following blocks | ||
24 | + | owner | type | | ||
25 | + | environment | CategoriesBlock | | ||
26 | + And I am logged in as admin | ||
27 | + | ||
28 | + @selenium | ||
29 | + Scenario: List just product categories | ||
30 | + Given I go to /admin/environment_design | ||
31 | + And I follow "Edit" within ".categories-block" | ||
32 | + And I check "Product" | ||
33 | + When I press "Save" | ||
34 | + Then I should see "Food" | ||
35 | + And I should see "Book" | ||
36 | + And I should not see "Vegetarian" | ||
37 | + And I should not see "Steak" | ||
38 | + And I should not see "Fiction" | ||
39 | + | ||
40 | + @selenium | ||
41 | + Scenario: Show submenu if it exists | ||
42 | + Given I go to /admin/environment_design | ||
43 | + And I follow "Edit" within ".categories-block" | ||
44 | + And I check "Product" | ||
45 | + And I press "Save" | ||
46 | + Then I should see "Food" | ||
47 | + And I should see "Book" | ||
48 | + And I should not see "Vegetarian" | ||
49 | + And I should not see "Steak" | ||
50 | + And I should not see "Literature" | ||
51 | + When I click ".category-link-expand category-root" | ||
52 | + Then I should see "Literature" | ||
53 | + When I click ".category-link-expand category-root" | ||
54 | + Then I should see "Vegetarian" | ||
55 | + And I should see "Steak" | ||
56 | + And I should not see "Fiction" | ||
57 | + | ||
58 | + @selenium | ||
59 | + Scenario: Show only one submenu per time | ||
60 | + Given I go to /admin/environment_design | ||
61 | + And I follow "Edit" within ".categories-block" | ||
62 | + And I check "Product" | ||
63 | + And I press "Save" | ||
64 | + Then I should see "Food" | ||
65 | + And I should not see "Vegetarian" | ||
66 | + And I should not see "Steak" | ||
67 | + When I click ".category-link-expand category-root" | ||
68 | + Then I should see "Vegetarian" | ||
69 | + And I should see "Steak" | ||
70 | + | ||
71 | + @selenium | ||
72 | + Scenario: List just general categories | ||
73 | + Given I go to /admin/environment_design | ||
74 | + And I follow "Edit" within ".categories-block" | ||
75 | + And I check "Generic Category" | ||
76 | + When I press "Save" | ||
77 | + Then I should see "Wood" | ||
78 | + | ||
79 | + @selenium | ||
80 | + Scenario: List just regions | ||
81 | + Given I go to /admin/environment_design | ||
82 | + And I follow "Edit" within ".categories-block" | ||
83 | + And I check "Region" | ||
84 | + When I press "Save" | ||
85 | + Then I should see "Bahia" | ||
86 | + |
features/step_definitions/noosfero_steps.rb
@@ -21,7 +21,12 @@ end | @@ -21,7 +21,12 @@ end | ||
21 | Given /^the following blocks$/ do |table| | 21 | Given /^the following blocks$/ do |table| |
22 | table.hashes.map{|item| item.dup}.each do |item| | 22 | table.hashes.map{|item| item.dup}.each do |item| |
23 | klass = item.delete('type') | 23 | klass = item.delete('type') |
24 | - owner = Profile[item.delete('owner')] | 24 | + owner_type = item.delete('owner') |
25 | + owner = owner_type == 'environment' ? Environment.default : Profile[owner_type] | ||
26 | + if owner.boxes.empty? | ||
27 | + owner.boxes<< Box.new | ||
28 | + owner.boxes.first.blocks << MainBlock.new | ||
29 | + end | ||
25 | box_id = owner.boxes.last.id | 30 | box_id = owner.boxes.last.id |
26 | klass.constantize.create!(item.merge(:box_id => box_id)) | 31 | klass.constantize.create!(item.merge(:box_id => box_id)) |
27 | end | 32 | end |
public/javascripts/application.js
@@ -193,3 +193,24 @@ function render_jquery_ui_buttons(element_id) { | @@ -193,3 +193,24 @@ function render_jquery_ui_buttons(element_id) { | ||
193 | }) | 193 | }) |
194 | } | 194 | } |
195 | } | 195 | } |
196 | + | ||
197 | +function expandCategory(block, id) { | ||
198 | + var link = jQuery('#block_' + block + '_category_' + id); | ||
199 | + if (category_expanded['block'] > 0 && category_expanded['category'] > 0 && category_expanded['block'] == block && category_expanded['category'] != id && link.hasClass('category-root')) { | ||
200 | + expandCategory(category_expanded['block'], category_expanded['category']); | ||
201 | + category_expanded['category'] = id; | ||
202 | + category_expanded['block'] = block; | ||
203 | + } | ||
204 | + if (category_expanded['block'] == 0) category_expanded['block'] = block; | ||
205 | + if (category_expanded['category'] == 0) category_expanded['category'] = id; | ||
206 | + jQuery('#block_' + block + '_category_content_' + id).slideToggle('slow'); | ||
207 | + link.toggleClass('category-expanded'); | ||
208 | + if (link.hasClass('category-expanded')) link.html(expanded_icon); | ||
209 | + else { | ||
210 | + link.html(collapsed_icon); | ||
211 | + if (link.hasClass('category-root')) { | ||
212 | + category_expanded['block'] = 0; | ||
213 | + category_expanded['category'] = 0; | ||
214 | + } | ||
215 | + } | ||
216 | +} |
public/stylesheets/application.css
@@ -4051,3 +4051,35 @@ h1#agenda-title { | @@ -4051,3 +4051,35 @@ h1#agenda-title { | ||
4051 | .treeitem .button{ | 4051 | .treeitem .button{ |
4052 | display: inline; | 4052 | display: inline; |
4053 | } | 4053 | } |
4054 | + | ||
4055 | +/* Categories block stuff */ | ||
4056 | + | ||
4057 | +.categories-block ul { | ||
4058 | + margin: 0; | ||
4059 | + padding: 0; | ||
4060 | + float: left; | ||
4061 | +} | ||
4062 | +.categories-block li { | ||
4063 | + display: block; | ||
4064 | + float: left; | ||
4065 | + width: 100%; | ||
4066 | +} | ||
4067 | +.categories-block ul a { | ||
4068 | + text-decoration: none; | ||
4069 | + background-color: transparent; | ||
4070 | + border-bottom: 1px solid #fff; | ||
4071 | + border-color: #fff; | ||
4072 | + line-height: 22px; | ||
4073 | +} | ||
4074 | +.categories-block div { | ||
4075 | + clear: both; | ||
4076 | +} | ||
4077 | +.categories-block p { | ||
4078 | + clear: both; | ||
4079 | +} | ||
4080 | +.categories-block li a:hover { | ||
4081 | + text-decoration: none; | ||
4082 | +} | ||
4083 | +.categories-block .ui-icon { | ||
4084 | + margin-top: 2px; | ||
4085 | +} |
test/functional/environment_design_controller_test.rb
@@ -6,7 +6,7 @@ class EnvironmentDesignController; def rescue_action(e) raise e end; end | @@ -6,7 +6,7 @@ class EnvironmentDesignController; def rescue_action(e) raise e end; end | ||
6 | 6 | ||
7 | class EnvironmentDesignControllerTest < Test::Unit::TestCase | 7 | class EnvironmentDesignControllerTest < Test::Unit::TestCase |
8 | 8 | ||
9 | - ALL_BLOCKS = [ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, PeopleBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock ] | 9 | + ALL_BLOCKS = [ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, PeopleBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock ] |
10 | 10 | ||
11 | def setup | 11 | def setup |
12 | @controller = EnvironmentDesignController.new | 12 | @controller = EnvironmentDesignController.new |
@@ -0,0 +1,42 @@ | @@ -0,0 +1,42 @@ | ||
1 | +require File.dirname(__FILE__) + '/../test_helper' | ||
2 | + | ||
3 | +class CategoriesBlockTest < ActiveSupport::TestCase | ||
4 | + | ||
5 | + should 'default describe' do | ||
6 | + assert_not_equal Block.description, CategoriesBlock.description | ||
7 | + end | ||
8 | + | ||
9 | + should 'default title' do | ||
10 | + block = Block.new | ||
11 | + category_block = CategoriesBlock.new | ||
12 | + assert_not_equal block.title, category_block.default_title | ||
13 | + end | ||
14 | + | ||
15 | + should 'have a help defined' do | ||
16 | + category_block = CategoriesBlock.new | ||
17 | + assert_not_nil category_block.help | ||
18 | + end | ||
19 | + | ||
20 | + should 'display category block' do | ||
21 | + block = CategoriesBlock.new | ||
22 | + | ||
23 | + self.expects(:render).with(:file => 'blocks/categories', :locals => { :block => block}) | ||
24 | + instance_eval(& block.content) | ||
25 | + end | ||
26 | + | ||
27 | + should 'be editable' do | ||
28 | + assert CategoriesBlock.new.editable? | ||
29 | + end | ||
30 | + | ||
31 | + should 'default category types is an empty array' do | ||
32 | + category_block = CategoriesBlock.new | ||
33 | + assert_kind_of Array, category_block.category_types | ||
34 | + assert category_block.category_types.empty? | ||
35 | + end | ||
36 | + | ||
37 | + should 'available category types' do | ||
38 | + category_block = CategoriesBlock.new | ||
39 | + assert_equal({ _('Generic category') => nil, _('Region') => 'Region', _('Product') => 'ProductCategory' }, category_block.available_category_types) | ||
40 | + end | ||
41 | + | ||
42 | +end |
test/unit/category_test.rb
@@ -389,4 +389,50 @@ class CategoryTest < Test::Unit::TestCase | @@ -389,4 +389,50 @@ class CategoryTest < Test::Unit::TestCase | ||
389 | assert Category.new.accept_products? | 389 | assert Category.new.accept_products? |
390 | end | 390 | end |
391 | 391 | ||
392 | + should 'get categories by type including nil' do | ||
393 | + category = Category.create!(:name => 'test category', :environment => Environment.default) | ||
394 | + region = Region.create!(:name => 'test region', :environment => Environment.default) | ||
395 | + product = ProductCategory.create!(:name => 'test product', :environment => Environment.default) | ||
396 | + result = Category.from_types(['ProductCategory', '']).all | ||
397 | + assert_equal 2, result.size | ||
398 | + assert result.include?(product) | ||
399 | + assert result.include?(category) | ||
400 | + end | ||
401 | + | ||
402 | + should 'get categories by type and not nil' do | ||
403 | + category = Category.create!(:name => 'test category', :environment => Environment.default) | ||
404 | + region = Region.create!(:name => 'test region', :environment => Environment.default) | ||
405 | + product = ProductCategory.create!(:name => 'test product', :environment => Environment.default) | ||
406 | + result = Category.from_types(['Region', 'ProductCategory']).all | ||
407 | + assert_equal 2, result.size | ||
408 | + assert result.include?(region) | ||
409 | + assert result.include?(product) | ||
410 | + end | ||
411 | + | ||
412 | + should 'define a leaf to be displayed in menu' do | ||
413 | + c1 = fast_create(Category, :display_in_menu => true) | ||
414 | + c11 = fast_create(Category, :display_in_menu => true, :parent_id => c1.id) | ||
415 | + c2 = fast_create(Category, :display_in_menu => true) | ||
416 | + c21 = fast_create(Category, :display_in_menu => false, :parent_id => c2.id) | ||
417 | + c22 = fast_create(Category, :display_in_menu => false, :parent_id => c2.id) | ||
418 | + | ||
419 | + assert_equal false, c1.is_leaf_displayable_in_menu? | ||
420 | + assert_equal true, c11.is_leaf_displayable_in_menu? | ||
421 | + assert_equal true, c2.is_leaf_displayable_in_menu? | ||
422 | + assert_equal false, c21.is_leaf_displayable_in_menu? | ||
423 | + assert_equal false, c22.is_leaf_displayable_in_menu? | ||
424 | + end | ||
425 | + | ||
426 | + should 'filter top_level categories by type' do | ||
427 | + toplevel_productcategory = fast_create(ProductCategory) | ||
428 | + leaf_productcategory = fast_create(ProductCategory, :parent_id => toplevel_productcategory.id) | ||
429 | + | ||
430 | + toplevel_category = fast_create(Category) | ||
431 | + leaf_category = fast_create(Category, :parent_id => toplevel_category.id) | ||
432 | + | ||
433 | + assert_includes Category.top_level_for(Environment.default).from_types(['ProductCategory']), toplevel_productcategory | ||
434 | + assert_not_includes Category.top_level_for(Environment.default).from_types(['ProductCategory']), leaf_productcategory | ||
435 | + assert_not_includes Category.top_level_for(Environment.default).from_types(['ProductCategory']), toplevel_category | ||
436 | + end | ||
437 | + | ||
392 | end | 438 | end |