Commit 44627b4a898e59f5a5082404e65bc09a19b65a88
1 parent
688b3512
Exists in
master
and in
29 other branches
use anti_spam plugin in suggest_articles
ActionItem2691
Showing
26 changed files
with
544 additions
and
58 deletions
Show diff stats
app/controllers/my_profile/cms_controller.rb
@@ -267,7 +267,10 @@ class CmsController < MyProfileController | @@ -267,7 +267,10 @@ class CmsController < MyProfileController | ||
267 | @back_to = params[:back_to] || request.referer || url_for(profile.public_profile_url) | 267 | @back_to = params[:back_to] || request.referer || url_for(profile.public_profile_url) |
268 | @task = SuggestArticle.new(params[:task]) | 268 | @task = SuggestArticle.new(params[:task]) |
269 | if request.post? | 269 | if request.post? |
270 | - @task.target = profile | 270 | + @task.target = profile |
271 | + @task.ip_address = request.remote_ip | ||
272 | + @task.user_agent = request.user_agent | ||
273 | + @task.referrer = request.referrer | ||
271 | if verify_recaptcha(:model => @task, :message => _('Please type the words correctly')) && @task.save | 274 | if verify_recaptcha(:model => @task, :message => _('Please type the words correctly')) && @task.save |
272 | session[:notice] = _('Thanks for your suggestion. The community administrators were notified.') | 275 | session[:notice] = _('Thanks for your suggestion. The community administrators were notified.') |
273 | redirect_to @back_to | 276 | redirect_to @back_to |
app/controllers/my_profile/profile_editor_controller.rb
@@ -4,7 +4,7 @@ class ProfileEditorController < MyProfileController | @@ -4,7 +4,7 @@ class ProfileEditorController < MyProfileController | ||
4 | protect 'destroy_profile', :profile, :only => [:destroy_profile] | 4 | protect 'destroy_profile', :profile, :only => [:destroy_profile] |
5 | 5 | ||
6 | def index | 6 | def index |
7 | - @pending_tasks = Task.to(profile).pending.select{|i| user.has_permission?(i.permission, profile)} | 7 | + @pending_tasks = Task.to(profile).pending.without_spam.select{|i| user.has_permission?(i.permission, profile)} |
8 | end | 8 | end |
9 | 9 | ||
10 | helper :profile | 10 | helper :profile |
app/controllers/my_profile/spam_controller.rb
@@ -14,9 +14,15 @@ class SpamController < MyProfileController | @@ -14,9 +14,15 @@ class SpamController < MyProfileController | ||
14 | if params[:remove_comment] | 14 | if params[:remove_comment] |
15 | profile.comments_received.find(params[:remove_comment]).destroy | 15 | profile.comments_received.find(params[:remove_comment]).destroy |
16 | end | 16 | end |
17 | + if params[:remove_task] | ||
18 | + Task.to(profile).find_by_id(params[:remove_task]).destroy | ||
19 | + end | ||
17 | if params[:mark_comment_as_ham] | 20 | if params[:mark_comment_as_ham] |
18 | profile.comments_received.find(params[:mark_comment_as_ham]).ham! | 21 | profile.comments_received.find(params[:mark_comment_as_ham]).ham! |
19 | end | 22 | end |
23 | + if params[:mark_task_as_ham] && (t = Task.to(profile).find_by_id(params[:mark_task_as_ham])) | ||
24 | + t.ham! | ||
25 | + end | ||
20 | if request.xhr? | 26 | if request.xhr? |
21 | json_response(true) | 27 | json_response(true) |
22 | else | 28 | else |
@@ -28,7 +34,8 @@ class SpamController < MyProfileController | @@ -28,7 +34,8 @@ class SpamController < MyProfileController | ||
28 | return | 34 | return |
29 | end | 35 | end |
30 | 36 | ||
31 | - @spam = profile.comments_received.spam.paginate({:page => params[:page]}) | 37 | + @comment_spam = profile.comments_received.spam.paginate({:page => params[:comments_page]}) |
38 | + @task_spam = Task.to(profile).spam.paginate({:page => params[:tasks_page]}) | ||
32 | end | 39 | end |
33 | 40 | ||
34 | protected | 41 | protected |
app/controllers/my_profile/tasks_controller.rb
@@ -4,12 +4,12 @@ class TasksController < MyProfileController | @@ -4,12 +4,12 @@ class TasksController < MyProfileController | ||
4 | 4 | ||
5 | def index | 5 | def index |
6 | @filter = params[:filter_type].blank? ? nil : params[:filter_type] | 6 | @filter = params[:filter_type].blank? ? nil : params[:filter_type] |
7 | - @tasks = Task.to(profile).pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page]) | 7 | + @tasks = Task.to(profile).without_spam.pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page]) |
8 | @failed = params ? params[:failed] : {} | 8 | @failed = params ? params[:failed] : {} |
9 | end | 9 | end |
10 | 10 | ||
11 | def processed | 11 | def processed |
12 | - @tasks = Task.to(profile).closed.sort_by(&:created_at) | 12 | + @tasks = Task.to(profile).without_spam.closed.sort_by(&:created_at) |
13 | end | 13 | end |
14 | 14 | ||
15 | VALID_DECISIONS = [ 'finish', 'cancel', 'skip' ] | 15 | VALID_DECISIONS = [ 'finish', 'cancel', 'skip' ] |
@@ -57,7 +57,7 @@ class TasksController < MyProfileController | @@ -57,7 +57,7 @@ class TasksController < MyProfileController | ||
57 | end | 57 | end |
58 | 58 | ||
59 | def list_requested | 59 | def list_requested |
60 | - @tasks = Task.find_all_by_requestor_id(profile.id) | 60 | + @tasks = Task.without_spam.find_all_by_requestor_id(profile.id) |
61 | end | 61 | end |
62 | 62 | ||
63 | def ticket_details | 63 | def ticket_details |
app/models/spammer_logger.rb
@@ -6,6 +6,8 @@ class SpammerLogger < Logger | @@ -6,6 +6,8 @@ class SpammerLogger < Logger | ||
6 | if object | 6 | if object |
7 | if object.kind_of?(Comment) | 7 | if object.kind_of?(Comment) |
8 | @logger << "[#{Time.now.strftime('%F %T %z')}] Comment-id: #{object.id} IP: #{spammer_ip}\n" | 8 | @logger << "[#{Time.now.strftime('%F %T %z')}] Comment-id: #{object.id} IP: #{spammer_ip}\n" |
9 | + elsif object.kind_of?(SuggestArticle) | ||
10 | + @logger << "[#{Time.now.strftime('%F %T %z')}] SuggestArticle-id: #{object.id} IP: #{spammer_ip}\n" | ||
9 | end | 11 | end |
10 | else | 12 | else |
11 | @logger << "[#{Time.now.strftime('%F %T %z')}] IP: #{spammer_ip}\n" | 13 | @logger << "[#{Time.now.strftime('%F %T %z')}] IP: #{spammer_ip}\n" |
app/models/suggest_article.rb
@@ -11,6 +11,21 @@ class SuggestArticle < Task | @@ -11,6 +11,21 @@ class SuggestArticle < Task | ||
11 | settings_items :source, :type => String | 11 | settings_items :source, :type => String |
12 | settings_items :source_name, :type => String | 12 | settings_items :source_name, :type => String |
13 | settings_items :highlighted, :type => :boolean, :default => false | 13 | settings_items :highlighted, :type => :boolean, :default => false |
14 | + settings_items :ip_address, :type => String | ||
15 | + settings_items :user_agent, :type => String | ||
16 | + settings_items :referrer, :type => String | ||
17 | + | ||
18 | + after_create :schedule_spam_checking | ||
19 | + | ||
20 | + def schedule_spam_checking | ||
21 | + self.delay.check_for_spam | ||
22 | + end | ||
23 | + | ||
24 | + include Noosfero::Plugin::HotSpot | ||
25 | + | ||
26 | + def check_for_spam | ||
27 | + plugins.dispatch(:check_suggest_article_for_spam, self) | ||
28 | + end | ||
14 | 29 | ||
15 | def sender | 30 | def sender |
16 | "#{name} (#{email})" | 31 | "#{name} (#{email})" |
@@ -61,4 +76,25 @@ class SuggestArticle < Task | @@ -61,4 +76,25 @@ class SuggestArticle < Task | ||
61 | _('You need to login on %{system} in order to approve or reject this article.') % { :system => target.environment.name } | 76 | _('You need to login on %{system} in order to approve or reject this article.') % { :system => target.environment.name } |
62 | end | 77 | end |
63 | 78 | ||
79 | + def spam! | ||
80 | + super | ||
81 | + SpammerLogger.log(ip_address, self) | ||
82 | + self.delay.marked_as_spam | ||
83 | + self | ||
84 | + end | ||
85 | + | ||
86 | + def ham! | ||
87 | + super | ||
88 | + self.delay.marked_as_ham | ||
89 | + self | ||
90 | + end | ||
91 | + | ||
92 | + def marked_as_spam | ||
93 | + plugins.dispatch(:suggest_article_marked_as_spam, self) | ||
94 | + end | ||
95 | + | ||
96 | + def marked_as_ham | ||
97 | + plugins.dispatch(:suggest_article_marked_as_ham, self) | ||
98 | + end | ||
99 | + | ||
64 | end | 100 | end |
app/models/task.rb
@@ -235,6 +235,26 @@ class Task < ActiveRecord::Base | @@ -235,6 +235,26 @@ class Task < ActiveRecord::Base | ||
235 | end | 235 | end |
236 | end | 236 | end |
237 | 237 | ||
238 | + def spam? | ||
239 | + !spam.nil? && spam | ||
240 | + end | ||
241 | + | ||
242 | + def ham? | ||
243 | + !spam.nil? && !spam | ||
244 | + end | ||
245 | + | ||
246 | + def spam! | ||
247 | + self.spam = true | ||
248 | + self.save! | ||
249 | + self | ||
250 | + end | ||
251 | + | ||
252 | + def ham! | ||
253 | + self.spam = false | ||
254 | + self.save! | ||
255 | + self | ||
256 | + end | ||
257 | + | ||
238 | protected | 258 | protected |
239 | 259 | ||
240 | # This method must be overrided in subclasses, and its implementation must do | 260 | # This method must be overrided in subclasses, and its implementation must do |
@@ -274,6 +294,9 @@ class Task < ActiveRecord::Base | @@ -274,6 +294,9 @@ class Task < ActiveRecord::Base | ||
274 | named_scope :opened, :conditions => { :status => [Task::Status::ACTIVE, Task::Status::HIDDEN] } | 294 | named_scope :opened, :conditions => { :status => [Task::Status::ACTIVE, Task::Status::HIDDEN] } |
275 | named_scope :of, lambda { |type| conditions = type ? "type LIKE '#{type}'" : "1=1"; {:conditions => [conditions]} } | 295 | named_scope :of, lambda { |type| conditions = type ? "type LIKE '#{type}'" : "1=1"; {:conditions => [conditions]} } |
276 | named_scope :order_by, lambda { |attribute, ord| {:order => "#{attribute} #{ord}"} } | 296 | named_scope :order_by, lambda { |attribute, ord| {:order => "#{attribute} #{ord}"} } |
297 | + named_scope :without_spam, :conditions => ['spam IS NULL OR spam = ?', false] | ||
298 | + named_scope :spam, :conditions => ['spam = ?', true] | ||
299 | + | ||
277 | 300 | ||
278 | named_scope :to, lambda { |profile| | 301 | named_scope :to, lambda { |profile| |
279 | environment_condition = nil | 302 | environment_condition = nil |
@@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
1 | +<%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %> | ||
2 | +<div id='article'> | ||
3 | + <div class="comments" id="comments_list"> | ||
4 | + <ul class="article-comments-list"> | ||
5 | + <%= render :partial => 'comment/comment', :collection => @comment_spam %> | ||
6 | + </ul> | ||
7 | + </div> | ||
8 | +</div> | ||
9 | + | ||
10 | +<%= pagination_links @comment_spam, :param_name => :comments_page %> | ||
11 | + |
@@ -0,0 +1,21 @@ | @@ -0,0 +1,21 @@ | ||
1 | +<% render :layout => 'task', :locals => { :task => task } do %> | ||
2 | + <% content_for :extra_buttons do %> | ||
3 | + <%= button_to_function('down', _('Show details'), "toggleDetails(this, '#{_('Hide details')}', '#{_('Show details')}')" ) %> | ||
4 | + <% end %> | ||
5 | + | ||
6 | + <% content_for :extra_content do %> | ||
7 | + <ul class="suggest-article-details" style="display: none"> | ||
8 | + <li><strong><%=_('Sent by')%></strong>: <%=task.name%> </li> | ||
9 | + <li><strong><%=_('Email')%></strong>: <%=task.email%> </li> | ||
10 | + <li><strong><%=_('Source')%></strong>: <%=task.source_name%> </li> | ||
11 | + <li><strong><%=_('Source URL')%></strong>: <%=task.source%> </li> | ||
12 | + <li><strong><%=_('Folder')%></strong>: <%=(a = Article.find_by_id(task.article_parent_id))?a.name : '<em>' + s_('Folder|none') + '</em>'%> </li> | ||
13 | + <li><strong><%=_('Lead')%></strong>: <%=task.article_abstract.blank? ? '<em>' + s_('Abstract|empty') + '</em>' : task.article_abstract%> </li> | ||
14 | + <li><strong><%=_('Body')%></strong>: | ||
15 | + <div class='suggest-article-body'> | ||
16 | + <%= task.article_body %> | ||
17 | + </div> | ||
18 | + </li> | ||
19 | + </ul> | ||
20 | + <% end %> | ||
21 | +<% end %> |
@@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
1 | +<div class="task_box" id="task-<%= task.id %>"> | ||
2 | + <%= render :partial => 'tasks/task_icon', :locals => {:task => task} %> | ||
3 | + <%= render :partial => 'tasks/task_title', :locals => {:task => task} %> | ||
4 | + <div class="task-information"> | ||
5 | + <%= task_information(task) %> | ||
6 | + </div> | ||
7 | + | ||
8 | + <%= yield %> <% # ??? %> | ||
9 | + | ||
10 | + <% button_bar do %> | ||
11 | + <%= button_to_function('new', _('Mark as NOT SPAM'), 'removeTaskBox(this, %s, "%s", "")' % [url_for(:mark_task_as_ham => task.id).to_json, "task-#{task.id}"]) %> | ||
12 | + <%= yield :extra_buttons %> | ||
13 | + <%= button_to_function('delete', _('Remove'), 'removeTaskBox(this, %s, "%s", %s)' % [url_for(:profile => params[:profile], :remove_task => task.id).to_json, "task-#{task.id}", _('Are you sure you want to remove this article suggestion?').to_json]) %> | ||
14 | + | ||
15 | + <% end %> | ||
16 | + | ||
17 | + <%= yield :extra_content %> | ||
18 | +</div> |
app/views/spam/index.rhtml
1 | +<%= stylesheet('tasks') %> | ||
2 | + | ||
1 | <h1><%= _('Manage SPAM') %></h1> | 3 | <h1><%= _('Manage SPAM') %></h1> |
2 | 4 | ||
3 | <% button_bar do %> | 5 | <% button_bar do %> |
@@ -5,16 +7,21 @@ | @@ -5,16 +7,21 @@ | ||
5 | <% end %> | 7 | <% end %> |
6 | 8 | ||
7 | <%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %> | 9 | <%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %> |
8 | -<div id='article'> | ||
9 | - <div class="comments" id="comments_list"> | ||
10 | - <ul class="article-comments-list"> | ||
11 | - <%= render :partial => 'comment/comment', :collection => @spam %> | ||
12 | - </ul> | ||
13 | - </div> | ||
14 | -</div> | ||
15 | 10 | ||
16 | -<%= pagination_links @spam %> | 11 | +<% if @task_spam.length > 0 %> |
12 | + <% tabs = [] %> | ||
13 | + <% tabs << {:title => _('Comment Spam'), :id => 'comment-spam', | ||
14 | + :content => (render :partial => 'comment_spam')} %> | ||
15 | + <% tabs << {:title => _('Task Spam'), :id => 'task-spam', | ||
16 | + :content => (render :partial => 'task_spam') } %> | ||
17 | + <%= render_tabs(tabs) %> | ||
18 | +<% else %> | ||
19 | + <%= render :partial => 'comment_spam' %> | ||
20 | +<% end %> | ||
21 | + | ||
17 | 22 | ||
18 | <% button_bar do %> | 23 | <% button_bar do %> |
19 | <%= button :back, _('Back to control panel'), :controller => :profile_editor %> | 24 | <%= button :back, _('Back to control panel'), :controller => :profile_editor %> |
20 | <% end %> | 25 | <% end %> |
26 | + | ||
27 | +<%= javascript_include_tag 'spam' %> |
app/views/tasks/_task.rhtml
1 | <div class="task_box" id="task-<%= task.id %>"> | 1 | <div class="task_box" id="task-<%= task.id %>"> |
2 | 2 | ||
3 | - <div class="task_icon"> | ||
4 | - <% | ||
5 | - icon_info = task.icon | ||
6 | - if icon_info[:type] == :profile_image | ||
7 | - icon = profile_image(icon_info[:profile], :minor) | ||
8 | - elsif icon_info[:type] == :defined_image | ||
9 | - icon = "<img src='#{icon_info[:src]}' alt='#{icon_info[:name]}' />" | ||
10 | - end | ||
11 | - %> | ||
12 | - <%= | ||
13 | - if icon_info[:url] | ||
14 | - link_to(icon, icon_info[:url]) | ||
15 | - else | ||
16 | - icon | ||
17 | - end | ||
18 | - %> | ||
19 | - | ||
20 | - </div> | 3 | + <%= render :partial => 'task_icon', :locals => {:task => task} %> |
21 | 4 | ||
22 | <div class="task_decisions"> | 5 | <div class="task_decisions"> |
23 | <%= | 6 | <%= |
@@ -39,9 +22,7 @@ | @@ -39,9 +22,7 @@ | ||
39 | %> | 22 | %> |
40 | </div><!-- class="task_decisions" --> | 23 | </div><!-- class="task_decisions" --> |
41 | 24 | ||
42 | - <strong class="task_title"> | ||
43 | - <%= task.title %> | ||
44 | - </strong> | 25 | + <%= render :partial => 'task_title', :locals => {:task => task} %> |
45 | 26 | ||
46 | <div class="task_information"> | 27 | <div class="task_information"> |
47 | <%= task_information(task) %> | 28 | <%= task_information(task) %> |
@@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
1 | +<% | ||
2 | + icon_info = task.icon | ||
3 | + if icon_info[:type] == :profile_image | ||
4 | + icon = profile_image(icon_info[:profile], :minor) | ||
5 | + elsif icon_info[:type] == :defined_image | ||
6 | + icon = "<img src='#{icon_info[:src]}' alt='#{icon_info[:name]}' />" | ||
7 | + end | ||
8 | + | ||
9 | + if icon_info[:url] | ||
10 | + icon = link_to(icon, icon_info[:url]) | ||
11 | + end | ||
12 | +%> | ||
13 | + | ||
14 | +<div class="task_icon"> | ||
15 | + <%= icon %> | ||
16 | +</div> |
@@ -0,0 +1,13 @@ | @@ -0,0 +1,13 @@ | ||
1 | +class AddSpamToTask < ActiveRecord::Migration | ||
2 | + def self.up | ||
3 | + change_table :tasks do |t| | ||
4 | + t.boolean :spam, :default => false | ||
5 | + end | ||
6 | + Task.update_all ["spam = ?", false] | ||
7 | + add_index :tasks, [:spam] | ||
8 | + end | ||
9 | + | ||
10 | + def self.down | ||
11 | + remove_column :tasks, :spam | ||
12 | + end | ||
13 | +end |
db/schema.rb
@@ -9,7 +9,7 @@ | @@ -9,7 +9,7 @@ | ||
9 | # | 9 | # |
10 | # It's strongly recommended to check this file into your version control system. | 10 | # It's strongly recommended to check this file into your version control system. |
11 | 11 | ||
12 | -ActiveRecord::Schema.define(:version => 20130711213046) do | 12 | +ActiveRecord::Schema.define(:version => 20131011164400) do |
13 | 13 | ||
14 | create_table "abuse_reports", :force => true do |t| | 14 | create_table "abuse_reports", :force => true do |t| |
15 | t.integer "reporter_id" | 15 | t.integer "reporter_id" |
@@ -233,6 +233,50 @@ ActiveRecord::Schema.define(:version => 20130711213046) do | @@ -233,6 +233,50 @@ ActiveRecord::Schema.define(:version => 20130711213046) do | ||
233 | t.datetime "updated_at" | 233 | t.datetime "updated_at" |
234 | end | 234 | end |
235 | 235 | ||
236 | + create_table "custom_forms_plugin_answers", :force => true do |t| | ||
237 | + t.text "value" | ||
238 | + t.integer "field_id" | ||
239 | + t.integer "submission_id" | ||
240 | + end | ||
241 | + | ||
242 | + create_table "custom_forms_plugin_fields", :force => true do |t| | ||
243 | + t.string "name" | ||
244 | + t.string "slug" | ||
245 | + t.string "type" | ||
246 | + t.string "default_value" | ||
247 | + t.string "choices" | ||
248 | + t.float "minimum" | ||
249 | + t.float "maximum" | ||
250 | + t.integer "form_id" | ||
251 | + t.boolean "mandatory", :default => false | ||
252 | + t.boolean "multiple" | ||
253 | + t.boolean "list" | ||
254 | + t.integer "position", :default => 0 | ||
255 | + end | ||
256 | + | ||
257 | + create_table "custom_forms_plugin_forms", :force => true do |t| | ||
258 | + t.string "name" | ||
259 | + t.string "slug" | ||
260 | + t.text "description" | ||
261 | + t.integer "profile_id" | ||
262 | + t.datetime "begining" | ||
263 | + t.datetime "ending" | ||
264 | + t.boolean "report_submissions", :default => false | ||
265 | + t.boolean "on_membership", :default => false | ||
266 | + t.string "access" | ||
267 | + t.datetime "created_at" | ||
268 | + t.datetime "updated_at" | ||
269 | + end | ||
270 | + | ||
271 | + create_table "custom_forms_plugin_submissions", :force => true do |t| | ||
272 | + t.string "author_name" | ||
273 | + t.string "author_email" | ||
274 | + t.integer "profile_id" | ||
275 | + t.integer "form_id" | ||
276 | + t.datetime "created_at" | ||
277 | + t.datetime "updated_at" | ||
278 | + end | ||
279 | + | ||
236 | create_table "delayed_jobs", :force => true do |t| | 280 | create_table "delayed_jobs", :force => true do |t| |
237 | t.integer "priority", :default => 0 | 281 | t.integer "priority", :default => 0 |
238 | t.integer "attempts", :default => 0 | 282 | t.integer "attempts", :default => 0 |
@@ -547,8 +591,11 @@ ActiveRecord::Schema.define(:version => 20130711213046) do | @@ -547,8 +591,11 @@ ActiveRecord::Schema.define(:version => 20130711213046) do | ||
547 | t.datetime "created_at" | 591 | t.datetime "created_at" |
548 | t.string "target_type" | 592 | t.string "target_type" |
549 | t.integer "image_id" | 593 | t.integer "image_id" |
594 | + t.boolean "spam", :default => false | ||
550 | end | 595 | end |
551 | 596 | ||
597 | + add_index "tasks", ["spam"], :name => "index_tasks_on_spam" | ||
598 | + | ||
552 | create_table "thumbnails", :force => true do |t| | 599 | create_table "thumbnails", :force => true do |t| |
553 | t.integer "size" | 600 | t.integer "size" |
554 | t.string "content_type" | 601 | t.string "content_type" |
plugins/anti_spam/lib/anti_spam_plugin.rb
@@ -5,7 +5,7 @@ class AntiSpamPlugin < Noosfero::Plugin | @@ -5,7 +5,7 @@ class AntiSpamPlugin < Noosfero::Plugin | ||
5 | end | 5 | end |
6 | 6 | ||
7 | def self.plugin_description | 7 | def self.plugin_description |
8 | - _("Checks comments against a spam checking service compatible with the Akismet API") | 8 | + _("Tests comments and suggested articles against a spam checking service compatible with the Akismet API") |
9 | end | 9 | end |
10 | 10 | ||
11 | def self.host_default_setting | 11 | def self.host_default_setting |
@@ -13,30 +13,44 @@ class AntiSpamPlugin < Noosfero::Plugin | @@ -13,30 +13,44 @@ class AntiSpamPlugin < Noosfero::Plugin | ||
13 | end | 13 | end |
14 | 14 | ||
15 | def check_comment_for_spam(comment) | 15 | def check_comment_for_spam(comment) |
16 | - if rakismet_call(comment, :spam?) | 16 | + if rakismet_call AntiSpamPlugin::CommentWrapper.new(comment), comment.environment, :spam? |
17 | comment.spam = true | 17 | comment.spam = true |
18 | comment.save! | 18 | comment.save! |
19 | end | 19 | end |
20 | end | 20 | end |
21 | 21 | ||
22 | def comment_marked_as_spam(comment) | 22 | def comment_marked_as_spam(comment) |
23 | - rakismet_call(comment, :spam!) | 23 | + rakismet_call AntiSpamPlugin::CommentWrapper.new(comment), comment.environment, :spam! |
24 | end | 24 | end |
25 | 25 | ||
26 | def comment_marked_as_ham(comment) | 26 | def comment_marked_as_ham(comment) |
27 | - rakismet_call(comment, :ham!) | 27 | + rakismet_call AntiSpamPlugin::CommentWrapper.new(comment), comment.environment, :ham! |
28 | + end | ||
29 | + | ||
30 | + def check_suggest_article_for_spam(suggest_article) | ||
31 | + if rakismet_call AntiSpamPlugin::SuggestArticleWrapper.new(suggest_article), suggest_article.environment, :spam? | ||
32 | + suggest_article.spam = true | ||
33 | + suggest_article.save! | ||
34 | + end | ||
35 | + end | ||
36 | + | ||
37 | + def suggest_article_marked_as_spam(suggest_article) | ||
38 | + rakismet_call AntiSpamPlugin::SuggestArticleWrapper.new(suggest_article), suggest_article.environment, :spam! | ||
39 | + end | ||
40 | + | ||
41 | + def suggest_article_marked_as_ham(suggest_article) | ||
42 | + rakismet_call AntiSpamPlugin::SuggestArticleWrapper.new(suggest_article), suggest_article.environment, :ham! | ||
28 | end | 43 | end |
29 | 44 | ||
30 | protected | 45 | protected |
31 | 46 | ||
32 | - def rakismet_call(comment, op) | ||
33 | - settings = Noosfero::Plugin::Settings.new(comment.environment, self.class) | 47 | + def rakismet_call(submission, environment, op) |
48 | + settings = Noosfero::Plugin::Settings.new(environment, self.class) | ||
34 | 49 | ||
35 | Rakismet.host = settings.host | 50 | Rakismet.host = settings.host |
36 | Rakismet.key = settings.api_key | 51 | Rakismet.key = settings.api_key |
37 | - Rakismet.url = comment.environment.top_url | 52 | + Rakismet.url = environment.top_url |
38 | 53 | ||
39 | - submission = AntiSpamPlugin::CommentWrapper.new(comment) | ||
40 | submission.send(op) | 54 | submission.send(op) |
41 | end | 55 | end |
42 | 56 |
plugins/anti_spam/lib/anti_spam_plugin/suggest_article_wrapper.rb
0 → 100644
@@ -0,0 +1,12 @@ | @@ -0,0 +1,12 @@ | ||
1 | +class AntiSpamPlugin::SuggestArticleWrapper < Struct.new(:suggest_article) | ||
2 | + | ||
3 | + delegate :name, :email, :article_body, :ip_address, :user_agent, :referrer, :to => :suggest_article | ||
4 | + | ||
5 | + include Rakismet::Model | ||
6 | + | ||
7 | + alias :author :name | ||
8 | + alias :author_email :email | ||
9 | + alias :user_ip :ip_address | ||
10 | + alias :content :article_body | ||
11 | + | ||
12 | +end |
plugins/anti_spam/test/unit/anti_spam_plugin/suggest_article_wrapper_test.rb
0 → 100644
@@ -0,0 +1,45 @@ | @@ -0,0 +1,45 @@ | ||
1 | +require 'test_helper' | ||
2 | + | ||
3 | +class AntiSpamPluginCommentWrapperTest < ActiveSupport::TestCase | ||
4 | + | ||
5 | + def setup | ||
6 | + @suggest_article = SuggestArticle.new( | ||
7 | + :article_body => 'comment body', | ||
8 | + :name => 'author', | ||
9 | + :email => 'foo@example.com', | ||
10 | + :ip_address => '1.2.3.4', | ||
11 | + :user_agent => 'Some Good Browser (I hope)', | ||
12 | + :referrer => 'http://noosfero.org/' | ||
13 | + ) | ||
14 | + @wrapper = AntiSpamPlugin::SuggestArticleWrapper.new(@suggest_article) | ||
15 | + end | ||
16 | + | ||
17 | + should 'use Rakismet::Model' do | ||
18 | + assert_includes @wrapper.class.included_modules, Rakismet::Model | ||
19 | + end | ||
20 | + | ||
21 | + should 'get contents' do | ||
22 | + assert_equal @suggest_article.article_body, @wrapper.content | ||
23 | + end | ||
24 | + | ||
25 | + should 'get author name' do | ||
26 | + assert_equal @suggest_article.name, @wrapper.author | ||
27 | + end | ||
28 | + | ||
29 | + should 'get author email' do | ||
30 | + assert_equal @suggest_article.email, @wrapper.author_email | ||
31 | + end | ||
32 | + | ||
33 | + should 'get IP address' do | ||
34 | + assert_equal @suggest_article.ip_address, @wrapper.user_ip | ||
35 | + end | ||
36 | + | ||
37 | + should 'get User-Agent' do | ||
38 | + assert_equal @suggest_article.user_agent, @wrapper.user_agent | ||
39 | + end | ||
40 | + | ||
41 | + should 'get get Referrer' do | ||
42 | + assert_equal @suggest_article.referrer, @wrapper.referrer | ||
43 | + end | ||
44 | + | ||
45 | +end |
plugins/anti_spam/test/unit/anti_spam_plugin_test.rb
@@ -7,6 +7,11 @@ class AntiSpamPluginTest < ActiveSupport::TestCase | @@ -7,6 +7,11 @@ class AntiSpamPluginTest < ActiveSupport::TestCase | ||
7 | article = fast_create(TextileArticle, :profile_id => profile.id) | 7 | article = fast_create(TextileArticle, :profile_id => profile.id) |
8 | @comment = fast_create(Comment, :source_id => article.id, :source_type => 'Article') | 8 | @comment = fast_create(Comment, :source_id => article.id, :source_type => 'Article') |
9 | 9 | ||
10 | + | ||
11 | + @suggest_article = SuggestArticle.new(:target_id => profile.id, :target_type => 'Profile', :article_name => 'article', :article_body => 'lorem ipsum', :email => 'invalid@example.com', :name => 'article') | ||
12 | + | ||
13 | + @suggest_article.save! | ||
14 | + | ||
10 | @settings = Noosfero::Plugin::Settings.new(@comment.environment, AntiSpamPlugin) | 15 | @settings = Noosfero::Plugin::Settings.new(@comment.environment, AntiSpamPlugin) |
11 | @settings.api_key = 'b8b80ddb8084062d0c9119c945ce3bc3' | 16 | @settings.api_key = 'b8b80ddb8084062d0c9119c945ce3bc3' |
12 | @settings.save! | 17 | @settings.save! |
@@ -23,14 +28,32 @@ class AntiSpamPluginTest < ActiveSupport::TestCase | @@ -23,14 +28,32 @@ class AntiSpamPluginTest < ActiveSupport::TestCase | ||
23 | assert @comment.spam | 28 | assert @comment.spam |
24 | end | 29 | end |
25 | 30 | ||
26 | - should 'report spam' do | 31 | + should 'report comment spam' do |
27 | AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam!) | 32 | AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam!) |
28 | @plugin.comment_marked_as_spam(@comment) | 33 | @plugin.comment_marked_as_spam(@comment) |
29 | end | 34 | end |
30 | 35 | ||
31 | - should 'report ham' do | 36 | + should 'report comment ham' do |
32 | AntiSpamPlugin::CommentWrapper.any_instance.expects(:ham!) | 37 | AntiSpamPlugin::CommentWrapper.any_instance.expects(:ham!) |
33 | @plugin.comment_marked_as_ham(@comment) | 38 | @plugin.comment_marked_as_ham(@comment) |
34 | end | 39 | end |
35 | 40 | ||
41 | + should 'check for spam and mark suggest_article as spam if server says it is spam' do | ||
42 | + AntiSpamPlugin::SuggestArticleWrapper.any_instance.expects(:spam?).returns(true) | ||
43 | + @suggest_article.expects(:save!) | ||
44 | + | ||
45 | + @plugin.check_suggest_article_for_spam(@suggest_article) | ||
46 | + assert @suggest_article.spam | ||
47 | + end | ||
48 | + | ||
49 | + should 'report suggest_article spam' do | ||
50 | + AntiSpamPlugin::SuggestArticleWrapper.any_instance.expects(:spam!) | ||
51 | + @plugin.suggest_article_marked_as_spam(@suggest_article) | ||
52 | + end | ||
53 | + | ||
54 | + should 'report suggest_article ham' do | ||
55 | + AntiSpamPlugin::SuggestArticleWrapper.any_instance.expects(:ham!) | ||
56 | + @plugin.suggest_article_marked_as_ham(@suggest_article) | ||
57 | + end | ||
58 | + | ||
36 | end | 59 | end |
@@ -0,0 +1,28 @@ | @@ -0,0 +1,28 @@ | ||
1 | +function removeTaskBox(button, url, task_box_id, msg) { | ||
2 | + var $ = jQuery; | ||
3 | + if (msg && !confirm(msg)) { | ||
4 | + return; | ||
5 | + } | ||
6 | + button = $(button); | ||
7 | + button.addClass('task-button-loading'); | ||
8 | + $.post(url, function (data) { | ||
9 | + if (data.ok) { | ||
10 | + $('#' + task_box_id).slideUp(); | ||
11 | + } else { | ||
12 | + button.removeClass('task-button-loading'); | ||
13 | + button.addClass('task-button-failure'); | ||
14 | + } | ||
15 | + }); | ||
16 | +} | ||
17 | + | ||
18 | +function toggleDetails(link, msg_hide, msg_show) { | ||
19 | + var $ = jQuery; | ||
20 | + $(link).toggleClass('icon-up icon-down'); | ||
21 | + details = $(link).closest('.task_box').find('.suggest-article-details'); | ||
22 | + if (details.css('display') == 'none') { | ||
23 | + link.innerHTML = msg_hide; | ||
24 | + } else { | ||
25 | + link.innerHTML = msg_show; | ||
26 | + } | ||
27 | + details.slideToggle(); | ||
28 | +} |
test/functional/spam_controller_test.rb
@@ -4,37 +4,55 @@ class SpamControllerTest < ActionController::TestCase | @@ -4,37 +4,55 @@ class SpamControllerTest < ActionController::TestCase | ||
4 | 4 | ||
5 | def setup | 5 | def setup |
6 | @profile = create_user.person | 6 | @profile = create_user.person |
7 | - @article = fast_create(TextileArticle, :profile_id => @profile.id) | ||
8 | - @spam = fast_create(Comment, :source_id => @article.id, :spam => true, :name => 'foo', :email => 'foo@example.com') | ||
9 | 7 | ||
8 | + @community = fast_create(Community, :name => 'testcommunity') | ||
9 | + @community.add_admin(@profile) | ||
10 | + @article = fast_create(TextileArticle, :profile_id => @community.id) | ||
11 | + @spam_comment = fast_create(Comment, :source_id => @article.id, :spam => true, :name => 'foo', :email => 'foo@example.com') | ||
12 | + | ||
13 | + @spam_suggest_article = SuggestArticle.create!(:name => 'spammer', :article_name => 'Spam article', :email => 'spammer@shady.place', :article_body => "Something you don't need", :target => @community, :spam => true) | ||
10 | login_as @profile.identifier | 14 | login_as @profile.identifier |
11 | end | 15 | end |
12 | 16 | ||
13 | - test "should only list spammy comments" do | 17 | + test "should only list spammy comments and spammy suggest articles" do |
14 | ham = fast_create(Comment, :source_id => @article.id) | 18 | ham = fast_create(Comment, :source_id => @article.id) |
15 | 19 | ||
16 | - get :index, :profile => @profile.identifier | 20 | + get :index, :profile => @community.identifier |
17 | 21 | ||
18 | - assert_equivalent [@spam], assigns(:spam) | 22 | + assert_equivalent [@spam_comment], assigns(:comment_spam) |
23 | + assert_equivalent [@spam_suggest_article], assigns(:task_spam) | ||
19 | end | 24 | end |
20 | 25 | ||
21 | test "should mark comments as ham" do | 26 | test "should mark comments as ham" do |
22 | - post :index, :profile => @profile.identifier, :mark_comment_as_ham => @spam.id | 27 | + post :index, :profile => @community.identifier, :mark_comment_as_ham => @spam_comment.id |
28 | + | ||
29 | + @spam_comment.reload | ||
30 | + assert @spam_comment.ham? | ||
31 | + end | ||
32 | + | ||
33 | + test "should mark suggest article as ham" do | ||
34 | + post :index, :profile => @community.identifier, :mark_task_as_ham => @spam_suggest_article.id | ||
23 | 35 | ||
24 | - @spam.reload | ||
25 | - assert @spam.ham? | 36 | + @spam_suggest_article.reload |
37 | + assert @spam_suggest_article.ham? | ||
26 | end | 38 | end |
27 | 39 | ||
28 | test "should remove comments" do | 40 | test "should remove comments" do |
29 | - post :index, :profile => @profile.identifier, :remove_comment => @spam.id | 41 | + post :index, :profile => @community.identifier, :remove_comment => @spam_comment.id |
42 | + | ||
43 | + assert !Comment.exists?(@spam_comment.id) | ||
44 | + end | ||
45 | + | ||
46 | + test "should remove suggest articles" do | ||
47 | + post :index, :profile => @community.identifier, :remove_task => @spam_suggest_article.id | ||
30 | 48 | ||
31 | - assert !Comment.exists?(@spam.id) | 49 | + assert !SuggestArticle.exists?(@spam_suggest_article.id) |
32 | end | 50 | end |
33 | 51 | ||
34 | should 'properly render spam that have replies' do | 52 | should 'properly render spam that have replies' do |
35 | - reply_spam = fast_create(Comment, :source_id => @article_id, :reply_of_id => @spam.id) | 53 | + reply_spam = fast_create(Comment, :source_id => @article_id, :reply_of_id => @spam_comment.id) |
36 | 54 | ||
37 | - get :index, :profile => @profile.identifier | 55 | + get :index, :profile => @community.identifier |
38 | assert_response :success | 56 | assert_response :success |
39 | end | 57 | end |
40 | 58 |
test/functional/tasks_controller_test.rb
@@ -38,6 +38,17 @@ class TasksControllerTest < ActionController::TestCase | @@ -38,6 +38,17 @@ class TasksControllerTest < ActionController::TestCase | ||
38 | assert_kind_of Array, assigns(:tasks) | 38 | assert_kind_of Array, assigns(:tasks) |
39 | end | 39 | end |
40 | 40 | ||
41 | + should 'list pending tasks without spam' do | ||
42 | + requestor = fast_create(Person) | ||
43 | + task_spam = Task.create!(:requestor => requestor, :target => profile, :spam => true) | ||
44 | + task_ham = Task.create!(:requestor => requestor, :target => profile, :spam => false) | ||
45 | + | ||
46 | + get :index | ||
47 | + assert_response :success | ||
48 | + assert_includes assigns(:tasks), task_ham | ||
49 | + assert_not_includes assigns(:tasks), task_spam | ||
50 | + end | ||
51 | + | ||
41 | should 'list processed tasks' do | 52 | should 'list processed tasks' do |
42 | get :processed | 53 | get :processed |
43 | 54 | ||
@@ -46,6 +57,17 @@ class TasksControllerTest < ActionController::TestCase | @@ -46,6 +57,17 @@ class TasksControllerTest < ActionController::TestCase | ||
46 | assert_kind_of Array, assigns(:tasks) | 57 | assert_kind_of Array, assigns(:tasks) |
47 | end | 58 | end |
48 | 59 | ||
60 | + should 'list processed tasks without spam' do | ||
61 | + requestor = fast_create(Person) | ||
62 | + task_spam = Task.create!(:status => Task::Status::FINISHED, :requestor => requestor, :target => profile, :spam => true) | ||
63 | + task_ham = Task.create!(:status => Task::Status::FINISHED, :requestor => requestor, :target => profile, :spam => false) | ||
64 | + | ||
65 | + get :processed | ||
66 | + assert_response :success | ||
67 | + assert_includes assigns(:tasks), task_ham | ||
68 | + assert_not_includes assigns(:tasks), task_spam | ||
69 | + end | ||
70 | + | ||
49 | should 'be able to finish a task' do | 71 | should 'be able to finish a task' do |
50 | t = profile.tasks.build; t.save! | 72 | t = profile.tasks.build; t.save! |
51 | 73 | ||
@@ -140,6 +162,15 @@ class TasksControllerTest < ActionController::TestCase | @@ -140,6 +162,15 @@ class TasksControllerTest < ActionController::TestCase | ||
140 | assert_includes assigns(:tasks), task | 162 | assert_includes assigns(:tasks), task |
141 | end | 163 | end |
142 | 164 | ||
165 | + should 'list tasks that this profile created without spam' do | ||
166 | + task_spam = Ticket.create!(:name => 'test', :requestor => profile, :spam => true) | ||
167 | + task_ham = Ticket.create!(:name => 'test', :requestor => profile, :spam => false) | ||
168 | + get :list_requested, :profile => profile.identifier | ||
169 | + | ||
170 | + assert_includes assigns(:tasks), task_ham | ||
171 | + assert_not_includes assigns(:tasks), task_spam | ||
172 | + end | ||
173 | + | ||
143 | should 'set target of ticket when creating it' do | 174 | should 'set target of ticket when creating it' do |
144 | f = create_user('friend').person | 175 | f = create_user('friend').person |
145 | profile.add_friend f | 176 | profile.add_friend f |
test/unit/suggest_article_test.rb
@@ -150,5 +150,79 @@ class SuggestArticleTest < ActiveSupport::TestCase | @@ -150,5 +150,79 @@ class SuggestArticleTest < ActiveSupport::TestCase | ||
150 | assert_match(/#{task.name}.*suggested the publication of the article: #{task.subject}/, email.subject) | 150 | assert_match(/#{task.name}.*suggested the publication of the article: #{task.subject}/, email.subject) |
151 | end | 151 | end |
152 | 152 | ||
153 | + class EverythingIsSpam < Noosfero::Plugin | ||
154 | + def check_suggest_article_for_spam(suggest_article) | ||
155 | + suggest_article.spam! | ||
156 | + end | ||
157 | + end | ||
158 | + | ||
159 | + should 'delegate spam detection to plugins' do | ||
160 | + Environment.default.enable_plugin(EverythingIsSpam) | ||
161 | + | ||
162 | + t1 = build(SuggestArticle, :target => @profile, :article_name => 'suggested article', :name => 'johndoe', :email => 'johndoe@example.com') | ||
163 | + | ||
164 | + EverythingIsSpam.any_instance.expects(:check_suggest_article_for_spam) | ||
165 | + | ||
166 | + t1.check_for_spam | ||
167 | + end | ||
168 | + | ||
169 | + class SpamNotification < Noosfero::Plugin | ||
170 | + class << self | ||
171 | + attr_accessor :marked_as_spam | ||
172 | + attr_accessor :marked_as_ham | ||
173 | + end | ||
174 | + | ||
175 | + def check_suggest_article_for_spam(c) | ||
176 | + # do nothing | ||
177 | + end | ||
178 | + | ||
179 | + def suggest_article_marked_as_spam(c) | ||
180 | + self.class.marked_as_spam = c | ||
181 | + end | ||
182 | + | ||
183 | + def suggest_article_marked_as_ham(c) | ||
184 | + self.class.marked_as_ham = c | ||
185 | + end | ||
186 | + end | ||
187 | + | ||
188 | + should 'notify plugins of suggest_articles being marked as spam' do | ||
189 | + Environment.default.enable_plugin(SpamNotification) | ||
190 | + | ||
191 | + t = SuggestArticle.create!(:target => @profile, :article_name => 'suggested article', :name => 'johndoe', :article_body => 'wanna feel my body? my body baaaby', :email => 'johndoe@example.com') | ||
192 | + | ||
193 | + t.spam! | ||
194 | + process_delayed_job_queue | ||
195 | + | ||
196 | + assert_equal t, SpamNotification.marked_as_spam | ||
197 | + end | ||
198 | + | ||
199 | + should 'notify plugins of suggest_articles being marked as ham' do | ||
200 | + Environment.default.enable_plugin(SpamNotification) | ||
201 | + | ||
202 | + t = SuggestArticle.create!(:target => @profile, :article_name => 'suggested article', :name => 'johndoe', :article_body => 'wanna feel my body? my body baaaby', :email => 'johndoe@example.com') | ||
203 | + | ||
204 | + t.ham! | ||
205 | + process_delayed_job_queue | ||
206 | + | ||
207 | + assert_equal t, SpamNotification.marked_as_ham | ||
208 | + end | ||
209 | + | ||
210 | + should 'store User-Agent' do | ||
211 | + t = SuggestArticle.new(:user_agent => 'foo') | ||
212 | + assert_equal 'foo', t.user_agent | ||
213 | + end | ||
214 | + | ||
215 | + should 'store referrer' do | ||
216 | + t = SuggestArticle.new(:referrer => 'bar') | ||
217 | + assert_equal 'bar', t.referrer | ||
218 | + end | ||
219 | + | ||
220 | + should 'log spammer ip after marking comment as spam' do | ||
221 | + t = SuggestArticle.create!(:target => @profile, :article_name => 'suggested article', :name => 'johndoe', :article_body => 'wanna feel my body? my body baaaby', :email => 'johndoe@example.com', :ip_address => '192.168.0.1') | ||
222 | + t.spam! | ||
223 | + log = File.open('log/test_spammers.log') | ||
224 | + assert_match "SuggestArticle-id: #{t.id} IP: 192.168.0.1", log.read | ||
225 | + SpammerLogger.clean_log | ||
226 | + end | ||
153 | 227 | ||
154 | end | 228 | end |
test/unit/task_test.rb
@@ -372,6 +372,55 @@ class TaskTest < ActiveSupport::TestCase | @@ -372,6 +372,55 @@ class TaskTest < ActiveSupport::TestCase | ||
372 | assert_includes Task.closed, canceled | 372 | assert_includes Task.closed, canceled |
373 | end | 373 | end |
374 | 374 | ||
375 | + should 'be ham by default' do # ham means not spam | ||
376 | + assert_equal false, Task.create.spam | ||
377 | + end | ||
378 | + | ||
379 | + should 'be able to mark tasks as spam/ham/unknown' do | ||
380 | + t = Task.new | ||
381 | + t.spam = true | ||
382 | + assert t.spam? | ||
383 | + assert !t.ham? | ||
384 | + | ||
385 | + t.spam = false | ||
386 | + assert t.ham? | ||
387 | + assert !t.spam? | ||
388 | + | ||
389 | + t.spam = nil | ||
390 | + assert !t.spam? | ||
391 | + assert !t.ham? | ||
392 | + end | ||
393 | + | ||
394 | + should 'be able to select non-spam tasks' do | ||
395 | + t1 = fast_create(Task) | ||
396 | + t2 = fast_create(Task, :spam => false) | ||
397 | + t3 = fast_create(Task, :spam => true) | ||
398 | + | ||
399 | + assert_equivalent [t1,t2], Task.without_spam | ||
400 | + end | ||
401 | + | ||
402 | + should 'be able to select spam tasks' do | ||
403 | + t1 = fast_create(Task) | ||
404 | + t2 = fast_create(Task, :spam => false) | ||
405 | + t3 = fast_create(Task, :spam => true) | ||
406 | + | ||
407 | + assert_equivalent [t3], Task.spam | ||
408 | + end | ||
409 | + | ||
410 | + should 'be able to mark as spam' do | ||
411 | + t1 = fast_create(Task) | ||
412 | + t1.spam! | ||
413 | + t1.reload | ||
414 | + assert t1.spam? | ||
415 | + end | ||
416 | + | ||
417 | + should 'be able to mark as ham' do | ||
418 | + t1 = fast_create(Task) | ||
419 | + t1.ham! | ||
420 | + t1.reload | ||
421 | + assert t1.ham? | ||
422 | + end | ||
423 | + | ||
375 | protected | 424 | protected |
376 | 425 | ||
377 | def sample_user | 426 | def sample_user |