From: Andy Allan Date: Wed, 28 Feb 2018 07:55:28 +0000 (+0800) Subject: Merge branch 'master' into moderation X-Git-Tag: live~2989^2~44 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/a20e28809f5ca1e3f3579faeaf118fff3ecc4994?hp=4e138f6bf3677f220e2d2d76ec1bd9004c272ab4 Merge branch 'master' into moderation --- diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2c8d29eb8..5846de44f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -67,7 +67,7 @@ Metrics/AbcSize: # Offense count: 41 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 240 + Max: 250 # Offense count: 12 # Configuration parameters: CountBlocks. diff --git a/Gemfile b/Gemfile index 09c662e4a..a9a6cb4b6 100644 --- a/Gemfile +++ b/Gemfile @@ -70,6 +70,9 @@ gem "omniauth-windowslive" # Markdown formatting support gem "redcarpet" +# For status transitions of Issues +gem "aasm" + # Load libxml support for XML parsing and generation gem "libxml-ruby", ">= 2.0.5", :require => "libxml" diff --git a/Gemfile.lock b/Gemfile.lock index bbfb73c5a..02bb3e5fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ GEM remote: https://rubygems.org/ specs: SystemTimer (1.2.3) + aasm (4.1.0) actioncable (5.1.5) actionpack (= 5.1.5) nio4r (~> 2.0) @@ -354,6 +355,7 @@ PLATFORMS DEPENDENCIES SystemTimer (>= 1.1.3) + aasm actionpack-page_caching annotate autoprefixer-rails diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 4353e5e65..665ea8c24 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -2818,3 +2818,44 @@ input.richtext_title[type="text"] { display: none; } } + +.read-reports { + background: #eee; + opacity: 0.7; +} + +.report-related-block { + display:inline-block; +} + +.report-block { + width:475px; + float:left; + margin-right:100px; +} + +.related-block{ + width:280px; + float:right; +} + +.issue-comments { + width:475px; +} + +.report-disclaimer { + background: #fff1f0; + color: #d85030; + border-color: rgba(216, 80, 48, 0.3); + padding: 5px; + margin-bottom: $lineheight; + + ul { + padding-left: $lineheight; + margin-bottom: 0; + + li { + list-style: disc; + } + } +} diff --git a/app/controllers/issue_comments_controller.rb b/app/controllers/issue_comments_controller.rb new file mode 100644 index 000000000..448b2a5d6 --- /dev/null +++ b/app/controllers/issue_comments_controller.rb @@ -0,0 +1,37 @@ +class IssueCommentsController < ApplicationController + layout "site" + + before_action :authorize_web + before_action :require_user + before_action :check_permission + + def create + @issue = Issue.find(params[:issue_id]) + comment = @issue.comments.build(issue_comment_params) + comment.user = current_user + comment.save! + notice = t("issues.comment.comment_created") + reassign_issue(@issue) if params[:reassign] + redirect_to @issue, :notice => notice + end + + private + + def issue_comment_params + params.require(:issue_comment).permit(:body) + end + + def check_permission + unless current_user.administrator? || current_user.moderator? + flash[:error] = t("application.require_admin.not_an_admin") + redirect_to root_path + end + end + + # This sort of assumes there are only two roles + def reassign_issue(issue) + role = (Issue::ASSIGNED_ROLES - [issue.assigned_role]).first + issue.assigned_role = role + issue.save! + end +end diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb new file mode 100644 index 000000000..90fd0306d --- /dev/null +++ b/app/controllers/issues_controller.rb @@ -0,0 +1,132 @@ +class IssuesController < ApplicationController + layout "site" + + before_action :authorize_web + before_action :require_user + before_action :set_issues + before_action :check_permission + before_action :find_issue, :only => [:show, :resolve, :reopen, :ignore] + + def index + @title = t ".title" + + if current_user.moderator? + @issue_types = @moderator_issues + @users = User.joins(:roles).where(:user_roles => { :role => "moderator" }) + else + @issue_types = @admin_issues + @users = User.joins(:roles).where(:user_roles => { :role => "administrator" }) + end + + @issues = Issue.where(:assigned_role => current_user.roles.map(&:role)) + + # If search + if params[:search_by_user] && params[:search_by_user].present? + @find_user = User.find_by(:display_name => params[:search_by_user]) + if @find_user + @issues = @issues.where(:reported_user_id => @find_user.id) + else + notice = t("issues.index.user_not_found") + end + end + + if params[:status] && params[:status][0].present? + @issues = @issues.where(:status => params[:status][0]) + end + + if params[:issue_type] && params[:issue_type][0].present? + @issues = @issues.where(:reportable_type => params[:issue_type][0]) + end + + if params[:last_updated_by] && params[:last_updated_by][0].present? + last_updated_by = params[:last_updated_by][0].to_s == "nil" ? nil : params[:last_updated_by][0].to_i + @issues = @issues.where(:updated_by => last_updated_by) + end + + redirect_to issues_path, :notice => notice if notice + end + + def show + @read_reports = @issue.read_reports + @unread_reports = @issue.unread_reports + @comments = @issue.comments + @related_issues = @issue.reported_user.issues.where(:assigned_role => current_user.roles.map(&:role)) if @issue.reported_user + @new_comment = IssueComment.new(:issue => @issue) + end + + # Status Transistions + def resolve + if @issue.resolve + @issue.save! + redirect_to @issue, :notice => t("issues.resolved") + else + render :show + end + end + + def ignore + if @issue.ignore + @issue.updated_by = current_user.id + @issue.save! + redirect_to @issue, :notice => t("issues.ignored") + else + render :show + end + end + + def reopen + if @issue.reopen + @issue.updated_by = current_user.id + @issue.save! + redirect_to @issue, :notice => t("issues.reopened") + else + render :show + end + end + + private + + def set_issues + @admin_issues = %w[DiaryEntry DiaryComment User] + @moderator_issues = %w[Note] + end + + def check_if_updated + if @issue.reportable && (@issue.ignored? || @issue.resolved?) && @issue.reportable.has_attribute?(:updated_by) && @issue.reportable.updated_at > @last_report.updated_at + true + else + false + end + end + + def find_issue + @issue = Issue.find(params[:id]) + end + + def check_permission + unless current_user.administrator? || current_user.moderator? + flash[:error] = t("application.require_admin.not_an_admin") + redirect_to root_path + end + end + + def issue_params + params[:issue].permit(:reportable_id, :reportable_type) + end + + def report_params + params[:report].permit(:details) + end + + def issue_comment_params + params.require(:issue_comment).permit(:body) + end + + def sort_column + Issue.column_names.include?(params[:sort]) ? params[:sort] : "status" + end + + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : "asc" + end +end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb new file mode 100644 index 000000000..5d0442731 --- /dev/null +++ b/app/controllers/reports_controller.rb @@ -0,0 +1,36 @@ +class ReportsController < ApplicationController + layout "site" + + before_action :authorize_web + before_action :require_user + + def new + if create_new_report_params.present? + @report = Report.new + @report.issue = Issue.find_or_initialize_by(create_new_report_params) + end + end + + def create + @report = current_user.reports.new(report_params) + @report.issue = Issue.find_or_initialize_by(:reportable_id => params[:report][:issue][:reportable_id], :reportable_type => params[:report][:issue][:reportable_type]) + + if @report.save + @report.issue.save + @report.issue.reopen! unless @report.issue.open? + redirect_to root_path, :notice => t("issues.create.successful_report") + else + redirect_to new_report_path(:reportable_type => @report.issue.reportable_type, :reportable_id => @report.issue.reportable_id), :notice => t("issues.create.provide_details") + end + end + + private + + def create_new_report_params + params.permit(:reportable_id, :reportable_type) + end + + def report_params + params[:report].permit(:details, :category) + end +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb new file mode 100644 index 000000000..3c13c6536 --- /dev/null +++ b/app/helpers/issues_helper.rb @@ -0,0 +1,27 @@ +module IssuesHelper + def reportable_url(reportable) + case reportable + when DiaryEntry + url_for(:controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.user.display_name, :id => reportable.id) + when User + url_for(:controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.display_name) + when DiaryComment + url_for(:controller => reportable.diary_entry.class.name.underscore, :action => :view, :display_name => reportable.diary_entry.user.display_name, :id => reportable.diary_entry.id, :anchor => "comment#{reportable.id}") + when Note + url_for(:controller => :browse, :action => :note, :id => reportable.id) + end + end + + def reportable_title(reportable) + case reportable + when DiaryEntry + reportable.title + when User + reportable.display_name + when DiaryComment + "#{reportable.diary_entry.title}, Comment id ##{reportable.id}" + when Note + "Note ##{reportable.id}" + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb new file mode 100644 index 000000000..d13f297a9 --- /dev/null +++ b/app/models/issue.rb @@ -0,0 +1,99 @@ +# == Schema Information +# +# Table name: issues +# +# id :integer not null, primary key +# reportable_type :string not null +# reportable_id :integer not null +# reported_user_id :integer +# status :integer +# assigned_role :enum not null +# resolved_at :datetime +# resolved_by :integer +# updated_by :integer +# reports_count :integer default(0) +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_issues_on_reportable_id_and_reportable_type (reportable_id,reportable_type) +# index_issues_on_reported_user_id (reported_user_id) +# index_issues_on_updated_by (updated_by) +# +# Foreign Keys +# +# issues_reported_user_id_fkey (reported_user_id => users.id) ON DELETE => cascade +# issues_updated_by_fkey (updated_by => users.id) ON DELETE => cascade +# + +class Issue < ActiveRecord::Base + belongs_to :reportable, :polymorphic => true + belongs_to :reported_user, :class_name => "User", :foreign_key => :reported_user_id + belongs_to :user_updated, :class_name => "User", :foreign_key => :updated_by + + has_many :reports, :dependent => :destroy + has_many :comments, :class_name => "IssueComment", :dependent => :destroy + + validates :reportable_id, :uniqueness => { :scope => [:reportable_type] } + + ASSIGNED_ROLES = %w[administrator moderator].freeze + validates :assigned_role, :presence => true, :inclusion => ASSIGNED_ROLES + + before_validation :set_default_assigned_role + before_validation :set_reported_user + + # Check if more statuses are needed + enum :status => %w[open ignored resolved] + + scope :with_status, ->(issue_status) { where(:status => statuses[issue_status]) } + + def read_reports + resolved_at.present? ? reports.where("updated_at < ?", resolved_at) : nil + end + + def unread_reports + resolved_at.present? ? reports.where("updated_at >= ?", resolved_at) : reports + end + + include AASM + aasm :column => :status, :no_direct_assignment => true do + state :open, :initial => true + state :ignored + state :resolved + + event :ignore do + transitions :from => :open, :to => :ignored + end + + event :resolve do + transitions :from => :open, :to => :resolved + after do + self.resolved_at = Time.now.getutc + end + end + + event :reopen do + transitions :from => :resolved, :to => :open + transitions :from => :ignored, :to => :open + end + end + + private + + def set_reported_user + self.reported_user = case reportable.class.name + when "User" + reportable + when "Note" + reportable.author + else + reportable.user + end + end + + def set_default_assigned_role + role = %w[Note].include?(reportable.class.name) ? "moderator" : "administrator" + self.assigned_role = role if assigned_role.blank? + end +end diff --git a/app/models/issue_comment.rb b/app/models/issue_comment.rb new file mode 100644 index 000000000..2968078b3 --- /dev/null +++ b/app/models/issue_comment.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: issue_comments +# +# id :integer not null, primary key +# issue_id :integer not null +# user_id :integer not null +# body :text not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_issue_comments_on_issue_id (issue_id) +# index_issue_comments_on_user_id (user_id) +# +# Foreign Keys +# +# issue_comments_issue_id_fkey (issue_id => issues.id) ON DELETE => cascade +# issue_comments_user_id (user_id => users.id) ON DELETE => cascade +# + +class IssueComment < ActiveRecord::Base + belongs_to :issue + belongs_to :user + + validates :body, :presence => true + validates :user, :presence => true + validates :issue, :presence => true +end diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 000000000..fcfa61b98 --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,40 @@ +# == Schema Information +# +# Table name: reports +# +# id :integer not null, primary key +# issue_id :integer +# user_id :integer +# details :text not null +# category :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_reports_on_issue_id (issue_id) +# index_reports_on_user_id (user_id) +# +# Foreign Keys +# +# reports_issue_id_fkey (issue_id => issues.id) ON DELETE => cascade +# reports_user_id_fkey (user_id => users.id) ON DELETE => cascade +# + +class Report < ActiveRecord::Base + belongs_to :issue, :counter_cache => true + belongs_to :user + + validates :details, :presence => true + validates :category, :presence => true + + def self.categories_for(reportable) + case reportable.class.name + when "DiaryEntry" then %w[spam offensive threat other] + when "DiaryComment" then %w[spam offensive threat other] + when "User" then %w[spam offensive threat vandal other] + when "Note" then %w[spam vandalism personal abusive other] + else %w[other] + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 1cd8c6d89..2d51b5af5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,6 +73,12 @@ class User < ActiveRecord::Base has_many :roles, :class_name => "UserRole" + has_many :issues, :class_name => "Issue", :foreign_key => :reported_user_id + has_one :issue, :class_name => "Issue", :foreign_key => :updated_by + has_many :issue_comments + + has_many :reports + scope :visible, -> { where(:status => %w[pending active confirmed]) } scope :active, -> { where(:status => %w[active confirmed]) } scope :identifiable, -> { where(:data_public => true) } diff --git a/app/views/browse/note.html.erb b/app/views/browse/note.html.erb index 1bacd27d6..075ac6462 100644 --- a/app/views/browse/note.html.erb +++ b/app/views/browse/note.html.erb @@ -17,6 +17,11 @@
<%= note_event(@note.status, @note.closed_at, @note_comments.last.author) %> <% end %> + <% if current_user && current_user != @note.author %> + <%= link_to new_report_url(reportable_id: @note.id, reportable_type: @note.class.name), :title => t('browse.note.report') do %> +  ⚐ + <% end %> + <% end %> <% if @note_comments.find { |comment| comment.author.nil? } -%> diff --git a/app/views/diary_entry/_diary_comment.html.erb b/app/views/diary_entry/_diary_comment.html.erb index c651c2943..ae1386fb2 100644 --- a/app/views/diary_entry/_diary_comment.html.erb +++ b/app/views/diary_entry/_diary_comment.html.erb @@ -1,6 +1,13 @@
<%= user_thumbnail diary_comment.user %> -

<%= raw(t('diary_entry.diary_comment.comment_from', :link_user => (link_to h(diary_comment.user.display_name), :controller => 'user', :action => 'view', :display_name => diary_comment.user.display_name), :comment_created_at => link_to(l(diary_comment.created_at, :format => :friendly), :anchor => "comment#{diary_comment.id}"))) %>

+

<%= raw(t('diary_entry.diary_comment.comment_from', :link_user => (link_to h(diary_comment.user.display_name), :controller => 'user', :action => 'view', :display_name => diary_comment.user.display_name), :comment_created_at => link_to(l(diary_comment.created_at, :format => :friendly), :anchor => "comment#{diary_comment.id}"))) %> + <% if current_user and diary_comment.user.id != current_user.id %> + <%= link_to new_report_url(reportable_id: diary_comment.id, reportable_type: diary_comment.class.name), :title => t('diary_entry.diary_comment.report') do %> +  ⚐ + <% end %> + <% end %> +

+
<%= diary_comment.body.to_html %>
<%= if_administrator(:span) do %> <%= link_to t('diary_entry.diary_comment.hide_link'), hide_diary_comment_path(:display_name => diary_comment.diary_entry.user.display_name, :id => diary_comment.diary_entry.id, :comment => diary_comment.id), :method => :post, :data=> { :confirm => t('diary_entry.diary_comment.confirm') } %> diff --git a/app/views/diary_entry/_diary_entry.html.erb b/app/views/diary_entry/_diary_entry.html.erb index 410e13047..872e31b3a 100644 --- a/app/views/diary_entry/_diary_entry.html.erb +++ b/app/views/diary_entry/_diary_entry.html.erb @@ -6,6 +6,12 @@

<%= link_to h(diary_entry.title), :action => 'view', :display_name => diary_entry.user.display_name, :id => diary_entry.id %>

+ <% if current_user and diary_entry.user != current_user %> + <%= link_to new_report_url(reportable_id: diary_entry.id, reportable_type: diary_entry.class.name), :title => t('diary_entry.diary_entry.report') do %> +  ⚐ + <% end %> + <% end %> + <%= raw(t 'diary_entry.diary_entry.posted_by', :link_user => (link_to h(diary_entry.user.display_name), :controller => 'user', :action => 'view', :display_name => diary_entry.user.display_name), :created => l(diary_entry.created_at, :format => :blog), :language_link => (link_to h(diary_entry.language.name), :controller => 'diary_entry', :action => 'list', :display_name => nil, :language => diary_entry.language_code)) %> diff --git a/app/views/issues/_comments.html.erb b/app/views/issues/_comments.html.erb new file mode 100644 index 000000000..ec885aa8c --- /dev/null +++ b/app/views/issues/_comments.html.erb @@ -0,0 +1,25 @@ +
+ <% comments.each do |comment| %> +
+
+ <%= link_to user_thumbnail(comment.user), :controller => :user, :action =>:view, :display_name => comment.user.display_name %> +
+ <%= link_to comment.user.display_name, :controller => :user, :action =>:view, :display_name => comment.user.display_name %>
+ <%= comment.body %> +
+ + <%= t(".created_at", :datetime => l(comment.created_at.to_datetime, :format => :friendly)) %> + +
+ <% end %> +
+
+
+ <%= form_for @new_comment, url: issue_comments_path(@issue) do |f| %> + <%= richtext_area :issue_comment, :body, :cols => 10, :rows => 8, :required => true %> + <%= label_tag :reassign, t('issues.show.comments.reassign_param') %> <%= check_box_tag :reassign, true %> +
+
+ <%= submit_tag 'Submit' %> + <% end %> +
diff --git a/app/views/issues/_reports.html.erb b/app/views/issues/_reports.html.erb new file mode 100644 index 000000000..5303ae0c5 --- /dev/null +++ b/app/views/issues/_reports.html.erb @@ -0,0 +1,19 @@ +<% reports.each do |report| %> +
+
+ <%= link_to user_thumbnail(report.user), :controller => :user, :action =>:view, :display_name => report.user.display_name %> +
+ <%= t(".reported_by_html", :user_name => report.user.display_name, :user_url => url_for(:controller => :user, :action => :view, :display_name => report.user.display_name)) %>
+ + <%= t(".updated_at", :datetime => l(report.updated_at.to_datetime, :format => :friendly)) %> + +
+ + <%= t ".category", category: report.category %> + +
+ <%= report.details %> +
+
+
+<% end %> diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb new file mode 100644 index 000000000..2980f5e01 --- /dev/null +++ b/app/views/issues/index.html.erb @@ -0,0 +1,44 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +<%= form_tag(issues_path, :method => :get) do %> +

<%= t ".search_guidance" %>

+<%= select :status, nil, Issue.aasm.states.map(&:name).map{|state| [t("issues.states.#{state}"), state]}, { :include_blank => t(".select_status")}, data: { behavior: 'category_dropdown' } %> +<%= select :issue_type, nil, @issue_types, { :include_blank => t(".select_type")}, data: { behavior: 'category_dropdown' } %> +<%= text_field_tag :search_by_user, params[:search_by_user], placeholder: t(".reported_user") %> +<%= select :last_updated_by, nil, @users.all.collect {|f| [f.display_name, f.id]} << [ t(".not_updated"), "nil"], { :include_blank => t(".select_last_updated_by")}, data: { behavior: 'category_dropdown' } %> +<%= submit_tag t(".search") %> +<% end %> +
+ +<% if @issues.length == 0 %> +

<%= t ".issues_not_found" %>

+<% end %> + +
+ + + + + + + + + + + + + + <% @issues.each do |issue| %> + + + + + + + + + <% end %> + +
<%= t ".status" %><%= t ".reports" %><%= t ".reported_item" %><%= t ".reported_user" %><%= t ".last_updated_by" %><%= t ".last_updated_at" %>
<%= t "issues.states.#{issue.status}" %><%= link_to t(".reports_count", :count => issue.reports_count), issue %><%= link_to reportable_title(issue.reportable), reportable_url(issue.reportable) %><%= link_to issue.reported_user.display_name, :controller => :user, :action => :view, :display_name => issue.reported_user.display_name if issue.reported_user %><% if issue.user_updated %> <%= issue.user_updated.display_name %> <% else %> - <% end %><%= l(issue.updated_at.to_datetime, :format => :friendly) %>
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb new file mode 100644 index 000000000..732fd2ef2 --- /dev/null +++ b/app/views/issues/show.html.erb @@ -0,0 +1,64 @@ +<% content_for :heading do %> +

<%= t ".title", :status => @issue.status.humanize, :issue_id => @issue.id %>

+

<%= @issue.reportable.model_name.human %> : <%= link_to reportable_title(@issue.reportable), reportable_url(@issue.reportable) %>

+

+ + <%= @issue.assigned_role %> + | <%= t ".reports", :count => @issue.reports.count %> + | <%= t ".report_created_at", :datetime => l(@issue.created_at.to_datetime, :format => :friendly) %> + <%= " | " + t(".last_resolved_at", :datetime => l(@issue.resolved_at.to_datetime, :format =>:friendly)) if @issue.resolved_at? %> + <%= " | " + t(".last_updated_at", :datetime => l(@issue.updated_at.to_datetime, :format => :friendly), :displayname => @issue.user_updated.display_name ) if @issue.user_updated %> + +

+

+ <%= link_to t('issues.resolve'), resolve_issue_url(@issue), :method => :post if @issue.may_resolve? %> + <% if @issue.may_ignore? %> + | <%= link_to t('issues.ignore'), ignore_issue_url(@issue), :method => :post %> + <% end %> +

+

<%= link_to t('issues.reopen'), reopen_issue_url(@issue), :method => :post if @issue.may_reopen? %>

+<% end %> + + + +

<%= t ".comments_on_this_issue" %>

+
+ <%= render 'comments', comments: @comments %> +
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index a5ab460ce..d8e5443b1 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -38,6 +38,9 @@