Commit d6a6f2a408ce2bbe17c4fa302f37a62d07b5f1f5
Exists in
staging
Merge branch 'private-notifications' into 'master'
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. See merge request !1009
Showing
34 changed files
with
488 additions
and
336 deletions
Show diff stats
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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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
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(_('« 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 |
... | ... | @@ -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 < 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 < 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 < 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 < 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 < 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 < 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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 < 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 < 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 < 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 < 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 < 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