Commit 4d9daa44dbf61490640b32a8fb9f6f49f0f8b152

Authored by Rodrigo Souto
2 parents 1fe6b276 4b00dad4
Exists in staging

Merge branch 'master' into staging

Showing 136 changed files with 1279 additions and 399 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 136 files displayed.

.travis.yml
... ... @@ -37,7 +37,7 @@ before_install:
37 37 cache: bundler
38 38  
39 39 before_script:
40   - - mkdir -p tmp/{pids,cache} log
  40 + - mkdir -p tmp/{pids,cache} log cache
41 41 - script/noosfero-plugins disableall
42 42 #- bundle exec rake makemo &>/dev/null
43 43 # database
... ...
RELEASING.md
... ... @@ -3,37 +3,30 @@ Noosfero release tasks
3 3  
4 4 This file documents release-related activities.
5 5  
6   -Working with translations
7   --------------------------
8   -
9   -* Update translation files: `rake updatepo`. Then `git commit` them.
10   -* Send the PO files to the translators.
11   -* Get the PO files back from translators, put in `po/` under the correct language name (e.,g. `po/pt_BR/`) and `git commit`.
12   -* test translations: `rake makemo` and browse the application on the web.
13   -
14 6 Releasing noosfero
15 7 ------------------
16 8  
17   -Considering you are on a Debian GNU/Linux or Debian-based system
  9 +Considering you are on a Debian GNU/Linux or Debian-based system, the following
  10 +packages are required during the release process:
18 11  
19   - # apt-get install devscripts debhelper
  12 +```
  13 +# apt install git devscripts debhelper
  14 +```
20 15  
21 16 To prepare a release of noosfero, you must follow the steps below:
22 17  
23   -* Finish all requirements and bugs assigned to the to-be-released version
  18 +* Disable the automatic pushing of translation updates in weblate.
24 19 * Make sure all tests pass
25   -* Write release notes at the version's wiki topic
26 20 * Generate packages with `rake noosfero:release[(stable|test)]`. This task will:
27 21 * Update the version in lib/noosfero.rb and debian/changelog.
28 22 * Create the tarbal and the deb pkg under pkg/ directory.
29 23 * Create a git tag and push it.
30   - * Upload the pkg to the configured repository (if configured) on ~/.dput.cf.
  24 + * Upload the packages to the configured repository (if configured) on ~/.dput.cf.
31 25 * Test that the tarball and deb package are ok
32   -* Go to the version's wiki topic and edit it to reflect the new reality
33   -* Edit the topic WebPreferences and update DEBIAN_REPOSITORY_TOPICS setting
34   -* Attach the generated packages to that topic. Before attaching calculate the sha1 of the package (with sha1sum and paste the SHA1 hash as comment in the attachment form)
35   -* Download the attached and verify the MD5 hash
36 26 * Update an eventual demonstration version that you run.
37   -* Write an announcement e-mail to the relevant mailing lists pointing to the release notes, and maybe to the demonstration version.
  27 +* Write an announcement e-mail to the relevant mailing lists pointing to the
  28 + release notes, and maybe to the demonstration version.
  29 +* Re-enable the automatic pushing of trasnlatio updates in weblate.
38 30  
39   -If you had any problem during these steps, you can do `rake clobber_package` to completely delete the generated packages and start the process again.
  31 +If you had any problem during these steps, you can do `rake clobber_package` to
  32 +completely delete the generated packages and start the process again.
... ...
app/controllers/box_organizer_controller.rb
... ... @@ -83,12 +83,9 @@ class BoxOrganizerController < ApplicationController
83 83  
84 84 def save
85 85 @block = boxes_holder.blocks.find(params[:id])
86   - if @block.kind_of?(RawHTMLBlock) && !user.is_admin?(environment)
87   - render_access_denied
88   - else
89   - @block.update(params[:block])
90   - redirect_to :action => 'index'
91   - end
  86 + return render_access_denied unless @block.editable?(user)
  87 + @block.update(params[:block])
  88 + redirect_to :action => 'index'
92 89 end
93 90  
94 91 def boxes_editor?
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -32,7 +32,8 @@ class CmsController < MyProfileController
32 32 end
33 33  
34 34 protect_if :only => [:new, :upload_files] do |c, user, profile|
35   - parent = profile.articles.find_by_id(c.params[:parent_id])
  35 + parent_id = c.params[:article].present? ? c.params[:article][:parent_id] : c.params[:parent_id]
  36 + parent = profile.articles.find_by_id(parent_id)
36 37 user && user.can_post_content?(profile, parent)
37 38 end
38 39  
... ...
app/controllers/my_profile/profile_members_controller.rb
... ... @@ -2,8 +2,27 @@ class ProfileMembersController < MyProfileController
2 2 protect 'manage_memberships', :profile
3 3  
4 4 def index
5   - @members = profile.members_by_name
6   - @member_role = environment.roles.find_by_name('member')
  5 + @filters = params[:filters] || {:roles => []}
  6 + all_roles = Profile::Roles.organization_member_roles(environment.id) | Profile::Roles.organization_custom_roles(environment.id, profile.id)
  7 + @filters[:roles] = all_roles unless @filters[:roles].present?
  8 + @data = {}
  9 + field = 'name'
  10 + field = 'email' if @filters[:name] =~ /\@/
  11 +
  12 + @data[:members] = profile.members_by(field,@filters[:name]).by_role(@filters[:roles])
  13 + session[:members_filtered] = @data[:members].map{|m|m.id} if request.post?
  14 + @data[:roles] = all_roles
  15 +
  16 + end
  17 +
  18 + def send_mail
  19 + session[:members_filtered] = params[:members_filtered].select{|value| value!="0"}
  20 + if session[:members_filtered].present?
  21 + redirect_to :controller => :profile, :action => :send_mail
  22 + else
  23 + session[:notice] = _("Select at least one member.")
  24 + redirect_to :action => :index
  25 + end
7 26 end
8 27  
9 28 def update_roles
... ... @@ -156,4 +175,13 @@ class ProfileMembersController < MyProfileController
156 175 end
157 176 end
158 177  
  178 + def search_members
  179 + field = 'name'
  180 + field = 'email' if params[:filter_name] =~ /\@/
  181 +
  182 + result = profile.members_like field, params[:filter_name]
  183 + result = result.select{|member| member.can_view_field?(current_person, "email") } if field=="email"
  184 + render :json => result.map { |member| {:label => "#{member.name}#{member.can_view_field?(current_person, "email") ? " <#{member.email}>" : ""}", :value => member.name }}
  185 + end
  186 +
159 187 end
... ...
app/controllers/public/account_controller.rb
... ... @@ -198,7 +198,7 @@ class AccountController &lt; ApplicationController
198 198 if request.post?
199 199 begin
200 200 unless verify_recaptcha
201   - @change_password.errors.add(:base, _('Please type the words correctly'))
  201 + @change_password.errors.add(:base, _('Please type the captcha text correctly'))
202 202 return false
203 203 end
204 204  
... ...
app/controllers/public/content_viewer_controller.rb
... ... @@ -68,11 +68,7 @@ class ContentViewerController &lt; ApplicationController
68 68 process_comments(params)
69 69  
70 70 if request.xhr? and params[:comment_order]
71   - if @comment_order == 'newest'
72   - @comments = @comments.reverse
73   - end
74   -
75   - return render :partial => 'comment/comment', :collection => @comments
  71 + return render :partial => 'comment/comments_with_pagination'
76 72 end
77 73  
78 74 if params[:slideshow]
... ... @@ -209,8 +205,6 @@ class ContentViewerController &lt; ApplicationController
209 205  
210 206 def rendered_file_download(view = nil)
211 207 if @page.download? view
212   - headers['Content-Type'] = @page.mime_type
213   - headers.merge! @page.download_headers
214 208 data = @page.data
215 209  
216 210 # TODO test the condition
... ... @@ -218,7 +212,12 @@ class ContentViewerController &lt; ApplicationController
218 212 raise "No data for file"
219 213 end
220 214  
221   - render :text => data, :layout => false
  215 + if @page.published && @page.uploaded_file?
  216 + redirect_to @page.public_filename
  217 + else
  218 + send_data data, @page.download_headers
  219 + end
  220 +
222 221 return true
223 222 end
224 223  
... ... @@ -244,8 +243,12 @@ class ContentViewerController &lt; ApplicationController
244 243  
245 244 def get_posts(year = nil, month = nil)
246 245 if year && month
247   - filter_date = DateTime.parse("#{year}-#{month}-01")
248   - return @page.posts.by_range(filter_date..filter_date.at_end_of_month)
  246 + begin
  247 + filter_date = DateTime.parse("#{year}-#{month}-01")
  248 + return @page.posts.by_range(filter_date..filter_date.at_end_of_month)
  249 + rescue ArgumentError
  250 + return @page.posts
  251 + end
249 252 else
250 253 return @page.posts
251 254 end
... ... @@ -276,8 +279,12 @@ class ContentViewerController &lt; ApplicationController
276 279 @comments = @page.comments.without_spam
277 280 @comments = @plugins.filter(:unavailable_comments, @comments)
278 281 @comments_count = @comments.count
279   - @comments = @comments.without_reply.paginate(:per_page => per_page, :page => params[:comment_page] )
280 282 @comment_order = params[:comment_order].nil? ? 'oldest' : params[:comment_order]
  283 + @comments = @comments.without_reply
  284 + if @comment_order == 'newest'
  285 + @comments = @comments.reverse
  286 + end
  287 + @comments = @comments.paginate(:per_page => per_page, :page => params[:comment_page] )
281 288 end
282 289  
283 290 private
... ...
app/controllers/public/profile_controller.rb
... ... @@ -155,6 +155,18 @@ class ProfileController &lt; PublicController
155 155 end
156 156 end
157 157  
  158 + def follow_article
  159 + article = profile.environment.articles.find params[:article_id]
  160 + article.person_followers << user
  161 + redirect_to article.url
  162 + end
  163 +
  164 + def unfollow_article
  165 + article = profile.environment.articles.find params[:article_id]
  166 + article.person_followers.delete(user)
  167 + redirect_to article.url
  168 + end
  169 +
158 170 def unblock
159 171 if current_user.person.is_admin?(profile.environment)
160 172 profile.unblock
... ... @@ -362,6 +374,7 @@ class ProfileController &lt; PublicController
362 374 def send_mail
363 375 @mailing = profile.mailings.build(params[:mailing])
364 376 @email_templates = profile.email_templates.find_all_by_template_type(:organization_members)
  377 + @mailing.data = session[:members_filtered] ? {:members_filtered => session[:members_filtered]} : {}
365 378 if request.post?
366 379 @mailing.locale = locale
367 380 @mailing.person = user
... ...
app/helpers/application_helper.rb
... ... @@ -150,14 +150,8 @@ module ApplicationHelper
150 150 link_to text, profile_path(:profile => profile) , options
151 151 end
152 152  
153   - def link_to_homepage(text, profile = nil, options = {})
154   - p = if profile
155   - Profile[profile]
156   - else
157   - user
158   - end
159   -
160   - link_to text, p.url, options
  153 + def link_to_homepage(text, profile, options = {})
  154 + link_to text, profile.url, options
161 155 end
162 156  
163 157 def link_if_permitted(link, permission = nil, target = nil)
... ... @@ -556,14 +550,25 @@ module ApplicationHelper
556 550 trigger_class = 'enterprise-trigger'
557 551 end
558 552 end
559   - extra_info = extra_info.nil? ? '' : content_tag( 'span', extra_info, :class => 'extra_info' )
  553 +
  554 + extra_info_tag = ''
  555 + img_class = 'profile-image'
  556 +
  557 + if extra_info.is_a? Hash
  558 + extra_info_tag = content_tag( 'span', extra_info[:value], :class => 'extra_info '+extra_info[:class])
  559 + img_class +=' '+extra_info[:class]
  560 + else
  561 + extra_info_tag = content_tag( 'span', extra_info, :class => 'extra_info' )
  562 + end
  563 +
560 564 links = links_for_balloon(profile)
561 565 content_tag('div', content_tag(tag,
562   - (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ? popover_menu(_('Profile links'),profile.short_name,links,{:class => trigger_class, :url => url}) : "") +
  566 + (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ?
  567 + popover_menu(_('Profile links'),profile.short_name,links,{:class => trigger_class, :url => url}) : "") +
563 568 link_to(
564   - content_tag( 'span', profile_image( profile, size ), :class => 'profile-image' ) +
  569 + content_tag( 'span', profile_image( profile, size ), :class => img_class ) +
565 570 content_tag( 'span', h(name), :class => ( profile.class == Person ? 'fn' : 'org' ) ) +
566   - extra_info + profile_sex_icon( profile ),
  571 + extra_info_tag + profile_sex_icon( profile ),
567 572 profile.url,
568 573 :class => 'profile_link url',
569 574 :help => _('Click on this icon to go to the <b>%s</b>\'s home page') % profile.name,
... ... @@ -711,7 +716,7 @@ module ApplicationHelper
711 716 class NoosferoFormBuilder < ActionView::Helpers::FormBuilder
712 717 extend ActionView::Helpers::TagHelper
713 718  
714   - def self.output_field(text, field_html, field_id = nil, options = {})
  719 + def self.output_field(text, field_html, field_id = nil)
715 720 # try to guess an id if none given
716 721 if field_id.nil?
717 722 field_html =~ /id=['"]([^'"]*)['"]/
... ... @@ -1040,10 +1045,11 @@ module ApplicationHelper
1040 1045 end
1041 1046  
1042 1047 def search_contents_menu
  1048 + host = environment.default_hostname
1043 1049 links = [
1044   - {s_('contents|More recent') => {:href => url_for({:controller => 'search', :action => 'contents', :filter => 'more_recent'})}},
1045   - {s_('contents|More viewed') => {:href => url_for({:controller => 'search', :action => 'contents', :filter => 'more_popular'})}},
1046   - {s_('contents|Most commented') => {:href => url_for({:controller => 'search', :action => 'contents', :filter => 'more_comments'})}}
  1050 + {s_('contents|More recent') => {href: url_for({host: host, controller: 'search', action: 'contents', filter: 'more_recent'})}},
  1051 + {s_('contents|More viewed') => {href: url_for({host: host, controller: 'search', action: 'contents', filter: 'more_popular'})}},
  1052 + {s_('contents|Most commented') => {href: url_for({host: host, controller: 'search', action: 'contents', filter: 'more_comments'})}}
1047 1053 ]
1048 1054 if logged_in?
1049 1055 links.push(_('New content') => modal_options({:href => url_for({:controller => 'cms', :action => 'new', :profile => current_user.login, :cms => true})}))
... ... @@ -1055,10 +1061,11 @@ module ApplicationHelper
1055 1061 alias :browse_contents_menu :search_contents_menu
1056 1062  
1057 1063 def search_people_menu
  1064 + host = environment.default_hostname
1058 1065 links = [
1059   - {s_('people|More recent') => {:href => url_for({:controller => 'search', :action => 'people', :filter => 'more_recent'})}},
1060   - {s_('people|More active') => {:href => url_for({:controller => 'search', :action => 'people', :filter => 'more_active'})}},
1061   - {s_('people|More popular') => {:href => url_for({:controller => 'search', :action => 'people', :filter => 'more_popular'})}}
  1066 + {s_('people|More recent') => {href: url_for({host: host, controller: 'search', action: 'people', filter: 'more_recent'})}},
  1067 + {s_('people|More active') => {href: url_for({host: host, controller: 'search', action: 'people', filter: 'more_active'})}},
  1068 + {s_('people|More popular') => {href: url_for({host: host, controller: 'search', action: 'people', filter: 'more_popular'})}}
1062 1069 ]
1063 1070 if logged_in?
1064 1071 links.push(_('My friends') => {:href => url_for({:profile => current_user.login, :controller => 'friends'})})
... ... @@ -1071,10 +1078,11 @@ module ApplicationHelper
1071 1078 alias :browse_people_menu :search_people_menu
1072 1079  
1073 1080 def search_communities_menu
  1081 + host = environment.default_hostname
1074 1082 links = [
1075   - {s_('communities|More recent') => {:href => url_for({:controller => 'search', :action => 'communities', :filter => 'more_recent'})}},
1076   - {s_('communities|More active') => {:href => url_for({:controller => 'search', :action => 'communities', :filter => 'more_active'})}},
1077   - {s_('communities|More popular') => {:href => url_for({:controller => 'search', :action => 'communities', :filter => 'more_popular'})}}
  1083 + {s_('communities|More recent') => {href: url_for({host: host, controller: 'search', action: 'communities', filter: 'more_recent'})}},
  1084 + {s_('communities|More active') => {href: url_for({host: host, controller: 'search', action: 'communities', filter: 'more_active'})}},
  1085 + {s_('communities|More popular') => {href: url_for({host: host, controller: 'search', action: 'communities', filter: 'more_popular'})}}
1078 1086 ]
1079 1087 if logged_in?
1080 1088 links.push(_('My communities') => {:href => url_for({:profile => current_user.login, :controller => 'memberships'})})
... ...
app/helpers/article_helper.rb
... ... @@ -169,4 +169,30 @@ module ArticleHelper
169 169 _('Edit')
170 170 end
171 171  
  172 + def follow_button_text(article)
  173 + if article.event?
  174 + _('Attend')
  175 + else
  176 + _('Follow')
  177 + end
  178 + end
  179 +
  180 + def unfollow_button_text(article)
  181 + if article.event?
  182 + _('Unattend')
  183 + else
  184 + _('Unfollow')
  185 + end
  186 + end
  187 +
  188 + def following_button(page, user)
  189 + if !user.blank? and user != page.author
  190 + if page.is_followed_by? user
  191 + button :cancel, unfollow_button_text(page), {:controller => 'profile', :action => 'unfollow_article', :article_id => page.id}
  192 + else
  193 + button :add, follow_button_text(page), {:controller => 'profile', :action => 'follow_article', :article_id => page.id}
  194 + end
  195 + end
  196 + end
  197 +
172 198 end
... ...
app/helpers/block_helper.rb
... ... @@ -14,6 +14,7 @@ module BlockHelper
14 14 </td>
15 15 <td>#{text_field_tag 'block[images][][address]', image[:address], :class => 'highlight-address', :size => 20}</td>
16 16 <td>#{text_field_tag 'block[images][][position]', image[:position], :class => 'highlight-position', :size => 1}</td>
  17 + <td>#{check_box_tag 'block[images][][new_window]', '1', image[:new_window], :class => 'highlight-new_window', :size => 1}</td>
17 18 </tr><tr class=\"image-title\" data-row-number='#{row_number}'>
18 19 <td colspan=\"3\"><label>#{
19 20 content_tag('span', _('Title')) +
... ...
app/helpers/boxes_helper.rb
... ... @@ -250,7 +250,7 @@ module BoxesHelper
250 250 end
251 251 end
252 252  
253   - if editable?(block)
  253 + if editable?(block, user)
254 254 buttons << modal_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
255 255 end
256 256  
... ... @@ -296,7 +296,7 @@ module BoxesHelper
296 296 return block.movable? || user.is_admin?
297 297 end
298 298  
299   - def editable?(block)
300   - return block.editable? || user.is_admin?
  299 + def editable?(block, user=nil)
  300 + return block.editable?(user) || user.is_admin?
301 301 end
302 302 end
... ...
app/helpers/forms_helper.rb
... ... @@ -7,9 +7,10 @@ module FormsHelper
7 7  
8 8 def labelled_check_box( human_name, name, value = "1", checked = false, options = {} )
9 9 options[:id] ||= 'checkbox-' + FormsHelper.next_id_number
10   - hidden_field_tag(name, '0') +
11   - check_box_tag( name, value, checked, options ) +
12   - content_tag( 'label', human_name, :for => options[:id] )
  10 + html = options[:add_hidden] == false ? "" : hidden_field_tag(name, '0')
  11 +
  12 + html += check_box_tag( name, value, checked, options ) +
  13 + content_tag( 'label', human_name, :for => options[:id] )
13 14 end
14 15  
15 16 def labelled_text_field( human_name, name, value=nil, options={} )
... ...
app/mailers/mailing.rb
... ... @@ -2,7 +2,10 @@ require_dependency &#39;mailing_job&#39;
2 2  
3 3 class Mailing < ActiveRecord::Base
4 4  
5   - attr_accessible :subject, :body
  5 + acts_as_having_settings :field => :data
  6 +
  7 + attr_accessible :subject, :body, :data
  8 +
6 9 validates_presence_of :source_id, :subject, :body
7 10 belongs_to :source, :foreign_key => :source_id, :polymorphic => true
8 11 belongs_to :person
... ...
app/mailers/organization_mailing.rb
... ... @@ -5,9 +5,17 @@ class OrganizationMailing &lt; Mailing
5 5 end
6 6  
7 7 def recipients(offset=0, limit=100)
8   - source.members.order(:id).offset(offset).limit(limit)
9   - .joins("LEFT OUTER JOIN mailing_sents m ON (m.mailing_id = #{id} AND m.person_id = profiles.id)")
  8 + result = source.members.order(:id).offset(offset).limit(limit)
  9 +
  10 + if data.present? and data.is_a?(Hash) and data[:members_filtered]
  11 + result = result.where('profiles.id IN (?)', data[:members_filtered])
  12 + end
  13 +
  14 + if result.blank?
  15 + result = result.joins("LEFT OUTER JOIN mailing_sents m ON (m.mailing_id = #{id} AND m.person_id = profiles.id)")
10 16 .where("m.person_id" => nil)
  17 + end
  18 + result
11 19 end
12 20  
13 21 def each_recipient
... ...
app/models/article.rb
... ... @@ -8,8 +8,9 @@ class Article &lt; ActiveRecord::Base
8 8 :accept_comments, :feed, :published, :source, :source_name,
9 9 :highlighted, :notify_comments, :display_hits, :slug,
10 10 :external_feed_builder, :display_versions, :external_link,
11   - :author, :published_at, :person_followers, :show_to_followers,
12   - :image_builder, :display_preview, :archived
  11 + :image_builder, :show_to_followers,
  12 + :author, :display_preview, :published_at, :person_followers,
  13 + :archived
13 14  
14 15 acts_as_having_image
15 16  
... ... @@ -83,6 +84,10 @@ class Article &lt; ActiveRecord::Base
83 84  
84 85 has_many :comments, :class_name => 'Comment', :foreign_key => 'source_id', :dependent => :destroy, :order => 'created_at asc'
85 86  
  87 + has_many :article_followers, :dependent => :destroy
  88 + has_many :person_followers, :class_name => 'Person', :through => :article_followers, :source => :person
  89 + has_many :person_followers_emails, :class_name => 'User', :through => :person_followers, :source => :user, :select => :email
  90 +
86 91 has_many :article_categorizations, -> { where 'articles_categories.virtual = ?', false }
87 92 has_many :categories, :through => :article_categorizations
88 93  
... ... @@ -95,7 +100,6 @@ class Article &lt; ActiveRecord::Base
95 100 settings_items :author_name, :type => :string, :default => ""
96 101 settings_items :allow_members_to_edit, :type => :boolean, :default => false
97 102 settings_items :moderate_comments, :type => :boolean, :default => false
98   - settings_items :followers, :type => Array, :default => []
99 103 has_and_belongs_to_many :article_privacy_exceptions, :class_name => 'Person', :join_table => 'article_privacy_exceptions'
100 104  
101 105 belongs_to :reference_article, :class_name => "Article", :foreign_key => 'reference_article_id'
... ... @@ -173,7 +177,6 @@ class Article &lt; ActiveRecord::Base
173 177 end
174 178 end
175 179  
176   -
177 180 def is_trackable?
178 181 self.published? && self.notifiable? && self.advertise? && self.profile.public_profile
179 182 end
... ... @@ -374,6 +377,10 @@ class Article &lt; ActiveRecord::Base
374 377 self.parent and self.parent.forum?
375 378 end
376 379  
  380 + def person_followers_email_list
  381 + person_followers_emails.map{|p|p.email}
  382 + end
  383 +
377 384 def info_from_last_update
378 385 last_comment = comments.last
379 386 if last_comment
... ... @@ -383,6 +390,10 @@ class Article &lt; ActiveRecord::Base
383 390 end
384 391 end
385 392  
  393 + def full_path
  394 + profile.hostname.blank? ? "/#{profile.identifier}/#{path}" : "/#{path}"
  395 + end
  396 +
386 397 def url
387 398 @url ||= self.profile.url.merge(:page => path.split('/'))
388 399 end
... ... @@ -408,13 +419,19 @@ class Article &lt; ActiveRecord::Base
408 419 end
409 420  
410 421 def download? view = nil
411   - (self.uploaded_file? and not self.image?) or
412   - (self.image? and view.blank?) or
413   - (not self.uploaded_file? and self.mime_type != 'text/html')
  422 + false
  423 + end
  424 +
  425 + def is_followed_by?(user)
  426 + self.person_followers.include? user
  427 + end
  428 +
  429 + def download_disposition
  430 + 'inline'
414 431 end
415 432  
416 433 def download_headers
417   - {}
  434 + { :filename => filename, :type => mime_type, :disposition => download_disposition}
418 435 end
419 436  
420 437 def alternate_languages
... ...
app/models/block.rb
... ... @@ -195,8 +195,8 @@ class Block &lt; ActiveRecord::Base
195 195 nil
196 196 end
197 197  
198   - # Is this block editable? (Default to <tt>false</tt>)
199   - def editable?
  198 + # Is this block editable? (Default to <tt>true</tt>)
  199 + def editable?(user=nil)
200 200 self.edit_modes == "all"
201 201 end
202 202  
... ...
app/models/comment.rb
... ... @@ -6,13 +6,14 @@ class Comment &lt; ActiveRecord::Base
6 6 :body => {:label => _('Content'), :weight => 2},
7 7 }
8 8  
9   - attr_accessible :body, :author, :name, :email, :title, :reply_of_id, :source
  9 + attr_accessible :body, :author, :name, :email, :title, :reply_of_id, :source, :follow_article
10 10  
11 11 validates_presence_of :body
12 12  
13 13 belongs_to :source, :counter_cache => true, :polymorphic => true
14 14 alias :article :source
15 15 alias :article= :source=
  16 + attr_accessor :follow_article
16 17  
17 18 belongs_to :author, :class_name => 'Person', :foreign_key => 'author_id'
18 19 has_many :children, :class_name => 'Comment', :foreign_key => 'reply_of_id', :dependent => :destroy
... ... @@ -102,10 +103,9 @@ class Comment &lt; ActiveRecord::Base
102 103  
103 104 after_create :new_follower
104 105 def new_follower
105   - if source.kind_of?(Article)
106   - article.followers += [author_email]
107   - article.followers -= article.profile.notification_emails
108   - article.followers.uniq!
  106 + if source.kind_of?(Article) and !author.nil? and @follow_article
  107 + article.person_followers += [author]
  108 + article.person_followers.uniq!
109 109 article.save
110 110 end
111 111 end
... ... @@ -147,7 +147,7 @@ class Comment &lt; ActiveRecord::Base
147 147 if !notification_emails.empty?
148 148 CommentNotifier.notification(self).deliver
149 149 end
150   - emails = article.followers - [author_email]
  150 + emails = article.person_followers_email_list - [author_email]
151 151 if !emails.empty?
152 152 CommentNotifier.mail_to_followers(self, emails).deliver
153 153 end
... ...
app/models/disabled_enterprise_message_block.rb
... ... @@ -19,7 +19,7 @@ class DisabledEnterpriseMessageBlock &lt; Block
19 19 end
20 20 end
21 21  
22   - def editable?
  22 + def editable?(user=nil)
23 23 false
24 24 end
25 25  
... ...
app/models/environment.rb
... ... @@ -56,6 +56,7 @@ class Environment &lt; ActiveRecord::Base
56 56 'manage_environment_trusted_sites' => N_('Manage environment trusted sites'),
57 57 'edit_appearance' => N_('Edit appearance'),
58 58 'manage_email_templates' => N_('Manage Email Templates'),
  59 + 'edit_raw_html_block' => N_('Edit Raw HTML block'),
59 60 }
60 61  
61 62 module Roles
... ...
app/models/highlights_block.rb
... ... @@ -15,6 +15,8 @@ class HighlightsBlock &lt; Block
15 15 if !Noosfero.root.nil? and !i[:address].start_with?(Noosfero.root + '/')
16 16 i[:address] = Noosfero.root + i[:address]
17 17 end
  18 + i[:new_window] = i[:new_window] == '1' ? true : false
  19 +
18 20 begin
19 21 file = UploadedFile.find(i[:image_id])
20 22 i[:image_src] = file.public_filename
... ...
app/models/person.rb
1 1 # A person is the profile of an user holding all relationships with the rest of the system
2 2 class Person < Profile
3 3  
4   - attr_accessible :organization, :contact_information, :sex, :birth_date, :cell_phone,
5   - :comercial_phone, :jabber_id, :personal_website, :nationality, :address_reference,
6   - :district, :schooling, :schooling_status, :formation, :custom_formation, :area_of_study,
7   - :custom_area_of_study, :professional_activity, :organization_website, :following_articles
  4 + attr_accessible :organization, :contact_information, :sex, :birth_date, :cell_phone, :comercial_phone, :jabber_id, :personal_website, :nationality, :address_reference, :district, :schooling, :schooling_status, :formation, :custom_formation, :area_of_study, :custom_area_of_study, :professional_activity, :organization_website, :following_articles
8 5  
9 6 SEARCH_FILTERS = {
10 7 :order => %w[more_recent more_popular more_active],
... ... @@ -19,26 +16,29 @@ class Person &lt; Profile
19 16 acts_as_trackable :after_add => Proc.new {|p,t| notify_activity(t)}
20 17 acts_as_accessor
21 18  
22   - scope :members_of, -> resources {
  19 + scope :members_of, lambda { |resources, field = ''|
23 20 resources = Array(resources)
  21 + joins = [:role_assignments]
  22 + joins << :user if User.attribute_names.include? field
  23 +
24 24 conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
25   - select('DISTINCT profiles.*').joins(:role_assignments).where([conditions])
  25 + distinct.select('profiles.*').joins(joins).where([conditions])
26 26 }
27 27  
28 28 scope :not_members_of, -> resources {
29 29 resources = Array(resources)
30 30 conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
31   - select('DISTINCT profiles.*').where('"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "role_assignments" ON "role_assignments"."accessor_id" = "profiles"."id" AND "role_assignments"."accessor_type" = (Profile) WHERE "profiles"."type" IN (Person) AND (%s))' % conditions)
  31 + distinct.select('profiles.*').where('"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "role_assignments" ON "role_assignments"."accessor_id" = "profiles"."id" AND "role_assignments"."accessor_type" = (Profile) WHERE "profiles"."type" IN (Person) AND (%s))' % conditions)
32 32 }
33 33  
34 34 scope :by_role, -> roles {
35 35 roles = Array(roles)
36   - select('DISTINCT profiles.*').joins(:role_assignments).where('role_assignments.role_id IN (?)', roles)
  36 + distinct.select('profiles.*').joins(:role_assignments).where('role_assignments.role_id IN (?)', roles)
37 37 }
38 38  
39 39 scope :not_friends_of, -> resources {
40 40 resources = Array(resources)
41   - select('DISTINCT profiles.*').where('"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "friendships" ON "friendships"."person_id" = "profiles"."id" WHERE "friendships"."friend_id" IN (%s))' % resources.map(&:id))
  41 + distinct.select('profiles.*').where('"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "friendships" ON "friendships"."person_id" = "profiles"."id" WHERE "friendships"."friend_id" IN (%s))' % resources.map(&:id))
42 42 }
43 43  
44 44 scope :visible_for_person, lambda { |person|
... ... @@ -51,8 +51,7 @@ class Person &lt; Profile
51 51 ['( roles.key = ? AND role_assignments.accessor_type = ? AND role_assignments.accessor_id = ? ) OR (
52 52 ( ( friendships.person_id = ? ) OR (profiles.public_profile = ?)) AND (profiles.visible = ?) )', 'environment_administrator', Profile.name, person.id, person.id, true, true]
53 53 ).uniq
54   - }
55   -
  54 + }
56 55  
57 56 def has_permission_with_admin?(permission, resource)
58 57 return true if resource.blank? || resource.admins.include?(self)
... ... @@ -90,7 +89,8 @@ class Person &lt; Profile
90 89 has_many :article_followers, :dependent => :destroy
91 90 has_many :following_articles, :class_name => 'Article', :through => :article_followers, :source => :article
92 91 has_many :comments, :foreign_key => :author_id
93   -
  92 + has_many :article_followers, :dependent => :destroy
  93 + has_many :following_articles, :class_name => 'Article', :through => :article_followers, :source => :article
94 94 has_many :friendships, :dependent => :destroy
95 95 has_many :friends, :class_name => 'Person', :through => :friendships
96 96  
... ... @@ -123,10 +123,10 @@ class Person &lt; Profile
123 123 scope :more_popular, -> { order 'friends_count DESC' }
124 124  
125 125 scope :abusers, -> {
126   - joins(:abuse_complaints).where('tasks.status = 3').select('DISTINCT profiles.*')
  126 + joins(:abuse_complaints).where('tasks.status = 3').distinct.select('profiles.*')
127 127 }
128 128 scope :non_abusers, -> {
129   - select("DISTINCT profiles.*").
  129 + distinct.select("profiles.*").
130 130 joins("LEFT JOIN tasks ON profiles.id = tasks.requestor_id AND tasks.type='AbuseComplaint'").
131 131 where("tasks.status != 3 OR tasks.id is NULL")
132 132 }
... ... @@ -135,6 +135,11 @@ class Person &lt; Profile
135 135 scope :activated, -> { joins(:user).where('users.activation_code IS NULL AND users.activated_at IS NOT NULL') }
136 136 scope :deactivated, -> { joins(:user).where('NOT (users.activation_code IS NULL AND users.activated_at IS NOT NULL)') }
137 137  
  138 + scope :with_role, -> role_id {
  139 + distinct.joins(:role_assignments).
  140 + where("role_assignments.role_id = #{role_id}")
  141 + }
  142 +
138 143 after_destroy do |person|
139 144 Friendship.where(friend_id: person.id).each{ |friendship| friendship.destroy }
140 145 end
... ...
app/models/profile.rb
... ... @@ -78,6 +78,9 @@ class Profile &lt; ActiveRecord::Base
78 78 def self.organization_member_roles(env_id)
79 79 all_roles(env_id).select{ |r| r.key.match(/^profile_/) unless r.key.blank? || !r.profile_id.nil?}
80 80 end
  81 + def self.organization_custom_roles(env_id, profile_id)
  82 + all_roles(env_id).where('profile_id = ?', profile_id)
  83 + end
81 84 def self.all_roles(env_id)
82 85 Role.where(environment_id: env_id)
83 86 end
... ... @@ -119,7 +122,7 @@ class Profile &lt; ActiveRecord::Base
119 122 include Noosfero::Plugin::HotSpot
120 123  
121 124 scope :memberships_of, -> person {
122   - select('DISTINCT profiles.*').
  125 + distinct.select('profiles.*').
123 126 joins(:role_assignments).
124 127 where('role_assignments.accessor_type = ? AND role_assignments.accessor_id = ?', person.class.base_class.name, person.id)
125 128 }
... ... @@ -185,15 +188,23 @@ class Profile &lt; ActiveRecord::Base
185 188  
186 189 include TimeScopes
187 190  
188   - def members
  191 + def members(by_field = '')
189 192 scopes = plugins.dispatch_scopes(:organization_members, self)
190   - scopes << Person.members_of(self)
  193 + scopes << Person.members_of(self,by_field)
191 194 return scopes.first if scopes.size == 1
192 195 ScopeTool.union *scopes
193 196 end
194 197  
195   - def members_by_name
196   - members.order('profiles.name')
  198 + def members_by(field,value = nil)
  199 + if value and !value.blank?
  200 + members_like(field,value).order('profiles.name')
  201 + else
  202 + members.order('profiles.name')
  203 + end
  204 + end
  205 +
  206 + def members_like(field,value)
  207 + members(field).where("LOWER(#{field}) LIKE ?", "%#{value.downcase}%") if value
197 208 end
198 209  
199 210 class << self
... ... @@ -781,13 +792,13 @@ private :generate_url, :url_options
781 792 end
782 793  
783 794 # Adds a person as member of this Profile.
784   - def add_member(person)
  795 + def add_member(person, attributes={})
785 796 if self.has_members?
786 797 if self.closed? && members.count > 0
787 798 AddMember.create!(:person => person, :organization => self) unless self.already_request_membership?(person)
788 799 else
789   - self.affiliate(person, Profile::Roles.admin(environment.id)) if members.count == 0
790   - self.affiliate(person, Profile::Roles.member(environment.id))
  800 + self.affiliate(person, Profile::Roles.admin(environment.id), attributes) if members.count == 0
  801 + self.affiliate(person, Profile::Roles.member(environment.id), attributes)
791 802 end
792 803 person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
793 804 remove_from_suggestion_list person
... ... @@ -1164,6 +1175,10 @@ private :generate_url, :url_options
1164 1175 end
1165 1176 end
1166 1177  
  1178 + def can_view_field? current_person, field
  1179 + display_private_info_to?(current_person) || (public_fields.include?(field) && public?)
  1180 + end
  1181 +
1167 1182 validates_inclusion_of :redirection_after_login, :in => Environment.login_redirection_options.keys, :allow_nil => true
1168 1183 def preferred_login_redirection
1169 1184 redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login
... ...
app/models/raw_html_block.rb
... ... @@ -19,4 +19,9 @@ class RawHTMLBlock &lt; Block
19 19 def has_macro?
20 20 true
21 21 end
  22 +
  23 + def editable?(user)
  24 + user.has_permission?('edit_raw_html_block', environment)
  25 + end
  26 +
22 27 end
... ...
app/models/rss_feed.rb
... ... @@ -65,6 +65,10 @@ class RssFeed &lt; Article
65 65 'text/xml'
66 66 end
67 67  
  68 + def download?(view = nil)
  69 + true
  70 + end
  71 +
68 72 include Rails.application.routes.url_helpers
69 73 def fetch_articles
70 74 if parent && parent.has_posts?
... ...
app/models/uploaded_file.rb
... ... @@ -2,6 +2,9 @@
2 2 #
3 3 # Limitation: only file metadata are versioned. Only the latest version
4 4 # of the file itself is kept. (FIXME?)
  5 +
  6 +require 'sdbm'
  7 +
5 8 class UploadedFile < Article
6 9  
7 10 attr_accessible :uploaded_data, :title
... ... @@ -10,6 +13,19 @@ class UploadedFile &lt; Article
10 13 _('File')
11 14 end
12 15  
  16 + DBM_PRIVATE_FILE = 'cache/private_files'
  17 + after_save do |uploaded_file|
  18 + if uploaded_file.published_changed?
  19 + dbm = SDBM.open(DBM_PRIVATE_FILE)
  20 + if uploaded_file.published
  21 + dbm.delete(uploaded_file.public_filename)
  22 + else
  23 + dbm.store(uploaded_file.public_filename, uploaded_file.full_path)
  24 + end
  25 + dbm.close
  26 + end
  27 + end
  28 +
13 29 track_actions :upload_image, :after_create, :keep_params => ["view_url", "thumbnail_path", "parent.url", "parent.name"], :if => Proc.new { |a| a.published? && a.image? && !a.parent.nil? && a.parent.gallery? }, :custom_target => :parent
14 30  
15 31 def title
... ... @@ -106,10 +122,13 @@ class UploadedFile &lt; Article
106 122 self.name ||= self.filename
107 123 end
108 124  
109   - def download_headers
110   - {
111   - 'Content-Disposition' => "attachment; filename=\"#{self.filename}\"",
112   - }
  125 + def download_disposition
  126 + case content_type
  127 + when 'application/pdf'
  128 + 'inline'
  129 + else
  130 + 'attachment'
  131 + end
113 132 end
114 133  
115 134 def data
... ...
app/views/account/forgot_password.html.erb
... ... @@ -5,7 +5,7 @@
5 5 <%= form_tag({:action => 'forgot_password'}, :method => 'post', :id => 'forgot-password-form') do %>
6 6 <%= labelled_form_field fields_label, text_field_tag(:value) %>
7 7  
8   - <h3><%= _('Please type the two words below') %></h3>
  8 + <h3><%= _('Please type the captcha text below') %></h3>
9 9 <%= recaptcha_tags(:display => { :theme => 'clean' }, :ajax => true) %>
10 10  
11 11 <div>
... ...
app/views/account/index.html.erb
... ... @@ -11,7 +11,7 @@
11 11 </p>
12 12  
13 13 <p>
14   -<%= link_to_homepage(_('My home page.')) %>
  14 +<%= link_to_homepage(_('My home page.'), user) %>
15 15 <%= _('See your homepage.') %>
16 16 </p>
17 17  
... ...
app/views/blocks/highlights.html.erb
... ... @@ -3,7 +3,7 @@
3 3 <div class='highlights-border'>
4 4 <div class='highlights-container'>
5 5 <% block.featured_images.each do |img| %>
6   - <a href="<%= img[:address] %>" title="<%= img[:title] %>" class="highlights-image-link">
  6 + <a href="<%= img[:address] %>" <%= 'target="_blank"' if img[:new_window] %> title="<%= img[:title] %>" class="highlights-image-link">
7 7 <%= image_tag [Noosfero.root, img[:image_src]].join, alt: img[:title] %>
8 8 <p class="highlights-label"><%= img[:title] %></p>
9 9 </a>
... ...
app/views/box_organizer/_highlights_block.html.erb
... ... @@ -3,7 +3,7 @@
3 3 <strong><%= _('Highlights') %></strong>
4 4  
5 5 <table class="noborder"><tbody id="highlights-data-table">
6   - <tr><th><%= _('Image') %></th><th><%= _('Address') %></th><th><%= _('Position') %></th></tr>
  6 + <tr><th><%= _('Image') %></th><th><%= _('Address') %></th><th><%= _('Position') %></th><th><%= _('New Window') %></th></tr>
7 7 <% @block.images.each_with_index do |image, index| %>
8 8 <%= highlights_block_config_image_fields @block, image, index %>
9 9 <% end %>
... ...
app/views/cms/_drag_and_drop_note.html.erb
1 1 <p>
2 2 <em>
3   - <%= _('Drag images to add them to the text.') %>
4   - <%= _('Click on file names to add links to the text.') %>
  3 + <%= _('Drag images to add them to the text or click on file names to add links to the text.') %>
5 4 </em>
6 5 </p>
... ...
app/views/cms/_text_editor_sidebar.html.erb
... ... @@ -17,8 +17,8 @@
17 17 :parent_id, profile, default_folder, {}, {},
18 18 "type='Folder' or type='Gallery'"
19 19 ) %>
  20 + <%= button(:newfolder, _('New folder'), '#', :id => 'new-folder-button') %>
20 21 </div>
21   - <%= button(:newfolder, _('New folder'), '#', :id => 'new-folder-button') %>
22 22 <p><%= file_field_tag('file', :multiple => true) %></p>
23 23 <% end %>
24 24 </div>
... ... @@ -31,7 +31,7 @@
31 31 <div id='published-media' class='text-editor-sidebar-box' data-url='<%= url_for({:controller => 'cms', :action => 'published_media_items', :profile => profile.identifier}) %>'>
32 32 <%= select_profile_folder(nil, :parent_id, profile, 'recent-media', {}, {},
33 33 "type='Folder' or type='Gallery'", {:root_label => _('Recent media')}) %>
34   - <%= labelled_form_field _('Search'), text_field_tag('q') %>
  34 + <%= labelled_form_field _('Search among your uploaded files'), text_field_tag('q', '', placeholder: _('Write words about the file you are looking for')) %>
35 35 <%= render :partial => 'drag_and_drop_note' %>
36 36 <div class='items'>
37 37 <%= render :partial => 'published_media_items' %>
... ...
app/views/comment/_comment_form.html.erb
... ... @@ -77,6 +77,10 @@ function check_captcha(button, confirm_action) {
77 77 <%= labelled_form_field(_('Title'), f.text_field(:title)) %>
78 78 <%= required labelled_form_field(_('Enter your comment'), f.text_area(:body, :rows => 5)) %>
79 79  
  80 + <% if logged_in? %>
  81 + <%= labelled_form_field check_box(:comment, :follow_article, {}, true, false) + _('Follow this article'), '' %>
  82 + <% end%>
  83 +
80 84 <%= hidden_field_tag(:confirm, 'false') %>
81 85 <%= hidden_field_tag(:view, params[:view])%>
82 86 <%= f.hidden_field(:reply_of_id) %>
... ...
app/views/comment/_comments_with_pagination.html.erb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<% if @comments.present? %>
  2 + <%= render :partial => 'comment/comment', :collection => @comments %>
  3 + <%= pagination_links @comments, :param_name => 'comment_page', :params => { :comment_order => @comment_order } %>
  4 +<% end %>
... ...
app/views/content_viewer/_article_toolbar.html.erb
... ... @@ -57,6 +57,8 @@
57 57 <%= button plugin_button[:icon], plugin_button[:title], plugin_button[:url], plugin_button[:html_options] %>
58 58 <% end %>
59 59  
  60 + <%= following_button @page, user %>
  61 +
60 62 <%= report_abuse(profile, :link, @page) %>
61 63  
62 64 </div>
... ...
app/views/content_viewer/_publishing_info.html.erb
... ... @@ -10,6 +10,24 @@
10 10 <%= (" - %s") % link_to_comments(@page)%>
11 11 </span>
12 12 <% end %>
  13 +
  14 +<span class="followers-count">
  15 +|
  16 +<% if @page.event? %>
  17 + <% if @page.person_followers.size > 0 %>
  18 + <%= _("%s will attend this event.") % [ pluralize(@page.person_followers.size, _("person"))]%>
  19 + <% else %>
  20 + <%= _("No one attending this event yet.") %>
  21 + <% end %>
  22 +<% else %>
  23 + <% if @page.person_followers.size > 0 %>
  24 + <%= _("%s following this article.") % [ pluralize(@page.person_followers.size, _("person"))]%>
  25 + <% else %>
  26 + <%= _("No one following this article yet.") %>
  27 + <% end %>
  28 +<% end %>
  29 +</span>
  30 +
13 31 </span>
14 32  
15 33 <% if @page.display_hits? || @page.license.present? %>
... ...
app/views/content_viewer/view_page.html.erb
... ... @@ -81,7 +81,7 @@
81 81 <ul class="article-comments-list">
82 82 <% if @comments.present? %>
83 83 <%= render :partial => 'comment/comment', :collection => @comments %>
84   - <%= pagination_links @comments, :param_name => 'comment_page' %>
  84 + <%= pagination_links @comments, :param_name => 'comment_page', :params => { :comment_order => @comment_order } %>
85 85 <% end %>
86 86 </ul>
87 87  
... ...
app/views/file_presenter/_generic.html.erb
... ... @@ -2,4 +2,4 @@
2 2 <%= generic.abstract %>
3 3 </div>
4 4  
5   -<%= button(:download, _('Download'), [Noosfero.root, generic.public_filename].join, class:'download-link', option:'primary', size:'lg') %>
  5 +<%= button(:download, _('Download'), generic.url, class:'download-link', option:'primary', size:'lg', :target => "_blank") %>
... ...
app/views/profile/_profile_members_list.html.erb
... ... @@ -9,7 +9,8 @@
9 9 </div>
10 10 <ul class="profile-list-<%= role %>" >
11 11 <% users.each do |u| %>
12   - <%= profile_image_link(u, :thumb) %>
  12 + <% extra_info = u.member_since_date(profile) == Date.today ? {:value =>_('New'), :class => 'new-profile'}:'' %>
  13 + <%= profile_image_link(u, :thumb, 'li', extra_info) %>
13 14 <% end %>
14 15 </ul>
15 16  
... ...
app/views/profile/members.html.erb
... ... @@ -19,7 +19,6 @@
19 19 :id => "members-tab",
20 20 :content => div_members
21 21 } %>
22   -
23 22 <% div_admins = content_tag :div, :class => "profile-admins" do
24 23 render :partial => 'profile_members_list',
25 24 :locals => {
... ...
app/views/profile/send_mail.html.erb
... ... @@ -10,6 +10,9 @@
10 10 </div>
11 11 <% end %>
12 12  
  13 + <% to = @mailing.data[:members_filtered].present? ? @mailing.recipients.map{|r| r.name}.join(', ') : _('All members')%>
  14 + <%= labelled_form_field(_('To:'), text_area(:data, 'members_filtered', :value => to, :rows => 4, :disabled => 'disabled', :class => 'send-mail-recipients')) %>
  15 +
13 16 <%= form_for :mailing, :url => {:action => 'send_mail'}, :html => {:id => 'mailing-form'} do |f| %>
14 17  
15 18 <%= labelled_form_field(_('Subject:'), f.text_field(:subject)) %>
... ...
app/views/profile_members/_index_buttons.html.erb
... ... @@ -5,7 +5,7 @@
5 5 <%= button :person, _('Invite people to join'), :controller => 'invite', :action => 'invite_friends' %>
6 6 <% end %>
7 7 <% if profile.community? and user.has_permission?(:send_mail_to_members, profile) %>
8   - <%= button :send, _('Send e-mail to members'), :controller => 'profile', :action => 'send_mail' %>
  8 + <%= submit_button(:send, _('Send e-mail to members')) %>
9 9 <% end %>
10 10 <% @plugins.dispatch(:manage_members_extra_buttons).each do |plugin_button| %>
11 11 <%= button plugin_button[:icon], plugin_button[:title], plugin_button[:url] %>
... ...
app/views/profile_members/_members_filter.erb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +<%= form_tag '#', :method => 'post' do %>
  2 +
  3 + <%= field_set_tag _('Filter'), :class => 'filter_fields' do %>
  4 + <p>
  5 + <%= labelled_text_field(_('Name or Email')+': ', "filters[name]", @filters[:name], {:id => 'filter-name-autocomplete',:size => 30}) %>
  6 + </p>
  7 +
  8 + <p><%= _('Roles:') %> </p>
  9 + <% @data[:roles].each do |r| %>
  10 + <%= labelled_check_box(r.name, 'filters[roles][]', r.id, @filters[:roles].include?(r.id.to_s), :add_hidden => false) %><br/>
  11 + <% end %>
  12 + <p>
  13 + <%= submit_button(:search, _('Search')) %>
  14 + </p>
  15 + <% end %>
  16 +<% end %>
  17 +
  18 +<%= javascript_include_tag params[:controller] %>
0 19 \ No newline at end of file
... ...
app/views/profile_members/_members_list.html.erb
1   -<% collection = @collection == :profile_admins ? profile.admins : profile.members_by_name %>
  1 +<% members = @data ? @data[:members] : profile.members_by('name') %>
  2 +<% collection = @collection == :profile_admins ? profile.admins : members %>
2 3 <% title = @title ? @title : _('Current members') %>
3 4 <% remove_action = @remove_action ? @remove_action : {:action => 'unassociate'} %>
  5 +<%= javascript_include_tag params[:controller] %>
4 6  
5 7 <h3><%= title %></h3>
6 8  
7 9 <table>
  10 + <col width="1">
8 11 <tr>
  12 + <th><%= check_box_tag 'checkbox-all', 1, false, :onClick => "toggle(this)" %></th>
9 13 <th><%= _('Member') %></th>
10 14 <th><%= _('Actions') %></th>
11 15 </tr>
  16 +
12 17 <% collection.each do |m| %>
13 18 <tr title="<%= m.name %>">
  19 + <td><%= labelled_check_box('', 'members_filtered[]', m.id.to_s, false, :id => 'checkbox-'+m.identifier) %></td>
14 20 <td><%= link_to_profile m.short_name, m.identifier, :title => m.name %> </td>
15 21 <td>
16 22 <div class="members-buttons-cell">
... ... @@ -27,3 +33,8 @@
27 33 </tr>
28 34 <% end %>
29 35 </table>
  36 +<% if collection.empty? %>
  37 + <p>
  38 + <em><%= _('No members found to: %s') % profile.name %></em>
  39 + </p>
  40 +<% end %>
... ...
app/views/profile_members/index.html.erb
1 1 <h1><%= h profile.short_name(50) %></h1>
2 2  
3   -<%= render :partial => 'index_buttons' %>
  3 +<%= render :partial => 'members_filter' %>
4 4  
5   -<div id="members-list">
6   - <%= render :partial => 'members_list' %>
7   -</div>
  5 +<%= form_tag 'profile_members/send_mail', :method => 'post' do %>
  6 + <div id="members-list">
  7 + <%= render :partial => 'members_list' %>
  8 + </div>
8 9  
9   -<%= render :partial => 'index_buttons' %>
  10 + <%= render :partial => 'index_buttons' %>
  11 +
  12 +<% end %>
... ...
app/views/region_validators/region.html.erb
... ... @@ -5,7 +5,7 @@
5 5 <ul>
6 6 <% for validator in @region.validators %>
7 7 <li>
8   - <%= link_to_homepage validator.name, validator.identifier %>
  8 + <%= link_to_homepage validator.name, validator %>
9 9 <%= link_to _('Remove validation rights'), { :action => 'remove', :id => @region.id, :validator_id => validator }, :method => 'post' %>
10 10 </li>
11 11 <% end %>
... ...
app/views/search/_full_enterprise.html.erb
... ... @@ -5,7 +5,7 @@
5 5 @order == 'more_recent' ? enterprise.send(@order + '_label') + show_date(enterprise.created_at) : enterprise.send(@order + '_label') %>
6 6 </div>
7 7 <div class="search-enterprise-item-column-right">
8   - <%= link_to_homepage(enterprise.name, enterprise.identifier, :class => "search-result-title") %>
  8 + <%= link_to_homepage enterprise.name, enterprise, class: "search-result-title" %>
9 9 <div class="search-enterprise-description">
10 10 <% if enterprise.description %>
11 11 <% body_stripped = strip_tags(enterprise.description) %>
... ...
app/views/search/_full_product.html.erb
... ... @@ -44,7 +44,7 @@
44 44 <div class="search-product-item-second-column">
45 45 <%= link_to_product product, :class => 'search-result-title' %>
46 46 <div class="search-product-supplier">
47   - <span class="search-field-label"><%= _('Supplier') %> </span><%= link_to_homepage(product.enterprise.name, product.enterprise.identifier) %>
  47 + <span class="search-field-label"><%= _('Supplier') %> </span><%= link_to_homepage product.enterprise.name, product.enterprise %>
48 48 </div>
49 49 <div class="search-product-description">
50 50 <% if product.description %>
... ...
app/views/search/_image.html.erb
... ... @@ -26,7 +26,7 @@
26 26 <% end %>
27 27 <% elsif image.is_a? Gallery %>
28 28 <div class="search-gallery-items">
29   - <% r = image.children.order(:updated_at).where('type = ?', 'UploadedFile').last(3) %>
  29 + <% r = image.children.latest.images.limit(3) %>
30 30 <% if r.length > 0 %>
31 31 <% r.each_index do |i| img = r[i] %>
32 32 <%= link_to '', img.view_url, :class => "search-image-pic pic-num#{i+1}",
... ... @@ -47,6 +47,8 @@
47 47 <% else %>
48 48 <div class="search-no-image"><span><%= _('No image') %></span></div>
49 49 <% end %>
  50 + <% elsif image.first_image.present? %>
  51 + <img src="<%= image.first_image %>" class="automatic-abstract-thumb search-image-pic">
50 52 <% else %>
51 53 <div class="search-content-type-icon icon-content-<%=image.class.to_s.underscore.dasherize%>"></div>
52 54 <% end %>
... ...
app/views/user_mailer/activation_code.text.erb
1 1 <%= _('Hi, %{recipient}!') % { :recipient => @recipient } %>
2 2  
3   -<%= word_wrap(_('Welcome to %{environment}! To activate your account, follow the link: %{activation_url}') % { :environment => @environment.name, :activation_url => @url + url_for(:controller => :account, :action => :activate, :activation_code => @activation_code, :redirection => @redirection, :join => @join) }) %>
  3 +<%= word_wrap(_('Welcome to %{environment}! To activate your account, follow the link: %{activation_url}') % { :environment => @environment.name, :activation_url => url_for(:controller => :account, :action => :activate, :activation_code => @activation_code, :redirection => @redirection, :join => @join) }) %>
4 4  
5 5 <%= _("Greetings,") %>
6 6  
7 7 --
8 8 <%= _('%s team.') % @environment.name %>
9   -<%= url_for @url %>
  9 +<%= @url %>
... ...
db/migrate/20150103134141_add_edit_raw_html_block_to_admin_role.rb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +class AddEditRawHtmlBlockToAdminRole < ActiveRecord::Migration
  2 + def self.up
  3 + Environment.all.map(&:id).each do |id|
  4 + role = Environment::Roles.admin(id)
  5 + role.permissions << 'edit_raw_html_block'
  6 + role.save!
  7 + end
  8 + end
  9 +
  10 + def self.down
  11 + Environment.all.map(&:id).each do |id|
  12 + role = Environment::Roles.admin(id)
  13 + role.permissions -= ['edit_raw_html_block']
  14 + role.save!
  15 + end
  16 + end
  17 +end
... ...
db/migrate/20151105175041_create_article_followers.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class CreateArticleFollowers < ActiveRecord::Migration
  2 + def self.up
  3 + execute("CREATE TABLE article_followers AS (SELECT profiles.id AS person_id, t.id AS article_id, clock_timestamp() AS since FROM (SELECT articles.id, regexp_split_to_table(replace(replace(substring(articles.setting FROM ':followers:[^:]*'), ':followers:', ''), '- ', ''), '\n') AS follower FROM articles) t INNER JOIN users ON users.email = follower INNER JOIN profiles ON users.id = profiles.user_id WHERE follower != '');")
  4 + add_timestamps :article_followers
  5 + add_index :article_followers, :person_id
  6 + add_index :article_followers, :article_id
  7 + add_index :article_followers, [:person_id, :article_id], :unique => true
  8 + end
  9 +
  10 + def self.down
  11 + drop_table :article_followers
  12 + end
  13 +end
... ...
db/migrate/20151210230319_add_followers_count_to_article.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +class AddFollowersCountToArticle < ActiveRecord::Migration
  2 + def self.up
  3 + add_column :articles, :followers_count, :integer, :default => 0
  4 + execute "update articles set followers_count = (select count(*) from article_followers where article_followers.article_id = articles.id)"
  5 + end
  6 +
  7 + def self.down
  8 + remove_column :articles, :followers_count
  9 + end
  10 +end
... ...
db/migrate/20160202142247_add_timestamps_to_role_assignments.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddTimestampsToRoleAssignments < ActiveRecord::Migration
  2 + def change
  3 + add_timestamps :role_assignments
  4 + end
  5 +end
... ...
db/migrate/20160224132937_add_data_to_mailing.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddDataToMailing < ActiveRecord::Migration
  2 + def change
  3 + add_column :mailings, :data, :text
  4 + end
  5 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended that you check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(version: 20151221105330) do
  14 +ActiveRecord::Schema.define(version: 20160224132937) do
15 15  
16 16 # These are extensions that must be enabled in order to support this database
17 17 enable_extension "plpgsql"
... ... @@ -88,9 +88,9 @@ ActiveRecord::Schema.define(version: 20151221105330) do
88 88 t.integer "profile_id"
89 89 end
90 90  
91   - create_table "article_followers", force: :cascade do |t|
92   - t.integer "person_id", null: false
93   - t.integer "article_id", null: false
  91 + create_table "article_followers", id: false, force: :cascade do |t|
  92 + t.integer "person_id"
  93 + t.integer "article_id"
94 94 t.datetime "since"
95 95 t.datetime "created_at"
96 96 t.datetime "updated_at"
... ... @@ -699,6 +699,7 @@ ActiveRecord::Schema.define(version: 20151221105330) do
699 699 t.string "locale"
700 700 t.datetime "created_at"
701 701 t.datetime "updated_at"
  702 + t.text "data"
702 703 end
703 704  
704 705 create_table "mark_comment_as_read_plugin", force: :cascade do |t|
... ... @@ -1071,12 +1072,14 @@ ActiveRecord::Schema.define(version: 20151221105330) do
1071 1072 end
1072 1073  
1073 1074 create_table "role_assignments", force: :cascade do |t|
1074   - t.integer "accessor_id", null: false
1075   - t.string "accessor_type"
1076   - t.integer "resource_id"
1077   - t.string "resource_type"
1078   - t.integer "role_id", null: false
1079   - t.boolean "is_global"
  1075 + t.integer "accessor_id", null: false
  1076 + t.string "accessor_type"
  1077 + t.integer "resource_id"
  1078 + t.string "resource_type"
  1079 + t.integer "role_id", null: false
  1080 + t.boolean "is_global"
  1081 + t.datetime "created_at"
  1082 + t.datetime "updated_at"
1080 1083 end
1081 1084  
1082 1085 create_table "roles", force: :cascade do |t|
... ...
debian/apache2/virtualhost.conf
... ... @@ -8,6 +8,19 @@ DocumentRoot &quot;/usr/share/noosfero/public&quot;
8 8  
9 9 RewriteEngine On
10 10  
  11 +# If your XMPP XMPP/BOSH isn't in localhost, change the config below to correct
  12 +# point to address
  13 +RewriteRule /http-bind http://localhost:5280/http-bind [P,QSA,L]
  14 +<Proxy http://localhost:5280/http-bind>
  15 + Order Allow,Deny
  16 + Allow from All
  17 +</Proxy>
  18 +
  19 +# Pass access to private files to backend
  20 +RewriteMap private_files "dbm=sdbm:/usr/share/noosfero/cache/private_files"
  21 +RewriteCond ${private_files:$1|NOT_FOUND} !NOT_FOUND
  22 +RewriteRule ^(/articles/.*) ${private_files:$1} [P,QSA,L]
  23 +
11 24 # Rewrite index to check for static index.html
12 25 RewriteRule ^/$ /index.html [QSA]
13 26  
... ...
debian/changelog
  1 +noosfero (1.4) jessie-test; urgency=medium
  2 +
  3 + * Noosfero 1.4
  4 +
  5 + -- Antonio Terceiro <terceiro@colivre.coop.br> Thu, 18 Feb 2016 16:20:23 -0200
  6 +
1 7 noosfero (1.4~rc3) jessie-test; urgency=medium
2 8  
3 9 * Noosfero 1.4 RC3
... ...
debian/dbinstall
... ... @@ -5,8 +5,6 @@ set -e
5 5 # dbconfig-common uses "pgsql", but we want "postgresql"
6 6 sed -i -e 's/adapter: pgsql/adapter: postgresql/' /etc/noosfero/database.yml
7 7  
8   -/etc/init.d/noosfero setup
9   -
10 8 cd /usr/share/noosfero && su noosfero -c "rake db:schema:load RAILS_ENV=production"
11 9 cd /usr/share/noosfero && su noosfero -c "rake db:migrate RAILS_ENV=production SCHEMA=/dev/null"
12 10 cd /usr/share/noosfero && su noosfero -c "rake db:data:minimal RAILS_ENV=production"
... ...
debian/dbupgrade
... ... @@ -2,7 +2,5 @@
2 2  
3 3 set -e
4 4  
5   -/etc/init.d/noosfero setup
6   -
7 5 cd /usr/share/noosfero && su noosfero -c "rake db:migrate RAILS_ENV=production SCHEMA=/dev/null"
8 6  
... ...
debian/noosfero.links
1 1 var/tmp/noosfero usr/share/noosfero/tmp
2 2 var/log/noosfero usr/share/noosfero/log
  3 +var/cache/noosfero usr/share/noosfero/cache
3 4 etc/noosfero/database.yml usr/share/noosfero/config/database.yml
4 5 etc/noosfero/unicorn.rb usr/share/noosfero/config/unicorn.rb
5 6 etc/noosfero/plugins usr/share/noosfero/config/plugins
... ...
debian/noosfero.postinst
... ... @@ -68,10 +68,17 @@ if [ ! -z &quot;$RET&quot; ]; then
68 68 export NOOSFERO_DOMAIN="$RET"
69 69 fi
70 70  
  71 +/etc/init.d/noosfero setup
  72 +
71 73 # dbconfig-common magic
72 74 . /usr/share/dbconfig-common/dpkg/postinst
73 75 dbc_go noosfero $@
74 76  
  77 +if [ ! -f /usr/share/noosfero/cache/private_files.pag ] && [ $1 = "configure" ] && [ -n $2 ]; then
  78 + echo "Creating private files dbm map..."
  79 + cd /usr/share/noosfero && su noosfero -c "rake cache:private_files RAILS_ENV=production"
  80 +fi
  81 +
75 82 # stop debconf to avoid the problem with infinite hanging, cfe
76 83 # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=295477
77 84 db_stop
... ...
debian/update-noosfero-apache
... ... @@ -17,13 +17,13 @@ if test -x /usr/share/noosfero/script/apacheconf; then
17 17 if ! test -e "$apache_site"; then
18 18 echo "Generating apache virtual host ..."
19 19 cd /usr/share/noosfero && su noosfero -c "RAILS_ENV=production ./script/apacheconf virtualhosts" > "$apache_site"
20   - else
21   - pattern="Include \/etc\/noosfero\/apache\/virtualhost.conf"
22   - include="Include \/usr\/share\/noosfero\/util\/chat\/apache\/xmpp.conf"
23   - if ! cat $apache_site | grep "^ *$include" > /dev/null ; then
24   - echo "Updating apache virtual host ..."
25   - sed -i "s/.*$pattern.*/ $include\n&/" $apache_site
26   - fi
  20 + fi
  21 +
  22 + # remove old way to include chat config
  23 + pattern="Include \/usr\/share\/noosfero\/util\/chat\/apache\/xmpp.conf"
  24 + if cat $apache_site | grep "^ *$pattern" > /dev/null ; then
  25 + echo "Removing obsolete chat configuration ..."
  26 + sed -i "/.*$pattern.*/d" $apache_site
27 27 fi
28 28  
29 29 echo 'Noosfero Apache configuration updated.'
... ...
etc/init.d/noosfero
... ... @@ -86,6 +86,13 @@ do_setup() {
86 86 chmod 750 /var/tmp/noosfero
87 87 fi
88 88  
  89 + # Noosfero cache directory
  90 + if [ ! -d /var/cache/noosfero ]; then
  91 + mkdir /var/cache/noosfero
  92 + chown $NOOSFERO_USER:root /var/cache/noosfero
  93 + chmod 755 /var/cache/noosfero
  94 + fi
  95 +
89 96 # symlink the directories into Noosfero directory
90 97 if [ ! -e $NOOSFERO_DIR/tmp ]; then
91 98 ln -s /var/tmp/noosfero $NOOSFERO_DIR/tmp
... ... @@ -96,6 +103,9 @@ do_setup() {
96 103 if [ ! -e $NOOSFERO_DIR/log ]; then
97 104 ln -s /var/log/noosfero $NOOSFERO_DIR/log
98 105 fi
  106 + if [ ! -e $NOOSFERO_DIR/cache ]; then
  107 + ln -s /var/cache/noosfero $NOOSFERO_DIR/cache
  108 + fi
99 109 }
100 110  
101 111 do_start() {
... ...
features/admin_categories.feature
... ... @@ -45,7 +45,7 @@ Feature: manage categories
45 45 And I should see "Steak"
46 46 When I follow "Hide"
47 47 Then I should not see "Vegetarian"
48   - And "Steak" should not be visible within "div"
  48 + And I should not see "Steak"
49 49  
50 50 @selenium
51 51 Scenario: the show link is available just for categories with category tree
... ...
features/categories_block.feature
... ... @@ -44,6 +44,7 @@ Feature: categories_block
44 44 And I follow "Edit" within ".block-outer .categories-block"
45 45 And I check "Product"
46 46 And I press "Save"
  47 + And I go to /
47 48 Then I should see "Food"
48 49 And I should see "Book"
49 50 And "Vegetarian" should not be visible within "span#category-name"
... ... @@ -62,6 +63,7 @@ Feature: categories_block
62 63 And I follow "Edit" within ".block-outer .categories-block"
63 64 And I check "Product"
64 65 And I press "Save"
  66 + And I go to /
65 67 Then I should see "Book"
66 68 And "Literature" should not be visible within "span#category-name"
67 69 When I follow "block_2_category_2"
... ...
features/comment.feature
... ... @@ -96,4 +96,4 @@ Feature: comment
96 96 Scenario: hide post a comment button when clicked
97 97 Given I am on /booking/article-to-comment
98 98 And I follow "Post a comment"
99   - Then "Post a comment" should not be visible within "#article"
  99 + Then "Post comment" should not be visible within "#article"
... ...
features/manage_categories.feature
... ... @@ -27,5 +27,7 @@ Feature: manage categories
27 27 Then I should not see "Beans"
28 28 And I should not see "Potatoes"
29 29 When I follow "Show"
  30 + And I wait 0.5 seconds for Services show animation to finish
  31 + And I follow "Show"
30 32 Then I should see "Beans"
31 33 And I should see "Potatoes"
... ...
features/manage_users.feature
... ... @@ -15,7 +15,7 @@ Background:
15 15 Scenario: deactive user
16 16 Given I follow "Deactivate user" within "tr[title='Joao Silva']"
17 17 When I confirm the browser dialog
18   - Then the "tr[title='Joao Silva'] td.actions a.icon-activate-user" button should be enabled
  18 + Then the field "tr[title='Joao Silva'] td.actions a.icon-activate-user" should be enabled
19 19  
20 20 @selenium
21 21 Scenario: activate user
... ... @@ -23,7 +23,7 @@ Background:
23 23 And I confirm the browser dialog
24 24 And I follow "Activate user" within "tr[title='Paulo Santos']"
25 25 When I confirm the browser dialog
26   - Then the "tr[title='Paulo Santos'] td.actions a.icon-deactivate-user" button should be enabled
  26 + Then the field "tr[title='Paulo Santos'] td.actions a.icon-deactivate-user" should be enabled
27 27  
28 28 @selenium
29 29 Scenario: remove user
... ... @@ -36,7 +36,7 @@ Background:
36 36 Scenario: admin user
37 37 Given I follow "Set admin role" within "tr[title='Joao Silva']"
38 38 When I confirm the browser dialog
39   - Then the "tr[title='Joao Silva'] td.actions a.icon-reset-admin-role" button should be enabled
  39 + Then the field "tr[title='Joao Silva'] td.actions a.icon-reset-admin-role" should be enabled
40 40  
41 41 @selenium
42 42 Scenario: unadmin user
... ... @@ -44,4 +44,4 @@ Background:
44 44 And I confirm the browser dialog
45 45 And I follow "Reset admin role" within "tr[title='Paulo Santos']"
46 46 When I confirm the browser dialog
47   - Then the "tr[title='Paulo Santos'] td.actions a.icon-set-admin-role" button should be enabled
  47 + Then the field "tr[title='Paulo Santos'] td.actions a.icon-set-admin-role" should be enabled
... ...
features/secret_community.feature
... ... @@ -17,6 +17,7 @@ Feature: Use a secret community
17 17 And I check "Secret"
18 18 And I press "Save"
19 19 And I follow "Logout"
  20 + And I go to /account/login
20 21  
21 22 @selenium
22 23 Scenario: Hide privacity options when secret is checked
... ...
features/send_email_to_organization_members.feature
... ... @@ -31,7 +31,8 @@ Feature: send emails to organization members
31 31 Scenario: Send e-mail to members
32 32 Given I am logged in as "joaosilva"
33 33 And I go to Sample Community's members management
34   - And I follow "Send e-mail to members"
  34 + And I check "checkbox-manoel"
  35 + And I press "Send e-mail to members"
35 36 And I fill in "Subject" with "Hello, member!"
36 37 And I fill in "Body" with "We have some news"
37 38 When I press "Send"
... ... @@ -40,7 +41,8 @@ Feature: send emails to organization members
40 41 Scenario: Not send e-mail to members if subject is blank
41 42 Given I am logged in as "joaosilva"
42 43 And I go to Sample Community's members management
43   - And I follow "Send e-mail to members"
  44 + And I check "checkbox-manoel"
  45 + And I press "Send e-mail to members"
44 46 And I fill in "Body" with "We have some news"
45 47 When I press "Send"
46 48 Then I should be on /profile/sample-community/send_mail
... ... @@ -48,7 +50,8 @@ Feature: send emails to organization members
48 50 Scenario: Not send e-mail to members if body is blank
49 51 Given I am logged in as "joaosilva"
50 52 And I go to Sample Community's members management
51   - And I follow "Send e-mail to members"
  53 + And I check "checkbox-manoel"
  54 + And I press "Send e-mail to members"
52 55 And I fill in "Subject" with "Hello, user!"
53 56 When I press "Send"
54 57 Then I should be on /profile/sample-community/send_mail
... ... @@ -56,7 +59,8 @@ Feature: send emails to organization members
56 59 Scenario: Cancel creation of mailing
57 60 Given I am logged in as "joaosilva"
58 61 And I go to Sample Community's members management
59   - And I follow "Send e-mail to members"
  62 + And I check "checkbox-manoel"
  63 + And I press "Send e-mail to members"
60 64 When I follow "Cancel e-mail"
61 65 Then I should be on Sample Community's members management
62 66  
... ...
features/step_definitions/custom_web_steps.rb
1 1 Then /^"([^"]*)" should not be visible within "([^"]*)"$/ do |text, selector|
2   - if page.has_content?(text)
3   - page.should have_no_css(selector, :text => text, :visible => false)
4   - end
  2 + page.should have_no_css selector, text: text, visible: false
5 3 end
6 4  
7 5 Then /^"([^"]*)" should be visible within "([^"]*)"$/ do |text, selector|
8   - if page.has_content?(text)
9   - page.should have_css(selector, :text => text, :visible => false)
10   - end
  6 + page.should have_css selector, text: text, visible: false
11 7 end
12 8  
13 9 Then /^I should see "([^"]*)" link$/ do |text|
... ... @@ -22,14 +18,14 @@ When /^I should see &quot;([^\&quot;]+)&quot; linking to &quot;([^\&quot;]+)&quot;$/ do |text, href|
22 18 page.should have_xpath("//a[@href='#{href}']")
23 19 end
24 20  
25   -Then /^the "([^"]*)" button should be disabled$/ do |selector|
26   - field = find(selector)
27   - field['disabled'].should be_truthy
28   -end
  21 +Then /^the field "([^"]*)" should be (enabled|disabled)$/ do |selector, status|
  22 + field = page.find(:css, selector)
29 23  
30   -Then /^the "([^"]*)" button should be enabled$/ do |selector|
31   - field = find(selector)
32   - field['disabled'].should_not be_truthy
  24 + if status == 'enabled'
  25 + field.disabled?.should_not be_truthy
  26 + else
  27 + field.disabled?.should be_truthy
  28 + end
33 29 end
34 30  
35 31 When /^I reload and wait for the page$/ do
... ...
features/step_definitions/web_steps.rb
... ... @@ -7,7 +7,7 @@
7 7  
8 8 require 'uri'
9 9 require 'cgi'
10   -require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
  10 +require_relative '../support/paths'
11 11  
12 12 module WithinHelpers
13 13 def with_scope(locator)
... ... @@ -39,37 +39,15 @@ end
39 39  
40 40 When /^(?:|I )follow "([^"]*)"(?: within "([^"]*)")?$/ do |link, selector|
41 41 with_scope(selector) do
42   - begin
43   - click_link(link, :match => :prefer_exact)
44   - rescue Selenium::WebDriver::Error::UnknownError => selenium_error
45   - if selenium_error.message.start_with? 'Element is not clickable at point'
46   - link = find_link(link)
47   - href = link[:href]
48   - onclick = link[:onClick]
49   -
50   - warn "#{selenium_error.message}\n\n"\
51   - "Trying to overcome this by:\n"
52   -
53   - onclick_return = true
54   -
55   - unless onclick.nil?
56   - warn "\t* Running onClick JS:\n"\
57   - "\t\t'#{onclick}'\n"
58   - onclick_return = page.execute_script onclick
59   - end
60   -
61   - if onclick_return
62   - warn "\t* Redirecting you to the link's href:\n"\
63   - "\t\t'#{href}'\n"
64   -
65   - visit href
66   - end
67   -
68   - warn "\nGood luck and be careful that this may produce hidden links to work on tests!\n"
69   - else
70   - raise selenium_error
71   - end
  42 + link = find :link_or_button, link, match: :prefer_exact
  43 + # If the link has child elements, then $(link).click() has no effect,
  44 + # so find the first child and click on it.
  45 + if Capybara.default_driver == :selenium
  46 + target = link.all('*').first || link
  47 + else
  48 + target = link
72 49 end
  50 + target.click
73 51 end
74 52 end
75 53  
... ...
features/support/debug.rb
... ... @@ -21,3 +21,8 @@ Before do |scenario|
21 21 puts "Can't find debugger or pry to debug"
22 22 end
23 23 end
  24 +
  25 +Then /^I open pry$/ do
  26 + require'pry';binding.pry
  27 +end
  28 +
... ...
features/support/env.rb
... ... @@ -65,5 +65,13 @@ Before do
65 65 fixtures_folder = Rails.root.join('test', 'fixtures')
66 66 fixtures = ['environments', 'roles']
67 67 ActiveRecord::Fixtures.create_fixtures(fixtures_folder, fixtures)
  68 +
  69 + # The same browser session is used across tests, so expire caching
  70 + # can create changes from scenario to another.
  71 + e=Environment.default
  72 + e.home_cache_in_minutes = 0
  73 + e.general_cache_in_minutes = 0
  74 + e.profile_cache_in_minutes = 0
  75 + e.save
68 76 end
69 77  
... ...
gitignore.example
... ... @@ -22,6 +22,7 @@ public/thumbnails
22 22 public/user_themes
23 23 public/designs/themes/default
24 24 public/designs/icons/default
  25 +cache
25 26  
26 27 public/assets
27 28 .sass-cache
... ...
lib/file_presenter.rb
... ... @@ -52,8 +52,8 @@ class FilePresenter
52 52 nil
53 53 end
54 54  
55   - def download?(view=nil)
56   - false
  55 + def download? view = nil
  56 + view.blank?
57 57 end
58 58  
59 59 def short_description
... ...
lib/noosfero/api/v1/articles.rb
... ... @@ -165,6 +165,37 @@ module Noosfero
165 165 end
166 166 end
167 167  
  168 + desc "Returns the total followers for the article" do
  169 + detail 'Get the followers of a specific article by id'
  170 + failure [[403, 'Forbidden']]
  171 + named 'ArticleFollowers'
  172 + end
  173 + get ':id/followers' do
  174 + article = find_article(environment.articles, params[:id])
  175 + total = article.person_followers.count
  176 + {:total_followers => total}
  177 + end
  178 +
  179 + desc "Add a follower for the article" do
  180 + detail 'Add the current user identified by private token, like a follower of a article'
  181 + params Noosfero::API::Entities::UserLogin.documentation
  182 + failure [[401, 'Unauthorized']]
  183 + named 'ArticleFollow'
  184 + end
  185 + post ':id/follow' do
  186 + authenticate!
  187 + article = find_article(environment.articles, params[:id])
  188 + if article.article_followers.exists?(:person_id => current_person.id)
  189 + {:success => false, :already_follow => true}
  190 + else
  191 + article_follower = ArticleFollower.new
  192 + article_follower.article = article
  193 + article_follower.person = current_person
  194 + article_follower.save!
  195 + {:success => true}
  196 + end
  197 + end
  198 +
168 199 desc 'Return the children of a article identified by id' do
169 200 detail 'Get all children articles of a specific article'
170 201 params Noosfero::API::Entities::Article.documentation
... ...
lib/noosfero/version.rb
1 1 module Noosfero
2 2 PROJECT = 'noosfero'
3   - VERSION = '1.4~rc3'
  3 + VERSION = '1.4'
4 4 end
5 5  
6 6 root = File.expand_path(File.dirname(__FILE__) + '/../..')
... ...
lib/noosfero/vote_ext.rb
... ... @@ -2,10 +2,10 @@ require_dependency &#39;models/vote&#39;
2 2  
3 3 class Vote
4 4  
5   - validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id], :if => :allow_duplicated_vote?
  5 + validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id], :unless => :allow_duplicate?
6 6  
7   - def allow_duplicated_vote?
8   - voter.present?
  7 + def allow_duplicate?
  8 + voter.blank?
9 9 end
10 10  
11 11 validate :verify_target_archived
... ...
lib/tasks/cache.rake 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +namespace :cache do
  2 + task :private_files => :environment do
  3 + require 'sdbm'
  4 +
  5 + hash = {}
  6 + UploadedFile.where(:published => false).find_each do |uploaded_file|
  7 + hash[uploaded_file.public_filename] = uploaded_file.full_path
  8 + end
  9 +
  10 + dbm = SDBM.open(UploadedFile::DBM_PRIVATE_FILE)
  11 + dbm.update(hash)
  12 + dbm.close
  13 + end
  14 +end
... ...
plugins/oauth_client/lib/ext/profile.rb
1 1 require_dependency 'profile'
2 2  
3   -Profile.descendants.each do |subclass|
4   - subclass.class_eval do
  3 +class Profile
5 4  
6   - has_many :oauth_auths, foreign_key: :profile_id, class_name: 'OauthClientPlugin::Auth', dependent: :destroy
7   - has_many :oauth_providers, through: :oauth_auths, source: :provider
  5 + has_many :oauth_auths, foreign_key: :profile_id, class_name: 'OauthClientPlugin::Auth', dependent: :destroy
  6 + has_many :oauth_providers, through: :oauth_auths, source: :provider
8 7  
9   - end
10 8 end
... ...
plugins/organization_ratings/features/rate_community.feature
... ... @@ -33,4 +33,4 @@ Feature: rate_community
33 33 Given I am on mycommunity's homepage
34 34 When I follow "Rate this Community"
35 35 Then I should see "Joao Silva" within ".star-profile-name"
36   - And I should see Joao Silva's profile image
37 36 \ No newline at end of file
  37 + And I should see Joao Silva's profile image
... ...
plugins/organization_ratings/features/vote_once_disable_cooldown.feature 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +Feature: vote_once_disable_cooldown
  2 + As a admin
  3 + I want to disable the cooldown time when vote once is enabled
  4 + Making it clearly that there is no cooldown when vote once is enabled
  5 +
  6 + Background:
  7 + Given plugin "OrganizationRatings" is enabled on environment
  8 + And I am logged in as admin
  9 + And I go to /admin/plugins
  10 + And I check "Organization Ratings"
  11 + And I press "Save changes"
  12 +
  13 + @selenium
  14 + Scenario: disable or enable the cooldown field when vote on is checked or unchecked
  15 + Given I follow "Administration"
  16 + And I follow "Plugins"
  17 + And I follow "Configuration"
  18 + And the field "#organization_ratings_config_cooldown" should be enabled
  19 + And I check "Vote once"
  20 + And the field "#organization_ratings_config_cooldown" should be disabled
  21 + And I uncheck "Vote once"
  22 + Then the field "#organization_ratings_config_cooldown" should be enabled
  23 +
... ...
plugins/organization_ratings/lib/organization_ratings_plugin.rb
... ... @@ -73,8 +73,8 @@ class OrganizationRatingsPlugin &lt; Noosfero::Plugin
73 73  
74 74 def js_files
75 75 %w(
76   - public/rate.js
77   - public/organization_rating_management.js
  76 + rate.js
  77 + organization_rating_management.js
78 78 )
79 79 end
80 80  
... ...
plugins/organization_ratings/public/organization_rating_management.js
... ... @@ -9,8 +9,8 @@
9 9  
10 10  
11 11 cacheDom: function() {
12   - this.$vote_once_checkbox = $("#environment_organization_ratings_vote_once");
13   - this.$hours_timer_input = $("#environment_organization_ratings_cooldown");
  12 + this.$vote_once_checkbox = $("#organization_ratings_config_vote_once");
  13 + this.$hours_timer_input = $("#organization_ratings_config_cooldown");
14 14 },
15 15  
16 16  
... ... @@ -21,10 +21,22 @@
21 21  
22 22 verifyHoursTimerDisable: function() {
23 23 if (this.$vote_once_checkbox.is(":checked")) {
24   - this.$hours_timer_input.attr("disabled", "disabled");
  24 + //this.$hours_timer_input.attr("disabled", "disabled");
  25 + this.disableVoteOnce();
25 26 } else {
26   - this.$hours_timer_input.removeAttr("disabled");
  27 + //this.$hours_timer_input.removeAttr("disabled");
  28 + this.enableVoteOnce();
27 29 }
  30 + },
  31 +
  32 +
  33 + enableVoteOnce: function() {
  34 + this.$hours_timer_input.removeAttr("disabled");
  35 + },
  36 +
  37 +
  38 + disableVoteOnce: function() {
  39 + this.$hours_timer_input.attr("disabled", "disabled");
28 40 }
29 41 }
30 42  
... ...
plugins/organization_ratings/public/style.css
... ... @@ -14,11 +14,11 @@
14 14 }
15 15  
16 16 .star-negative {
17   - background-image: url('public/images/star-negative.png');
  17 + background-image: url('images/star-negative.png');
18 18 }
19 19  
20 20 .star-positive {
21   - background-image: url('public/images/star-positive.png');
  21 + background-image: url('images/star-positive.png');
22 22 }
23 23  
24 24 .small-star-negative, .small-star-positive {
... ... @@ -31,11 +31,11 @@
31 31 }
32 32  
33 33 .small-star-negative {
34   - background-image: url('public/images/small-star-negative.png');
  34 + background-image: url('images/small-star-negative.png');
35 35 }
36 36  
37 37 .small-star-positive {
38   - background-image: url('public/images/small-star-positive.png');
  38 + background-image: url('images/small-star-positive.png');
39 39 }
40 40  
41 41 .medium-star-negative, .medium-star-positive {
... ... @@ -48,11 +48,11 @@
48 48 }
49 49  
50 50 .medium-star-positive {
51   - background-image: url('public/images/star-positive-medium.png');
  51 + background-image: url('images/star-positive-medium.png');
52 52 }
53 53  
54 54 .medium-star-negative {
55   - background-image: url('public/images/star-negative-medium.png');
  55 + background-image: url('images/star-negative-medium.png');
56 56 }
57 57  
58 58 .star-hide {
... ...
plugins/people_block/lib/ext/person.rb
... ... @@ -1,10 +0,0 @@
1   -require_dependency 'person'
2   -
3   -class Person
4   -
5   - scope :with_role, -> role_id {
6   - joins(:role_assignments).
7   - where("role_assignments.role_id = #{role_id}")
8   - }
9   -
10   -end
plugins/people_block/test/unit/members_block_test.rb
... ... @@ -280,6 +280,21 @@ class MembersBlockTest &lt; ActionView::TestCase
280 280 assert_includes block.roles, Profile::Roles.moderator(owner.environment.id)
281 281 end
282 282  
  283 + should 'count number of profiles by role' do
  284 + owner = fast_create(Community)
  285 + profile1 = fast_create(Person, {:public_profile => true})
  286 + profile2 = fast_create(Person, {:public_profile => true})
  287 +
  288 + owner.add_member profile2
  289 + owner.add_moderator profile1
  290 +
  291 + block = MembersBlock.new
  292 + block.visible_role = Profile::Roles.moderator(owner.environment.id).key
  293 + block.expects(:owner).returns(owner).at_least_once
  294 +
  295 + assert_equivalent [profile1], block.profile_list
  296 + end
  297 +
283 298 protected
284 299 include NoosferoTestHelper
285 300  
... ...
plugins/pg_search/test/unit/pg_search_plugin_test.rb
... ... @@ -25,7 +25,8 @@ class PgSearchPluginTest &lt; ActiveSupport::TestCase
25 25 profile1 = fast_create(Profile, :identifier => 'profile1', :name => 'debugger')
26 26 profile2 = fast_create(Profile, :identifier => 'profile2', :name => 'profile admin debugger')
27 27 profile3 = fast_create(Profile, :identifier => 'profile3', :name => 'admin debugger')
28   - assert_equal [profile2, profile3, profile1], search(Profile, 'profile admin deb')
  28 + profile4 = fast_create(Profile, :identifier => 'profile4', :name => 'simple user')
  29 + assert_equal [profile2, profile3, profile1, profile4], search(Profile, 'profile admin deb')
29 30 end
30 31  
31 32 should 'locate profile escaping special characters' do
... ...
plugins/site_tour/lib/ext/person.rb
1   -class Person
  1 +require_dependency 'person'
2 2  
  3 +class Person
3 4 settings_items :site_tour_plugin_actions, :type => Array, :default => []
4   -
5 5 end
... ...
plugins/stoa/test/functional/account_controller_test.rb
... ... @@ -15,6 +15,7 @@ class AccountControllerTest &lt; ActionController::TestCase
15 15 t.date "dtanas"
16 16 end
17 17 ActiveRecord::Base.establish_connection(:test)
  18 + StoaPlugin::UspUser.reset_column_information
18 19  
19 20 def setup
20 21 @controller = AccountController.new
... ...
plugins/sub_organizations/views/sub_organizations_plugin_profile/_full_related_organizations.html.erb
... ... @@ -10,7 +10,7 @@
10 10 <%= profile_image_link organization, :big, 'div' %>
11 11 </div>
12 12 <div class="related-organizations-item-column-right">
13   - <%= link_to_homepage(organization.name, organization.identifier, :class => "search-result-title") %>
  13 + <%= link_to_homepage(organization.name, organization, :class => "search-result-title") %>
14 14 <div class="related-organizations-description">
15 15 <% if organization.description %>
16 16 <% body_stripped = strip_tags(organization.description) %>
... ...
plugins/video/lib/video_plugin.rb
... ... @@ -33,10 +33,14 @@ class VideoPlugin &lt; Noosfero::Plugin
33 33 end
34 34  
35 35 def article_extra_toolbar_buttons(content)
36   - if content.kind_of?(VideoPlugin::VideoGallery)
37   - url = url_for(:action => 'new', :type=>'VideoPlugin::Video', :controller=>'cms', :parent_id => content.id)
38   - {:title => _('New Video'), :url => url, :icon => 'button with-text icon-new'}
39   - end
  36 + return [] if !content.kind_of?(VideoPlugin::VideoGallery)
  37 + {
  38 + :id=>"new-video-btn",
  39 + :class=>"button with-text icon-new",
  40 + :url=> {:action => 'new', :type=>'VideoPlugin::Video', :controller=>'cms', :parent_id => content.id},
  41 + :title=>_("New Video"),
  42 + :icon => :new
  43 + }
40 44 end
41 45  
42 46 end
... ...
plugins/video/views/content_viewer/video_plugin/_video_gallery.html.erb
... ... @@ -11,4 +11,4 @@
11 11 <em><%= _('(empty video gallery)') %></em>
12 12 <% else %>
13 13 <%= list_videos(:contents=>video_gallery.children) %>
14   -<% end %>
15 14 \ No newline at end of file
  15 +<% end %>
... ...
public/designs/themes/noosfero/style.css
... ... @@ -48,3 +48,9 @@
48 48 width: 80px;
49 49 }
50 50  
  51 +.action-profile-send_mail .send-mail-recipients {
  52 + color: #888888;
  53 + padding: 10px;
  54 + width: 475px;
  55 + line-height: 15px;
  56 +}
... ...
public/javascripts/comment_order.js
... ... @@ -10,10 +10,9 @@ function send_order(order, url) {
10 10 });
11 11 }
12 12  
13   -
14 13 jQuery(document).ready(function(){
15 14 jQuery("#comment_order").change(function(){
16   - var url = jQuery("#page_url").val();
  15 + var url = window.location.href;
17 16 send_order(this.value, url);
18 17 });
19   -});
20 18 \ No newline at end of file
  19 +});
... ...
public/javascripts/profile_members.js 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +(function($) {
  2 +
  3 + //Autocomplete to list members
  4 + $('#filter-name-autocomplete').autocomplete({
  5 + minLength:2,
  6 + source:function(request,response){
  7 + $.ajax({
  8 + url:document.location.pathname+'/search_members',
  9 + dataType:'json',
  10 + data:{
  11 + filter_name:request.term
  12 + },
  13 + success:response
  14 + });
  15 + }
  16 + });
  17 +})(jQuery);
  18 +
  19 +
  20 +function toggle(source) {
  21 + checkboxes = document.getElementsByName('members_filtered[]');
  22 + for(var i=0, n=checkboxes.length;i<n;i++) {
  23 + checkboxes[i].checked = source.checked;
  24 + }
  25 +}
... ...
public/stylesheets/profile-list.scss
... ... @@ -207,3 +207,23 @@
207 207 height: auto;
208 208 font-size: 12px;
209 209 }
  210 +.action-profile-members .profile_link{
  211 + position: relative;
  212 +}
  213 +.action-profile-members .profile_link span.new-profile:last-child{
  214 + position: absolute;
  215 + top: 3px;
  216 + right: 2px;
  217 + text-transform: uppercase;
  218 + color: #FFF;
  219 + font-size: 9px;
  220 + background: #66CC33;
  221 + padding: 2px;
  222 + display: block;
  223 + width: 35px;
  224 + font-weight: 700;
  225 +}
  226 +.action-profile-members .profile_link .fn{
  227 + font-style: normal;
  228 + color: #000;
  229 +}
... ...
script/apacheconf
... ... @@ -17,7 +17,6 @@ when &#39;virtualhosts&#39;
17 17 puts " #{server_directive} #{domain.name}"
18 18 server_directive = 'ServerAlias'
19 19 end
20   - puts " Include /usr/share/noosfero/util/chat/apache/xmpp.conf"
21 20 puts " Include /etc/noosfero/apache/virtualhost.conf"
22 21 puts "</VirtualHost>"
23 22 end
... ...