From: Andy Allan Date: Wed, 29 Nov 2017 12:18:39 +0000 (+0000) Subject: Merge branch 'master' into moderation X-Git-Tag: live~3986^2~91 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/effb1b7f4170bb7244c4dfffcbe6134fe00e2bc4?hp=8f1676b71ae32eca2706b582f65bcefec2285976 Merge branch 'master' into moderation --- diff --git a/Gemfile b/Gemfile index 79d95f9d5..43f39340a 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" @@ -112,6 +115,7 @@ end # Gems needed for running tests group :test do gem "minitest", "~> 5.1", :platforms => [:ruby_19, :ruby_20] + gem "minitest-rails-capybara" gem "rails-controller-testing" gem "rubocop" gem "webmock" diff --git a/Gemfile.lock b/Gemfile.lock index b65ec4ed9..d67d34d47 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.4) actionpack (= 5.1.4) nio4r (~> 2.0) @@ -162,6 +163,20 @@ GEM mini_mime (0.1.4) mini_portile2 (2.3.0) minitest (5.10.3) + minitest-capybara (0.8.2) + capybara (~> 2.2) + minitest (~> 5.0) + rake + minitest-metadata (0.6.0) + minitest (>= 4.7, < 6.0) + minitest-rails (3.0.0) + minitest (~> 5.8) + railties (~> 5.0) + minitest-rails-capybara (3.0.1) + capybara (~> 2.7) + minitest-capybara (~> 0.8) + minitest-metadata (~> 0.6) + minitest-rails (~> 3.0) multi_json (1.12.2) multi_xml (0.6.0) multipart-post (2.0.0) @@ -355,6 +370,7 @@ PLATFORMS DEPENDENCIES SystemTimer (>= 1.1.3) + aasm actionpack-page_caching annotate autoprefixer-rails @@ -382,6 +398,7 @@ DEPENDENCIES listen logstasher minitest (~> 5.1) + minitest-rails-capybara oauth-plugin (>= 0.5.1) omniauth omniauth-facebook diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 6b99662a4..4e89c04c9 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -2814,3 +2814,48 @@ 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; +} + +.new-report-checkbox{ + float:left; + margin-left:10px; + margin-top:3px; +} + +.new-report-string { + font-size:15px; +} + +.report-button { + float:right; +} + +.disclaimer { + width: 600px; + background: #fff1f0; + color: #d85030; + border-color: rgba(216, 80, 48, 0.3); +} diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index 9e0fd4991..88febbe2f 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -186,6 +186,9 @@ class DiaryEntryController < ApplicationController @entry = @this_user.diary_entries.visible.where(:id => params[:id]).first if @entry @title = t "diary_entry.view.title", :user => params[:display_name], :title => @entry.title + if params[:comment_id] + @reported_comment = DiaryComment.where(:id => params[:comment_id]) + end else @title = t "diary_entry.no_such_entry.title", :id => params[:id] render :action => "no_such_entry", :status => :not_found diff --git a/app/controllers/issue_comments_controller.rb b/app/controllers/issue_comments_controller.rb new file mode 100644 index 000000000..ba35b7978 --- /dev/null +++ b/app/controllers/issue_comments_controller.rb @@ -0,0 +1,33 @@ +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 + # if params[:reassign] + # reassign_issue + # @issue_comment.reassign = true + # end + comment.save! + notice = t("issues.comment.comment_created") + 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 +end diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb new file mode 100644 index 000000000..e156ea004 --- /dev/null +++ b/app/controllers/issues_controller.rb @@ -0,0 +1,200 @@ +class IssuesController < ApplicationController + layout "site" + + before_action :authorize_web + before_action :require_user + before_action :set_issues + before_action :check_permission, :only => [:index, :show, :resolve, :open, :ignore, :comment] + before_action :find_issue, :only => [:show, :resolve, :reopen, :ignore] + before_action :setup_user_role, :only => [:show, :index] + + helper_method :sort_column, :sort_direction + + def index + 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(:issue_type => @user_role).order(sort_column + " " + sort_direction) + + # 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.search.user_not_found") + end + end + + if params[:status] && params[:status][0].present? + @issues = @issues.where(:status => params[:status][0].to_i) + end + + if params[:issue_type] && params[:issue_type][0].present? + @issues = @issues.where(:reportable_type => params[:issue_type][0]) + end + + # If last_updated_by + 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 + + if params[:last_reported_by] && params[:last_reported_by][0].present? + last_reported_by = params[:last_reported_by][0].to_s == "nil" ? nil : params[:last_reported_by][0].to_i + @issues = @issues.where(:updated_by => last_reported_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(:issue_type => @user_role) + @new_comment = IssueComment.new(:issue => @issue) + end + + def update + @issue = Issue.find_by(issue_params) + # Check if details provided are sufficient + if check_report_params + @report = @issue.reports.where(:reporter_user_id => current_user.id).first + + if @report.nil? + @report = @issue.reports.build(report_params) + @report.reporter_user_id = current_user.id + notice = t("issues.update.new_report") + end + + details = report_details + @report.details = details + + # Checking if instance has been updated since last report + @last_report = @issue.reports.order(:updated_at => :desc).last + if check_if_updated + @issue.reopen + @issue.save! + end + + notice = t("issues.update.successful_update") if notice.nil? + + if @report.save! + @issue.report_count = @issue.reports.count + @issue.save! + redirect_back :fallback_location => "/", :notice => notice + end + else + redirect_to new_issue_path(:reportable_type => @issue.reportable_type, :reportable_id => @issue.reportable_id), :notice => t("issues.update.provide_details") + end + 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 + + # Reassign Issues between Administrators and Moderators + def reassign_issue + @issue.issue_type = upgrade_issue(@issue.issue_type) + @issue.save! + end + + private + + def upgrade_issue(type) + if type == "moderator" + "administrator" + else + "moderator" + end + end + + def set_issues + @admin_issues = %w[DiaryEntry DiaryComment User] + @moderator_issues = %w[Changeset Note] + end + + def setup_user_role + # Get user role + @user_role = current_user.administrator? ? "administrator" : "moderator" + 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 report_details + params[:report][:details] + "--||--" + params[:report_type].to_s + "--||--" + end + + def check_report_params + params[:report] && params[:report][:details] && params[:report_type] + 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..6c62deb30 --- /dev/null +++ b/app/controllers/reports_controller.rb @@ -0,0 +1,39 @@ +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) + path = "issues.report_strings." + @report.issue.reportable.class.name.to_s + @report_strings_yaml = t(path) + 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 + # FIXME: reopen issue if necessary + # FIXME: new issue notification (or via model observer) + 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) + end +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb new file mode 100644 index 000000000..715696445 --- /dev/null +++ b/app/helpers/issues_helper.rb @@ -0,0 +1,74 @@ +module IssuesHelper + def reportable_url(reportable) + class_name = reportable.class.name + case class_name + when "DiaryEntry" + link_to reportable.title, :controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.user.display_name, :id => reportable.id + when "User" + link_to reportable.display_name.to_s, :controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.display_name + when "DiaryComment" + link_to "#{reportable.diary_entry.title}, Comment id ##{reportable.id}", :controller => reportable.diary_entry.class.name.underscore, :action => :view, :display_name => reportable.diary_entry.user.display_name, :id => reportable.diary_entry.id, :comment_id => reportable.id + when "Changeset" + link_to "Changeset ##{reportable.id}", :controller => :browse, :action => :changeset, :id => reportable.id + when "Note" + link_to "Note ##{reportable.id}", :controller => :browse, :action => :note, :id => reportable.id + end + end + + def reports_url(issue) + class_name = issue.reportable.class.name + case class_name + when "DiaryEntry" + link_to issue.reportable.title, issue + when "User" + link_to issue.reportable.display_name.to_s, issue + when "DiaryComment" + link_to "#{issue.reportable.diary_entry.title}, Comment id ##{issue.reportable.id}", issue + when "Changeset" + link_to "Changeset ##{issue.reportable.id}", issue + when "Note" + link_to "Note ##{issue.reportable.id}", issue + end + end + + def instance_url(reportable) + class_name = reportable.class.name + case class_name + when "DiaryEntry" + link_to "Show Instance", :controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.user.display_name, :id => reportable.id + when "User" + link_to "Show Instance", :controller => reportable.class.name.underscore, :action => :view, :display_name => reportable.display_name + when "DiaryComment" + link_to "Show Instance", :controller => reportable.diary_entry.class.name.underscore, :action => :view, :display_name => reportable.diary_entry.user.display_name, :id => reportable.diary_entry.id, :comment_id => reportable.id + when "Changeset" + link_to "Show Instance", :controller => :browse, :action => :changeset, :id => reportable.id + when "Note" + link_to "Show Instance", :controller => :browse, :action => :note, :id => reportable.id + end + end + + def sortable(column, title = nil) + title ||= column.titleize + direction = column == sort_column && sort_direction == "asc" ? "desc" : "asc" + if column == sort_column + arrow = direction == "desc" ? ["25B2".hex].pack("U") : ["25BC".hex].pack("U") + title += arrow + end + # FIXME: link_to title, params.merge(:sort => column, :direction => direction) + end + + def report_type(report_class) + case report_class + when "DiaryEntry" + t("activerecord.models.diary_entry") + when "User" + t("activerecord.models.user") + when "DiaryComment" + t("activerecord.models.diary_comment") + when "Changeset" + t("activerecord.models.changeset") + when "Note" + t("activerecord.models.note") + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb new file mode 100644 index 000000000..472c860c6 --- /dev/null +++ b/app/models/issue.rb @@ -0,0 +1,63 @@ +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] } + validates :reported_user_id, :presence => true + + before_validation :set_reported_user + + # Check if more statuses are needed + enum :status => %w[open ignored resolved] + enum :type => %w[administrator moderator] + + 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 +end diff --git a/app/models/issue_comment.rb b/app/models/issue_comment.rb new file mode 100644 index 000000000..bbc626164 --- /dev/null +++ b/app/models/issue_comment.rb @@ -0,0 +1,8 @@ +class IssueComment < ActiveRecord::Base + belongs_to :issue + belongs_to :user, :class_name => "User", :foreign_key => :commenter_user_id + + validates :body, :presence => true + validates :user, :presence => true + validates :issue, :presence => true +end diff --git a/app/models/notifier.rb b/app/models/notifier.rb index 8f9e3e295..54eb9b418 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -189,6 +189,17 @@ class Notifier < ActionMailer::Base end end + def new_issue_notification(issue_id, recipient) + with_recipient_locale recipient do + @url = url_for(:host => SERVER_URL, + :controller => "issues", + :action => "show", + :id => issue_id) + subject = I18n.t("notifier.new_issue_notification.subject") + mail :to => recipient.email, :subject => subject + end + end + private def set_shared_template_vars diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 000000000..b857d7375 --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,6 @@ +class Report < ActiveRecord::Base + belongs_to :issue, :counter_cache => true + belongs_to :user, :class_name => "User", :foreign_key => :reporter_user_id + + validates :details, :presence => true +end diff --git a/app/models/user.rb b/app/models/user.rb index 7a8414ec0..678bbad9d 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/changeset.html.erb b/app/views/browse/changeset.html.erb index 86d190680..1fb1e74a4 100644 --- a/app/views/browse/changeset.html.erb +++ b/app/views/browse/changeset.html.erb @@ -3,6 +3,11 @@

<%= t('browse.changeset.title', :id => @changeset.id) %> + <% if current_user and current_user.id != @changeset.user.id %> + <%= link_to new_issue_url(reportable_id: @changeset.id, reportable_type: @changeset.class.name, referer: request.fullpath), :title => t('browse.changeset.report') do %> +  ⚐ + <% end %> + <% end %>

diff --git a/app/views/browse/note.html.erb b/app/views/browse/note.html.erb index 1bacd27d6..a7dee697b 100644 --- a/app/views/browse/note.html.erb +++ b/app/views/browse/note.html.erb @@ -3,6 +3,11 @@

<%= t "browse.note.#{@note.status}_title", :note_name => @note.id %> + <% if current_user && @note.author && current_user.id != @note.author.id %> + <%= link_to new_issue_url(reportable_id: @note.id, reportable_type: @note.class.name, referer: request.fullpath), :title => t('browse.note.report') do %> +  ⚐ + <% end %> + <% end %>

diff --git a/app/views/diary_entry/_diary_comment.html.erb b/app/views/diary_entry/_diary_comment.html.erb index c651c2943..a6cf08888 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_issue_url(reportable_id: diary_comment.id, reportable_type: diary_comment.class.name, referer: request.fullpath), :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/diary_entry/view.html.erb b/app/views/diary_entry/view.html.erb index 3c2264d3e..5079074e8 100644 --- a/app/views/diary_entry/view.html.erb +++ b/app/views/diary_entry/view.html.erb @@ -10,7 +10,11 @@
-<%= render :partial => 'diary_comment', :collection => @entry.visible_comments %> + <% if @reported_comment %> + <%= render :partial => 'diary_comment', :collection => @reported_comment %> + <% else %> + <%= render :partial => 'diary_comment', :collection => @entry.visible_comments %> + <% end %>
<%= if_logged_in(:div) do %>

<%= t 'diary_entry.view.leave_a_comment' %>

diff --git a/app/views/issues/_comments.html.erb b/app/views/issues/_comments.html.erb new file mode 100644 index 000000000..36a5ec43d --- /dev/null +++ b/app/views/issues/_comments.html.erb @@ -0,0 +1,30 @@ +
+ <% 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 %> + + <% if comment.reassign %> +
+ <%= t('issues.show.comments.reassign') %> + <% end %> +
+ + On <%= 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 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..68e69ee72 --- /dev/null +++ b/app/views/issues/_reports.html.erb @@ -0,0 +1,15 @@ +<% reports.each do |report| %> +
+
+ <%= link_to user_thumbnail(report.user), :controller => :user, :action =>:view, :display_name => report.user.display_name %> +
+ Reported by <%= link_to report.user.display_name, :controller => :user, :action =>:view, :display_name => report.user.display_name %>
+ + On <%= l report.updated_at.to_datetime, :format => :friendly %> + +
+ <%= 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..a490223ae --- /dev/null +++ b/app/views/issues/index.html.erb @@ -0,0 +1,46 @@ +<% content_for :heading do %> +

List of <%= @user_role %> issues:

+<% end %> + +<%= form_tag(issues_path, :method => :get) do %> +Search for a particular issue(s):
+<%= select :status, nil, [['open', 0],['resolved',2],['ignored',1]],{:include_blank => "Select status"},data: { behavior: 'category_dropdown' } %> +<%= select :issue_type, nil, @issue_types,{:include_blank => "Select type"}, data: { behavior: 'category_dropdown' } %> +<%= text_field_tag :search_by_user, params[:search_by_user], placeholder: "Reported User" %> +<%= select :last_reported_by, nil, @users.all.collect {|f| [f.display_name, f.id]} << ['Not updated',"nil"], {:include_blank => "Select last updated by"}, data: { behavior: 'category_dropdown' } %> +<%= submit_tag "Search" %> +<% end %> +
+ +<% if @issues.length == 0 %> +

<%= t ".search.issues_not_found" %>

+<% end %> + +
+ + + + + + + + + + + + + + + <% @issues.each do |issue| %> + + + + + + + + + + <% end %> + +
<%= sortable("status") %> <%= sortable("reports_count", "Number of Reports") %> <%= sortable("updated_at","Last updated at") %> <%= sortable("updated_by","Last updated by") %> Link to reports <%= sortable("reported_user_id","Reported User") %> Link to reported instance
<%= issue.status.humanize %><%= issue.reports_count %><%= l(issue.updated_at.to_datetime, :format => :friendly) %><% if issue.user_updated %> <%= issue.user_updated.display_name %> <% else %> - <% end %><%= reports_url(issue) %><%= link_to issue.reported_user.display_name , :controller => :user, :action => :view, :display_name => issue.reported_user.display_name %><%= instance_url(issue.reportable) %>
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb new file mode 100644 index 000000000..e9d68d27c --- /dev/null +++ b/app/views/issues/show.html.erb @@ -0,0 +1,60 @@ +<% content_for :heading do %> +

<%= @issue.status.humanize %> Issue #<%= @issue.id %>

+

<%= report_type(@issue.reportable_type) %> : <%= reportable_url(@issue.reportable) %>

+

+ + <%= @issue.reports.count %> reports | First reported: <%= l @issue.created_at.to_datetime, :format => :friendly %> <%= "| Last resolved at #{l(@issue.resolved_at.to_datetime, :format =>:friendly)}" if @issue.resolved_at? %> <%= "| Last updated at #{l(@issue.updated_at.to_datetime, :format => :friendly)} by #{@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 %> + + + +

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 @@
+ <% if current_user and @this_user.id != current_user.id %> +
+ <%= link_to new_issue_url(reportable_id: @this_user.id, reportable_type: @this_user.class.name, referer: request.fullpath), :title => t('user.view.report') do%> +  ⚐ + <% end %> +
+ <% end %> +
<%= @this_user.description.to_html %>
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 07f8eaeec..e6ebbb6a8 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -150,6 +150,7 @@ en-GB: title_comment: Changeset %{id} - %{comment} join_discussion: Log in to join the discussion discussion: Discussion + report: Report this changeset? node: title: 'Node: %{name}' history_title: 'Node History: %{name}' @@ -231,6 +232,7 @@ en-GB: reopened_by_anonymous: Reactivated by anonymous %{when} ago hidden_by: Hidden by %{user} %{when} ago + report: Report this note? query: title: Query Features introduction: Click on the map to find nearby features. @@ -322,10 +324,12 @@ en-GB: edit_link: Edit this entry hide_link: Hide this entry confirm: Confirm + report: Report this entry? diary_comment: comment_from: Comment from %{link_user} on %{comment_created_at} hide_link: Hide this comment confirm: Confirm + report: Report this comment? location: location: 'Location:' view: View @@ -939,6 +943,118 @@ en-GB: results: no_results: No results found more_results: More results + issues: + report: Report + resolve: Resolve + ignore: Ignore + reopen: Reopen + index: + search: + user_not_found: User does not exist + issues_not_found: No such issues found + create: + successful_report: Your report has been registered sucessfully + provide_details: Please provide the required details + update: + new_report: Your report been registered sucessfully + successful_update: Your report has been updated successfully + provide_details: Please provide the required details + new: + details: Please provide some more details into the problem. (This field cannot be left blank!) + select: Select a reason for your report + disclaimer: + placeholder: Before sending in a report for official action, be sure that + placeholder1: You are sure that the problem is not just a mistake + placeholder2: You are unable to fix the problem yourself + placeholder3: You have tried to resolve the problem with the user + show: + comments: + reassign: The Issue was reassigned + reassign_param: Reassign Issue? + comment: + provide_details: Please provide the required details + comment_created: Your comment was successfully created + resolved: Issue status has been set to 'Resolved' + ignored: Issue status has been set to 'Ignored' + reopened: Issue status has been set to 'Open' + report_strings: + DiaryEntry: + spam: + type: "[SPAM]" + details: This Diary Entry is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This Diary Entry is obscene/offensive + threat: + type: "[THREAT]" + details: This Diary Entry contains a threat + other: + type: "[OTHER]" + details: Other + DiaryComment: + spam: + type: "[SPAM]" + details: This Diary Comment is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This Diary Comment is obscene/offensive + threat: + type: "[THREAT]" + details: This Diary Comment contains a threat + other: + type: "[OTHER]" + details: Other + User: + spam: + type: "[SPAM]" + details: This User profile is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This User profile is obscene/offensive + threat: + type: "[THREAT]" + details: This User profile contains a threat + vandal: + type: "[VANDAL]" + details: This User is a vandal + other: + type: "[OTHER]" + details: Other + Changeset: + undiscussed_import: + type: "[UNDISCUSSED-IMPORT]" + details: This changeset is an undiscussed import + mechanical_edit: + type: "[MECH-EDIT]" + details: This changeset is a mechanical edit + edit_error: + type: "[EDIT-ERROR]" + details: This changeset contains a newbie or an editor error + spam: + type: "[SPAM]" + details: This changeset is/contains spam + vandalism: + type: "[VANDALISM]" + details: This changeset is/contains vandalism + other: + type: "[OTHER]" + details: Other + Note: + spam: + type: "[SPAM]" + details: This note is spam + vandalism: + type: "[VANDALISM]" + details: This note is vandalism + personal: + type: "[PERSONAL]" + details: This note contains personal data + abusive: + type: "[ABUSIVE]" + details: This note is abusive + other: + type: "[OTHER]" + details: Other layouts: project_name: title: OpenStreetMap @@ -955,6 +1071,7 @@ en-GB: edit: Edit history: History export: Export + reports: Reports data: Data export_data: Export Data gps_traces: GPS Traces @@ -1360,6 +1477,11 @@ en-GB: details: More details about the changeset can be found at %{url}. unsubscribe: To unsubscribe from updates to this changeset, visit %{url} and click "Unsubscribe". + new_issue_notification: + subject: "[OpenStreetMap] New Issue" + greeting: "Hi," + new_issue: "A new issue has been created" + url: You can view the issue here message: inbox: title: Inbox @@ -1689,6 +1811,8 @@ en-GB: require_cookies: cookies_needed: You appear to have cookies disabled - please enable cookies in your browser before continuing. + require_admin: + not_an_admin: You need to be an admin to perform that action. require_moderator: not_a_moderator: You need to be a moderator to perform that action. setup_user_auth: @@ -1992,6 +2116,7 @@ en-GB: friends_diaries: friends' diary entries nearby_changesets: nearby user changesets nearby_diaries: nearby user diary entries + report: Report this user? popup: your location: Your location nearby mapper: Nearby mapper diff --git a/config/locales/en.yml b/config/locales/en.yml index 062fd95d4..7393d6efd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -131,6 +131,7 @@ en: title_comment: "Changeset %{id} - %{comment}" join_discussion: "Log in to join the discussion" discussion: Discussion + report: Report this changeset? node: title: "Node: %{name}" history_title: "Node History: %{name}" @@ -206,6 +207,7 @@ en: reopened_by: "Reactivated by %{user} %{when} ago" reopened_by_anonymous: "Reactivated by anonymous %{when} ago" hidden_by: "Hidden by %{user} %{when} ago" + report: Report this note? query: title: "Query Features" introduction: "Click on the map to find nearby features." @@ -296,10 +298,12 @@ en: edit_link: Edit this entry hide_link: Hide this entry confirm: Confirm + report: Report this entry? diary_comment: comment_from: "Comment from %{link_user} on %{comment_created_at}" hide_link: Hide this comment confirm: Confirm + report: Report this comment? location: location: "Location:" view: "View" @@ -903,6 +907,118 @@ en: results: no_results: "No results found" more_results: "More results" + issues: + report: Report + resolve: Resolve + ignore: Ignore + reopen: Reopen + index: + search: + user_not_found: User does not exist + issues_not_found: No such issues found + create: + successful_report: Your report has been registered sucessfully + provide_details: Please provide the required details + update: + new_report: Your report been registered sucessfully + successful_update: Your report has been updated successfully + provide_details: Please provide the required details + new: + details: Please provide some more details into the problem. (This field cannot be left blank!) + select: Select a reason for your report + disclaimer: + intro: Before sending in a report for official action, be sure that + not_just_mistake: You are sure that the problem is not just a mistake + unable_to_fix: You are unable to fix the problem yourself + resolve_with_user: You have tried to resolve the problem with the user + show: + comments: + reassign: The Issue was reassigned + reassign_param: Reassign Issue? + comment: + provide_details: Please provide the required details + comment_created: Your comment was successfully created + resolved: Issue status has been set to 'Resolved' + ignored: Issue status has been set to 'Ignored' + reopened: Issue status has been set to 'Open' + report_strings: + DiaryEntry: + spam: + type: "[SPAM]" + details: This Diary Entry is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This Diary Entry is obscene/offensive + threat: + type: "[THREAT]" + details: This Diary Entry contains a threat + other: + type: "[OTHER]" + details: Other + DiaryComment: + spam: + type: "[SPAM]" + details: This Diary Comment is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This Diary Comment is obscene/offensive + threat: + type: "[THREAT]" + details: This Diary Comment contains a threat + other: + type: "[OTHER]" + details: Other + User: + spam: + type: "[SPAM]" + details: This User profile is/contains spam + offensive: + type: "[OFFENSIVE]" + details: This User profile is obscene/offensive + threat: + type: "[THREAT]" + details: This User profile contains a threat + vandal: + type: "[VANDAL]" + details: This User is a vandal + other: + type: "[OTHER]" + details: Other + Changeset: + undiscussed_import: + type: "[UNDISCUSSED-IMPORT]" + details: This changeset is an undiscussed import + mechanical_edit: + type: "[MECH-EDIT]" + details: This changeset is a mechanical edit + edit_error: + type: "[EDIT-ERROR]" + details: This changeset contains a newbie or an editor error + spam: + type: "[SPAM]" + details: This changeset is/contains spam + vandalism: + type: "[VANDALISM]" + details: This changeset is/contains vandalism + other: + type: "[OTHER]" + details: Other + Note: + spam: + type: "[SPAM]" + details: This note is spam + vandalism: + type: "[VANDALISM]" + details: This note is vandalism + personal: + type: "[PERSONAL]" + details: This note contains personal data + abusive: + type: "[ABUSIVE]" + details: This note is abusive + other: + type: "[OTHER]" + details: Other layouts: project_name: # in @@ -921,6 +1037,7 @@ en: edit: Edit history: History export: Export + issues: Issues data: Data export_data: Export Data gps_traces: GPS Traces @@ -1113,8 +1230,8 @@ en: paragraph_1_html: | OpenStreetMap has few formal rules but we expect all participants to collaborate with, and communicate with, the community. If you are considering - any activities other than editing by hand, please read and follow the guidelines on - <a href='http://wiki.openstreetmap.org/wiki/Import/Guidelines'>Imports</a> and + any activities other than editing by hand, please read and follow the guidelines on + <a href='http://wiki.openstreetmap.org/wiki/Import/Guidelines'>Imports</a> and <a href='http://wiki.openstreetmap.org/wiki/Automated_Edits_code_of_conduct'>Automated Edits</a>. questions: title: Any questions? @@ -1140,7 +1257,7 @@ en: title: Join the community explanation_html: | If you have noticed a problem with our map data, for example a road is missing or your address, the best way to - proceed is to join the OpenStreetMap community and add or repair the data yourself. + proceed is to join the OpenStreetMap community and add or repair the data yourself. add_a_note: instructions_html: | Just click <a class='icon note'></a> or the same icon on the map display. @@ -1150,8 +1267,8 @@ en: title: Other concerns explanation_html: | If you have concerns about how our data is being used or about the contents please consult our - <a href='/copyright'>copyright page</a> for more legal information, or contact the appropriate - <a href='http://wiki.osmfoundation.org/wiki/Working_Groups'>OSMF working group</a>. + <a href='/copyright'>copyright page</a> for more legal information, or contact the appropriate + <a href='http://wiki.osmfoundation.org/wiki/Working_Groups'>OSMF working group</a>. help_page: title: Getting Help introduction: | @@ -1221,13 +1338,13 @@ en: License page</a> for details. legal_title: Legal legal_html: | - This site and many other related services are formally operated by the - <a href='http://osmfoundation.org/'>OpenStreetMap Foundation</a> (OSMF) - on behalf of the community. Use of all OSMF operated services is subject + This site and many other related services are formally operated by the + <a href='http://osmfoundation.org/'>OpenStreetMap Foundation</a> (OSMF) + on behalf of the community. Use of all OSMF operated services is subject to our <a href="http://wiki.openstreetmap.org/wiki/Acceptable_Use_Policy"> Acceptable Use Policies</a> and our <a href="http://wiki.osmfoundation.org/wiki/Privacy_Policy">Privacy Policy</a> - <br> - Please <a href='http://osmfoundation.org/Contact'>contact the OSMF</a> + <br> + Please <a href='http://osmfoundation.org/Contact'>contact the OSMF</a> if you have licensing, copyright or other legal questions and issues. partners_title: Partners notifier: @@ -1319,6 +1436,11 @@ en: partial_changeset_without_comment: "without comment" details: "More details about the changeset can be found at %{url}." unsubscribe: 'To unsubscribe from updates to this changeset, visit %{url} and click "Unsubscribe".' + new_issue_notification: + subject: "[OpenStreetMap] New Issue" + greeting: "Hi," + new_issue: "A new issue has been created" + url: You can view the issue here message: inbox: title: "Inbox" @@ -1623,6 +1745,8 @@ en: application: require_cookies: cookies_needed: "You appear to have cookies disabled - please enable cookies in your browser before continuing." + require_admin: + not_an_admin: You need to be an admin to perform that action. require_moderator: not_a_moderator: "You need to be a moderator to perform that action." setup_user_auth: @@ -1894,6 +2018,7 @@ en: friends_diaries: "friends' diary entries" nearby_changesets: "nearby user changesets" nearby_diaries: "nearby user diary entries" + report: "Report this user?" popup: your location: "Your location" nearby mapper: "Nearby mapper" diff --git a/config/routes.rb b/config/routes.rb index 98bb332f2..8a5dca166 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -223,7 +223,7 @@ OpenStreetMap::Application.routes.draw do match "/user/:display_name/diary" => "diary_entry#list", :via => :get match "/diary/:language" => "diary_entry#list", :via => :get match "/diary" => "diary_entry#list", :via => :get - match "/user/:display_name/diary/:id" => "diary_entry#view", :via => :get, :id => /\d+/ + match "/user/:display_name/diary/:id" => "diary_entry#view", :via => :get, :id => /\d+/, :as => :diary_entry match "/user/:display_name/diary/:id/newcomment" => "diary_entry#comment", :via => :post, :id => /\d+/ match "/user/:display_name/diary/:id/edit" => "diary_entry#edit", :via => [:get, :post], :id => /\d+/ match "/user/:display_name/diary/:id/hide" => "diary_entry#hide", :via => :post, :id => /\d+/, :as => :hide_diary_entry @@ -289,6 +289,19 @@ OpenStreetMap::Application.routes.draw do resources :user_blocks match "/blocks/:id/revoke" => "user_blocks#revoke", :via => [:get, :post], :as => "revoke_user_block" + # issues and reports + resources :issues do + resources :comments, :controller => :issue_comments + member do + post "resolve" + post "assign" + post "ignore" + post "reopen" + end + end + + resources :reports + # redactions resources :redactions end diff --git a/db/migrate/20160822153055_create_issues_and_reports.rb b/db/migrate/20160822153055_create_issues_and_reports.rb new file mode 100644 index 000000000..a25523cb9 --- /dev/null +++ b/db/migrate/20160822153055_create_issues_and_reports.rb @@ -0,0 +1,35 @@ +require "migrate" + +class CreateIssuesAndReports < ActiveRecord::Migration + def change + create_table :issues do |t| + t.string :reportable_type, :null => false + t.integer :reportable_id, :null => false + t.integer :reported_user_id, :null => false + t.integer :status + t.string :issue_type + t.datetime :resolved_at + t.integer :resolved_by + t.integer :updated_by + t.timestamps :null => false + end + + add_foreign_key :issues, :users, :column => :reported_user_id, :name => "issues_reported_user_id_fkey", :on_delete => :cascade + + add_index :issues, :reported_user_id + add_index :issues, [:reportable_id, :reportable_type] + + create_table :reports do |t| + t.integer :issue_id + t.integer :reporter_user_id + t.text :details, :null => false + t.timestamps :null => false + end + + add_foreign_key :reports, :issues, :name => "reports_issue_id_fkey", :on_delete => :cascade + add_foreign_key :reports, :users, :column => :reporter_user_id, :name => "reports_reporter_user_id_fkey", :on_delete => :cascade + + add_index :reports, :reporter_user_id + add_index :reports, :issue_id + end +end diff --git a/db/migrate/20160822153115_create_issue_comments.rb b/db/migrate/20160822153115_create_issue_comments.rb new file mode 100644 index 000000000..b41dde8a7 --- /dev/null +++ b/db/migrate/20160822153115_create_issue_comments.rb @@ -0,0 +1,17 @@ +class CreateIssueComments < ActiveRecord::Migration + def change + create_table :issue_comments do |t| + t.integer :issue_id, :null => false + t.integer :commenter_user_id, :null => false + t.text :body, :null => false + t.boolean :reassign + t.timestamps :null => false + end + + add_foreign_key :issue_comments, :issues, :name => "issue_comments_issue_id_fkey", :on_delete => :cascade + add_foreign_key :issue_comments, :users, :column => :commenter_user_id, :name => "issue_comments_commenter_user_id", :on_delete => :cascade + + add_index :issue_comments, :commenter_user_id + add_index :issue_comments, :issue_id + end +end diff --git a/db/migrate/20160822153153_add_reports_count_to_issues.rb b/db/migrate/20160822153153_add_reports_count_to_issues.rb new file mode 100644 index 000000000..a7ccd228f --- /dev/null +++ b/db/migrate/20160822153153_add_reports_count_to_issues.rb @@ -0,0 +1,7 @@ +class AddReportsCountToIssues < ActiveRecord::Migration + def change + add_column :issues, :reports_count, :integer, :default => 0 + add_foreign_key :issues, :users, :column => :updated_by, :name => "issues_updated_by_fkey", :on_delete => :cascade + add_index :issues, :updated_by + end +end diff --git a/db/structure.sql b/db/structure.sql index 08aafa8d7..7888d8a9c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -685,6 +685,79 @@ CREATE SEQUENCE gpx_files_id_seq ALTER SEQUENCE gpx_files_id_seq OWNED BY gpx_files.id; +-- +-- Name: issue_comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE issue_comments ( + id integer NOT NULL, + issue_id integer NOT NULL, + commenter_user_id integer NOT NULL, + body text NOT NULL, + reassign boolean, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: issue_comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE issue_comments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: issue_comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE issue_comments_id_seq OWNED BY issue_comments.id; + + +-- +-- Name: issues; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE issues ( + id integer NOT NULL, + reportable_type character varying NOT NULL, + reportable_id integer NOT NULL, + reported_user_id integer NOT NULL, + status integer, + issue_type character varying, + resolved_at timestamp without time zone, + resolved_by integer, + updated_by integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + reports_count integer DEFAULT 0 +); + + +-- +-- Name: issues_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE issues_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: issues_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE issues_id_seq OWNED BY issues.id; + + -- -- Name: languages; Type: TABLE; Schema: public; Owner: - -- @@ -986,6 +1059,39 @@ CREATE TABLE relations ( ); +-- +-- Name: reports; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE reports ( + id integer NOT NULL, + issue_id integer, + reporter_user_id integer, + details text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: reports_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE reports_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: reports_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE reports_id_seq OWNED BY reports.id; + + -- -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - -- @@ -1288,6 +1394,20 @@ ALTER TABLE ONLY gpx_file_tags ALTER COLUMN id SET DEFAULT nextval('gpx_file_tag ALTER TABLE ONLY gpx_files ALTER COLUMN id SET DEFAULT nextval('gpx_files_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issue_comments ALTER COLUMN id SET DEFAULT nextval('issue_comments_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issues ALTER COLUMN id SET DEFAULT nextval('issues_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1330,6 +1450,13 @@ ALTER TABLE ONLY oauth_tokens ALTER COLUMN id SET DEFAULT nextval('oauth_tokens_ ALTER TABLE ONLY redactions ALTER COLUMN id SET DEFAULT nextval('redactions_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY reports ALTER COLUMN id SET DEFAULT nextval('reports_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1510,6 +1637,22 @@ ALTER TABLE ONLY gpx_files ADD CONSTRAINT gpx_files_pkey PRIMARY KEY (id); +-- +-- Name: issue_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issue_comments + ADD CONSTRAINT issue_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: issues_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issues + ADD CONSTRAINT issues_pkey PRIMARY KEY (id); + + -- -- Name: languages_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1606,6 +1749,14 @@ ALTER TABLE ONLY relations ADD CONSTRAINT relations_pkey PRIMARY KEY (relation_id, version); +-- +-- Name: reports_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY reports + ADD CONSTRAINT reports_pkey PRIMARY KEY (id); + + -- -- Name: user_blocks_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1880,6 +2031,41 @@ CREATE INDEX index_client_applications_on_user_id ON client_applications USING b CREATE INDEX index_diary_entry_subscriptions_on_diary_entry_id ON diary_entry_subscriptions USING btree (diary_entry_id); +-- +-- Name: index_issue_comments_on_commenter_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_issue_comments_on_commenter_user_id ON issue_comments USING btree (commenter_user_id); + + +-- +-- Name: index_issue_comments_on_issue_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_issue_comments_on_issue_id ON issue_comments USING btree (issue_id); + + +-- +-- Name: index_issues_on_reportable_id_and_reportable_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_issues_on_reportable_id_and_reportable_type ON issues USING btree (reportable_id, reportable_type); + + +-- +-- Name: index_issues_on_reported_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_issues_on_reported_user_id ON issues USING btree (reported_user_id); + + +-- +-- Name: index_issues_on_updated_by; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_issues_on_updated_by ON issues USING btree (updated_by); + + -- -- Name: index_note_comments_on_body; Type: INDEX; Schema: public; Owner: - -- @@ -1915,6 +2101,20 @@ CREATE UNIQUE INDEX index_oauth_tokens_on_token ON oauth_tokens USING btree (tok CREATE INDEX index_oauth_tokens_on_user_id ON oauth_tokens USING btree (user_id); +-- +-- Name: index_reports_on_issue_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_reports_on_issue_id ON reports USING btree (issue_id); + + +-- +-- Name: index_reports_on_reporter_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_reports_on_reporter_user_id ON reports USING btree (reporter_user_id); + + -- -- Name: index_user_blocks_on_user_id; Type: INDEX; Schema: public; Owner: - -- @@ -2327,6 +2527,38 @@ ALTER TABLE ONLY gpx_files ADD CONSTRAINT gpx_files_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); +-- +-- Name: issue_comments_commenter_user_id; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issue_comments + ADD CONSTRAINT issue_comments_commenter_user_id FOREIGN KEY (commenter_user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: issue_comments_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issue_comments + ADD CONSTRAINT issue_comments_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; + + +-- +-- Name: issues_reported_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issues + ADD CONSTRAINT issues_reported_user_id_fkey FOREIGN KEY (reported_user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: issues_updated_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY issues + ADD CONSTRAINT issues_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE CASCADE; + + -- -- Name: messages_from_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2439,6 +2671,22 @@ ALTER TABLE ONLY relations ADD CONSTRAINT relations_redaction_id_fkey FOREIGN KEY (redaction_id) REFERENCES redactions(id); +-- +-- Name: reports_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY reports + ADD CONSTRAINT reports_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; + + +-- +-- Name: reports_reporter_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY reports + ADD CONSTRAINT reports_reporter_user_id_fkey FOREIGN KEY (reporter_user_id) REFERENCES users(id) ON DELETE CASCADE; + + -- -- Name: user_blocks_moderator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2582,6 +2830,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20150111192335'), ('20150222101847'), ('20150818224516'), +('20160822153055'), +('20160822153115'), +('20160822153153'), ('20161002153425'), ('20161011010929'), ('20170222134109'), @@ -2629,5 +2880,3 @@ INSERT INTO "schema_migrations" (version) VALUES ('7'), ('8'), ('9'); - - diff --git a/test/controllers/issues_controller_test.rb b/test/controllers/issues_controller_test.rb new file mode 100644 index 000000000..161fd6d4c --- /dev/null +++ b/test/controllers/issues_controller_test.rb @@ -0,0 +1,244 @@ +require "test_helper" + +class IssuesControllerTest < ActionController::TestCase + teardown do + # cleanup any emails set off by the test + ActionMailer::Base.deliveries.clear + end + + def test_view_dashboard_without_auth + # Access issues_path without login + get :index + assert_response :redirect + assert_redirected_to login_path(:referer => issues_path) + + # Access issues_path as normal user + session[:user] = create(:user).id + get :index + assert_response :redirect + assert_redirected_to root_path + + # Access issues_path by admin + session[:user] = create(:administrator_user).id + get :index + # this is redirected because there are no issues?! + assert_response :redirect + assert_redirected_to issues_path + + # Access issues_path by moderator + session[:user] = create(:moderator_user).id + get :index + # this is redirected because there are no issues?! + assert_response :redirect + assert_redirected_to issues_path + end + + def test_new_issue_without_login + # Test creation of a new issue and a new report without logging in + get :new, :params => { :reportable_id => 1, :reportable_type => "User", :reported_user_id => 1 } + assert_response :redirect + assert_redirected_to login_path(:referer => new_issue_path(:reportable_id => 1, :reportable_type => "User", :reported_user_id => 1)) + end + + def test_new_issue_after_login + # Test creation of a new issue and a new report + target_user = create(:user) + + # Login + session[:user] = create(:user).id + + assert_equal Issue.count, 0 + + # Create an Issue and a report + get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + assert_response :success + assert_difference "Issue.count", 1 do + details = "Details of a report" + post :create, + :params => { + :report => { :details => details }, + :report_type => "[OFFENSIVE]", + :issue => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + } + end + assert_equal Issue.count, 1 + assert_response :redirect + assert_redirected_to root_path + end + + def test_new_report_with_incomplete_details + # Test creation of a new issue and a new report + target_user = create(:user) + + # Login + session[:user] = create(:user).id + + assert_equal Issue.count, 0 + + # Create an Issue and a report + get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + assert_response :success + assert_difference "Issue.count", 1 do + details = "Details of a report" + post :create, + :params => { + :report => { :details => details }, + :report_type => "[OFFENSIVE]", + :issue => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + } + end + assert_equal Issue.count, 1 + assert_response :redirect + assert_redirected_to root_path + + get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + assert_response :success + + # Report without report_type + assert_no_difference "Issue.count" do + details = "Details of another report under the same issue" + post :create, + :params => { + :report => { :details => details }, + :issue => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + } + end + assert_response :redirect + assert_equal Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").reports.count, 1 + + # Report without details + assert_no_difference "Issue.count" do + post :create, + :params => { + :report_type => "[OFFENSIVE]", + :issue => { :reportable_id => 1, :reportable_type => "User", :reported_user_id => 2 } + } + end + assert_response :redirect + assert_equal Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").reports.count, 1 + end + + def test_new_report_with_complete_details + # Test creation of a new issue and a new report + target_user = create(:user) + + # Login + session[:user] = create(:user).id + + assert_equal Issue.count, 0 + + # Create an Issue and a report + get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + assert_response :success + assert_difference "Issue.count", 1 do + details = "Details of a report" + post :create, + :params => { + :report => { :details => details }, + :report_type => "[OFFENSIVE]", + :issue => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + } + end + assert_equal Issue.count, 1 + assert_response :redirect + assert_redirected_to root_path + + # Create a report for an existing Issue + get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + assert_response :success + assert_no_difference "Issue.count" do + details = "Details of another report under the same issue" + post :create, + :params => { + :report => { :details => details }, + :report_type => "[OFFENSIVE]", + :issue => { :reportable_id => target_user.id, :reportable_type => "User", :reported_user_id => target_user.id } + } + end + assert_response :redirect + report_count = Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").reports.count + assert_equal report_count, 2 + end + + def test_change_status_by_normal_user + target_user = create(:user) + issue = create(:issue, :reportable => target_user, :reported_user => target_user) + + # Login as normal user + session[:user] = create(:user).id + + assert_equal Issue.count, 1 + + get :resolve, :params => { :id => issue.id } + + assert_response :redirect + assert_redirected_to root_path + end + + def test_change_status_by_admin + target_user = create(:user) + issue = create(:issue, :reportable => target_user, :reported_user => target_user) + + # Login as administrator + session[:user] = create(:administrator_user).id + + # Test 'Resolved' + get :resolve, :params => { :id => issue.id } + assert_equal Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").resolved?, true + assert_response :redirect + + # Test 'Reopen' + get :reopen, :params => { :id => issue.id } + assert_equal Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").open?, true + assert_response :redirect + + # Test 'Ignored' + get :ignore, :params => { :id => issue.id } + assert_equal Issue.find_by(:reportable_id => target_user, :reportable_type => "User").ignored?, true + assert_response :redirect + end + + def test_search_issues + good_user = create(:user) + bad_user = create(:user) + create(:issue, :reportable => bad_user, :reported_user => bad_user, :issue_type => "administrator") + # Login as administrator + session[:user] = create(:administrator_user).id + + # No issues against the user + get :index, :params => { :search_by_user => good_user.display_name } + assert_response :redirect + assert_redirected_to issues_path + + # User doesn't exist + get :index, :params => { :search_by_user => "test1000" } + assert_response :redirect + assert_redirected_to issues_path + + # Find Issue against bad_user + get :index, :params => { :search_by_user => bad_user.display_name } + assert_response :success + end + + def test_comment_by_normal_user + issue = create(:issue) + + # Login as normal user + session[:user] = create(:user).id + + get :comment, :params => { :id => issue.id } + assert_response :redirect + assert_redirected_to root_path + end + + def test_comment + issue = create(:issue) + + # Login as administrator + session[:user] = create(:administrator_user).id + + get :comment, :params => { :id => issue.id, :issue_comment => { :body => "test comment" } } + assert_response :redirect + assert_redirected_to issue + end +end diff --git a/test/factories/issues.rb b/test/factories/issues.rb new file mode 100644 index 000000000..b6dec4478 --- /dev/null +++ b/test/factories/issues.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :issue do + # Default to reporting users + association :reportable, :factory => :user + association :reported_user, :factory => :user + end +end diff --git a/test/factories/reports.rb b/test/factories/reports.rb new file mode 100644 index 000000000..7c0076677 --- /dev/null +++ b/test/factories/reports.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :report do + sequence(:details) { |n| "Report details #{n}" } + issue + user + end +end diff --git a/test/features/can_access_home_test.rb b/test/features/can_access_home_test.rb new file mode 100644 index 000000000..396ffc927 --- /dev/null +++ b/test/features/can_access_home_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +class CanAccessHomeTest < Capybara::Rails::TestCase + def setup + stub_hostip_requests + end + + def test_it_works + visit root_path + assert page.has_content? "BOpenStreetMap" + end +end diff --git a/test/features/issues_test.rb b/test/features/issues_test.rb new file mode 100644 index 000000000..04ff7f5a6 --- /dev/null +++ b/test/features/issues_test.rb @@ -0,0 +1,40 @@ +require "test_helper" + +class IssuesTest < Capybara::Rails::TestCase + def test_view_issues_normal_user + sign_in_as(create(:user)) + + visit issues_path + assert page.has_content?(I18n.t("application.require_admin.not_an_admin")) + end + + def test_view_no_issues + sign_in_as(create(:moderator_user)) + + visit issues_path + assert page.has_content?(I18n.t(".issues.index.search.issues_not_found")) + end + + def test_view_issues + sign_in_as(create(:moderator_user)) + issues = create_list(:issue, 3, :issue_type => "moderator") + + visit issues_path + assert page.has_content?(issues.first.reported_user.display_name) + end + + def test_commenting + issue = create(:issue) + sign_in_as(create(:moderator_user)) + + visit issue_path(issue) + + fill_in :issue_comment_body, :with => "test comment" + click_on "Submit" + assert page.has_content?(I18n.t(".issues.comment.comment_created")) + assert page.has_content?("test comment") + + issue.reload + assert_equal issue.comments.first.body, "test comment" + end +end diff --git a/test/features/report_diary_entry_test.rb b/test/features/report_diary_entry_test.rb new file mode 100644 index 000000000..820b5f77b --- /dev/null +++ b/test/features/report_diary_entry_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +class ReportDiaryEntryTest < Capybara::Rails::TestCase + def setup + create(:language, :code => "en") + @diary_entry = create(:diary_entry) + end + + def test_no_flag_when_not_logged_in + visit diary_entry_path(@diary_entry.user.display_name, @diary_entry) + assert page.has_content?(@diary_entry.title) + + assert !page.has_content?("\u2690") + end + + def test_it_works + sign_in_as(create(:user)) + visit diary_entry_path(@diary_entry.user.display_name, @diary_entry) + assert page.has_content? @diary_entry.title + + click_on "\u2690" + assert page.has_content? "Report" + assert page.has_content? I18n.t("issues.new.disclaimer.intro") + + choose "report_type__SPAM" # FIXME: use label text when the radio button labels are working + fill_in "report_details", :with => "This is advertising" + click_on "Save changes" + + assert page.has_content? "Your report has been registered sucessfully" + end +end diff --git a/test/models/issue_comment_test.rb b/test/models/issue_comment_test.rb new file mode 100644 index 000000000..53ba35889 --- /dev/null +++ b/test/models/issue_comment_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class IssueCommentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/issue_test.rb b/test/models/issue_test.rb new file mode 100644 index 000000000..7ee700124 --- /dev/null +++ b/test/models/issue_test.rb @@ -0,0 +1,24 @@ +require "test_helper" + +class IssueTest < ActiveSupport::TestCase + def test_reported_user + note = create(:note_comment, :author => create(:user)).note + user = create(:user) + create(:language, :code => "en") + diary_entry = create(:diary_entry) + issue = Issue.new + + issue.reportable = user + issue.save! + assert_equal issue.reported_user, user + + # FIXME: doesn't handle anonymous notes + issue.reportable = note + issue.save! + assert_equal issue.reported_user, note.author + + issue.reportable = diary_entry + issue.save! + assert_equal issue.reported_user, diary_entry.user + end +end diff --git a/test/models/report_test.rb b/test/models/report_test.rb new file mode 100644 index 000000000..a0de9448c --- /dev/null +++ b/test/models/report_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class ReportTest < ActiveSupport::TestCase + def test_details_required + report = create(:report) + + assert report.valid? + report.details = '' + assert !report.valid? + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 70f69a3ae..552bda7d5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,6 +5,7 @@ ENV["RAILS_ENV"] = "test" require File.expand_path("../../config/environment", __FILE__) require "rails/test_help" require "webmock/minitest" +require "minitest/rails/capybara" module ActiveSupport class TestCase @@ -150,5 +151,13 @@ module ActiveSupport end end end + + def sign_in_as(user) + stub_hostip_requests + visit login_path + fill_in "username", :with => user.email + fill_in "password", :with => "test" + click_on "Login", :match => :first + end end end