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 | 3 | protect 'edit_environment_design', :environment |
4 | 4 | |
5 | 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 | 7 | end |
8 | 8 | |
9 | 9 | end | ... | ... |
app/helpers/application_helper.rb
... | ... | @@ -985,4 +985,38 @@ module ApplicationHelper |
985 | 985 | number_to_currency(value, :unit => environment.currency_unit, :separator => environment.currency_separator, :delimiter => environment.currency_delimiter, :format => "%u %n") |
986 | 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 | 1022 | end | ... | ... |
... | ... | @@ -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 | 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 | 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 | 16 | acts_as_filesystem |
17 | 17 | |
... | ... | @@ -30,6 +30,12 @@ class Category < ActiveRecord::Base |
30 | 30 | |
31 | 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 | 39 | def recent_articles(limit = 10) |
34 | 40 | self.articles.recent(limit) |
35 | 41 | end |
... | ... | @@ -58,4 +64,9 @@ class Category < ActiveRecord::Base |
58 | 64 | results |
59 | 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 | 72 | end | ... | ... |
... | ... | @@ -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 @@ |
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 | 21 | Given /^the following blocks$/ do |table| |
22 | 22 | table.hashes.map{|item| item.dup}.each do |item| |
23 | 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 | 30 | box_id = owner.boxes.last.id |
26 | 31 | klass.constantize.create!(item.merge(:box_id => box_id)) |
27 | 32 | end | ... | ... |
public/javascripts/application.js
... | ... | @@ -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 | 4051 | .treeitem .button{ |
4052 | 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 | 6 | |
7 | 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 | 11 | def setup |
12 | 12 | @controller = EnvironmentDesignController.new | ... | ... |
... | ... | @@ -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 | 389 | assert Category.new.accept_products? |
390 | 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 | 438 | end | ... | ... |