Commit 0d7f79273bf29965639763a0987388b55a67f98a

Authored by Francisco Marcelo A. Lima Júnior
2 parents 39ea55d3 8b6dc0a2

Merge branch 'ActionItem2892' of github.com:SerproLivre/noosfero into ActionItem2892

Showing 213 changed files with 4328 additions and 594 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 213 files displayed.

COPYRIGHT
... ... @@ -4,8 +4,9 @@ Copyright (c) 2007-2009,
4 4 Cáritas Brasileira <http://www.caritasbrasileira.org/>
5 5 Copyright (c) 2007-2009,
6 6 Ynternet.org Foundation <http://www.ynternet.org/>
7   -Copyright (c) 2008-2009,
  7 +Copyright (c) 2008-2013,
8 8 Colivre <http://www.colivre.coop.br/>
  9 +Copyright (c) the Noosfero contributors. See AUTHORS
9 10  
10 11 This program is free software: you can redistribute it and/or modify
11 12 it under the terms of the GNU Affero General Public License as published by
... ...
HACKING
... ... @@ -52,3 +52,12 @@ If you write such script for your own OS, *please* share it with us at the
52 52 development mailing list so that we can include it in the official repository.
53 53 This way other people using the same OS will have to put less effort to develop
54 54 Noosfero.
  55 +
  56 +== Submitting your changes back
  57 +
  58 +For now please read:
  59 +
  60 +- Coding conventions
  61 + http://noosfero.org/Development/CodingConventions
  62 +- Patch guidelines
  63 + http://noosfero.org/Development/PatchGuidelines
... ...
HACKING.rails235
... ... @@ -1,13 +0,0 @@
1   -This is a draft of how to create a environment to Rails 2.3.5 to Noosfero
2   -development.
3   -
4   -Install dependencies:
5   -
6   -gem install rails -v 2.3.5
7   -gem install i18n
8   -gem install will_paginate -v 2.3.12
9   -gem install cucumber
10   -
11   -Creating initial environment:
12   -
13   -rake db:schema:load
INSTALL.chat
... ... @@ -6,7 +6,7 @@ To configure XMPP/BOSH in Noosfero you need:
6 6 * SystemTimer - http://ph7spot.com/musings/system-timer
7 7 * Pidgin data files - http://www.pidgin.im/
8 8  
9   -If you use Debian Wheezy:
  9 +If you use Debian 6.0 (squeeze):
10 10  
11 11 # apt-get install librestclient-ruby pidgin-data ruby1.8-dev
12 12 # gem install SystemTimer
... ...
README
1   -noosfero - a web-based social platform
  1 +Noosfero - a web-based social platform
2 2 ======================================
3 3  
4   -:: About the project
  4 +http://www.noosfero.org/
5 5  
6   -Homepage: http://www.noosfero.org/
  6 +Documentation
  7 +-------------
7 8  
8   -:: Authors and copyright
  9 +The following documentation is available:
9 10  
10   -Authors: see file AUTHORS
11   -Copyright information: see file COPYRIGHT
12   -Full license text; see file COPYING
  11 +File Purpose
  12 +~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  13 +INSTALL install instructions
  14 +INSTALL.awstats install instructions - access statistics service
  15 +INSTALL.chat install instructions - chat service
  16 +INSTALL.email install instructions - email service
  17 +INSTALL.multitenancy install instructions - multiple sites
  18 +INSTALL.varnish install instructions - varnish HTTP caching (recommended)
  19 +HACKING development instruction
  20 +RELEASING instructions for doing releases
  21 +doc/noosfero/* user documentation (available through the app itself)
  22 +
  23 +
  24 +Authors and copyright
  25 +---------------------
  26 +
  27 +Authorship and copyright information is available in the files listed below.
  28 +
  29 +File Purpose
  30 +~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  31 +AUTHORS list of authors (updated at each release)
  32 +COPYRIGHT Copyright statement for the project
  33 +COPYING Full text of the project license
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -267,7 +267,10 @@ class CmsController &lt; MyProfileController
267 267 @back_to = params[:back_to] || request.referer || url_for(profile.public_profile_url)
268 268 @task = SuggestArticle.new(params[:task])
269 269 if request.post?
270   - @task.target = profile
  270 + @task.target = profile
  271 + @task.ip_address = request.remote_ip
  272 + @task.user_agent = request.user_agent
  273 + @task.referrer = request.referrer
271 274 if verify_recaptcha(:model => @task, :message => _('Please type the words correctly')) && @task.save
272 275 session[:notice] = _('Thanks for your suggestion. The community administrators were notified.')
273 276 redirect_to @back_to
... ...
app/controllers/my_profile/memberships_controller.rb
... ... @@ -9,9 +9,10 @@ class MembershipsController &lt; MyProfileController
9 9 def new_community
10 10 @community = Community.new(params[:community])
11 11 @community.environment = environment
  12 + @back_to = params[:back_to] || url_for(:action => 'index')
12 13 if request.post? && @community.valid?
13 14 @community = Community.create_after_moderation(user, {:environment => environment}.merge(params[:community]))
14   - redirect_to :action => 'index'
  15 + redirect_to @back_to
15 16 return
16 17 end
17 18 end
... ...
app/controllers/my_profile/profile_editor_controller.rb
... ... @@ -4,7 +4,7 @@ class ProfileEditorController &lt; MyProfileController
4 4 protect 'destroy_profile', :profile, :only => [:destroy_profile]
5 5  
6 6 def index
7   - @pending_tasks = Task.to(profile).pending.select{|i| user.has_permission?(i.permission, profile)}
  7 + @pending_tasks = Task.to(profile).pending.without_spam.select{|i| user.has_permission?(i.permission, profile)}
8 8 end
9 9  
10 10 helper :profile
... ...
app/controllers/my_profile/spam_controller.rb
... ... @@ -14,9 +14,15 @@ class SpamController &lt; MyProfileController
14 14 if params[:remove_comment]
15 15 profile.comments_received.find(params[:remove_comment]).destroy
16 16 end
  17 + if params[:remove_task]
  18 + Task.to(profile).find_by_id(params[:remove_task]).destroy
  19 + end
17 20 if params[:mark_comment_as_ham]
18 21 profile.comments_received.find(params[:mark_comment_as_ham]).ham!
19 22 end
  23 + if params[:mark_task_as_ham] && (t = Task.to(profile).find_by_id(params[:mark_task_as_ham]))
  24 + t.ham!
  25 + end
20 26 if request.xhr?
21 27 json_response(true)
22 28 else
... ... @@ -28,7 +34,8 @@ class SpamController &lt; MyProfileController
28 34 return
29 35 end
30 36  
31   - @spam = profile.comments_received.spam.paginate({:page => params[:page]})
  37 + @comment_spam = profile.comments_received.spam.paginate({:page => params[:comments_page]})
  38 + @task_spam = Task.to(profile).spam.paginate({:page => params[:tasks_page]})
32 39 end
33 40  
34 41 protected
... ...
app/controllers/my_profile/tasks_controller.rb
... ... @@ -4,12 +4,12 @@ class TasksController &lt; MyProfileController
4 4  
5 5 def index
6 6 @filter = params[:filter_type].blank? ? nil : params[:filter_type]
7   - @tasks = Task.to(profile).pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page])
  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 8 @failed = params ? params[:failed] : {}
9 9 end
10 10  
11 11 def processed
12   - @tasks = Task.to(profile).closed.sort_by(&:created_at)
  12 + @tasks = Task.to(profile).without_spam.closed.sort_by(&:created_at)
13 13 end
14 14  
15 15 VALID_DECISIONS = [ 'finish', 'cancel', 'skip' ]
... ... @@ -57,7 +57,7 @@ class TasksController &lt; MyProfileController
57 57 end
58 58  
59 59 def list_requested
60   - @tasks = Task.find_all_by_requestor_id(profile.id)
  60 + @tasks = Task.without_spam.find_all_by_requestor_id(profile.id)
61 61 end
62 62  
63 63 def ticket_details
... ...
app/controllers/public/content_viewer_controller.rb
... ... @@ -53,7 +53,9 @@ class ContentViewerController &lt; ApplicationController
53 53 # At this point the page will be showed
54 54 @page.hit
55 55  
56   - unless @page.mime_type == 'text/html' || (@page.image? && params[:view])
  56 + @page = FilePresenter.for @page
  57 +
  58 + unless @page.mime_type == 'text/html' || params[:view]
57 59 headers['Content-Type'] = @page.mime_type
58 60 data = @page.data
59 61  
... ...
app/helpers/application_helper.rb
... ... @@ -558,6 +558,9 @@ module ApplicationHelper
558 558 # displays a link to the profile homepage with its image (as generated by
559 559 # #profile_image) and its name below it.
560 560 def profile_image_link( profile, size=:portrait, tag='li', extra_info = nil )
  561 + if content = @plugins.dispatch_first(:profile_image_link, profile, size, tag, extra_info)
  562 + return instance_eval(&content)
  563 + end
561 564 name = profile.short_name
562 565 if profile.person?
563 566 url = url_for(profile.check_friendship_url)
... ... @@ -574,16 +577,16 @@ module ApplicationHelper
574 577 extra_info = extra_info.nil? ? '' : content_tag( 'span', extra_info, :class => 'extra_info' )
575 578 links = links_for_balloon(profile)
576 579 content_tag('div', content_tag(tag,
577   - (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ? link_to( content_tag( 'span', _('Profile links')), '#', :onclick => "toggleSubmenu(this, '#{profile.short_name}', #{links.to_json}); return false", :class => "menu-submenu-trigger #{trigger_class}", :url => url) : "") +
578   - link_to(
579   - content_tag( 'span', profile_image( profile, size ), :class => 'profile-image' ) +
580   - content_tag( 'span', h(name), :class => ( profile.class == Person ? 'fn' : 'org' ) ) +
581   - extra_info + profile_sex_icon( profile ) + profile_cat_icons( profile ),
582   - profile.url,
583   - :class => 'profile_link url',
584   - :help => _('Click on this icon to go to the <b>%s</b>\'s home page') % profile.name,
585   - :title => profile.name ),
586   - :class => 'vcard'), :class => 'common-profile-list-block')
  580 + (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ? link_to( content_tag( 'span', _('Profile links')), '#', :onclick => "toggleSubmenu(this, '#{profile.short_name}', #{links.to_json}); return false", :class => "menu-submenu-trigger #{trigger_class}", :url => url) : "") +
  581 + link_to(
  582 + content_tag( 'span', profile_image( profile, size ), :class => 'profile-image' ) +
  583 + content_tag( 'span', h(name), :class => ( profile.class == Person ? 'fn' : 'org' ) ) +
  584 + extra_info + profile_sex_icon( profile ) + profile_cat_icons( profile ),
  585 + profile.url,
  586 + :class => 'profile_link url',
  587 + :help => _('Click on this icon to go to the <b>%s</b>\'s home page') % profile.name,
  588 + :title => profile.name ),
  589 + :class => 'vcard'), :class => 'common-profile-list-block')
587 590 end
588 591  
589 592 def gravatar_url_for(email, options = {})
... ... @@ -1113,15 +1116,34 @@ module ApplicationHelper
1113 1116 result
1114 1117 end
1115 1118  
1116   - def manage_enterprises
1117   - if user && !user.enterprises.empty?
1118   - enterprises_link = user.enterprises.map do |enterprise|
1119   - link_to(content_tag('strong', [_('<span>Manage</span> %s') % enterprise.short_name(25)]), @environment.top_url + "/myprofile/#{enterprise.identifier}", :class => "icon-menu-"+enterprise.class.identification.underscore, :title => [_('Manage %s') % enterprise.short_name])
  1119 + def manage_link(list, kind)
  1120 + if list.present?
  1121 + link_to_all = nil
  1122 + if list.count > 5
  1123 + list = list.first(5)
  1124 + link_to_all = link_to(content_tag('strong', _('See all')), :controller => 'memberships', :profile => current_user.login)
  1125 + end
  1126 + link = list.map do |element|
  1127 + link_to(content_tag('strong', [_('<span>Manage</span> %s') % element.short_name(25)]), @environment.top_url + "/myprofile/#{element.identifier}", :class => "icon-menu-"+element.class.identification.underscore, :title => [_('Manage %s') % element.short_name])
1120 1128 end
1121   - render :partial => 'shared/manage_enterprises', :locals => {:enterprises_link => enterprises_link}
  1129 + if link_to_all
  1130 + link << link_to_all
  1131 + end
  1132 + render :partial => "shared/manage_link", :locals => {:link => link, :kind => kind.to_s}
1122 1133 end
1123 1134 end
1124 1135  
  1136 + def manage_enterprises
  1137 + return unless user && user.environment.enabled?(:display_my_enterprises_on_user_menu)
  1138 + manage_link(user.enterprises, :enterprises)
  1139 + end
  1140 +
  1141 + def manage_communities
  1142 + return unless user && user.environment.enabled?(:display_my_communities_on_user_menu)
  1143 + administered_communities = user.communities.more_popular.select {|c| c.admins.include? user}
  1144 + manage_link(administered_communities, :communities)
  1145 + end
  1146 +
1125 1147 def usermenu_logged_in
1126 1148 pending_tasks_count = ''
1127 1149 count = user ? Task.to(user).pending.count : -1
... ... @@ -1133,6 +1155,7 @@ module ApplicationHelper
1133 1155 render_environment_features(:usermenu) +
1134 1156 link_to('<i class="icon-menu-admin"></i><strong>' + _('Administration') + '</strong>', @environment.top_url + '/admin', :id => "controlpanel", :title => _("Configure the environment"), :class => 'admin-link', :style => 'display: none') +
1135 1157 manage_enterprises.to_s +
  1158 + manage_communities.to_s +
1136 1159 link_to('<i class="icon-menu-ctrl-panel"></i><strong>' + _('Control panel') + '</strong>', @environment.top_url + '/myprofile/{login}', :id => "controlpanel", :title => _("Configure your personal account and content")) +
1137 1160 pending_tasks_count +
1138 1161 link_to('<i class="icon-menu-logout"></i><strong>' + _('Logout') + '</strong>', { :controller => 'account', :action => 'logout'} , :id => "logout", :title => _("Leave the system"))
... ... @@ -1401,7 +1424,7 @@ module ApplicationHelper
1401 1424 end
1402 1425  
1403 1426 def filter_html(html, source)
1404   - if @plugins
  1427 + if @plugins && source.has_macro?
1405 1428 html = convert_macro(html, source)
1406 1429 #TODO This parse should be done through the macro infra, but since there
1407 1430 # are old things that do not support it we are keeping this hot spot.
... ...
app/helpers/blog_helper.rb
... ... @@ -42,7 +42,7 @@ module BlogHelper
42 42  
43 43 def display_post(article, format = 'full')
44 44 no_comments = (format == 'full') ? false : true
45   - html = send("display_#{format}_format", article).html_safe
  45 + html = send("display_#{format}_format", FilePresenter.for(article)).html_safe
46 46  
47 47 article_title(article, :no_comments => no_comments) + html
48 48 end
... ...
app/helpers/boxes_helper.rb
... ... @@ -100,9 +100,7 @@ module BoxesHelper
100 100 options[:title] = _("This block is invisible. Your visitors will not see it.")
101 101 end
102 102  
103   - if @controller.send(:content_editor?)
104   - result = filter_html(result, block)
105   - end
  103 + result = filter_html(result, block)
106 104  
107 105 box_decorator.block_target(block.box, block) +
108 106 content_tag('div',
... ...
app/helpers/categories_helper.rb
... ... @@ -3,10 +3,21 @@ module CategoriesHelper
3 3  
4 4 COLORS = [
5 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 ],
  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]
10 21 ]
11 22  
12 23 TYPES = [
... ...
app/helpers/cms_helper.rb
... ... @@ -33,7 +33,7 @@ module CmsHelper
33 33 link_to article_name, {:action => 'view', :id => article.id}, :class => icon_for_article(article)
34 34 else
35 35 if article.image?
36   - image_tag(icon_for_article(article)) + link_to(article_name, article.url)
  36 + image_tag(icon_for_article(article)) + link_to(article_name, article.url)
37 37 else
38 38 link_to article_name, article.url, :class => icon_for_article(article)
39 39 end
... ...
app/helpers/content_viewer_helper.rb
... ... @@ -17,7 +17,7 @@ module ContentViewerHelper
17 17 title = article.display_title if article.kind_of?(UploadedFile) && article.image?
18 18 title = article.title if title.blank?
19 19 title = content_tag('h1', h(title), :class => 'title')
20   - if article.belongs_to_blog?
  20 + if article.belongs_to_blog? || article.belongs_to_forum?
21 21 unless args[:no_link]
22 22 title = content_tag('h1', link_to(article.name, article.url), :class => 'title')
23 23 end
... ...
app/helpers/folder_helper.rb
... ... @@ -21,6 +21,7 @@ module FolderHelper
21 21 end
22 22  
23 23 def display_article_in_listing(article, recursive = false, level = 0)
  24 + article = FilePresenter.for article
24 25 article_link = if article.image?
25 26 link_to('&nbsp;' * (level * 4) + image_tag(icon_for_article(article)) + short_filename(article.name), article.url.merge(:view => true))
26 27 else
... ... @@ -40,12 +41,15 @@ module FolderHelper
40 41 end
41 42  
42 43 def icon_for_article(article)
43   - icon = article.class.icon_name(article)
  44 + article = FilePresenter.for article
  45 + icon = article.respond_to?(:icon_name) ?
  46 + article.icon_name :
  47 + article.class.icon_name(article)
44 48 if (icon =~ /\//)
45 49 icon
46 50 else
47   - klasses = 'icon icon-' + icon
48   - if article.kind_of?(UploadedFile)
  51 + klasses = 'icon ' + [icon].flatten.map{|name| 'icon-'+name}.join(' ')
  52 + if article.kind_of?(UploadedFile) || article.kind_of?(FilePresenter)
49 53 klasses += ' icon-upload-file'
50 54 end
51 55 klasses
... ...
app/helpers/sweeper_helper.rb
... ... @@ -44,4 +44,30 @@ module SweeperHelper
44 44 def expire_profile_index(profile)
45 45 expire_timeout_fragment(profile.relationships_cache_key)
46 46 end
  47 +
  48 + def expire_blocks_cache(context, causes)
  49 + if context.kind_of?(Profile)
  50 + profile = context
  51 + environment = profile.environment
  52 + else
  53 + environment = context
  54 + profile = nil
  55 + end
  56 +
  57 + blocks_to_expire = []
  58 + if profile
  59 + profile.blocks.each {|block|
  60 + conditions = block.class.expire_on
  61 + blocks_to_expire << block unless (conditions[:profile] & causes).empty?
  62 + }
  63 + end
  64 + environment.blocks.each {|block|
  65 + conditions = block.class.expire_on
  66 + blocks_to_expire << block unless (conditions[:environment] & causes).empty?
  67 + }
  68 +
  69 + blocks_to_expire.uniq!
  70 + BlockSweeper.expire_blocks(blocks_to_expire)
  71 + end
  72 +
47 73 end
... ...
app/models/article.rb
... ... @@ -156,8 +156,12 @@ class Article &lt; ActiveRecord::Base
156 156 end
157 157 end
158 158  
  159 + def css_class_list
  160 + [self.class.name.underscore.dasherize]
  161 + end
  162 +
159 163 def css_class_name
160   - self.class.name.underscore.dasherize
  164 + [css_class_list].flatten.compact.join(' ')
161 165 end
162 166  
163 167 def pending_categorizations
... ... @@ -312,6 +316,10 @@ class Article &lt; ActiveRecord::Base
312 316 def belongs_to_blog?
313 317 self.parent and self.parent.blog?
314 318 end
  319 +
  320 + def belongs_to_forum?
  321 + self.parent and self.parent.forum?
  322 + end
315 323  
316 324 def info_from_last_update
317 325 last_comment = comments.last
... ... @@ -327,7 +335,7 @@ class Article &lt; ActiveRecord::Base
327 335 end
328 336  
329 337 def view_url
330   - @view_url ||= image? ? url.merge(:view => true) : url
  338 + @view_url ||= is_a?(UploadedFile) ? url.merge(:view => true) : url
331 339 end
332 340  
333 341 def comment_url_structure(comment, action = :edit)
... ... @@ -675,6 +683,10 @@ class Article &lt; ActiveRecord::Base
675 683  
676 684 delegate :region, :region_id, :environment, :environment_id, :to => :profile, :allow_nil => true
677 685  
  686 + def has_macro?
  687 + true
  688 + end
  689 +
678 690 private
679 691  
680 692 def sanitize_tag_list
... ...
app/models/article_block.rb
... ... @@ -12,7 +12,7 @@ class ArticleBlock &lt; Block
12 12 block = self
13 13 lambda do
14 14 block_title(block.title) +
15   - (block.article ? article_to_html(block.article,
  15 + (block.article ? article_to_html(FilePresenter.for(block.article),
16 16 :gallery_view => false,
17 17 :inside_block => block, # For Blogs and folders
18 18 :format => block.visualization_format # For Articles and contents
... ... @@ -23,7 +23,7 @@ class ArticleBlock &lt; Block
23 23 def article_id
24 24 self.settings[:article_id]
25 25 end
26   -
  26 +
27 27 def article_id= value
28 28 self.settings[:article_id] = value.blank? ? nil : value.to_i
29 29 end
... ... @@ -63,4 +63,9 @@ class ArticleBlock &lt; Block
63 63 end
64 64  
65 65 settings_items :visualization_format, :type => :string, :default => 'short'
  66 +
  67 + def self.expire_on
  68 + { :profile => [:article], :environment => [:article] }
  69 + end
  70 +
66 71 end
... ...
app/models/block.rb
... ... @@ -138,4 +138,19 @@ class Block &lt; ActiveRecord::Base
138 138 4.hours
139 139 end
140 140  
  141 + def has_macro?
  142 + false
  143 + end
  144 +
  145 + # Override in your subclasses.
  146 + # Define which events and context should cause the block cache to expire
  147 + # Possible events are: :article, :profile, :friendship, :category
  148 + # Possible contexts are: :profile, :environment
  149 + def self.expire_on
  150 + {
  151 + :profile => [],
  152 + :environment => []
  153 + }
  154 + end
  155 +
141 156 end
... ...
app/models/blog_archives_block.rb
... ... @@ -45,4 +45,7 @@ class BlogArchivesBlock &lt; Block
45 45 content_tag('div', link_to(_('Subscribe RSS Feed'), owner_blog.feed.url), :class => 'subscribe-feed')
46 46 end
47 47  
  48 + def self.expire_on
  49 + { :profile => [:article], :environment => [:article] }
  50 + end
48 51 end
... ...
app/models/categories_block.rb
... ... @@ -35,4 +35,7 @@ class CategoriesBlock &lt; Block
35 35 end
36 36 end
37 37  
  38 + def self.expire_on
  39 + { :profile => [], :environment => [:category] }
  40 + end
38 41 end
... ...
app/models/category.rb
... ... @@ -12,7 +12,7 @@ class Category &lt; ActiveRecord::Base
12 12 validates_uniqueness_of :slug,:scope => [ :environment_id, :parent_id ], :message => N_('%{fn} is already being used by another category.').fix_i18n
13 13 belongs_to :environment
14 14  
15   - validates_inclusion_of :display_color, :in => [ 1, 2, 3, 4, nil ]
  15 + validates_inclusion_of :display_color, :in => 1..15, :allow_nil => true
16 16 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
17 17  
18 18 # Finds all top level categories for a given environment.
... ...
app/models/comment.rb
... ... @@ -16,9 +16,7 @@ class Comment &lt; ActiveRecord::Base
16 16 has_many :children, :class_name => 'Comment', :foreign_key => 'reply_of_id', :dependent => :destroy
17 17 belongs_to :reply_of, :class_name => 'Comment', :foreign_key => 'reply_of_id'
18 18  
19   - named_scope :without_spam, :conditions => ['spam IS NULL OR spam = ?', false]
20 19 named_scope :without_reply, :conditions => ['reply_of_id IS NULL']
21   - named_scope :spam, :conditions => ['spam = ?', true]
22 20  
23 21 # unauthenticated authors:
24 22 validates_presence_of :name, :if => (lambda { |record| !record.email.blank? })
... ... @@ -108,6 +106,17 @@ class Comment &lt; ActiveRecord::Base
108 106  
109 107 include Noosfero::Plugin::HotSpot
110 108  
  109 + include Spammable
  110 +
  111 + def after_spam!
  112 + SpammerLogger.log(ip_address, self)
  113 + Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_spam))
  114 + end
  115 +
  116 + def after_ham!
  117 + Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_ham))
  118 + end
  119 +
111 120 def verify_and_notify
112 121 check_for_spam
113 122 unless spam?
... ... @@ -115,10 +124,6 @@ class Comment &lt; ActiveRecord::Base
115 124 end
116 125 end
117 126  
118   - def check_for_spam
119   - plugins.dispatch(:check_comment_for_spam, self)
120   - end
121   -
122 127 def notify_by_mail
123 128 if source.kind_of?(Article) && article.notify_comments?
124 129 if !notification_emails.empty?
... ... @@ -205,37 +210,6 @@ class Comment &lt; ActiveRecord::Base
205 210 @rejected = true
206 211 end
207 212  
208   - def spam?
209   - !spam.nil? && spam
210   - end
211   -
212   - def ham?
213   - !spam.nil? && !spam
214   - end
215   -
216   - def spam!
217   - self.spam = true
218   - self.save!
219   - SpammerLogger.log(ip_address, self)
220   - Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_spam))
221   - self
222   - end
223   -
224   - def ham!
225   - self.spam = false
226   - self.save!
227   - Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_ham))
228   - self
229   - end
230   -
231   - def marked_as_spam
232   - plugins.dispatch(:comment_marked_as_spam, self)
233   - end
234   -
235   - def marked_as_ham
236   - plugins.dispatch(:comment_marked_as_ham, self)
237   - end
238   -
239 213 def need_moderation?
240 214 article.moderate_comments? && (author.nil? || article.author != author)
241 215 end
... ...
app/models/environment.rb
... ... @@ -127,7 +127,9 @@ class Environment &lt; ActiveRecord::Base
127 127 'captcha_for_logged_users' => _('Ask captcha when a logged user comments too'),
128 128 'skip_new_user_email_confirmation' => _('Skip e-mail confirmation for new users'),
129 129 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'),
130   - 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login')
  130 + 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'),
  131 + 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'),
  132 + 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage')
131 133 }
132 134 end
133 135  
... ...
app/models/link_list_block.rb
... ... @@ -33,6 +33,12 @@ class LinkListBlock &lt; Block
33 33 ['chat', N_('Chat')]
34 34 ]
35 35  
  36 + TARGET_OPTIONS = [
  37 + [N_('Same page'), '_self'],
  38 + [N_('New tab'), '_blank'],
  39 + [N_('New window'), '_new'],
  40 + ]
  41 +
36 42 settings_items :links, Array, :default => []
37 43  
38 44 before_save do |block|
... ... @@ -57,7 +63,7 @@ class LinkListBlock &lt; Block
57 63 def link_html(link)
58 64 klass = 'icon-' + link[:icon] if link[:icon]
59 65 sanitize_link(
60   - link_to(link[:name], expand_address(link[:address]), :class => klass)
  66 + link_to(link[:name], expand_address(link[:address]), :target => link[:target], :class => klass)
61 67 )
62 68 end
63 69  
... ...
app/models/raw_html_block.rb
... ... @@ -10,4 +10,7 @@ class RawHTMLBlock &lt; Block
10 10 (title.blank? ? '' : block_title(title)).html_safe + html.to_s.html_safe
11 11 end
12 12  
  13 + def has_macro?
  14 + true
  15 + end
13 16 end
... ...
app/models/recent_documents_block.rb
... ... @@ -30,4 +30,7 @@ class RecentDocumentsBlock &lt; Block
30 30 end
31 31 end
32 32  
  33 + def self.expire_on
  34 + { :profile => [:article], :environment => [:article] }
  35 + end
33 36 end
... ...
app/models/spammer_logger.rb
... ... @@ -6,6 +6,8 @@ class SpammerLogger &lt; Logger
6 6 if object
7 7 if object.kind_of?(Comment)
8 8 @logger << "[#{Time.now.strftime('%F %T %z')}] Comment-id: #{object.id} IP: #{spammer_ip}\n"
  9 + elsif object.kind_of?(SuggestArticle)
  10 + @logger << "[#{Time.now.strftime('%F %T %z')}] SuggestArticle-id: #{object.id} IP: #{spammer_ip}\n"
9 11 end
10 12 else
11 13 @logger << "[#{Time.now.strftime('%F %T %z')}] IP: #{spammer_ip}\n"
... ...
app/models/suggest_article.rb
... ... @@ -11,6 +11,17 @@ class SuggestArticle &lt; Task
11 11 settings_items :source, :type => String
12 12 settings_items :source_name, :type => String
13 13 settings_items :highlighted, :type => :boolean, :default => false
  14 + settings_items :ip_address, :type => String
  15 + settings_items :user_agent, :type => String
  16 + settings_items :referrer, :type => String
  17 +
  18 + after_create :schedule_spam_checking
  19 +
  20 + def schedule_spam_checking
  21 + self.delay.check_for_spam
  22 + end
  23 +
  24 + include Noosfero::Plugin::HotSpot
14 25  
15 26 def sender
16 27 "#{name} (#{email})"
... ... @@ -61,4 +72,12 @@ class SuggestArticle &lt; Task
61 72 _('You need to login on %{system} in order to approve or reject this article.') % { :system => target.environment.name }
62 73 end
63 74  
  75 + def after_spam!
  76 + SpammerLogger.log(ip_address, self)
  77 + self.delay.marked_as_spam
  78 + end
  79 +
  80 + def after_ham!
  81 + self.delay.marked_as_ham
  82 + end
64 83 end
... ...
app/models/tags_block.rb
... ... @@ -59,4 +59,8 @@ class TagsBlock &lt; Block
59 59 15.minutes
60 60 end
61 61  
  62 + def self.expire_on
  63 + { :profile => [:article], :environment => [:article] }
  64 + end
  65 +
62 66 end
... ...
app/models/task.rb
... ... @@ -235,6 +235,8 @@ class Task &lt; ActiveRecord::Base
235 235 end
236 236 end
237 237  
  238 + include Spammable
  239 +
238 240 protected
239 241  
240 242 # This method must be overrided in subclasses, and its implementation must do
... ... @@ -275,6 +277,7 @@ class Task &lt; ActiveRecord::Base
275 277 named_scope :of, lambda { |type| conditions = type ? "type LIKE '#{type}'" : "1=1"; {:conditions => [conditions]} }
276 278 named_scope :order_by, lambda { |attribute, ord| {:order => "#{attribute} #{ord}"} }
277 279  
  280 +
278 281 named_scope :to, lambda { |profile|
279 282 environment_condition = nil
280 283 if profile.person?
... ...
app/models/uploaded_file.rb
... ... @@ -41,7 +41,25 @@ class UploadedFile &lt; Article
41 41 end
42 42  
43 43 def self.max_size
44   - UploadedFile.attachment_options[:max_size]
  44 + default = 5.megabytes
  45 +
  46 + multipliers = {
  47 + :KB => :kilobytes,
  48 + :MB => :megabytes,
  49 + :GB => :gigabytes,
  50 + :TB => :terabytes,
  51 + }
  52 + max_upload_size = NOOSFERO_CONF['max_upload_size']
  53 +
  54 + if max_upload_size =~ /^(\d+(\.\d+)?)\s*(KB|MB|GB|TB)?$/
  55 + number = $1.to_f
  56 + unit = $3 || :MB
  57 + multiplier = multipliers[unit.to_sym]
  58 +
  59 + number.send(multiplier).to_i
  60 + else
  61 + default
  62 + end
45 63 end
46 64  
47 65 # FIXME need to define min/max file size
... ... @@ -52,20 +70,28 @@ class UploadedFile &lt; Article
52 70 has_attachment :storage => :file_system,
53 71 :thumbnails => { :icon => [24,24], :thumb => '130x130>', :slideshow => '320x240>', :display => '640X480>' },
54 72 :thumbnail_class => Thumbnail,
55   - :max_size => 5.megabytes # remember to update validate message below
  73 + :max_size => self.max_size
56 74  
57   - validates_attachment :size => N_("%{fn} of uploaded file was larger than the maximum size of 5.0 MB").fix_i18n
  75 + validates_attachment :size => N_("%{fn} of uploaded file was larger than the maximum size of %{size}").sub('%{size}', self.max_size.to_humanreadable).fix_i18n
58 76  
59 77 delay_attachment_fu_thumbnails
60 78  
61 79 postgresql_attachment_fu
62 80  
  81 + # Use this method only to get the generic icon for this kind of content.
  82 + # If you want the specific icon for a file type or the iconified version
  83 + # of an image, use FilePresenter.for(uploaded_file).icon_name
63 84 def self.icon_name(article = nil)
64   - if article
65   - article.image? ? article.public_filename(:icon) : (article.mime_type ? article.mime_type.gsub(/[\/+.]/, '-') : 'upload-file')
66   - else
67   - 'upload-file'
  85 + unless article.nil?
  86 + warn = ('='*80) + "\n" +
  87 + 'The method `UploadedFile.icon_name(obj)` is deprecated. ' +
  88 + 'You must to encapsulate UploadedFile with `FilePresenter.for()`.' +
  89 + "\n" + ('='*80)
  90 + raise NoMethodError, warn if ENV['RAILS_ENV'] == 'test'
  91 + Rails.logger.warn warn if Rails.logger
  92 + puts warn if ENV['RAILS_ENV'] == 'development'
68 93 end
  94 + 'upload-file'
69 95 end
70 96  
71 97 def mime_type
... ... @@ -91,40 +117,27 @@ class UploadedFile &lt; Article
91 117 end
92 118  
93 119 def to_html(options = {})
  120 + warn = ('='*80) + "\n" +
  121 + 'The method `UploadedFile#to_html()` is deprecated. ' +
  122 + 'You must to encapsulate UploadedFile with `FilePresenter.for()`.' +
  123 + "\n" + ('='*80)
  124 + raise NoMethodError, warn if ENV['RAILS_ENV'] == 'test'
  125 + Rails.logger.warn warn if Rails.logger
  126 + puts warn if ENV['RAILS_ENV'] == 'development'
94 127 article = self
95 128 if image?
96 129 lambda do
97   - if article.gallery? && options[:gallery_view]
98   - images = article.parent.images
99   - current_index = images.index(article)
100   - total_of_images = images.count
101   -
102   - link_to_previous = if current_index >= 1
103   - link_to(_('&laquo; Previous'), images[current_index - 1].view_url, :class => 'left')
104   - else
105   - content_tag('span', _('&laquo; Previous'), :class => 'left')
106   - end
107   -
108   - link_to_next = if current_index < total_of_images - 1
109   - link_to(_('Next &raquo;'), images[current_index + 1].view_url, :class => 'right')
110   - else
111   - content_tag('span', _('Next &raquo;'), :class => 'right')
112   - end
113   -
114   - content_tag(
115   - 'div',
116   - link_to_previous + (content_tag('span', _('image %d of %d'), :class => 'total-of-images') % [current_index + 1, total_of_images]).html_safe + link_to_next,
117   - :class => 'gallery-navigation'
118   - )
119   - end.to_s +
120   - image_tag(article.public_filename(:display), :class => article.css_class_name, :style => 'max-width: 100%') +
121   - content_tag('p', article.abstract, :class => 'uploaded-file-description')
122   -
  130 + image_tag(article.public_filename(:display),
  131 + :class => article.css_class_name,
  132 + :style => 'max-width: 100%') +
  133 + content_tag('div', article.abstract, :class => 'uploaded-file-description')
123 134 end
124 135 else
125 136 lambda do
126   - content_tag('ul', content_tag('li', link_to(article.name, article.url, :class => article.css_class_name))) +
127   - content_tag('p', article.abstract, :class => 'uploaded-file-description')
  137 + content_tag('div',
  138 + link_to(article.name, article.url),
  139 + :class => article.css_class_name) +
  140 + content_tag('div', article.abstract, :class => 'uploaded-file-description')
128 141 end
129 142 end
130 143 end
... ...
app/presenters/generic.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +# Made to encapsulate any UploadedFile
  2 +class FilePresenter::Generic < FilePresenter
  3 + # if returns low priority, because it is generic.
  4 + def self.accepts?(f)
  5 + 1 if f.is_a? UploadedFile
  6 + end
  7 +end
... ...
app/presenters/image.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class FilePresenter::Image < FilePresenter
  2 + def self.accepts?(f)
  3 + return nil unless f.respond_to? :image?
  4 + f.image? ? 10 : nil
  5 + end
  6 +
  7 + def icon_name
  8 + public_filename :icon
  9 + end
  10 +
  11 + def short_description
  12 + _('Image (%s)') % content_type.split('/')[1].upcase
  13 + end
  14 +end
... ...
app/sweepers/article_sweeper.rb
... ... @@ -16,15 +16,15 @@ class ArticleSweeper &lt; ActiveRecord::Observer
16 16 end
17 17 end
18 18  
  19 +
19 20 protected
20 21  
21 22 def expire_caches(article)
  23 + expire_blocks_cache(article.profile, [:article])
  24 +
22 25 return if !article.environment
  26 +
23 27 article.hierarchy(true).each { |a| a.touch if a != article }
24   - blocks = article.profile.blocks
25   - blocks += article.profile.environment.blocks if article.profile.environment
26   - blocks = blocks.select{|b|[RecentDocumentsBlock, BlogArchivesBlock].any?{|c| b.kind_of?(c)}}
27   - BlockSweeper.expire_blocks(blocks)
28 28 env = article.profile.environment
29 29 if env && (env.portal_community == article.profile)
30 30 article.environment.locales.keys.each do |locale|
... ...
app/sweepers/category_sweeper.rb
... ... @@ -3,7 +3,13 @@ class CategorySweeper &lt; ActiveRecord::Observer
3 3 include SweeperHelper
4 4  
5 5 def after_save(category)
  6 + expire_blocks_cache(category.environment, [:category])
  7 +
  8 + # Needed for environments with application layout
6 9 expire_fragment(category.environment.id.to_s + "_categories_menu")
7 10 end
8 11  
  12 + def after_destroy(category)
  13 + expire_blocks_cache(category.environment, [:category])
  14 + end
9 15 end
... ...
app/views/box_organizer/_link_list_block.rhtml
1 1 <strong><%= _('Links') %></strong>
2 2 <div id='edit-link-list-block' style='width:450px'>
3 3 <table id='links' class='noborder'>
4   - <tr><th><%= _('Icon') %></th><th><%= _('Name') %></th><th><%= _('Address') %></th></tr>
  4 + <tr><th><%= _('Icon') %></th><th><%= _('Name') %></th><th><%= _('Address') %></th><th><%= _('Target') %></th></tr>
5 5 <% for link in @block.links do %>
6 6 <tr>
7 7 <td>
... ... @@ -9,6 +9,9 @@
9 9 </td>
10 10 <td><%= text_field_tag 'block[links][][name]', link[:name], :class => 'link-name', :maxlength => 20 %></td>
11 11 <td class='cel-address'><%= text_field_tag 'block[links][][address]', link[:address], :class => 'link-address' %></td>
  12 + <td>
  13 + <%= select_tag('block[links][][target]', options_for_select(LinkListBlock::TARGET_OPTIONS, link[:target])) %>
  14 + </td>
12 15 </tr>
13 16 <% end %>
14 17 </table>
... ... @@ -18,7 +21,9 @@
18 21 page.insert_html :bottom, 'links', content_tag('tr',
19 22 content_tag('td', icon_selector('ok')) +
20 23 content_tag('td', text_field_tag('block[links][][name]', '', :maxlength => 20)) +
21   - content_tag('td', text_field_tag('block[links][][address]', nil, :class => 'cel-address'))
  24 + content_tag('td', text_field_tag('block[links][][address]', nil, :class => 'link-address'), :class => 'cel-address') +
  25 + content_tag('td', select_tag('block[links][][target]',
  26 +options_for_select(LinkListBlock::TARGET_OPTIONS, link[:target])))
22 27 ) +
23 28 javascript_tag("$('edit-link-list-block').scrollTop = $('edit-link-list-block').scrollHeight")
24 29 end %>
... ...
app/views/cms/view.rhtml
... ... @@ -40,13 +40,16 @@
40 40 </tr>
41 41 <% end %>
42 42  
43   - <% @articles.each do |article| %>
  43 + <% @articles.each do |article| article = FilePresenter.for article %>
44 44 <tr title="<%= article.title%>" >
45   - <td>
  45 + <td class="article-name">
46 46 <%= link_to_article(article) %>
47 47 </td>
48   - <td>
49   - <%= article.class.short_description %>
  48 + <% short_description = article.respond_to?(:short_description) ?
  49 + article.short_description :
  50 + article.class.short_description %>
  51 + <td class="article-mime" title=<%= short_description.to_json %>>
  52 + <%= short_description %>
50 53 </td>
51 54 <td class="article-controls">
52 55 <%= expirable_button article, :edit, _('Edit'), {:action => 'edit', :id => article.id} if !remove_content_button(:edit) %>
... ...
app/views/file_presenter/_generic.html.erb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +<span class="download-link">
  2 + <span>Download</span>
  3 + <strong><%= link_to generic.filename, generic.public_filename %></strong>
  4 +</span>
  5 +
  6 +<div class="uploaded-file-description <%= 'empty' if generic.abstract.blank? %>">
  7 + <%= generic.abstract %>
  8 +</div>
  9 +
... ...
app/views/file_presenter/_image.html.erb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +<% if image.gallery? && options[:gallery_view] %>
  2 +<%
  3 + images = image.parent.images
  4 + current_index = images.index(image.encapsulated_file)
  5 + total_of_images = images.count
  6 + link_to_previous = if current_index >= 1
  7 + link_to(_('&laquo; Previous'), images[current_index - 1].view_url, :class => 'previous')
  8 + else
  9 + content_tag('span', _('&laquo; Previous'), :class => 'previous')
  10 + end
  11 +
  12 + link_to_next = if current_index < total_of_images - 1
  13 + link_to(_('Next &raquo;'), images[current_index + 1].view_url, :class => 'next')
  14 + else
  15 + content_tag('span', _('Next &raquo;'), :class => 'next')
  16 + end
  17 +%>
  18 +
  19 +<div class="gallery-navigation">
  20 + <%= link_to_previous %>
  21 + <span class="total-of-images">
  22 + <%= _('image %d of %d') % [current_index + 1, total_of_images] %>
  23 + </span>
  24 + <%= link_to_next %>
  25 +</div>
  26 +
  27 +<% end %>
  28 +
  29 +<%# image_tag(article.public_filename(:display), :class => article.css_class_name, :style => 'max-width: 100%') %>
  30 +
  31 +<img src="<%=image.public_filename(:display)%>" class="<%=image.css_class_name%>">
  32 +
  33 +<div class="uploaded-file-description <%= 'empty' if image.abstract.blank? %>">
  34 + <%= image.abstract %>
  35 +</div>
  36 +
... ...
app/views/memberships/new_community.rhtml
... ... @@ -46,9 +46,11 @@
46 46  
47 47 <%= template_options(Community, 'community')%>
48 48  
  49 + <%= hidden_field_tag('back_to', @back_to) %>
  50 +
49 51 <% button_bar do %>
50 52 <%= submit_button(:save, _('Create')) %>
51   - <%= button(:cancel, _('Cancel'), :action => 'index') %>
  53 + <%= button(:cancel, _('Cancel'), @back_to ) %>
52 54 <% end %>
53 55  
54 56 <% end %>
... ...
app/views/shared/_manage_enterprises.rhtml
... ... @@ -1,8 +0,0 @@
1   -<div id='manage-enterprises'>
2   - <a href="#" id='manage-enterprises-link' class='simplemenu-trigger' title='<%= _('Manage enterprises') %>'><i class="icon-menu-enterprise"></i><strong><%= ui_icon('ui-icon-triangle-1-s') + _('My enterprises') %></strong></a>
3   - <ul class='simplemenu-submenu'>
4   - <% enterprises_link.each do |link| %>
5   - <li class='simplemenu-item'><%= link %></li>
6   - <% end %>
7   - </ul>
8   -</div>
app/views/shared/_manage_link.html.erb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<div id=<%= "manage-#{kind}" %> class="manage-groups">
  2 + <a href="#" id=<%= "manage-#{kind}-link" %> class="simplemenu-trigger" title="<%= _('Manage %s') % kind %>"><i class=<%= "icon-menu-#{kind.singularize}" %>></i><strong><%= ui_icon('ui-icon-triangle-1-s') + _('My %s') % kind %></strong></a>
  3 + <ul class="simplemenu-submenu">
  4 + <% link.each do |link| %>
  5 + <li class="simplemenu-item"><%= link %></li>
  6 + <% end %>
  7 + </ul>
  8 +</div>
... ...
app/views/spam/_comment_spam.rhtml 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +<%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %>
  2 +<div id='article'>
  3 + <div class="comments" id="comments_list">
  4 + <ul class="article-comments-list">
  5 + <%= render :partial => 'comment/comment', :collection => @comment_spam %>
  6 + </ul>
  7 + </div>
  8 +</div>
  9 +
  10 +<%= pagination_links @comment_spam, :param_name => :comments_page %>
  11 +
... ...
app/views/spam/_suggest_article.html.erb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +<% render :layout => 'task', :locals => { :task => task } do %>
  2 + <% content_for :extra_buttons do %>
  3 + <%= button_to_function('down', _('Show details'), "toggleDetails(this, '#{_('Hide details')}', '#{_('Show details')}')" ) %>
  4 + <% end %>
  5 +
  6 + <% content_for :extra_content do %>
  7 + <ul class="suggest-article-details" style="display: none">
  8 + <li><strong><%=_('Sent by')%></strong>: <%=task.name%> </li>
  9 + <li><strong><%=_('Email')%></strong>: <%=task.email%> </li>
  10 + <li><strong><%=_('Source')%></strong>: <%=task.source_name%> </li>
  11 + <li><strong><%=_('Source URL')%></strong>: <%=task.source%> </li>
  12 + <li><strong><%=_('Folder')%></strong>: <%=(a = Article.find_by_id(task.article_parent_id))?a.name : '<em>' + s_('Folder|none') + '</em>'%> </li>
  13 + <li><strong><%=_('Lead')%></strong>: <%=task.article_abstract.blank? ? '<em>' + s_('Abstract|empty') + '</em>' : task.article_abstract%> </li>
  14 + <li><strong><%=_('Body')%></strong>:
  15 + <div class='suggest-article-body'>
  16 + <%= task.article_body %>
  17 + </div>
  18 + </li>
  19 + </ul>
  20 + <% end %>
  21 +<% end %>
... ...
app/views/spam/_task.rhtml 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +<div class="task_box" id="task-<%= task.id %>">
  2 + <%= render :partial => 'tasks/task_icon', :locals => {:task => task} %>
  3 + <%= render :partial => 'tasks/task_title', :locals => {:task => task} %>
  4 + <div class="task-information">
  5 + <%= task_information(task) %>
  6 + </div>
  7 +
  8 + <%= yield %> <% # ??? %>
  9 +
  10 + <% button_bar do %>
  11 + <%= button_to_function('new', _('Mark as NOT SPAM'), 'removeTaskBox(this, %s, "%s", "")' % [url_for(:mark_task_as_ham => task.id).to_json, "task-#{task.id}"]) %>
  12 + <%= yield :extra_buttons %>
  13 + <%= button_to_function('delete', _('Remove'), 'removeTaskBox(this, %s, "%s", %s)' % [url_for(:profile => params[:profile], :remove_task => task.id).to_json, "task-#{task.id}", _('Are you sure you want to remove this article suggestion?').to_json]) %>
  14 +
  15 + <% end %>
  16 +
  17 + <%= yield :extra_content %>
  18 +</div>
... ...
app/views/spam/_task_spam.rhtml 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<% @task_spam.each do |t| %>
  2 + <%= render :partial => partial_for_class(t.class), :locals => { :task => t } %>
  3 +<% end %>
  4 +<%= pagination_links @task_spam, :param_name => :tasks_page %>
... ...
app/views/spam/index.rhtml
  1 +<%= stylesheet('tasks') %>
  2 +
1 3 <h1><%= _('Manage SPAM') %></h1>
2 4  
  5 +<% no_tabs = @comment_spam.blank? && @task_spam.blank? %>
  6 +
  7 +<%= _('There are no spams to review.') if no_tabs %>
  8 +
3 9 <% button_bar do %>
4 10 <%= button :back, _('Back to control panel'), :controller => :profile_editor %>
5 11 <% end %>
6 12  
7 13 <%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %>
8   -<div id='article'>
9   - <div class="comments" id="comments_list">
10   - <ul class="article-comments-list">
11   - <%= render :partial => 'comment/comment', :collection => @spam %>
12   - </ul>
13   - </div>
14   -</div>
15 14  
16   -<%= pagination_links @spam %>
  15 +<% tabs = [] %>
  16 +<% tabs << {:title => _('Comment Spam'), :id => 'comment-spam',
  17 + :content => (render :partial => 'comment_spam')} if @comment_spam.present? %>
  18 +<% tabs << {:title => _('Task Spam'), :id => 'task-spam',
  19 + :content => (render :partial => 'task_spam') } if @task_spam.present? %>
  20 +<%= render_tabs(tabs) %>
17 21  
18   -<% button_bar do %>
19   - <%= button :back, _('Back to control panel'), :controller => :profile_editor %>
  22 +<% unless no_tabs %>
  23 + <% button_bar do %>
  24 + <%= button :back, _('Back to control panel'), :controller => :profile_editor %>
  25 + <% end %>
20 26 <% end %>
  27 +
  28 +<%= javascript_include_tag 'spam' %>
... ...
app/views/tasks/_task.rhtml
1 1 <div class="task_box" id="task-<%= task.id %>">
2 2  
3   - <div class="task_icon">
4   - <%
5   - icon_info = task.icon
6   - if icon_info[:type] == :profile_image
7   - icon = profile_image(icon_info[:profile], :minor)
8   - elsif icon_info[:type] == :defined_image
9   - icon = "<img src='#{icon_info[:src]}' alt='#{icon_info[:name]}' />"
10   - end
11   - %>
12   - <%=
13   - if icon_info[:url]
14   - link_to(icon, icon_info[:url])
15   - else
16   - icon
17   - end
18   - %>
19   -
20   - </div>
  3 + <%= render :partial => 'task_icon', :locals => {:task => task} %>
21 4  
22 5 <div class="task_decisions">
23 6 <%=
... ... @@ -39,9 +22,7 @@
39 22 %>
40 23 </div><!-- class="task_decisions" -->
41 24  
42   - <strong class="task_title">
43   - <%= task.title %>
44   - </strong>
  25 + <%= render :partial => 'task_title', :locals => {:task => task} %>
45 26  
46 27 <div class="task_information">
47 28 <%= task_information(task) %>
... ...
app/views/tasks/_task_icon.rhtml 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +<%
  2 + icon_info = task.icon
  3 + if icon_info[:type] == :profile_image
  4 + icon = profile_image(icon_info[:profile], :minor)
  5 + elsif icon_info[:type] == :defined_image
  6 + icon = "<img src='#{icon_info[:src]}' alt='#{icon_info[:name]}' />"
  7 + end
  8 +
  9 + if icon_info[:url]
  10 + icon = link_to(icon, icon_info[:url])
  11 + end
  12 +%>
  13 +
  14 +<div class="task_icon">
  15 + <%= icon %>
  16 +</div>
... ...
app/views/tasks/_task_title.rhtml 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<strong class="task_title">
  2 + <%= task.title %>
  3 +</strong>
... ...
config/initializers/plugins.rb
... ... @@ -4,4 +4,5 @@ require &#39;noosfero/plugin/manager&#39;
4 4 require 'noosfero/plugin/active_record'
5 5 require 'noosfero/plugin/mailer_base'
6 6 require 'noosfero/plugin/settings'
  7 +require 'noosfero/plugin/spammable'
7 8 Noosfero::Plugin.init_system if $NOOSFERO_LOAD_PLUGINS
... ...
config/noosfero.yml.dist
... ... @@ -8,6 +8,7 @@ development:
8 8 gravatar: wavatar
9 9 googlemaps_initial_zoom: 4
10 10 exception_recipients: [admin@example.com]
  11 + max_upload_size: 5MB
11 12  
12 13 test:
13 14  
... ...
db/migrate/20131011164400_add_spam_to_task.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class AddSpamToTask < ActiveRecord::Migration
  2 + def self.up
  3 + change_table :tasks do |t|
  4 + t.boolean :spam, :default => false
  5 + end
  6 + Task.update_all ["spam = ?", false]
  7 + add_index :tasks, [:spam]
  8 + end
  9 +
  10 + def self.down
  11 + remove_column :tasks, :spam
  12 + end
  13 +end
... ...
db/migrate/20131011172930_add_image_to_article.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class AddImageToArticle < ActiveRecord::Migration
  2 +
  3 + def self.up
  4 + add_column :articles, :image_id, :integer
  5 + add_column :article_versions, :image_id, :integer
  6 + end
  7 +
  8 + def self.down
  9 + remove_column :articles, :image_id
  10 + remove_column :article_versions, :image_id
  11 + end
  12 +
  13 +end
... ...
db/migrate/20131015161830_add_position_to_article.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class AddPositionToArticle < ActiveRecord::Migration
  2 +
  3 + def self.up
  4 + add_column :articles, :position, :integer
  5 + add_column :article_versions, :position, :integer
  6 + end
  7 +
  8 + def self.down
  9 + remove_column :articles, :position
  10 + remove_column :article_versions, :position
  11 + end
  12 +
  13 +end
... ...
db/migrate/20131113151730_add_image_to_article.rb
... ... @@ -1,13 +0,0 @@
1   -class AddImageToArticle < ActiveRecord::Migration
2   -
3   - def self.up
4   - add_column :articles, :image_id, :integer
5   - add_column :article_versions, :image_id, :integer
6   - end
7   -
8   - def self.down
9   - remove_column :articles, :image_id
10   - remove_column :article_versions, :image_id
11   - end
12   -
13   -end
db/migrate/20131113151835_add_position_to_article.rb
... ... @@ -1,13 +0,0 @@
1   -class AddPositionToArticle < ActiveRecord::Migration
2   -
3   - def self.up
4   - add_column :articles, :position, :integer
5   - add_column :article_versions, :position, :integer
6   - end
7   -
8   - def self.down
9   - remove_column :articles, :position
10   - remove_column :article_versions, :position
11   - end
12   -
13   -end
db/migrate/20131116165327_enable_enterprises_list_on_user_menu.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class EnableEnterprisesListOnUserMenu < ActiveRecord::Migration
  2 + def self.up
  3 + # The enterprises were always listed on user menu.
  4 + # As now it is configured by admin, the running environments should not need to enable it
  5 + select_all("select id from environments").each do |environment|
  6 + env = Environment.find(environment['id'])
  7 + env.enable(:display_my_enterprises_on_user_menu)
  8 + end
  9 + end
  10 +
  11 + def self.down
  12 + #nothing to be done
  13 + end
  14 +end
... ...
db/schema.rb
... ... @@ -9,7 +9,7 @@
9 9 #
10 10 # It's strongly recommended to check this file into your version control system.
11 11  
12   -ActiveRecord::Schema.define(:version => 20130711213046) do
  12 +ActiveRecord::Schema.define(:version => 20131116165327) do
13 13  
14 14 create_table "abuse_reports", :force => true do |t|
15 15 t.integer "reporter_id"
... ... @@ -86,6 +86,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130711213046) do
86 86 t.string "language"
87 87 t.string "source_name"
88 88 t.integer "license_id"
  89 + t.integer "image_id"
  90 + t.integer "position"
89 91 end
90 92  
91 93 add_index "article_versions", ["article_id"], :name => "index_article_versions_on_article_id"
... ... @@ -129,6 +131,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130711213046) do
129 131 t.string "language"
130 132 t.string "source_name"
131 133 t.integer "license_id"
  134 + t.integer "image_id"
  135 + t.integer "position"
132 136 end
133 137  
134 138 add_index "articles", ["name"], :name => "index_articles_on_name"
... ... @@ -547,8 +551,11 @@ ActiveRecord::Schema.define(:version =&gt; 20130711213046) do
547 551 t.datetime "created_at"
548 552 t.string "target_type"
549 553 t.integer "image_id"
  554 + t.boolean "spam", :default => false
550 555 end
551 556  
  557 + add_index "tasks", ["spam"], :name => "index_tasks_on_spam"
  558 +
552 559 create_table "thumbnails", :force => true do |t|
553 560 t.integer "size"
554 561 t.string "content_type"
... ...
features/browse_catalogs.feature
... ... @@ -9,7 +9,7 @@ Feature: browse catalogs
9 9 And the following enterprises
10 10 | identifier | owner | name | enabled |
11 11 | artebonito | joaosilva | Associação de Artesanato de Bonito | true |
12   - And feature "disable_products_for_enterprises" is disabled on environment
  12 + And feature "products_for_enterprises" is enabled on environment
13 13 And the following product_categories
14 14 | name |
15 15 | categ1 |
... ...
features/browse_enterprises.feature
... ... @@ -6,7 +6,7 @@ Background:
6 6 Given the following enterprises
7 7 | identifier | name |
8 8 | shop1 | Shoes Shop |
9   - And feature "disable_products_for_enterprises" is disabled on environment
  9 + And feature "products_for_enterprises" is enabled on environment
10 10 And feature "show_balloon_with_profile_links_when_clicked" is enabled on environment
11 11  
12 12 Scenario: show all enterprises
... ...
features/enterprise_homepage.feature
... ... @@ -21,6 +21,7 @@ Feature: enterprise homepage
21 21 And the following product
22 22 | name | category | owner |
23 23 | Natural Handmade | soap | mayhem |
  24 + And feature "products_for_enterprises" is enabled on environment
24 25  
25 26  
26 27 Scenario: display profile info
... ...
features/manage_inputs.feature
... ... @@ -19,7 +19,7 @@ Feature: manage inputs
19 19 And the following product
20 20 | owner | category | name |
21 21 | redemoinho | rock | Abbey Road |
22   - And feature "disable_products_for_enterprises" is disabled on environment
  22 + And feature "products_for_enterprises" is enabled on environment
23 23 And the following units
24 24 | singular | plural |
25 25 | Meter | Meters |
... ...
features/manage_product_price_details.feature
... ... @@ -19,7 +19,7 @@ Feature: manage product price details
19 19 And the following product
20 20 | owner | category | name | price |
21 21 | redemoinho | rock | Abbey Road | 80.0 |
22   - And feature "disable_products_for_enterprises" is disabled on environment
  22 + And feature "products_for_enterprises" is enabled on environment
23 23 And the following inputs
24 24 | product | category | price_per_unit | amount_used |
25 25 | Abbey Road | Rock | 10.0 | 2 |
... ...
features/manage_products.feature
... ... @@ -9,7 +9,7 @@ Feature: manage products
9 9 And the following enterprises
10 10 | identifier | owner | name | enabled |
11 11 | redemoinho | joaosilva | Rede Moinho | true |
12   - And feature "disable_products_for_enterprises" is disabled on environment
  12 + And feature "products_for_enterprises" is enabled on environment
13 13  
14 14 Scenario: display "create new product" button
15 15 Given I am logged in as "joaosilva"
... ...
lib/feed_updater.rb
... ... @@ -20,14 +20,20 @@ end
20 20 class FeedUpdater
21 21  
22 22 class ExceptionNotification < ActionMailer::Base
23   - def mail error
  23 + def mail container, error
24 24 environment = Environment.default
25 25  
26 26 recipients NOOSFERO_CONF['exception_recipients']
27 27 from environment.contact_email
28 28 reply_to environment.contact_email
29 29 subject "[#{environment.name}] Feed-updater: #{error.message}"
30   - body render(:text => error.backtrace.join("\n"))
  30 + body render(:text => "
  31 +Container:
  32 +#{container.inspect}
  33 +
  34 +Backtrace:
  35 +#{error.backtrace.join("\n")}
  36 + ")
31 37 end
32 38 end
33 39  
... ... @@ -88,11 +94,13 @@ class FeedUpdater
88 94 if !running
89 95 break
90 96 end
91   - feed_handler.process(container)
  97 + begin
  98 + feed_handler.process(container)
  99 + rescue Exception => e
  100 + FeedUpdater::ExceptionNotification.deliver_mail container, e if NOOSFERO_CONF['exception_recipients'].present?
  101 + end
92 102 end
93 103 end
94   - rescue Exception => e
95   - FeedUpdater::ExceptionNotification.deliver_mail e if NOOSFERO_CONF['exception_recipients'].present?
96 104 end
97 105 end
98 106  
... ...
lib/file_presenter.rb 0 → 100644
... ... @@ -0,0 +1,107 @@
  1 +# All file presenters must extends `FilePresenter` not only to ensure the
  2 +# same interface, but also to make `FilePresenter.for(file)` to work.
  3 +class FilePresenter
  4 +
  5 + # Will return a encapsulated `UploadedFile` or the same object if no
  6 + # one accepts it. That behave allow to give any model to this class,
  7 + # like a Article and have no trouble with that.
  8 + def self.for(f)
  9 + return f if f.is_a? FilePresenter
  10 + klass = FilePresenter.subclasses.sort_by {|class_name|
  11 + class_name.constantize.accepts?(f) || 0
  12 + }.last.constantize
  13 + klass.accepts?(f) ? klass.new(f) : f
  14 + end
  15 +
  16 + def initialize(f)
  17 + @file = f
  18 + end
  19 +
  20 + # Allows to use the original `UploadedFile` reference.
  21 + def encapsulated_file
  22 + @file
  23 + end
  24 +
  25 + def id
  26 + @file.id
  27 + end
  28 +
  29 + def reload
  30 + @file.reload
  31 + self
  32 + end
  33 +
  34 + # This method must be overridden in subclasses.
  35 + #
  36 + # If the class accepts the file, return a number that represents the
  37 + # priority the class should be given to handle that file. Higher numbers
  38 + # mean higher priority.
  39 + #
  40 + # If the class does not accept the file, return false.
  41 + def self.accepts?(f)
  42 + nil
  43 + end
  44 +
  45 + def short_description
  46 + _("File (%s)") % content_type.sub(/^application\//, '').sub(/^x-/, '').sub(/^image\//, '')
  47 + end
  48 +
  49 + # Define the css classes to style the page fragment with the file related
  50 + # content. If you want other classes to identify this area to your
  51 + # customized presenter, so do this:
  52 + # def css_class_list
  53 + # [super, 'myclass'].flatten
  54 + # end
  55 + def css_class_list
  56 + [ @file.css_class_list,
  57 + 'file-' + self.class.to_s.split(/:+/).map(&:underscore)[1..-1].join('-'),
  58 + 'content-type_' + self.content_type.split('/')[0],
  59 + 'content-type_' + self.content_type.gsub(/[^a-z0-9]/i,'-')
  60 + ].flatten
  61 + end
  62 +
  63 + # Enable file presenter to customize the css classes on view_page.rhtml
  64 + # You may not overwrite this method on your customized presenter.
  65 + def css_class_name
  66 + [css_class_list].flatten.compact.join(' ')
  67 + end
  68 +
  69 + # The generic icon class-name or the specific file path.
  70 + # You may replace this method on your custom FilePresenter.
  71 + # See the current used icons class-names in public/designs/icons/tango/style.css
  72 + def icon_name
  73 + if mime_type
  74 + [ mime_type.split('/')[0], mime_type.gsub(/[^a-z0-9]/i, '-') ]
  75 + else
  76 + 'upload-file'
  77 + end
  78 + end
  79 +
  80 + # Automatic render `file_presenter/<custom>.html.erb` to display your
  81 + # custom presenter html content.
  82 + # You may not overwrite this method on your customized presenter.
  83 + # A variable with the same presenter name will be created to refer
  84 + # to the file object.
  85 + # Example:
  86 + # The `FilePresenter::Image` render `file_presenter/image.html.erb`
  87 + # inside the `file_presenter/image.html.erb` you can access the
  88 + # required `FilePresenter::Image` instance in the `image` variable.
  89 + def to_html(options = {})
  90 + file = self
  91 + lambda do
  92 + render :partial => file.class.to_s.underscore,
  93 + :locals => { :options => options },
  94 + :object => file
  95 + end
  96 + end
  97 +
  98 + # That makes the presenter to works like any other `UploadedFile` instance.
  99 + def method_missing(m, *args)
  100 + @file.send(m, *args)
  101 + end
  102 +end
  103 +
  104 +# Preload FilePresenters to allow `FilePresenter.for()` to work
  105 +Dir.glob(File.join('app', 'presenters', '*.rb')) do |file|
  106 + load file
  107 +end
... ...
lib/noosfero/plugin.rb
... ... @@ -155,6 +155,7 @@ class Noosfero::Plugin
155 155  
156 156 # Here the developer may specify the events to which the plugins can
157 157 # register and must return true or false. The default value must be false.
  158 + # Must also explicitly define its returning variables.
158 159  
159 160 # -> If true, noosfero will include plugin_dir/public/style.css into
160 161 # application
... ... @@ -162,10 +163,6 @@ class Noosfero::Plugin
162 163 false
163 164 end
164 165  
165   - # Here the developer should specify the events to which the plugins can
166   - # register to. Must be explicitly defined its returning
167   - # variables.
168   -
169 166 # -> Adds buttons to the control panel
170 167 # returns = { :title => title, :icon => icon, :url => url }
171 168 # title = name that will be displayed.
... ... @@ -175,6 +172,13 @@ class Noosfero::Plugin
175 172 nil
176 173 end
177 174  
  175 + # -> Customize profile block design and behavior
  176 + # (overwrites profile_image_link function)
  177 + # returns = lambda block that creates html code.
  178 + def profile_image_link(profile, size, tag, extra_info)
  179 + nil
  180 + end
  181 +
178 182 # -> Adds tabs to the profile
179 183 # returns = { :title => title, :id => id, :content => content, :start => start }
180 184 # title = name that will be displayed.
... ... @@ -304,45 +308,16 @@ class Noosfero::Plugin
304 308 scope
305 309 end
306 310  
307   - # This method is called by the CommentHandler background job before sending
308   - # the notification email. If the comment is marked as spam (i.e. by calling
309   - # <tt>comment.spam!</tt>), then the notification email will *not* be sent.
310   - #
311   - # example:
312   - #
313   - # def check_comment_for_spam(comment)
314   - # if anti_spam_service.is_spam?(comment)
315   - # comment.spam!
316   - # end
317   - # end
318   - #
319   - def check_comment_for_spam(comment)
  311 + # -> Allows plugins to check weather object is a spam
  312 + def check_for_spam(object)
320 313 end
321 314  
322   - # This method is called when the user manually marks a comment as SPAM. A
323   - # plugin implementing this method should train its spam detection mechanism
324   - # by submitting this comment as a confirmed spam.
325   - #
326   - # example:
327   - #
328   - # def comment_marked_as_spam(comment)
329   - # anti_spam_service.train_with_spam(comment)
330   - # end
331   - #
332   - def comment_marked_as_spam(comment)
  315 + # -> Allows plugins to know when an object is marked as a spam
  316 + def marked_as_spam(object)
333 317 end
334 318  
335   - # This method is called when the user manually marks a comment a NOT SPAM. A
336   - # plugin implementing this method should train its spam detection mechanism
337   - # by submitting this coimment as a confirmed ham.
338   - #
339   - # example:
340   - #
341   - # def comment_marked_as_ham(comment)
342   - # anti_spam_service.train_with_ham(comment)
343   - # end
344   - #
345   - def comment_marked_as_ham(comment)
  319 + # -> Allows plugins to know when an object is marked as a ham
  320 + def marked_as_ham(object)
346 321 end
347 322  
348 323 # Adds extra actions for comments
... ...
lib/noosfero/plugin/manager.rb
... ... @@ -34,18 +34,20 @@ class Noosfero::Plugin::Manager
34 34 alias :dispatch_scopes :dispatch_without_flatten
35 35  
36 36 def dispatch_first(event, *args)
37   - result = nil
  37 + default = Noosfero::Plugin.new.send(event, *args)
  38 + result = default
38 39 each do |plugin|
39 40 result = plugin.send(event, *args)
40   - break if result.present?
  41 + break if result != default
41 42 end
42 43 result
43 44 end
44 45  
45 46 def fetch_first_plugin(event, *args)
  47 + default = Noosfero::Plugin.new.send(event, *args)
46 48 result = nil
47 49 each do |plugin|
48   - if plugin.send(event, *args)
  50 + if plugin.send(event, *args) != default
49 51 result = plugin.class
50 52 break
51 53 end
... ...
lib/noosfero/plugin/spammable.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +Spammable.module_eval do
  2 + def marked_as_spam
  3 + plugins.dispatch(:marked_as_spam, self)
  4 + end
  5 +
  6 + def marked_as_ham
  7 + plugins.dispatch(:marked_as_ham, self)
  8 + end
  9 +
  10 + def check_for_spam
  11 + plugins.dispatch(:check_for_spam, self)
  12 + end
  13 +end
... ...
lib/spammable.rb 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +module Spammable
  2 + def self.included(recipient)
  3 + #TODO This line crashes the migration which includes the spam attribute to
  4 + # Task... =P
  5 + # No fail-safe until someone find out how to use this without crashing
  6 + # the migration process
  7 + #raise "This model (#{recipient.to_s}) should have a spam attribute!" if !recipient.new.respond_to?('spam=')
  8 + recipient.extend(ClassMethods)
  9 + end
  10 +
  11 + module ClassMethods
  12 + def self.extended (base)
  13 + if base.respond_to?(:named_scope)
  14 + base.class_eval do
  15 + named_scope :without_spam, :conditions => ['spam IS NULL OR spam = ?', false]
  16 + named_scope :spam, :conditions => ['spam = ?', true]
  17 + end
  18 + end
  19 + end
  20 + end
  21 +
  22 + def spam?
  23 + !spam.nil? && spam
  24 + end
  25 +
  26 + def ham?
  27 + !spam.nil? && !spam
  28 + end
  29 +
  30 + def spam!
  31 + before_spam!
  32 + self.spam = true
  33 + self.save!
  34 + after_spam!
  35 + self
  36 + end
  37 +
  38 + def ham!
  39 + before_ham!
  40 + self.spam = false
  41 + self.save!
  42 + after_ham!
  43 + self
  44 + end
  45 +
  46 + def after_spam!; end
  47 + def before_spam!; end
  48 +
  49 + def after_ham!; end
  50 + def before_ham!; end
  51 +end
... ...
plugins/anti_spam/lib/anti_spam_plugin.rb
... ... @@ -5,38 +5,37 @@ class AntiSpamPlugin &lt; Noosfero::Plugin
5 5 end
6 6  
7 7 def self.plugin_description
8   - _("Checks comments against a spam checking service compatible with the Akismet API")
  8 + _("Tests comments and suggested articles against a spam checking service compatible with the Akismet API")
9 9 end
10 10  
11 11 def self.host_default_setting
12 12 'api.antispam.typepad.com'
13 13 end
14 14  
15   - def check_comment_for_spam(comment)
16   - if rakismet_call(comment, :spam?)
17   - comment.spam = true
18   - comment.save!
  15 + def check_for_spam(object)
  16 + if rakismet_call AntiSpamPlugin::Wrapper.wrap(object), object.environment, :spam?
  17 + object.spam = true
  18 + object.save!
19 19 end
20 20 end
21 21  
22   - def comment_marked_as_spam(comment)
23   - rakismet_call(comment, :spam!)
  22 + def marked_as_spam(object)
  23 + rakismet_call AntiSpamPlugin::Wrapper.wrap(object), object.environment, :spam!
24 24 end
25 25  
26   - def comment_marked_as_ham(comment)
27   - rakismet_call(comment, :ham!)
  26 + def marked_as_ham(object)
  27 + rakismet_call AntiSpamPlugin::Wrapper.wrap(object), object.environment, :ham!
28 28 end
29 29  
30 30 protected
31 31  
32   - def rakismet_call(comment, op)
33   - settings = Noosfero::Plugin::Settings.new(comment.environment, self.class)
  32 + def rakismet_call(submission, environment, op)
  33 + settings = Noosfero::Plugin::Settings.new(environment, self.class)
34 34  
35 35 Rakismet.host = settings.host
36 36 Rakismet.key = settings.api_key
37   - Rakismet.url = comment.environment.top_url
  37 + Rakismet.url = environment.top_url
38 38  
39   - submission = AntiSpamPlugin::CommentWrapper.new(comment)
40 39 submission.send(op)
41 40 end
42 41  
... ...
plugins/anti_spam/lib/anti_spam_plugin/comment_wrapper.rb
1   -class AntiSpamPlugin::CommentWrapper < Struct.new(:comment)
2   -
3   - delegate :author_name, :author_email, :title, :body, :ip_address, :user_agent, :referrer, :to => :comment
4   -
5   - include Rakismet::Model
6   -
7   - alias :author :author_name
8   - alias :user_ip :ip_address
9   - alias :content :body
10   -
  1 +class AntiSpamPlugin::CommentWrapper < AntiSpamPlugin::Wrapper
  2 + alias_attribute :author, :author_name
  3 + alias_attribute :user_ip, :ip_address
  4 + alias_attribute :content, :body
  5 +
  6 + def self.wraps?(object)
  7 + object.kind_of?(Comment)
  8 + end
11 9 end
... ...
plugins/anti_spam/lib/anti_spam_plugin/suggest_article_wrapper.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +class AntiSpamPlugin::SuggestArticleWrapper < AntiSpamPlugin::Wrapper
  2 + alias_attribute :author, :name
  3 + alias_attribute :author_email, :email
  4 + alias_attribute :user_ip, :ip_address
  5 + alias_attribute :content, :article_body
  6 +
  7 + def self.wraps?(object)
  8 + object.kind_of?(SuggestArticle)
  9 + end
  10 +end
... ...
plugins/anti_spam/lib/anti_spam_plugin/wrapper.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +class AntiSpamPlugin::Wrapper < SimpleDelegator
  2 + include Rakismet::Model
  3 +
  4 + @@wrappers = []
  5 +
  6 + def self.wrap(object)
  7 + wrapper = @@wrappers.find { |wrapper| wrapper.wraps?(object) }
  8 + wrapper ? wrapper.new(object) : object
  9 + end
  10 +
  11 + def self.wraps?(object)
  12 + false
  13 + end
  14 +
  15 + def self.inherited(child)
  16 + @@wrappers << child
  17 + end
  18 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/comment_wrapper_test.rb
1 1 require 'test_helper'
2 2  
3   -class AntiSpamPluginCommentWrapperTest < ActiveSupport::TestCase
  3 +class AntiSpamPlugin::CommentWrapperTest < ActiveSupport::TestCase
4 4  
5 5 def setup
6 6 @comment = Comment.new(
... ... @@ -15,10 +15,6 @@ class AntiSpamPluginCommentWrapperTest &lt; ActiveSupport::TestCase
15 15 @wrapper = AntiSpamPlugin::CommentWrapper.new(@comment)
16 16 end
17 17  
18   - should 'use Rakismet::Model' do
19   - assert_includes @wrapper.class.included_modules, Rakismet::Model
20   - end
21   -
22 18 should 'get contents' do
23 19 assert_equal @comment.body, @wrapper.content
24 20 end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/suggest_article_wrapper_test.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +require 'test_helper'
  2 +
  3 +class AntiSpamPlugin::SuggestArticleWrapperTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @suggest_article = SuggestArticle.new(
  7 + :article_body => 'comment body',
  8 + :name => 'author',
  9 + :email => 'foo@example.com',
  10 + :ip_address => '1.2.3.4',
  11 + :user_agent => 'Some Good Browser (I hope)',
  12 + :referrer => 'http://noosfero.org/'
  13 + )
  14 + @wrapper = AntiSpamPlugin::SuggestArticleWrapper.new(@suggest_article)
  15 + end
  16 +
  17 + should 'get contents' do
  18 + assert_equal @suggest_article.article_body, @wrapper.content
  19 + end
  20 +
  21 + should 'get author name' do
  22 + assert_equal @suggest_article.name, @wrapper.author
  23 + end
  24 +
  25 + should 'get author email' do
  26 + assert_equal @suggest_article.email, @wrapper.author_email
  27 + end
  28 +
  29 + should 'get IP address' do
  30 + assert_equal @suggest_article.ip_address, @wrapper.user_ip
  31 + end
  32 +
  33 + should 'get User-Agent' do
  34 + assert_equal @suggest_article.user_agent, @wrapper.user_agent
  35 + end
  36 +
  37 + should 'get get Referrer' do
  38 + assert_equal @suggest_article.referrer, @wrapper.referrer
  39 + end
  40 +
  41 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/wrapper_test.rb 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +require 'test_helper'
  2 +require 'anti_spam_plugin/wrapper'
  3 +
  4 +class AntiSpamPluginWrapperTest < ActiveSupport::TestCase
  5 + should 'use Rakismet::Model' do
  6 + wrapped = AntiSpamPlugin::Wrapper.new(mock)
  7 + assert_includes wrapped.class.included_modules, Rakismet::Model
  8 + end
  9 +
  10 + should 'wrap object according to wraps? method' do
  11 + class EvenWrapper < AntiSpamPlugin::Wrapper
  12 + def self.wraps?(object)
  13 + object % 2 == 0
  14 + end
  15 + end
  16 + class OddWrapper < AntiSpamPlugin::Wrapper
  17 + def self.wraps?(object)
  18 + object % 2 != 0
  19 + end
  20 + end
  21 +
  22 + assert AntiSpamPlugin::Wrapper.wrap(5).kind_of?(OddWrapper)
  23 + assert AntiSpamPlugin::Wrapper.wrap(6).kind_of?(EvenWrapper)
  24 + end
  25 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin_test.rb
... ... @@ -2,35 +2,36 @@ require &#39;test_helper&#39;
2 2  
3 3 class AntiSpamPluginTest < ActiveSupport::TestCase
4 4  
5   - def setup
6   - profile = fast_create(Profile)
7   - article = fast_create(TextileArticle, :profile_id => profile.id)
8   - @comment = fast_create(Comment, :source_id => article.id, :source_type => 'Article')
  5 + class SpammableContent
  6 + attr_accessor :spam
  7 + include Spammable
9 8  
10   - @settings = Noosfero::Plugin::Settings.new(@comment.environment, AntiSpamPlugin)
11   - @settings.api_key = 'b8b80ddb8084062d0c9119c945ce3bc3'
12   - @settings.save!
  9 + def save!; end
  10 + def environment; Environment.default; end
  11 + end
13 12  
  13 + def setup
  14 + @spammable = SpammableContent.new
14 15 @plugin = AntiSpamPlugin.new
15   - @plugin.context = @comment
16 16 end
17 17  
18   - should 'check for spam and mark comment as spam if server says it is spam' do
19   - AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam?).returns(true)
20   - @comment.expects(:save!)
  18 + attr_accessor :spammable
21 19  
22   - @plugin.check_comment_for_spam(@comment)
23   - assert @comment.spam
24   - end
  20 + should 'check for spam and mark as spam if server says it is spam' do
  21 + spammable.expects(:spam?).returns(true)
  22 + spammable.expects(:save!)
25 23  
26   - should 'report spam' do
27   - AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam!)
28   - @plugin.comment_marked_as_spam(@comment)
  24 + @plugin.check_for_spam(spammable)
  25 + assert spammable.spam
29 26 end
30 27  
31   - should 'report ham' do
32   - AntiSpamPlugin::CommentWrapper.any_instance.expects(:ham!)
33   - @plugin.comment_marked_as_ham(@comment)
  28 + should 'report comment spam' do
  29 + spammable.expects(:spam!)
  30 + @plugin.marked_as_spam(spammable)
34 31 end
35 32  
  33 + should 'report comment ham' do
  34 + spammable.expects(:ham!)
  35 + @plugin.marked_as_ham(spammable)
  36 + end
36 37 end
... ...
plugins/community_track/controllers/myprofile/community_track_plugin_myprofile_controller.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +class CommunityTrackPluginMyprofileController < MyProfileController
  2 + append_view_path File.join(File.dirname(__FILE__) + '/../../views')
  3 +
  4 + before_filter :allow_edit_track, :only => :save_order
  5 +
  6 + def save_order
  7 + track = profile.articles.find(params[:track])
  8 + track.reorder_steps(params[:step_ids])
  9 + redirect_to track.url
  10 + end
  11 +
  12 + protected
  13 +
  14 + def allow_edit_track
  15 + render_access_denied unless profile.articles.find(params[:track]).allow_edit?(user)
  16 + end
  17 +
  18 +end
... ...
plugins/community_track/controllers/public/community_track_plugin_public_controller.rb 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +class CommunityTrackPluginPublicController < PublicController
  2 + append_view_path File.join(File.dirname(__FILE__) + '/../../views')
  3 +
  4 + no_design_blocks
  5 +
  6 + before_filter :login_required, :only => :select_community
  7 +
  8 + def view_tracks
  9 + block = Block.find(params[:id])
  10 + p = params[:page].to_i
  11 + per_page = params[:per_page]
  12 + per_page ||= block.limit
  13 + per_page = per_page.to_i
  14 + tracks = block.tracks(p, per_page)
  15 +
  16 + render :update do |page|
  17 + page.insert_html :bottom, "track_list_#{block.id}", :partial => "blocks/#{block.track_partial}", :collection => tracks, :locals => {:block => block}
  18 +
  19 + if block.has_page?(p+1, per_page)
  20 + page.replace_html "track_list_more_#{block.id}", :partial => 'blocks/track_list_more', :locals => {:block => block, :page => p+1, :force_same_page => params[:force_same_page], :per_page => per_page}
  21 + else
  22 + page.replace_html "track_list_more_#{block.id}", ''
  23 + end
  24 + end
  25 + end
  26 +
  27 + def all_tracks
  28 + @per_page = 5 #FIXME
  29 + @block = Block.find(params[:id])
  30 + @tracks = @block.tracks(1, @per_page)
  31 + @show_more = @block.has_page?(2, @per_page)
  32 + end
  33 +
  34 + def select_community
  35 + @communities = user.memberships.select{ |community| user.has_permission?('post_content', community) }
  36 + @back_to = request.url
  37 + if request.post?
  38 + community_identifier = params[:community_identifier]
  39 + if community_identifier.nil?
  40 + @failed = [_('Select one community to proceed')]
  41 + else
  42 + redirect_to :controller => 'cms', :action => 'new', :type => "CommunityTrackPlugin::Track", :profile => community_identifier
  43 + end
  44 + end
  45 + end
  46 +
  47 +end
... ...
plugins/community_track/lib/community_track_plugin.rb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +class CommunityTrackPlugin < Noosfero::Plugin
  2 +
  3 + def self.plugin_name
  4 + 'Community Track'
  5 + end
  6 +
  7 + def self.plugin_description
  8 + _("New kind of content for communities.")
  9 + end
  10 +
  11 + def stylesheet?
  12 + true
  13 + end
  14 +
  15 + def content_types
  16 + if context.respond_to?(:params) && context.params
  17 + types = []
  18 + parent_id = context.params[:parent_id]
  19 + types << CommunityTrackPlugin::Track if context.profile.community? && !parent_id
  20 + parent = parent_id ? context.profile.articles.find(parent_id) : nil
  21 + types << CommunityTrackPlugin::Step if parent.kind_of?(CommunityTrackPlugin::Track)
  22 + types
  23 + else
  24 + [CommunityTrackPlugin::Track, CommunityTrackPlugin::Step]
  25 + end
  26 + end
  27 +
  28 + def self.extra_blocks
  29 + { CommunityTrackPlugin::TrackListBlock => {:position => 1}, CommunityTrackPlugin::TrackCardListBlock => {} }
  30 + end
  31 +
  32 + def content_remove_new(page)
  33 + page.kind_of?(CommunityTrackPlugin::Track)
  34 + end
  35 +
  36 +end
... ...
plugins/community_track/lib/community_track_plugin/step.rb 0 → 100644
... ... @@ -0,0 +1,105 @@
  1 +class CommunityTrackPlugin::Step < Folder
  2 +
  3 + settings_items :hidden, :type => :boolean, :default => false
  4 +
  5 + alias :tools :children
  6 +
  7 + acts_as_list :scope => :parent
  8 +
  9 + def belong_to_track
  10 + errors.add(:parent, "Step not allowed at this parent.") if !parent.kind_of?(CommunityTrackPlugin::Track)
  11 + end
  12 +
  13 + validate :belong_to_track
  14 + validates_presence_of :start_date, :end_date
  15 + validate :end_date_equal_or_after_start_date
  16 +
  17 + after_save :schedule_activation
  18 +
  19 + before_create do |step|
  20 + step.published = false
  21 + true
  22 + end
  23 +
  24 + before_create :set_hidden_position
  25 + before_save :set_hidden_position
  26 +
  27 + def set_hidden_position
  28 + if hidden
  29 + decrement_positions_on_lower_items
  30 + self[:position] = 0
  31 + elsif position == 0
  32 + add_to_list_bottom
  33 + end
  34 + end
  35 +
  36 + def end_date_equal_or_after_start_date
  37 + if end_date && start_date
  38 + errors.add(:end_date, _('must be equal or after start date.')) unless end_date >= start_date
  39 + end
  40 + end
  41 +
  42 + def self.short_description
  43 + _("Step")
  44 + end
  45 +
  46 + def self.description
  47 + _('Defines a step.')
  48 + end
  49 +
  50 + def accept_comments?
  51 + false
  52 + end
  53 +
  54 + def enabled_tools
  55 + {TinyMceArticle => {:name => _('Article')}, Forum => {:name => _('Forum')}}
  56 + end
  57 +
  58 + def to_html(options = {})
  59 + step = self
  60 + lambda do
  61 + render :file => 'content_viewer/step.rhtml', :locals => {:step => step}
  62 + end
  63 + end
  64 +
  65 + def active?
  66 + (start_date..end_date).include?(Date.today)
  67 + end
  68 +
  69 + def finished?
  70 + Date.today > end_date
  71 + end
  72 +
  73 + def waiting?
  74 + Date.today < start_date
  75 + end
  76 +
  77 + def schedule_activation
  78 + return if !changes['start_date'] && !changes['end_date'] && !changes['published']
  79 + today = Date.today
  80 + if today <= end_date || published
  81 + schedule_date = !published ? start_date : end_date + 1.day
  82 + CommunityTrackPlugin::ActivationJob.find(id).destroy_all
  83 + Delayed::Job.enqueue(CommunityTrackPlugin::ActivationJob.new(self.id), 0, schedule_date)
  84 + end
  85 + end
  86 +
  87 + def publish
  88 + self[:published] = active? && !hidden
  89 + save!
  90 + end
  91 +
  92 + class CommunityTrackPlugin::ActivationJob < Struct.new(:step_id)
  93 +
  94 + def self.find(step_id)
  95 + Delayed::Job.where(:handler => "--- !ruby/struct:CommunityTrackPlugin::ActivationJob \nstep_id: #{step_id}\n")
  96 + end
  97 +
  98 + def perform
  99 + step = CommunityTrackPlugin::Step.find(step_id)
  100 + step.publish
  101 + end
  102 +
  103 + end
  104 +
  105 +end
... ...
plugins/community_track/lib/community_track_plugin/step_helper.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +module CommunityTrackPlugin::StepHelper
  2 +
  3 + def self.status_descriptions
  4 + [_('Finished'), _('In progress'), _('Waiting')]
  5 + end
  6 +
  7 + def self.status_classes
  8 + ['step_finished', 'step_active', 'step_waiting']
  9 + end
  10 +
  11 + def status_description(step)
  12 + CommunityTrackPlugin::StepHelper.status_descriptions[status_index(step)]
  13 + end
  14 +
  15 + def status_class(step)
  16 + CommunityTrackPlugin::StepHelper.status_classes[status_index(step)]
  17 + end
  18 +
  19 + def custom_options_for_article(article)
  20 + #no options for step?
  21 + nil
  22 + end
  23 +
  24 + protected
  25 +
  26 + def status_index(step)
  27 + [step.finished?, step.active?, step.waiting?].find_index(true)
  28 + end
  29 +
  30 +end
... ...
plugins/community_track/lib/community_track_plugin/track.rb 0 → 100644
... ... @@ -0,0 +1,70 @@
  1 +class CommunityTrackPlugin::Track < Folder
  2 +
  3 + settings_items :goals, :type => :string
  4 + settings_items :expected_results, :type => :string
  5 +
  6 + def self.icon_name(article = nil)
  7 + 'community-track'
  8 + end
  9 +
  10 + def self.short_description
  11 + _("Track")
  12 + end
  13 +
  14 + def self.description
  15 + _('Defines a track.')
  16 + end
  17 +
  18 + def steps
  19 + #XXX article default order is name (acts_as_filesystem) -> should use reorder (rails3)
  20 + steps_unsorted.sort_by(&:position).select{|s| !s.hidden}
  21 + end
  22 +
  23 + def hidden_steps
  24 + steps_unsorted.select{|s| s.hidden}
  25 + end
  26 +
  27 + def reorder_steps(step_ids)
  28 + transaction do
  29 + step_ids.each_with_index do |step_id, i|
  30 + step = steps_unsorted.find(step_id)
  31 + step.update_attribute(:position, step.position = i + 1)
  32 + end
  33 + end
  34 + end
  35 +
  36 + def steps_unsorted
  37 + children.where(:type => 'CommunityTrackPlugin::Step')
  38 + end
  39 +
  40 + def accept_comments?
  41 + false
  42 + end
  43 +
  44 + def comments_count
  45 + steps_unsorted.joins(:children).sum('childrens_articles.comments_count')
  46 + end
  47 +
  48 + def css_class_name
  49 + "community-track-plugin-track"
  50 + end
  51 +
  52 + #FIXME make this test
  53 + def first_paragraph
  54 + paragraphs = Hpricot(body).search('p')
  55 + paragraphs.empty? ? '' : paragraphs.first.to_html
  56 + end
  57 +
  58 + def category_name
  59 + category = categories.first
  60 + category ? category.name : ''
  61 + end
  62 +
  63 + def to_html(options = {})
  64 + track = self
  65 + lambda do
  66 + render :file => 'content_viewer/track.rhtml', :locals => {:track => track}
  67 + end
  68 + end
  69 +
  70 +end
... ...
plugins/community_track/lib/community_track_plugin/track_card_list_block.rb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +class CommunityTrackPlugin::TrackCardListBlock < CommunityTrackPlugin::TrackListBlock
  2 +
  3 + def self.description
  4 + _('Track Card List')
  5 + end
  6 +
  7 + def help
  8 + _('This block displays a list of most relevant tracks as cards.')
  9 + end
  10 +
  11 + def track_partial
  12 + 'track_card'
  13 + end
  14 +
  15 +end
... ...
plugins/community_track/lib/community_track_plugin/track_helper.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +module CommunityTrackPlugin::TrackHelper
  2 +
  3 + def category_class(track)
  4 + 'category_' + (track.categories.empty? ? 'not_defined' : track.categories.first.name.to_slug)
  5 + end
  6 +
  7 + def track_card_lead(track)
  8 + lead_stripped = strip_tags(track.lead)
  9 + excerpt(lead_stripped, lead_stripped.first(3), track.image ? 180 : 300)
  10 + end
  11 +
  12 +end
... ...
plugins/community_track/lib/community_track_plugin/track_list_block.rb 0 → 100644
... ... @@ -0,0 +1,64 @@
  1 +class CommunityTrackPlugin::TrackListBlock < Block
  2 +
  3 + include CommunityTrackPlugin::StepHelper
  4 +
  5 + settings_items :limit, :type => :integer, :default => 3
  6 + settings_items :more_another_page, :type => :boolean, :default => false
  7 + settings_items :category_ids, :type => Array, :default => []
  8 +
  9 + def self.description
  10 + _('Track List')
  11 + end
  12 +
  13 + def help
  14 + _('This block displays a list of most relevant tracks.')
  15 + end
  16 +
  17 + def track_partial
  18 + 'track'
  19 + end
  20 +
  21 + def tracks(page=1, per_page=limit)
  22 + all_tracks.order('hits DESC').paginate(:per_page => per_page, :page => page)
  23 + end
  24 +
  25 + def count_tracks
  26 + all_tracks.count
  27 + end
  28 +
  29 + def accept_category?(cat)
  30 + true #accept all?
  31 + end
  32 +
  33 + def category_ids=(ids)
  34 + settings[:category_ids] = ids.uniq.map{|item| item.to_i unless item.to_i.zero?}.compact
  35 + end
  36 +
  37 + def all_tracks
  38 + tracks = owner.articles.where(:type => 'CommunityTrackPlugin::Track')
  39 + if !category_ids.empty?
  40 + tracks = tracks.joins(:article_categorizations).where(:articles_categories => {:category_id => category_ids})
  41 + end
  42 + tracks
  43 + end
  44 +
  45 + def content(args={})
  46 + block = self
  47 + lambda do
  48 + render :file => 'blocks/track_list.rhtml', :locals => {:block => block}
  49 + end
  50 + end
  51 +
  52 + def has_page?(page, per_page=limit)
  53 + return (page-1) * per_page < count_tracks
  54 + end
  55 +
  56 + def footer
  57 + block = self
  58 + return nil if !has_page?(2)
  59 + lambda do
  60 + render :partial => 'blocks/track_list_more', :locals => {:block => block, :page => 2, :per_page => block.limit}
  61 + end
  62 + end
  63 +
  64 +end
... ...
plugins/community_track/public/icons/community-track.png 0 → 100644

1.49 KB

plugins/community_track/public/style.css 0 → 100644
... ... @@ -0,0 +1,195 @@
  1 +.icon-newcommunity-track,
  2 +.icon-community-track {
  3 + background-image: url(/plugins/community_track/icons/community-track.png)
  4 +}
  5 +
  6 +.step_active, #article .step_active a {
  7 + background-color: #CCEBD6;
  8 + color: #338533;
  9 +}
  10 +
  11 +.step_waiting, #article .step_waiting a {
  12 + background-color: #FFFFD1;
  13 + color: #D17519;
  14 +}
  15 +
  16 +.step_finished, #article .step_finished a {
  17 + background-color: #D1FFFF;
  18 + color: #00297A;
  19 +}
  20 +
  21 +.step_status_description {
  22 + float: right;
  23 +}
  24 +
  25 +.step {
  26 + font-weight: bold;
  27 +}
  28 +
  29 +.track_list .item .step {
  30 + padding: 8px 5px;
  31 +}
  32 +
  33 +#article .step a {
  34 + text-decoration: none;
  35 +}
  36 +
  37 +.track_list .item .track_content .lead {
  38 + float: left;
  39 + width: 50%;
  40 +}
  41 +
  42 +.track_list .item .track_content .steps {
  43 + float: right;
  44 + width: 50%;
  45 +}
  46 +
  47 +.track_list .item {
  48 + border-bottom: 1px solid #DDDDDD;
  49 +}
  50 +
  51 +.track_stats, .track_content {
  52 + clear: both;
  53 +}
  54 +
  55 +.track_stats .comments {
  56 + float: left;
  57 +}
  58 +
  59 +.track_stats .hits {
  60 + float: right;
  61 +}
  62 +
  63 +.track_list .item_card {
  64 + width: 155px;
  65 + border: 1px solid #DDDDDD;
  66 + float: left;
  67 + padding: 0px 8px;
  68 + margin-left: 3px;
  69 + margin-right: 3px;
  70 + margin-bottom: 8px;
  71 +}
  72 +
  73 +.steps .step {
  74 + margin-top: 3px;
  75 + margin-bottom: 3px;
  76 +}
  77 +
  78 +.track_list .item_card .track_stats {
  79 + border-top: 1px solid #DDDDDD;
  80 +}
  81 +
  82 +.track_list .item_card a, .track_list .item_card a:hover, .track_list .item_card a:visited {
  83 + text-decoration: none;
  84 + color: #444;
  85 +}
  86 +
  87 +.track_list .item_card:hover {
  88 + background: #EEE;
  89 +}
  90 +
  91 +.track_list .title {
  92 + font-size: 16px;
  93 + font-weight: bold;
  94 + border-bottom: 1px solid #DDDDDD;
  95 + padding: 2px 0px;
  96 + margin-bottom: 5px;
  97 + min-height: 10px;
  98 +}
  99 +
  100 +.track_list .image img {
  101 + max-width: 100%;
  102 + max-height: 100px;
  103 + display: block;
  104 + margin-left: auto;
  105 + margin-right: auto;
  106 +}
  107 +
  108 +.track_list .name {
  109 + padding-top: 5px;
  110 +}
  111 +
  112 +.track_list .item_card {
  113 + height: 270px;
  114 +}
  115 +
  116 +.track_list .track_content {
  117 + height: 250px;
  118 +}
  119 +
  120 +#track .step_list {
  121 + list-style-type: none;
  122 + margin: 0;
  123 + padding: 0;
  124 +}
  125 +
  126 +#track .position {
  127 + font-size: 24px;
  128 + font-weight: bold;
  129 + float: left;
  130 + margin: 0 10px;
  131 +}
  132 +
  133 +#track .step .name, #track .step .name a {
  134 + font-weight: bold;
  135 + color: #333;
  136 +}
  137 +
  138 +#track .step .name a:hover {
  139 + color: #555;
  140 +}
  141 +
  142 +#track .step .date {
  143 + font-size: 12px;
  144 + color: #AAA;
  145 +}
  146 +
  147 +#track .step .lead {
  148 + margin: 0px 10px;
  149 + color: #555;
  150 +}
  151 +
  152 +#track .content {
  153 + margin: 6px 0px;
  154 + border-bottom: 1px solid #DDDDDD;
  155 +}
  156 +
  157 +#track .ui-state-default .content {
  158 + border-bottom: 0px;
  159 +}
  160 +
  161 +.track_list .item .step .position {
  162 + float: left;
  163 + padding-right: 5px;
  164 +}
  165 +
  166 +#track .actions .save_button {
  167 + display: none;
  168 +}
  169 +
  170 +#track .actions, #step .actions {
  171 + margin-bottom: 20px;
  172 +}
  173 +
  174 +#edit-track-list-block .categorie_box a {
  175 + float: left;
  176 +}
  177 +
  178 +.all_tracks .more_button {
  179 + text-align: center;
  180 +}
  181 +
  182 +#step .tools .item .name a, #step .tools .item .name a:hover {
  183 + border: none;
  184 + background-color: transparent;
  185 + color: #666;
  186 + font-weight: bold;
  187 +}
  188 +
  189 +#step .tools .item .name a:hover {
  190 + color: #888;
  191 +}
  192 +
  193 +.community-track textarea {
  194 + width: 100%;
  195 +}
... ...
plugins/community_track/test/functional/community_track_plugin_cms_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +# Re-raise errors caught by the controller.
  4 +class CmsController; def rescue_action(e) raise e end; end
  5 +
  6 +class CmsControllerTest < ActionController::TestCase
  7 +
  8 + def setup
  9 + @profile = fast_create(Community)
  10 + @track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => 'track', :profile => @profile)
  11 + @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  12 +
  13 + user = create_user('testinguser')
  14 + @profile.add_admin(user.person)
  15 + login_as(user.login)
  16 + end
  17 +
  18 + should 'be able to edit track' do
  19 + get :edit, :id => @track.id, :profile => @profile.identifier
  20 + assert_tag :tag => 'input', :attributes => { :id => 'article_name' }
  21 + end
  22 +
  23 + should 'be able to edit step' do
  24 + get :edit, :id => @step.id, :profile => @profile.identifier
  25 + assert_tag :tag => 'input', :attributes => { :id => 'article_name' }
  26 + end
  27 +
  28 + should 'be able to save track' do
  29 + get :edit, :id => @track.id, :profile => @profile.identifier
  30 + post :edit, :id => @track.id, :profile => @profile.identifier, :article => {:name => 'changed'}
  31 + @track.reload
  32 + assert_equal 'changed', @track.name
  33 + end
  34 +
  35 + should 'be able to save step' do
  36 + get :edit, :id => @step.id, :profile => @profile.identifier
  37 + post :edit, :id => @step.id, :profile => @profile.identifier, :article => {:name => 'changed'}
  38 + @step.reload
  39 + assert_equal 'changed', @step.name
  40 + end
  41 +
  42 + should 'do not be able to edit visibility of step' do
  43 + get :edit, :id => @step.id, :profile => @profile.identifier
  44 + assert_no_tag :tag => 'input', :attributes => { :name => 'article[published]' }
  45 + end
  46 +
  47 +end
... ...
plugins/community_track/test/functional/community_track_plugin_content_viewer_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,147 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ContentViewerController
  4 + append_view_path File.join(File.dirname(__FILE__) + '/../../views')
  5 + def rescue_action(e)
  6 + raise e
  7 + end
  8 +end
  9 +
  10 +class ContentViewerControllerTest < ActionController::TestCase
  11 +
  12 + def setup
  13 + @profile = fast_create(Community)
  14 + @track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => 'track', :profile => @profile)
  15 + category = fast_create(Category, :name => "education")
  16 + @track.add_category(category)
  17 +
  18 + @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  19 +
  20 + user = create_user('testinguser')
  21 + login_as(user.login)
  22 + @profile.add_admin(user.person)
  23 + end
  24 +
  25 + should 'show actions for tracks when user has permission for edit' do
  26 + get :view_page, @track.url
  27 + assert_tag :tag => 'div', :attributes => {:id => 'track' }, :descendant => { :tag => 'div', :attributes => { :class => 'actions' } }
  28 + end
  29 +
  30 + should 'do not show actions for tracks when user has not permission for edit' do
  31 + user = create_user('intruder')
  32 + logout
  33 + login_as(user.login)
  34 + get :view_page, @track.url
  35 + assert_no_tag :tag => 'div', :attributes => {:id => 'track' }, :descendant => { :tag => 'div', :attributes => { :class => 'actions' } }
  36 + end
  37 +
  38 + should 'do not show new button at article toolbar for tracks' do
  39 + user = create_user('intruder')
  40 + logout
  41 + login_as(user.login)
  42 + get :view_page, @track.url
  43 + assert_no_tag :tag => 'div', :attributes => {:id => 'article-actions'}, :descendant => { :tag => 'div', :attributes => { :id => 'icon-new' } }
  44 + end
  45 +
  46 + should 'display steps for tracks' do
  47 + get :view_page, @track.url
  48 + assert_tag :tag => 'ul', :attributes => { :id => 'sortable' }, :descendant => {:tag => 'li', :attributes => { :class => 'step' } }
  49 + end
  50 +
  51 + should 'display hidden field with step id' do
  52 + get :view_page, @track.url
  53 + assert_tag :tag => 'input', :attributes => { :name => 'step_ids[]' }
  54 + end
  55 +
  56 + should 'show step' do
  57 + get :view_page, @step.url
  58 + assert_tag :tag => 'div', :attributes => { :id => 'step' }
  59 + end
  60 +
  61 + should 'show tools for a step' do
  62 + Article.create!(:profile => @profile, :name => 'article', :parent => @step)
  63 + get :view_page, @step.url
  64 + assert_tag :tag => 'div', :attributes => { :class => 'tools' }, :descendant => { :tag => 'div', :attributes => { :class => 'item' } }
  65 + end
  66 +
  67 + should 'show actions for steps when user has permission for edit' do
  68 + get :view_page, @step.url
  69 + assert_tag :tag => 'div', :attributes => {:id => 'step' }, :descendant => { :tag => 'div', :attributes => { :class => 'actions' } }
  70 + end
  71 +
  72 + should 'show actions for enabled tools in step' do
  73 + get :view_page, @step.url
  74 + assert_tag 'div', :attributes => {:class => 'actions' }, :descendant => { :tag => 'a', :attributes => { :class => 'button with-text icon-new icon-newforum' } }
  75 + assert_tag 'div', :attributes => {:class => 'actions' }, :descendant => { :tag => 'a', :attributes => { :class => 'button with-text icon-new icon-newtext-html' } }
  76 + end
  77 +
  78 + should 'do not show actions for steps when user has not permission for edit' do
  79 + user = create_user('intruder')
  80 + logout
  81 + login_as(user.login)
  82 + get :view_page, @step.url
  83 + assert_no_tag :tag => 'div', :attributes => {:id => 'step' }, :descendant => { :tag => 'div', :attributes => { :class => 'actions' } }
  84 + end
  85 +
  86 + should 'render a div with block id for track list block' do
  87 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  88 + @block = CommunityTrackPlugin::TrackListBlock.create!(:box => box)
  89 + @profile.boxes << box
  90 + get :view_page, @step.url
  91 + assert_tag :tag => 'div', :attributes => { :class => 'track_list', :id => "track_list_#{@block.id}" }
  92 + end
  93 +
  94 + should 'render a div with block id for track card list block' do
  95 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  96 + @block = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  97 + @profile.boxes << box
  98 + get :view_page, @step.url
  99 + assert_tag :tag => 'div', :attributes => { :class => 'track_list', :id => "track_list_#{@block.id}" }
  100 + end
  101 +
  102 + should 'render tracks in track list block' do
  103 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  104 + @block = CommunityTrackPlugin::TrackListBlock.create!(:box => box)
  105 + @profile.boxes << box
  106 + get :view_page, @step.url
  107 + assert_tag :tag => 'div', :attributes => { :class => 'item category_education' }, :descendant => { :tag => 'div', :attributes => { :class => 'steps' }, :descendant => { :tag => 'div', :attributes => { :class => "step #{@block.status_class(@step)}" } } }
  108 + end
  109 +
  110 + should 'render tracks in track card list block' do
  111 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  112 + @block = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  113 + @profile.boxes << box
  114 + get :view_page, @step.url
  115 + assert_tag :tag => 'div', :attributes => { :class => 'item_card category_education' }, :descendant => { :tag => 'div', :attributes => { :class => 'track_content' } }
  116 + assert_tag :tag => 'div', :attributes => { :class => 'item_card category_education' }, :descendant => { :tag => 'div', :attributes => { :class => 'track_stats' } }
  117 + end
  118 +
  119 + should 'render link to display more tracks in track list block' do
  120 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  121 + @block = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  122 + @profile.boxes << box
  123 +
  124 + (@block.limit+1).times do |i|
  125 + CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => "track#{i}", :profile => @profile)
  126 + end
  127 +
  128 + get :view_page, @step.url
  129 + assert_tag :tag => 'div', :attributes => { :id => "track_list_more_#{@block.id}" }, :descendant => { :tag => 'div', :attributes => { :class => 'more' } }
  130 + end
  131 +
  132 + should 'render link to show all tracks in track list block' do
  133 + box = fast_create(Box, :owner_id => @profile.id, :owner_type => @profile.class.name)
  134 + @block = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  135 + @profile.boxes << box
  136 + @block.more_another_page = true
  137 + @block.save!
  138 +
  139 + (@block.limit+1).times do |i|
  140 + CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => "track#{i}", :profile => @profile)
  141 + end
  142 +
  143 + get :view_page, @step.url
  144 + assert_tag :tag => 'div', :attributes => { :id => "track_list_more_#{@block.id}" }, :descendant => { :tag => 'div', :attributes => { :class => 'view_all' } }
  145 + end
  146 +
  147 +end
... ...
plugins/community_track/test/functional/community_track_plugin_environment_design_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +# Re-raise errors caught by the controller.
  4 +class EnvironmentDesignController; def rescue_action(e) raise e end; end
  5 +
  6 +class EnvironmentDesignControllerTest < ActionController::TestCase
  7 +
  8 + def setup
  9 + Environment.delete_all
  10 + @environment = Environment.new(:name => 'testenv', :is_default => true)
  11 + @environment.enabled_plugins = ['CommunityTrackPlugin']
  12 + @environment.save!
  13 +
  14 + user = create_user('testinguser')
  15 + @environment.add_admin(user.person)
  16 + login_as(user.login)
  17 +
  18 + box = Box.create!(:owner => @environment)
  19 + @block = CommunityTrackPlugin::TrackListBlock.create!(:box => box)
  20 + @block_card = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  21 + end
  22 +
  23 + should 'be able to edit TrackListBlock' do
  24 + get :edit, :id => @block.id
  25 + assert_tag :tag => 'input', :attributes => { :id => 'block_title' }
  26 + end
  27 +
  28 + should 'be able to save TrackListBlock' do
  29 + get :edit, :id => @block.id
  30 + post :save, :id => @block.id, :block => {:title => 'Tracks' }
  31 + @block.reload
  32 + assert_equal 'Tracks', @block.title
  33 + end
  34 +
  35 + should 'be able to edit TrackCardListBlock' do
  36 + get :edit, :id => @block_card.id
  37 + assert_tag :tag => 'input', :attributes => { :id => 'block_title' }
  38 + end
  39 +
  40 + should 'be able to save TrackCardListBlock' do
  41 + get :edit, :id => @block_card.id
  42 + post :save, :id => @block_card.id, :block => {:title => 'Tracks' }
  43 + @block_card.reload
  44 + assert_equal 'Tracks', @block_card.title
  45 + end
  46 +
  47 +end
... ...
plugins/community_track/test/functional/community_track_plugin_myprofile_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,49 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +require File.dirname(__FILE__) + '/../../controllers/myprofile/community_track_plugin_myprofile_controller'
  3 +
  4 +# Re-raise errors caught by the controller.
  5 +class CommunityTrackPluginMyprofileController; def rescue_action(e) raise e end; end
  6 +
  7 +class CommunityTrackPluginMyprofileControllerTest < ActionController::TestCase
  8 +
  9 + def setup
  10 + @controller = CommunityTrackPluginMyprofileController.new
  11 + @request = ActionController::TestRequest.new
  12 + @response = ActionController::TestResponse.new
  13 +
  14 + @profile = fast_create(Community)
  15 + @track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => 'track', :profile => @profile)
  16 +
  17 + @user = create_user('testinguser')
  18 + login_as(@user.login)
  19 + @profile.add_admin(@user.person)
  20 + end
  21 +
  22 + should 'redirect to track on save order' do
  23 + get :save_order, :profile => @profile.identifier, :track => @track.id, :step_ids => []
  24 + assert_redirected_to @track.url
  25 + end
  26 +
  27 + should 'save new step positions on save order' do
  28 + step1 = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  29 + step2 = CommunityTrackPlugin::Step.create!(:name => 'step2', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  30 + assert_equal [step1, step2], @track.steps
  31 + get :save_order, :profile => @profile.identifier, :track => @track.id, :step_ids => [step2.id, step1.id]
  32 + assert_equal [step2, step1], @track.steps
  33 + end
  34 +
  35 + should 'do not allow a user without permission to save order' do
  36 + logout
  37 + user = create_user('intruder')
  38 + login_as(user.login)
  39 + get :save_order, :profile => @profile.identifier, :track => @track.id, :step_ids => []
  40 + assert_response 403
  41 + end
  42 +
  43 + should 'redirect to login page if there is no user logged in' do
  44 + logout
  45 + get :save_order, :profile => @profile.identifier, :track => @track.id, :step_ids => []
  46 + assert_response 302
  47 + end
  48 +
  49 +end
... ...
plugins/community_track/test/functional/community_track_plugin_public_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,109 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +require File.dirname(__FILE__) + '/../../controllers/public/community_track_plugin_public_controller'
  3 +
  4 +# Re-raise errors caught by the controller.
  5 +class CommunityTrackPluginPublicController; def rescue_action(e) raise e end; end
  6 +
  7 +class CommunityTrackPluginPublicControllerTest < ActionController::TestCase
  8 +
  9 + def setup
  10 + @community = fast_create(Community)
  11 + @track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => 'track', :profile => @community)
  12 +
  13 + box = fast_create(Box, :owner_id => @community.id, :owner_type => 'Community')
  14 + @card_block = CommunityTrackPlugin::TrackCardListBlock.create!(:box => box)
  15 + @block = CommunityTrackPlugin::TrackListBlock.create!(:box => box)
  16 + end
  17 +
  18 + should 'display tracks for card block' do
  19 + xhr :get, :view_tracks, :id => @card_block.id, :page => 1
  20 + assert_match /track_list_#{@card_block.id}/, @response.body
  21 + end
  22 +
  23 + should 'display tracks for list block' do
  24 + xhr :get, :view_tracks, :id => @block.id, :page => 1
  25 + assert_match /track_list_#{@block.id}/, @response.body
  26 + end
  27 +
  28 + should 'display tracks with page size' do
  29 + 20.times do |i|
  30 + track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => "track#{i}", :profile => @community)
  31 + end
  32 + xhr :get, :view_tracks, :id => @block.id, :page => 1, :per_page => 10
  33 + assert_equal 10, @response.body.scan(/item/).size
  34 + end
  35 +
  36 + should 'default page size is the block limit' do
  37 + 20.times do |i|
  38 + track = CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => "track#{i}", :profile => @community)
  39 + end
  40 + xhr :get, :view_tracks, :id => @block.id, :page => 1
  41 + assert_equal @block.limit, @response.body.scan(/item/).size
  42 + end
  43 +
  44 + should 'display page for all tracks' do
  45 + get :all_tracks, :id => @block.id
  46 + assert_match /track_list_#{@block.id}/, @response.body
  47 + end
  48 +
  49 + should 'show more link in all tracks if there is no more tracks to show' do
  50 + 10.times do |i|
  51 + CommunityTrackPlugin::Track.create!(:abstract => 'abstract', :body => 'body', :name => "track#{i}", :profile => @community)
  52 + end
  53 + get :all_tracks, :id => @block.id
  54 + assert assigns['show_more']
  55 + assert_match /track_list_more_#{@block.id}/, @response.body
  56 + end
  57 +
  58 + should 'do not show more link in all tracks if there is no more tracks to show' do
  59 + CommunityTrackPlugin::Track.destroy_all
  60 + get :all_tracks, :id => @block.id
  61 + assert !assigns['show_more']
  62 + assert_no_match /track_list_more_#{@block.id}/, @response.body
  63 + end
  64 +
  65 + should 'show select community page if user is logged in' do
  66 + user = create_user('testinguser')
  67 + login_as(user.login)
  68 + get :select_community
  69 + assert_template 'select_community'
  70 + end
  71 +
  72 + should 'redirect to login page if user try to access community selection' do
  73 + logout
  74 + get :select_community
  75 + assert_redirected_to :controller => 'account', :action => 'login'
  76 + end
  77 +
  78 + should 'display for selection communities where user has permission to post content' do
  79 + user = create_user('testinguser')
  80 + login_as(user.login)
  81 + @community.add_member(user.person)
  82 + get :select_community
  83 + assert_tag :tag => 'li', :attributes => {:class => 'search-profile-item'}
  84 + assert_tag :tag => 'input', :attributes => {:id => "community_identifier_#{@community.identifier}"}
  85 + end
  86 +
  87 + should 'do not display communities where user has not permission to post content' do
  88 + user = create_user('testinguser')
  89 + login_as(user.login)
  90 + get :select_community
  91 + assert_no_tag :tag => 'input', :attributes => {:id => "community_identifier_#{@community.identifier}"}
  92 + end
  93 +
  94 + should 'redirect to new content with track content type' do
  95 + user = create_user('testinguser')
  96 + login_as(user.login)
  97 + post :select_community, :profile => user.person.identifier, :community_identifier => @community.identifier
  98 + assert_redirected_to :controller => 'cms', :action => 'new', :type => "CommunityTrackPlugin::Track", :profile => @community.identifier
  99 + end
  100 +
  101 + should 'return error message if user do not select a community' do
  102 + user = create_user('testinguser')
  103 + login_as(user.login)
  104 + post :select_community, :profile => user.person.identifier, :community_identifier => nil
  105 + assert_equal 1, assigns(:failed).count
  106 + assert_tag :tag => 'div', :attributes => {:id => 'errorExplanation'}
  107 + end
  108 +
  109 +end
... ...
plugins/community_track/test/test_helper.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require File.dirname(__FILE__) + '/../../../test/test_helper'
... ...