Commit 18ac7d64cdc49755784c4c0800262d812d69f2ad

Authored by Rodrigo Souto
1 parent 9f3246db
Exists in staging

profile-activities: generate and filter private notifications

Notifications of private profiles or provite contents were not being
generated. This patch generates these notifications and filter
activities being displayed to the user according to her/his permissions.

Making this work through pure sql is just too complex. It was needed to
filter results after pagination, for performance reasons, and perform
some workarounds to not mess with the paginated results.

PS: Also refactoring presenters infra
app/controllers/public/profile_controller.rb
... ... @@ -13,17 +13,13 @@ class ProfileController < PublicController
13 13  
14 14 protect 'send_mail_to_members', :profile, :only => [:send_mail]
15 15  
16   - def index
17   - @network_activities = !@profile.is_a?(Person) ? @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) : []
18   - if logged_in? && current_person.follows?(@profile)
19   - @network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) if @network_activities.empty?
20   - @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page])
21   - end
22   -
23   - # TODO Find a way to filter these through sql
24   - @network_activities = filter_private_scraps(@network_activities)
25   - @activities = filter_private_scraps(@activities)
  16 + ACTIVITIES_PER_PAGE = 15
26 17  
  18 + def index
  19 + @offsets = {:wall => 0, :network => 0}
  20 + page = (params[:page] || 1).to_i
  21 + @network_activities = loop_fetch_activities(@profile.tracked_notifications, :network, page) if !@profile.is_a?(Person) || follow_profile?
  22 + @activities = loop_fetch_activities(@profile.activities, :wall, page) if follow_profile?
27 23 @tags = profile.article_tags
28 24 allow_access_to_page
29 25 end
... ... @@ -255,7 +251,7 @@ class ProfileController < PublicController
255 251 render :partial => 'profile_activities_list', :locals => {:activities => activities}
256 252 else
257 253 network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page])
258   - render :partial => 'profile_network_activities', :locals => {:network_activities => network_activities}
  254 + render :partial => 'profile_network_activities', :locals => {:activities => network_activities}
259 255 end
260 256 end
261 257  
... ... @@ -267,14 +263,32 @@ class ProfileController < PublicController
267 263 render :text => prepare_to_token_input_by_class(result).to_json
268 264 end
269 265  
270   - def view_more_activities
271   - @activities = @profile.activities.paginate(:per_page => 10, :page => params[:page])
272   - render :partial => 'profile_activities_list', :locals => {:activities => @activities}
  266 + def loop_fetch_activities(base_activities, kind, page)
  267 + activities = nil
  268 + while activities.nil? || (activities.empty? && page <= activities.total_pages)
  269 + activities = base_activities.offset(@offsets[kind.to_sym]).paginate(:per_page => ACTIVITIES_PER_PAGE, :page => page)
  270 + activities = filter_activities(activities, kind.to_sym)
  271 + page += 1
  272 + end
  273 + activities
273 274 end
274 275  
275   - def view_more_network_activities
276   - @activities = @profile.tracked_notifications.paginate(:per_page => 10, :page => params[:page])
277   - render :partial => 'profile_network_activities', :locals => {:network_activities => @activities}
  276 + def view_more_activities
  277 + @activities = nil
  278 + @offsets = params[:offsets]
  279 + page = (params[:page] || 1).to_i
  280 + kind = params[:kind]
  281 +
  282 + if kind == 'wall'
  283 + base_activities = @profile.activities
  284 + partial = 'profile_activities_list'
  285 + else
  286 + base_activities = @profile.tracked_notifications
  287 + partial = 'profile_network_activities'
  288 + end
  289 +
  290 + @activities = loop_fetch_activities(base_activities, kind, page)
  291 + render :partial => partial, :locals => {:activities => @activities}
278 292 end
279 293  
280 294 def more_comments
... ... @@ -510,22 +524,22 @@ class ProfileController &lt; PublicController
510 524 followed.uniq
511 525 end
512 526  
513   - def filter_private_scraps(activities)
  527 + def filter_activities(activities, kind)
  528 + @offsets ||= {:wall => 0, :network => 0}
  529 + return activities if environment.admins.include?(user)
514 530 activities = Array(activities)
515   - activities.delete_if do |item|
516   - if item.kind_of?(ProfileActivity)
517   - target = item.activity
518   - owner = profile
519   - else
520   - target = item.target
521   - owner = item.user
522   - end
523   - !environment.admins.include?(user) &&
524   - owner != user &&
525   - target.is_a?(Scrap) &&
526   - target.marked_people.present? &&
527   - !target.marked_people.include?(user)
528   - end
  531 + initial_count = activities.count
  532 + activities.delete_if do |activity|
  533 + activity = ActivityPresenter.for(activity)
  534 + next if activity.involved?(user)
  535 + activity.hidden_for?(user)
  536 + end
  537 + @offsets[kind] = @offsets[kind].to_i
  538 + @offsets[kind] += initial_count - activities.count
529 539 activities
530 540 end
  541 +
  542 + def follow_profile?
  543 + logged_in? && current_person.follows?(@profile)
  544 + end
531 545 end
... ...
app/jobs/notify_activity_to_profiles_job.rb
... ... @@ -11,7 +11,7 @@ class NotifyActivityToProfilesJob &lt; Struct.new(:tracked_action_id)
11 11 tracked_action = ActionTracker::Record.find(tracked_action_id)
12 12 return unless tracked_action.user.present?
13 13 target = tracked_action.target
14   - if target.is_a?(Community) && ( NOTIFY_ONLY_COMMUNITY.include?(tracked_action.verb) || ! target.public_profile )
  14 + if target.is_a?(Community) && NOTIFY_ONLY_COMMUNITY.include?(tracked_action.verb)
15 15 ActionTrackerNotification.create(:profile_id => target.id, :action_tracker_id => tracked_action.id)
16 16 return
17 17 end
... ...
app/models/article.rb
... ... @@ -63,7 +63,7 @@ class Article &lt; ApplicationRecord
63 63 _('Content')
64 64 end
65 65  
66   - track_actions :create_article, :after_create, :keep_params => [:name, :url, :lead, :first_image], :if => Proc.new { |a| a.is_trackable? && !a.image? }
  66 + track_actions :create_article, :after_create, :keep_params => [:name, :url, :lead, :first_image], :if => Proc.new { |a| a.notifiable? }
67 67  
68 68 # xss_terminate plugin can't sanitize array fields
69 69 # sanitize_tag_list is used with SanitizeHelper
... ... @@ -183,10 +183,6 @@ class Article &lt; ApplicationRecord
183 183 end
184 184 end
185 185  
186   - def is_trackable?
187   - self.published? && self.notifiable? && self.advertise? && self.profile.public_profile
188   - end
189   -
190 186 def external_link=(link)
191 187 if !link.blank? && link !~ /^[a-z]+:\/\//i
192 188 link = 'http://' + link
... ... @@ -845,7 +841,7 @@ class Article &lt; ApplicationRecord
845 841 end
846 842  
847 843 def create_activity
848   - if is_trackable? && !image?
  844 + if notifiable? && !image?
849 845 save_action_for_verb 'create_article', [:name, :url, :lead, :first_image], Proc.new{}, :author
850 846 end
851 847 end
... ...
app/models/favorite_enterprise_person.rb
... ... @@ -2,7 +2,7 @@ class FavoriteEnterprisePerson &lt; ApplicationRecord
2 2  
3 3 attr_accessible :person, :enterprise
4 4  
5   - track_actions :favorite_enterprise, :after_create, keep_params: [:enterprise_name, :enterprise_url], if: proc{ |f| f.is_trackable? }
  5 + track_actions :favorite_enterprise, :after_create, keep_params: [:enterprise_name, :enterprise_url], if: proc{ |f| f.notifiable? }
6 6  
7 7 belongs_to :enterprise
8 8 belongs_to :person
... ... @@ -13,7 +13,7 @@ class FavoriteEnterprisePerson &lt; ApplicationRecord
13 13  
14 14 protected
15 15  
16   - def is_trackable?
  16 + def notifiable?
17 17 self.enterprise.public?
18 18 end
19 19  
... ...
app/models/profile.rb
... ... @@ -824,13 +824,14 @@ private :generate_url, :url_options
824 824  
825 825 # returns +true+ if the given +user+ can see profile information about this
826 826 # +profile+, and +false+ otherwise.
827   - def display_info_to?(user)
  827 + def display_info_to?(user = nil)
828 828 if self.public?
829 829 true
830 830 else
831 831 display_private_info_to?(user)
832 832 end
833 833 end
  834 + alias_method :display_to?, :display_info_to?
834 835  
835 836 after_save :update_category_from_region
836 837 def update_category_from_region
... ...
app/models/scrap.rb
... ... @@ -67,6 +67,10 @@ class Scrap &lt; ApplicationRecord
67 67 sender != receiver && (is_root? ? root.receiver.receives_scrap_notification? : receiver.receives_scrap_notification?)
68 68 end
69 69  
  70 + def display_to?(user = nil)
  71 + marked_people.blank? || marked_people.include?(user)
  72 + end
  73 +
70 74 protected
71 75  
72 76 def create_activity
... ...
app/models/uploaded_file.rb
... ... @@ -190,8 +190,12 @@ class UploadedFile &lt; Article
190 190 true
191 191 end
192 192  
  193 + def image?
  194 + mime_type =~ /^image\//
  195 + end
  196 +
193 197 def notifiable?
194   - true
  198 + !image?
195 199 end
196 200  
197 201 end
... ...
app/presenters/activity/generic.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class ActivityPresenter::Generic < ActivityPresenter
  2 + def self.accepts?(instance)
  3 + 1
  4 + end
  5 +end
... ...
app/presenters/activity_presenter.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +class ActivityPresenter < Presenter
  2 + def self.base_class
  3 + ActionTracker::Record
  4 + end
  5 +
  6 + def self.available?(instance)
  7 + instance.kind_of?(ActionTracker::Record) || instance.kind_of?(ProfileActivity)
  8 + end
  9 +
  10 + def self.target(instance)
  11 + if instance.kind_of?(ProfileActivity)
  12 + target(instance.activity)
  13 + elsif instance.kind_of?(ActionTracker::Record)
  14 + instance.target
  15 + else
  16 + instance
  17 + end
  18 + end
  19 +
  20 + def self.owner(instance)
  21 + instance.kind_of?(ProfileActivity) ? instance.profile : instance.user
  22 + end
  23 +
  24 + def target
  25 + self.class.target(encapsulated_instance)
  26 + end
  27 +
  28 + def owner
  29 + self.class.owner(encapsulated_instance)
  30 + end
  31 +
  32 + def hidden_for?(user)
  33 + target.respond_to?(:display_to?) && !target.display_to?(user)
  34 + end
  35 +
  36 + def involved?(user)
  37 + owner == user || target == user
  38 + end
  39 +end
  40 +
  41 +# Preload ActivityPresenter subclasses to allow `Presenter.for()` to work
  42 +Dir.glob(File.join('app', 'presenters', 'activity', '*.rb')) do |file|
  43 + load file
  44 +end
... ...
app/presenters/file/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/file/image.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  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 sized_icon(size)
  8 + public_filename size
  9 + end
  10 +
  11 + def icon_name
  12 + public_filename :icon
  13 + end
  14 +
  15 + def short_description
  16 + _('Image (%s)') % content_type.split('/')[1].upcase
  17 + end
  18 +
  19 + #Overwriting method from FilePresenter to allow download of images
  20 + def download?(view = nil)
  21 + view.blank? || view == 'false'
  22 + end
  23 +end
... ...
app/presenters/file_presenter.rb 0 → 100644
... ... @@ -0,0 +1,80 @@
  1 +class FilePresenter < Presenter
  2 + def self.base_class
  3 + Article
  4 + end
  5 +
  6 + def self.available?(instance)
  7 + instance.kind_of?(UploadedFile) && !instance.kind_of?(Image)
  8 + end
  9 +
  10 + def download? view = nil
  11 + view.blank?
  12 + end
  13 +
  14 + def short_description
  15 + file_type = if content_type.present?
  16 + content_type.sub(/^application\//, '').sub(/^x-/, '').sub(/^image\//, '')
  17 + else
  18 + _('Unknown')
  19 + end
  20 + _("File (%s)") % file_type
  21 + end
  22 +
  23 + # Define the css classes to style the page fragment with the file related
  24 + # content. If you want other classes to identify this area to your
  25 + # customized presenter, so do this:
  26 + # def css_class_list
  27 + # [super, 'myclass'].flatten
  28 + # end
  29 + def css_class_list
  30 + [ encapsulated_instance.css_class_list,
  31 + 'file-' + self.class.to_s.split(/:+/).map(&:underscore)[1..-1].join('-'),
  32 + 'content-type_' + self.content_type.split('/')[0],
  33 + 'content-type_' + self.content_type.gsub(/[^a-z0-9]/i,'-')
  34 + ].flatten
  35 + end
  36 +
  37 + # Enable file presenter to customize the css classes on view_page.rhtml
  38 + # You may not overwrite this method on your customized presenter.
  39 + def css_class_name
  40 + [css_class_list].flatten.compact.join(' ')
  41 + end
  42 +
  43 + # The generic icon class-name or the specific file path.
  44 + # You may replace this method on your custom FilePresenter.
  45 + # See the current used icons class-names in public/designs/icons/tango/style.css
  46 + def icon_name
  47 + if mime_type
  48 + [ mime_type.split('/')[0], mime_type.gsub(/[^a-z0-9]/i, '-') ]
  49 + else
  50 + 'upload-file'
  51 + end
  52 + end
  53 +
  54 + # Automatic render `file_presenter/<custom>.html.erb` to display your
  55 + # custom presenter html content.
  56 + # You may not overwrite this method on your customized presenter.
  57 + # A variable with the same presenter name will be created to refer
  58 + # to the file object.
  59 + # Example:
  60 + # The `FilePresenter::Image` render `file_presenter/image.html.erb`
  61 + # inside the `file_presenter/image.html.erb` you can access the
  62 + # required `FilePresenter::Image` instance in the `image` variable.
  63 + def to_html(options = {})
  64 + file = self
  65 + proc do
  66 + render :partial => file.class.to_s.underscore,
  67 + :locals => { :options => options },
  68 + :object => file
  69 + end
  70 + end
  71 +end
  72 +
  73 +Dir.glob(File.join('app', 'presenters', 'file', '*.rb')) do |file|
  74 + load file
  75 +end
  76 +
  77 +# Preload FilePresenters from plugins to allow `FilePresenter.for()` to work
  78 +Dir.glob(File.join('plugins', '*', 'lib', 'presenters', '*.rb')) do |file|
  79 + load file
  80 +end
... ...
app/presenters/generic.rb
... ... @@ -1,7 +0,0 @@
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
... ... @@ -1,23 +0,0 @@
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 sized_icon(size)
8   - public_filename size
9   - end
10   -
11   - def icon_name
12   - public_filename :icon
13   - end
14   -
15   - def short_description
16   - _('Image (%s)') % content_type.split('/')[1].upcase
17   - end
18   -
19   - #Overwriting method from FilePresenter to allow download of images
20   - def download?(view = nil)
21   - view.blank? || view == 'false'
22   - end
23   -end
app/views/file_presenter/_image.html.erb
1 1 <% if image.gallery? && options[:gallery_view] %>
2 2 <%
3 3 images = image.parent.images
4   - current_index = images.index(image.encapsulated_file)
  4 + current_index = images.index(image.encapsulated_instance)
5 5 total_of_images = images.count
6 6 link_to_previous = if current_index >= 1
7 7 link_to(_('&laquo; Previous').html_safe, images[current_index - 1].view_url, :class => 'previous')
... ...
app/views/profile/_default_activity.html.erb
... ... @@ -2,7 +2,7 @@
2 2 <%= link_to(profile_image(activity.user, :minor), activity.user.url) %>
3 3 </div>
4 4 <div class='profile-activity-description'>
5   - <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe activity %></p>
  5 + <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe(activity).html_safe %></p>
6 6 <p class='profile-activity-time'><%= time_ago_in_words(activity.created_at) %></p>
7 7 <div class='profile-wall-actions'>
8 8 <%= link_to s_('profile|Comment'), '#', { :class => 'focus-on-comment'} %>
... ...
app/views/profile/_favorite_enterprise.html.erb
... ... @@ -3,7 +3,7 @@
3 3 </div>
4 4 <div class='profile-activity-description'>
5 5 <p class='profile-activity-text'>
6   - <%= link_to activity.user.short_name(nil), activity.user.url %> <%= describe activity %>
  6 + <%= link_to activity.user.short_name(nil), activity.user.url %> <%= describe(activity).html_safe %>
7 7 </p>
8 8 <p class='profile-activity-time'><%= time_ago_in_words activity.created_at %></p>
9 9  
... ...
app/views/profile/_leave_scrap.html.erb
... ... @@ -2,7 +2,7 @@
2 2 <%= link_to(profile_image(activity.user, :minor), activity.user.url) %>
3 3 </div>
4 4 <div class='profile-activity-description'>
5   - <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe activity %></p>
  5 + <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe(activity).html_safe %></p>
6 6 <p class='profile-activity-time'><%= time_ago_in_words(activity.created_at) %></p>
7 7 <div class='profile-wall-actions'>
8 8 <%= link_to_function(_('Remove'), 'remove_item_wall(this, \'%s\', \'%s\', \'%s\'); return false ;' % [".profile-activity-item", url_for(:profile => params[:profile], :action => :remove_activity, :activity_id => activity.id, :view => params[:view]), _('Are you sure you want to remove this activity and all its replies?')]) if logged_in? && current_person == @profile %>
... ...
app/views/profile/_profile_activities_list.html.erb
... ... @@ -11,6 +11,6 @@
11 11  
12 12 <% if activities.current_page < activities.total_pages %>
13 13 <div id='profile_activities_page_<%= activities.current_page %>'>
14   - <%= button_to_remote :add, _('View more'), :url => {:action => 'view_more_activities', :page => (activities.current_page + 1)}, :update => "profile_activities_page_#{activities.current_page}" %>
  14 + <%= button_to_remote :add, _('View more'), :url => {:action => 'view_more_activities', :page => (activities.current_page + 1), :offsets => @offsets, :kind => 'wall'}, :update => "profile_activities_page_#{activities.current_page}" %>
15 15 </div>
16 16 <% end %>
... ...
app/views/profile/_profile_network.html.erb
1 1 <h3><%= _("%s's network activity") % @profile.name %></h3>
2 2 <ul id='network-activities' class='profile-activities'>
3   - <%= render :partial => 'profile_network_activities', :locals => {:network_activities => @network_activities} %>
  3 + <%= render :partial => 'profile_network_activities', :locals => {:activities => @network_activities} %>
4 4 </ul>
... ...
app/views/profile/_profile_network_activities.html.erb
1   -<% network_activities.each do |activity| %>
  1 +<% activities.each do |activity| %>
2 2 <%= render :partial => 'profile_activity', :locals => { :activity => activity, :tab_action => 'network' } if activity.visible? %>
3 3 <% end %>
4   -<% if network_activities.current_page < network_activities.total_pages %>
5   - <div id='profile_network_activities_page_<%= network_activities.current_page %>'>
6   - <%= button_to_remote :add, _('View more'), :url => {:action => 'view_more_network_activities', :page => (network_activities.current_page + 1)}, :update => "profile_network_activities_page_#{network_activities.current_page}" %>
  4 +<% if activities.current_page < activities.total_pages %>
  5 + <div id='profile_network_activities_page_<%= activities.current_page %>'>
  6 + <%= button_to_remote :add, _('View more'), :url => {:action => 'view_more_activities', :page => (activities.current_page + 1), :offsets => @offsets, :kind => 'network'}, :update => "profile_network_activities_page_#{activities.current_page}" %>
7 7 </div>
8 8 <% end %>
... ...
lib/file_presenter.rb
... ... @@ -1,131 +0,0 @@
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   - #FIXME This check after the || is redundant but increases the blog_page
10   - # speed considerably.
11   - return f if f.is_a?(FilePresenter ) || (!f.kind_of?(UploadedFile) && !f.kind_of?(Image))
12   - klass = FilePresenter.subclasses.sort_by {|class_instance|
13   - class_instance.accepts?(f) || 0
14   - }.last
15   - klass.accepts?(f) ? klass.new(f) : f
16   - end
17   -
18   - def self.base_class
19   - Article
20   - end
21   -
22   - def initialize(f)
23   - @file = f
24   - end
25   -
26   - # Allows to use the original `UploadedFile` reference.
27   - def encapsulated_file
28   - @file
29   - end
30   -
31   - def id
32   - @file.id
33   - end
34   -
35   - def reload
36   - @file.reload
37   - self
38   - end
39   -
40   - def kind_of?(klass)
41   - @file.kind_of?(klass)
42   - end
43   -
44   - # This method must be overridden in subclasses.
45   - #
46   - # If the class accepts the file, return a number that represents the
47   - # priority the class should be given to handle that file. Higher numbers
48   - # mean higher priority.
49   - #
50   - # If the class does not accept the file, return false.
51   - def self.accepts?(f)
52   - nil
53   - end
54   -
55   - def download? view = nil
56   - view.blank?
57   - end
58   -
59   - def short_description
60   - file_type = if content_type.present?
61   - content_type.sub(/^application\//, '').sub(/^x-/, '').sub(/^image\//, '')
62   - else
63   - _('Unknown')
64   - end
65   - _("File (%s)") % file_type
66   - end
67   -
68   - # Define the css classes to style the page fragment with the file related
69   - # content. If you want other classes to identify this area to your
70   - # customized presenter, so do this:
71   - # def css_class_list
72   - # [super, 'myclass'].flatten
73   - # end
74   - def css_class_list
75   - [ @file.css_class_list,
76   - 'file-' + self.class.to_s.split(/:+/).map(&:underscore)[1..-1].join('-'),
77   - 'content-type_' + self.content_type.split('/')[0],
78   - 'content-type_' + self.content_type.gsub(/[^a-z0-9]/i,'-')
79   - ].flatten
80   - end
81   -
82   - # Enable file presenter to customize the css classes on view_page.rhtml
83   - # You may not overwrite this method on your customized presenter.
84   - def css_class_name
85   - [css_class_list].flatten.compact.join(' ')
86   - end
87   -
88   - # The generic icon class-name or the specific file path.
89   - # You may replace this method on your custom FilePresenter.
90   - # See the current used icons class-names in public/designs/icons/tango/style.css
91   - def icon_name
92   - if mime_type
93   - [ mime_type.split('/')[0], mime_type.gsub(/[^a-z0-9]/i, '-') ]
94   - else
95   - 'upload-file'
96   - end
97   - end
98   -
99   - # Automatic render `file_presenter/<custom>.html.erb` to display your
100   - # custom presenter html content.
101   - # You may not overwrite this method on your customized presenter.
102   - # A variable with the same presenter name will be created to refer
103   - # to the file object.
104   - # Example:
105   - # The `FilePresenter::Image` render `file_presenter/image.html.erb`
106   - # inside the `file_presenter/image.html.erb` you can access the
107   - # required `FilePresenter::Image` instance in the `image` variable.
108   - def to_html(options = {})
109   - file = self
110   - proc do
111   - render :partial => file.class.to_s.underscore,
112   - :locals => { :options => options },
113   - :object => file
114   - end
115   - end
116   -
117   - # That makes the presenter to works like any other `UploadedFile` instance.
118   - def method_missing(m, *args)
119   - @file.send(m, *args)
120   - end
121   -end
122   -
123   -# Preload FilePresenters to allow `FilePresenter.for()` to work
124   -Dir.glob(File.join('app', 'presenters', '*.rb')) do |file|
125   - load file
126   -end
127   -
128   -# Preload FilePresenters from plugins to allow `FilePresenter.for()` to work
129   -Dir.glob(File.join('plugins', '*', 'lib', 'presenters', '*.rb')) do |file|
130   - load file
131   -end
lib/presenter.rb 0 → 100644
... ... @@ -0,0 +1,63 @@
  1 +class Presenter
  2 + # Define presenter base_class
  3 + def self.base_class
  4 + end
  5 +
  6 + # Define base type condition
  7 + def self.available?(instance)
  8 + false
  9 + end
  10 +
  11 + def self.for(instance)
  12 + return instance if instance.is_a?(Presenter) || !available?(instance)
  13 +
  14 + klass = subclasses.sort_by {|class_instance|
  15 + class_instance.accepts?(instance) || 0
  16 + }.last
  17 +
  18 + klass.accepts?(instance) ? klass.new(instance) : f
  19 + end
  20 +
  21 + def initialize(instance)
  22 + @instance = instance
  23 + end
  24 +
  25 + # Allows to use the original instance reference.
  26 + def encapsulated_instance
  27 + @instance
  28 + end
  29 +
  30 + def id
  31 + @instance.id
  32 + end
  33 +
  34 + def reload
  35 + @instance.reload
  36 + self
  37 + end
  38 +
  39 + def kind_of?(klass)
  40 + @instance.kind_of?(klass)
  41 + end
  42 +
  43 + # This method must be overridden in subclasses.
  44 + #
  45 + # If the class accepts the instance, return a number that represents the
  46 + # priority the class should be given to handle that instance. Higher numbers
  47 + # mean higher priority.
  48 + #
  49 + # If the class does not accept the instance, return false.
  50 + def self.accepts?(f)
  51 + nil
  52 + end
  53 +
  54 + # That makes the presenter to works like any other not encapsulated instance.
  55 + def method_missing(m, *args)
  56 + @instance.send(m, *args)
  57 + end
  58 +end
  59 +
  60 +# Preload Presenters to allow `Presenter.for()` to work
  61 +Dir.glob(File.join('app', 'presenters', '*.rb')) do |file|
  62 + load file
  63 +end
... ...
plugins/metadata/lib/metadata_plugin/controllers.rb
... ... @@ -8,8 +8,8 @@ class MetadataPlugin::Controllers
8 8 lambda do
9 9 if profile and @page and profile.home_page_id == @page.id
10 10 @profile
11   - elsif @page.respond_to? :encapsulated_file
12   - @page.encapsulated_file
  11 + elsif @page.respond_to? :encapsulated_instance
  12 + @page.encapsulated_instance
13 13 else
14 14 @page
15 15 end
... ...
plugins/products/models/products_plugin/product.rb
... ... @@ -48,9 +48,9 @@ class ProductsPlugin::Product &lt; ApplicationRecord
48 48 extend ActsAsHavingSettings::ClassMethods
49 49 acts_as_having_settings field: :data
50 50  
51   - track_actions :create_product, :after_create, keep_params: [:name, :url ], if: Proc.new { |a| a.is_trackable? }, custom_user: :action_tracker_user
52   - track_actions :update_product, :before_update, keep_params: [:name, :url], if: Proc.new { |a| a.is_trackable? }, custom_user: :action_tracker_user
53   - track_actions :remove_product, :before_destroy, keep_params: [:name], if: Proc.new { |a| a.is_trackable? }, custom_user: :action_tracker_user
  51 + track_actions :create_product, :after_create, keep_params: [:name, :url ], if: Proc.new { |a| a.notifiable? }, custom_user: :action_tracker_user
  52 + track_actions :update_product, :before_update, keep_params: [:name, :url], if: Proc.new { |a| a.notifiable? }, custom_user: :action_tracker_user
  53 + track_actions :remove_product, :before_destroy, keep_params: [:name], if: Proc.new { |a| a.notifiable? }, custom_user: :action_tracker_user
54 54  
55 55 validates_uniqueness_of :name, scope: :profile_id, allow_nil: true, if: :validate_uniqueness_of_column_name?
56 56  
... ... @@ -290,7 +290,7 @@ class ProductsPlugin::Product &lt; ApplicationRecord
290 290 true
291 291 end
292 292  
293   - def is_trackable?
  293 + def notifiable?
294 294 # shopping_cart create products without profile
295 295 self.profile.present?
296 296 end
... ...
test/functional/profile_controller_test.rb
... ... @@ -888,17 +888,17 @@ class ProfileControllerTest &lt; ActionController::TestCase
888 888 login_as(profile.identifier)
889 889 ActionTracker::Record.delete_all
890 890 get :index, :profile => p1.identifier
891   - assert_equal [], assigns(:network_activities)
  891 + assert assigns(:network_activities).blank?
892 892 assert_response :success
893 893 assert_template 'index'
894 894  
895 895 get :index, :profile => p2.identifier
896   - assert_equal [], assigns(:network_activities)
  896 + assert assigns(:network_activities).blank?
897 897 assert_response :success
898 898 assert_template 'index'
899 899  
900 900 get :index, :profile => p3.identifier
901   - assert_equal [], assigns(:network_activities)
  901 + assert assigns(:network_activities).blank?
902 902 assert_response :success
903 903 assert_template 'index'
904 904 end
... ... @@ -1186,14 +1186,14 @@ class ProfileControllerTest &lt; ActionController::TestCase
1186 1186 40.times{ create(ActionTracker::Record, :user_id => profile.id, :user_type => 'Profile', :verb => 'create_article', :target_id => article.id, :target_type => 'Article', :params => {'name' => article.name, 'url' => article.url, 'lead' => article.lead, 'first_image' => article.first_image})}
1187 1187 assert_equal 40, profile.tracked_actions.count
1188 1188 assert_equal 40, profile.activities.size
1189   - get :view_more_activities, :profile => profile.identifier, :page => 2
  1189 + get :view_more_activities, :profile => profile.identifier, :page => 2, :kind => 'wall', :offsets => {:wall => 0, :network => 0}
1190 1190 assert_response :success
1191 1191 assert_template '_profile_activities_list'
1192   - assert_equal 10, assigns(:activities).size
  1192 + assert_equal ProfileController::ACTIVITIES_PER_PAGE, assigns(:activities).size
1193 1193 end
1194 1194  
1195 1195 should "be logged in to access the view_more_activities action" do
1196   - get :view_more_activities, :profile => profile.identifier
  1196 + get :view_more_activities, :profile => profile.identifier, :kind => 'wall', :offsets => {:wall => 0, :network => 0}
1197 1197 assert_redirected_to :controller => 'account', :action => 'login'
1198 1198 end
1199 1199  
... ... @@ -1201,14 +1201,14 @@ class ProfileControllerTest &lt; ActionController::TestCase
1201 1201 login_as(profile.identifier)
1202 1202 40.times{fast_create(ActionTrackerNotification, :profile_id => profile.id, :action_tracker_id => fast_create(ActionTracker::Record, :user_id => profile.id)) }
1203 1203 assert_equal 40, profile.tracked_notifications.count
1204   - get :view_more_network_activities, :profile => profile.identifier, :page => 2
  1204 + get :view_more_activities, :profile => profile.identifier, :page => 2, :kind => 'network', :offsets => {:wall => 0, :network => 0}
1205 1205 assert_response :success
1206 1206 assert_template '_profile_network_activities'
1207   - assert_equal 10, assigns(:activities).size
  1207 + assert_equal ProfileController::ACTIVITIES_PER_PAGE, assigns(:activities).size
1208 1208 end
1209 1209  
1210 1210 should "be logged in to access the view_more_network_activities action" do
1211   - get :view_more_network_activities, :profile => profile.identifier
  1211 + get :view_more_activities, :profile => profile.identifier, :kind => 'network', :offsets => {:wall => 0, :network => 0}
1212 1212 assert_redirected_to :controller => 'account', :action => 'login'
1213 1213 end
1214 1214  
... ... @@ -2260,4 +2260,47 @@ class ProfileControllerTest &lt; ActionController::TestCase
2260 2260 assert assigns(:network_activities).include?(scrap_activity)
2261 2261 end
2262 2262  
  2263 + should 'not filter any activity if the user is an environment admin' do
  2264 + admin = create_user('env-admin').person
  2265 + env = @profile.environment
  2266 + env.add_admin(admin)
  2267 + activities = mock
  2268 + activities.expects(:delete_if).never
  2269 + @controller.stubs(:user).returns(admin)
  2270 + @controller.stubs(:environment).returns(env)
  2271 + @controller.send(:filter_activities, activities, :wall)
  2272 + end
  2273 +
  2274 + should 'not call hidden_for? if the user is involved in the activity' do
  2275 + user = create_user('involved-user').person
  2276 + env = @profile.environment
  2277 + activity = mock
  2278 + activities = [activity]
  2279 + activity.stubs(:involved?).with(user).returns(true)
  2280 + activity.expects(:hidden_for?).never
  2281 + @controller.stubs(:user).returns(user)
  2282 + @controller.stubs(:environment).returns(env)
  2283 + result = @controller.send(:filter_activities, activities, :wall)
  2284 + assert_includes result, activity
  2285 + end
  2286 +
  2287 + should 'remove activities that should be hidden for the user' do
  2288 + user = create_user('sample-user').person
  2289 + env = @profile.environment
  2290 + a1 = mock
  2291 + a2 = mock
  2292 + a3 = mock
  2293 + activities = [a1, a2, a3]
  2294 + a1.stubs(:involved?).with(user).returns(false)
  2295 + a2.stubs(:involved?).with(user).returns(false)
  2296 + a3.stubs(:involved?).with(user).returns(false)
  2297 + a1.stubs(:hidden_for?).with(user).returns(false)
  2298 + a2.stubs(:hidden_for?).with(user).returns(true)
  2299 + a3.stubs(:hidden_for?).with(user).returns(false)
  2300 + @controller.stubs(:user).returns(user)
  2301 + @controller.stubs(:environment).returns(env)
  2302 + result = @controller.send(:filter_activities, activities, :wall)
  2303 + assert_equivalent [a1,a3], result
  2304 + end
  2305 +
2263 2306 end
... ...
test/unit/activity_presenter/generic.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +require_relative "../../test_helper"
  2 +
  3 +class ActivityPresenter::GenericTest < ActiveSupport::TestCase
  4 + should 'accept everything' do
  5 + activity = ActionTracker::Record.new
  6 +
  7 + activity.stubs(:target).returns(Profile.new)
  8 + assert ActivityPresenter::Generic.accepts?(activity)
  9 + activity.stubs(:target).returns(Article.new)
  10 + assert ActivityPresenter::Generic.accepts?(activity)
  11 + activity.stubs(:target).returns(Scrap.new)
  12 + assert ActivityPresenter::Generic.accepts?(activity)
  13 + activity.stubs(:target).returns(mock)
  14 + assert ActivityPresenter::Generic.accepts?(activity)
  15 + end
  16 +end
... ...
test/unit/activity_presenter_test.rb 0 → 100644
... ... @@ -0,0 +1,86 @@
  1 +require_relative "../test_helper"
  2 +
  3 +class ActivityPresenterTest < ActiveSupport::TestCase
  4 + should 'be available for ActionTracker::Record' do
  5 + assert ActivityPresenter.available?(ActionTracker::Record.new)
  6 + end
  7 +
  8 + should 'be available for ProfileActivity' do
  9 + assert ActivityPresenter.available?(ProfileActivity.new)
  10 + end
  11 +
  12 + should 'return correct target for ActionTracker::Record' do
  13 + target = mock
  14 + activity = ActionTracker::Record.new
  15 + activity.stubs(:target).returns(target)
  16 + assert_equal target, ActivityPresenter.target(activity)
  17 + end
  18 +
  19 + should 'return correct target for ProfileActivity' do
  20 + target = mock
  21 + notification = ProfileActivity.new
  22 + record = ActionTracker::Record.new
  23 + notification.stubs(:activity).returns(record)
  24 + record.stubs(:target).returns(target)
  25 +
  26 + assert_equal target, ActivityPresenter.target(notification)
  27 + end
  28 +
  29 + should 'return correct owner for ActionTracker::Record' do
  30 + owner = mock
  31 + activity = ActionTracker::Record.new
  32 + activity.stubs(:user).returns(owner)
  33 + assert_equal owner, ActivityPresenter.owner(activity)
  34 + end
  35 +
  36 + should 'return correct owner for ProfileActivity' do
  37 + owner = mock
  38 + notification = ProfileActivity.new
  39 + notification.stubs(:profile).returns(owner)
  40 +
  41 + assert_equal owner, ActivityPresenter.owner(notification)
  42 + end
  43 +
  44 + should 'not be hidden for user if target does not respond to display_to' do
  45 + user = fast_create(Person)
  46 + target = mock
  47 + presenter = ActivityPresenter.new(target)
  48 + refute presenter.hidden_for?(user)
  49 + end
  50 +
  51 + should 'be hidden for user based on target display_to' do
  52 + user = fast_create(Person)
  53 + target = mock
  54 + presenter = ActivityPresenter.new(target)
  55 +
  56 + target.stubs(:display_to?).with(user).returns(false)
  57 + assert presenter.hidden_for?(user)
  58 +
  59 + target.stubs(:display_to?).with(user).returns(true)
  60 + refute presenter.hidden_for?(user)
  61 + end
  62 +
  63 + should 'verify if user is involved as target with the activity' do
  64 + user = mock
  65 + presenter = ActivityPresenter.new(mock)
  66 + presenter.stubs(:target).returns(user)
  67 + presenter.stubs(:owner).returns(nil)
  68 + assert presenter.involved?(user)
  69 + end
  70 +
  71 + should 'verify if user is involved as owner with the activity' do
  72 + user = mock
  73 + presenter = ActivityPresenter.new(mock)
  74 + presenter.stubs(:target).returns(nil)
  75 + presenter.stubs(:owner).returns(user)
  76 + assert presenter.involved?(user)
  77 + end
  78 +
  79 + should 'refute if user is not involved' do
  80 + user = mock
  81 + presenter = ActivityPresenter.new(mock)
  82 + presenter.stubs(:target).returns(nil)
  83 + presenter.stubs(:owner).returns(nil)
  84 + refute presenter.involved?(user)
  85 + end
  86 +end
... ...
test/unit/approve_article_test.rb
... ... @@ -319,13 +319,6 @@ class ApproveArticleTest &lt; ActiveSupport::TestCase
319 319 assert_equal approved_article, ActionTracker::Record.last.target
320 320 end
321 321  
322   - should "have the same is_trackable method as original article" do
323   - a = create(ApproveArticle, :article => article, :target => community, :requestor => profile)
324   - a.finish
325   -
326   - assert_equal article.is_trackable?, article.class.last.is_trackable?
327   - end
328   -
329 322 should 'not have target notification message if it is not a moderated oganization' do
330 323 community.moderated_articles = false; community.save
331 324 task = build(ApproveArticle, :article => article, :target => community, :requestor => profile)
... ...
test/unit/article_test.rb
... ... @@ -1044,34 +1044,6 @@ class ArticleTest &lt; ActiveSupport::TestCase
1044 1044 assert_equal profile, article.action_tracker_target
1045 1045 end
1046 1046  
1047   - should "have defined the is_trackable method defined" do
1048   - assert Article.method_defined?(:is_trackable?)
1049   - end
1050   -
1051   - should "the common trackable conditions return the correct value" do
1052   - a = Article.new
1053   - a.published = a.advertise = true
1054   - assert_equal true, a.published?
1055   - assert_equal false, a.notifiable?
1056   - assert_equal true, a.advertise?
1057   - assert_equal false, a.is_trackable?
1058   -
1059   - a.published=false
1060   - assert_equal false, a.published?
1061   - assert_equal false, a.is_trackable?
1062   -
1063   - a.published=true
1064   - a.advertise=false
1065   - assert_equal false, a.advertise?
1066   - assert_equal false, a.is_trackable?
1067   - end
1068   -
1069   - should "not be trackable if article is inside a private community" do
1070   - private_community = fast_create(Community, :public_profile => false)
1071   - a = fast_create(TextArticle, :profile_id => private_community.id)
1072   - assert_equal false, a.is_trackable?
1073   - end
1074   -
1075 1047 should 'create the notification to organization and all organization members' do
1076 1048 Profile.destroy_all
1077 1049 ActionTracker::Record.destroy_all
... ...
test/unit/notify_activity_to_profiles_job_test.rb
... ... @@ -73,10 +73,11 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
73 73 assert not_marked.tracked_notifications.where(:target => scrap).blank?
74 74 end
75 75  
76   - should 'not notify the communities members' do
  76 + should 'notify the community members on private articles' do
77 77 person = fast_create(Person)
78 78 community = fast_create(Community)
79   - action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'create_article')
  79 + article = fast_create(TextArticle, :published => false, :profile_id => community.id)
  80 + action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Article', :target_id => article.id, :verb => 'create_article')
80 81 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb)
81 82 m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person)
82 83 fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id)
... ... @@ -116,7 +117,7 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
116 117 end
117 118 end
118 119  
119   - should 'notify only the community if it is private' do
  120 + should 'notify only the community and its members if it is private' do
120 121 person = fast_create(Person)
121 122 private_community = fast_create(Community, :public_profile => false)
122 123 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => private_community.id, :verb => 'create_article')
... ... @@ -131,14 +132,23 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
131 132 job.perform
132 133 process_delayed_job_queue
133 134  
134   - assert_equal 1, ActionTrackerNotification.count
135   - [person, p1, p2, m1, m2].each do |profile|
136   - notification = ActionTrackerNotification.find_by profile_id: profile.id
137   - assert notification.nil?
138   - end
  135 + assert_equal 4, ActionTrackerNotification.count
139 136  
  137 + # Community notification
140 138 notification = ActionTrackerNotification.find_by profile_id: private_community.id
141 139 assert_equal action_tracker, notification.action_tracker
  140 +
  141 + # User notification
  142 + notification = ActionTrackerNotification.find_by profile_id: person.id
  143 + assert_equal action_tracker, notification.action_tracker
  144 +
  145 + # Community members notifications
  146 + assert ActionTrackerNotification.find_by profile_id: m1.id
  147 + assert ActionTrackerNotification.find_by profile_id: m2.id
  148 +
  149 + # No user friends notification
  150 + assert_nil ActionTrackerNotification.find_by profile_id: p1.id
  151 + assert_nil ActionTrackerNotification.find_by profile_id: p2.id
142 152 end
143 153  
144 154 should 'not notify the community tracking join_community verb' do
... ...
test/unit/scrap_test.rb
... ... @@ -313,4 +313,20 @@ class ScrapTest &lt; ActiveSupport::TestCase
313 313 assert_equal s, p2.activities.first.activity
314 314 end
315 315  
  316 + should 'display to anyone if nobody marked' do
  317 + assert Scrap.new.display_to?(fast_create(Person))
  318 + end
  319 +
  320 + should 'display only to marked people' do
  321 + scrap = Scrap.new
  322 + u1 = mock
  323 + u2 = mock
  324 + u3 = mock
  325 + scrap.stubs(:marked_people).returns([u1, u3])
  326 +
  327 + assert scrap.display_to?(u1)
  328 + refute scrap.display_to?(u2)
  329 + assert scrap.display_to?(u3)
  330 + end
  331 +
316 332 end
... ...
test/unit/textile_article_test.rb
... ... @@ -97,38 +97,6 @@ class TextArticleTest &lt; ActiveSupport::TestCase
97 97 assert_equal article, ActionTracker::Record.last.target
98 98 end
99 99  
100   - should 'not notify activity if the article is not advertise' do
101   - ActionTracker::Record.delete_all
102   - a = create TextArticle, name: 'bar', profile_id: profile.id, published: true, advertise: false
103   - assert_equal true, a.published?
104   - assert_equal true, a.notifiable?
105   - assert_equal false, a.image?
106   - assert_equal false, a.profile.is_a?(Community)
107   - assert_equal 0, ActionTracker::Record.count
108   - end
109   -
110   - should "have defined the is_trackable method defined" do
111   - assert TextArticle.method_defined?(:is_trackable?)
112   - end
113   -
114   - should "the common trackable conditions return the correct value" do
115   - a = build(TextArticle, profile: profile)
116   - a.published = a.advertise = true
117   - assert_equal true, a.published?
118   - assert_equal true, a.notifiable?
119   - assert_equal true, a.advertise?
120   - assert_equal true, a.is_trackable?
121   -
122   - a.published=false
123   - assert_equal false, a.published?
124   - assert_equal false, a.is_trackable?
125   -
126   - a.published=true
127   - a.advertise=false
128   - assert_equal false, a.advertise?
129   - assert_equal false, a.is_trackable?
130   - end
131   -
132 100 should 'generate proper HTML for links' do
133 101 assert_tag_in_string build_article('"Noosfero":http://noosfero.org/').to_html, tag: 'a', attributes: { href: 'http://noosfero.org/' }
134 102 end
... ...
test/unit/tiny_mce_article_test.rb
... ... @@ -162,38 +162,6 @@ class TinyMceArticleTest &lt; ActiveSupport::TestCase
162 162 assert_equal article, ActionTracker::Record.last.target
163 163 end
164 164  
165   - should 'not notify activity if the article is not advertise' do
166   - ActionTracker::Record.delete_all
167   - a = create TextArticle, name: 'bar', profile_id: profile.id, published: true, advertise: false
168   - assert_equal true, a.published?
169   - assert_equal true, a.notifiable?
170   - assert_equal false, a.image?
171   - assert_equal false, a.profile.is_a?(Community)
172   - assert_equal 0, ActionTracker::Record.count
173   - end
174   -
175   - should "have defined the is_trackable method defined" do
176   - assert TextArticle.method_defined?(:is_trackable?)
177   - end
178   -
179   - should "the common trackable conditions return the correct value" do
180   - a = build(TextArticle, :profile => profile)
181   - a.published = a.advertise = true
182   - assert_equal true, a.published?
183   - assert_equal true, a.notifiable?
184   - assert_equal true, a.advertise?
185   - assert_equal true, a.is_trackable?
186   -
187   - a.published=false
188   - assert_equal false, a.published?
189   - assert_equal false, a.is_trackable?
190   -
191   - a.published=true
192   - a.advertise=false
193   - assert_equal false, a.advertise?
194   - assert_equal false, a.is_trackable?
195   - end
196   -
197 165 should 'not sanitize html5 audio tag on body' do
198 166 article = TextArticle.create!(:name => 'html5 audio', :body => "Audio: <audio controls='controls'><source src='http://example.ogg' type='audio/ogg' />Audio not playing?.</audio>", :profile => profile)
199 167 assert_tag_in_string article.body, :tag => 'audio', :attributes => {:controls => 'controls'}
... ...