Merge branch 'master' into next
authorTom Hughes <tom@compton.nu>
Sun, 10 Jun 2018 16:02:12 +0000 (17:02 +0100)
committerTom Hughes <tom@compton.nu>
Sun, 10 Jun 2018 16:02:12 +0000 (17:02 +0100)
40 files changed:
.rubocop_todo.yml
Gemfile
Gemfile.lock
app/assets/stylesheets/common.scss
app/controllers/issue_comments_controller.rb [new file with mode: 0644]
app/controllers/issues_controller.rb [new file with mode: 0644]
app/controllers/reports_controller.rb [new file with mode: 0644]
app/helpers/issues_helper.rb [new file with mode: 0644]
app/helpers/reports_helper.rb [new file with mode: 0644]
app/models/issue.rb [new file with mode: 0644]
app/models/issue_comment.rb [new file with mode: 0644]
app/models/report.rb [new file with mode: 0644]
app/models/user.rb
app/views/browse/note.html.erb
app/views/diary_entry/_diary_comment.html.erb
app/views/diary_entry/_diary_entry.html.erb
app/views/issues/_comments.html.erb [new file with mode: 0644]
app/views/issues/_reports.html.erb [new file with mode: 0644]
app/views/issues/index.html.erb [new file with mode: 0644]
app/views/issues/show.html.erb [new file with mode: 0644]
app/views/layouts/_header.html.erb
app/views/reports/new.html.erb [new file with mode: 0644]
app/views/user/view.html.erb
config/locales/en.yml
config/routes.rb
db/migrate/20160822153055_create_issues_and_reports.rb [new file with mode: 0644]
db/structure.sql
test/controllers/issue_comments_controller_test.rb [new file with mode: 0644]
test/controllers/issues_controller_test.rb [new file with mode: 0644]
test/controllers/reports_controller_test.rb [new file with mode: 0644]
test/factories/issues.rb [new file with mode: 0644]
test/factories/reports.rb [new file with mode: 0644]
test/models/issue_comment_test.rb [new file with mode: 0644]
test/models/issue_test.rb [new file with mode: 0644]
test/models/report_test.rb [new file with mode: 0644]
test/system/issues_test.rb [new file with mode: 0644]
test/system/report_diary_comment_test.rb [new file with mode: 0644]
test/system/report_diary_entry_test.rb [new file with mode: 0644]
test/system/report_note_test.rb [new file with mode: 0644]
test/system/report_user_test.rb [new file with mode: 0644]

index 362cfcb3235123751c8018c9fb69250bff842c42..da801d681ab11b8071dcf50a836d3b884cb36ccd 100644 (file)
@@ -67,7 +67,7 @@ Metrics/AbcSize:
 # Offense count: 41
 # Configuration parameters: CountComments, ExcludedMethods.
 Metrics/BlockLength:
-  Max: 247
+  Max: 257
 
 # Offense count: 12
 # Configuration parameters: CountBlocks.
diff --git a/Gemfile b/Gemfile
index 7d59cceb1d14338cd1aa45a32e21bcadd722ae8b..ef14d06c5b86eaed0853550541b81cfa01c8198d 100644 (file)
--- 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"
 
index 8634f8df8eb256ae8cd95bd12a260a66808211f0..49f9b5977231da55204cf6ac91542989ba31538b 100644 (file)
@@ -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)
@@ -360,6 +361,7 @@ PLATFORMS
 
 DEPENDENCIES
   SystemTimer (>= 1.1.3)
+  aasm
   actionpack-page_caching
   annotate
   autoprefixer-rails
index cef2efbb2d139326a70addbf41395a97c7943194..f9214b24dba1a94dd84c129541bfefbd6e09cd7b 100644 (file)
@@ -2820,3 +2820,59 @@ 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-reports {
+  width: 280px;
+  float: right;
+
+  ul {
+    padding-left: $lineheight;
+    margin-bottom: 0;
+
+    li {
+      list-style: disc;
+    }
+  }
+}
+
+.issue-comments {
+  width:475px;
+}
+
+.issues-list {
+  td:nth-child(2) {
+    white-space: nowrap;
+  }
+}
+
+.report-disclaimer {
+  background: #fff1f0;
+  color: #d85030;
+  border-color: rgba(216, 80, 48, 0.3);
+  padding: 10px 20px;
+  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 (file)
index 0000000..52904d5
--- /dev/null
@@ -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(".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_moderator_or_admin.not_a_moderator_or_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 (file)
index 0000000..7295289
--- /dev/null
@@ -0,0 +1,90 @@
+class IssuesController < ApplicationController
+  layout "site"
+
+  before_action :authorize_web
+  before_action :require_user
+  before_action :check_permission
+  before_action :find_issue, :only => [:show, :resolve, :reopen, :ignore]
+
+  def index
+    @title = t ".title"
+
+    @issue_types = []
+    @issue_types.concat %w[Note] if current_user.moderator?
+    @issue_types.concat %w[DiaryEntry DiaryComment User] if current_user.administrator?
+
+    @users = User.joins(:roles).where(:user_roles => { :role => current_user.roles.map(&:role) }).distinct
+    @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
+        @issues = @issues.none
+        flash.now[:warning] = t(".user_not_found")
+      end
+    end
+
+    @issues = @issues.where(:status => params[:status]) if params[:status] && params[:status].present?
+
+    @issues = @issues.where(:reportable_type => params[:issue_type]) if params[:issue_type] && params[:issue_type].present?
+
+    if params[:last_updated_by] && params[:last_updated_by].present?
+      last_updated_by = params[:last_updated_by].to_s == "nil" ? nil : params[:last_updated_by].to_i
+      @issues = @issues.where(:updated_by => last_updated_by)
+    end
+  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(".resolved")
+    else
+      render :show
+    end
+  end
+
+  def ignore
+    if @issue.ignore
+      @issue.updated_by = current_user.id
+      @issue.save!
+      redirect_to @issue, :notice => t(".ignored")
+    else
+      render :show
+    end
+  end
+
+  def reopen
+    if @issue.reopen
+      @issue.updated_by = current_user.id
+      @issue.save!
+      redirect_to @issue, :notice => t(".reopened")
+    else
+      render :show
+    end
+  end
+
+  private
+
+  def find_issue
+    @issue = Issue.find(params[:id])
+  end
+
+  def check_permission
+    unless current_user.administrator? || current_user.moderator?
+      flash[:error] = t("application.require_moderator_or_admin.not_a_moderator_or_admin")
+      redirect_to root_path
+    end
+  end
+end
diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb
new file mode 100644 (file)
index 0000000..95c9343
--- /dev/null
@@ -0,0 +1,42 @@
+class ReportsController < ApplicationController
+  layout "site"
+
+  before_action :authorize_web
+  before_action :require_user
+
+  def new
+    if required_new_report_params_present?
+      @report = Report.new
+      @report.issue = Issue.find_or_initialize_by(create_new_report_params)
+    else
+      redirect_to root_path, :notice => t(".missing_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 helpers.reportable_url(@report.issue.reportable), :notice => t(".successful_report")
+    else
+      redirect_to new_report_path(:reportable_type => @report.issue.reportable_type, :reportable_id => @report.issue.reportable_id), :notice => t(".provide_details")
+    end
+  end
+
+  private
+
+  def required_new_report_params_present?
+    create_new_report_params["reportable_id"].present? && create_new_report_params["reportable_type"].present?
+  end
+
+  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 (file)
index 0000000..3c13c65
--- /dev/null
@@ -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/helpers/reports_helper.rb b/app/helpers/reports_helper.rb
new file mode 100644 (file)
index 0000000..70c9ada
--- /dev/null
@@ -0,0 +1,5 @@
+module ReportsHelper
+  def report_link(name, reportable)
+    link_to name, new_report_url(:reportable_id => reportable.id, :reportable_type => reportable.class.name)
+  end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
new file mode 100644 (file)
index 0000000..dd06885
--- /dev/null
@@ -0,0 +1,104 @@
+# == 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           :enum             default("open"), not null
+#  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_assigned_role                      (assigned_role)
+#  index_issues_on_reportable_type_and_reportable_id  (reportable_type,reportable_id)
+#  index_issues_on_reported_user_id                   (reported_user_id)
+#  index_issues_on_status                             (status)
+#  index_issues_on_updated_by                         (updated_by)
+#
+# Foreign Keys
+#
+#  issues_reported_user_id_fkey  (reported_user_id => users.id)
+#  issues_resolved_by_fkey       (resolved_by => users.id)
+#  issues_updated_by_fkey        (updated_by => users.id)
+#
+
+class Issue < ActiveRecord::Base
+  belongs_to :reportable, :polymorphic => true
+  belongs_to :reported_user, :class_name => "User", :foreign_key => :reported_user_id
+  belongs_to :user_resolved, :class_name => "User", :foreign_key => :resolved_by
+  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
+
+  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
+    if assigned_role.blank?
+      self.assigned_role = case reportable
+                           when Note then "moderator"
+                           else "administrator"
+                           end
+    end
+  end
+end
diff --git a/app/models/issue_comment.rb b/app/models/issue_comment.rb
new file mode 100644 (file)
index 0000000..3a5894c
--- /dev/null
@@ -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)
+#  issue_comments_user_id_fkey   (user_id => users.id)
+#
+
+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 (file)
index 0000000..63296a0
--- /dev/null
@@ -0,0 +1,42 @@
+# == Schema Information
+#
+# Table name: reports
+#
+#  id         :integer          not null, primary key
+#  issue_id   :integer          not null
+#  user_id    :integer          not null
+#  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)
+#  reports_user_id_fkey   (user_id => users.id)
+#
+
+class Report < ActiveRecord::Base
+  belongs_to :issue, :counter_cache => true
+  belongs_to :user
+
+  validates :issue, :presence => true
+  validates :user, :presence => true
+  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 personal abusive other]
+    else %w[other]
+    end
+  end
+end
index 036f2fdc478692bc4f52b17ea8ed8f8aaf42d1f1..aaa00169f5210f2ac5fda89f0e1b8bd5d839811e 100644 (file)
@@ -75,6 +75,11 @@ class User < ActiveRecord::Base
 
   has_many :roles, :class_name => "UserRole"
 
+  has_many :issues, :class_name => "Issue", :foreign_key => :reported_user_id
+  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) }
index 37067c311fe812d3dab1606763094f0cbd8352bc..53ea0759e9f79866bbbfc97dfe416e0b0a1d0e8d 100644 (file)
     <p class='warning'><%= t "javascripts.notes.show.anonymous_warning" %></p>
   <% end -%>
 
+  <% if current_user && current_user != @note.author %>
+    <p class="deemphasize"><%= report_link(t(".report"), @note) %></p>
+  <% end %>
+
   <% if @note_comments.length > 1 %>
     <div class='note-comments'>
       <ul>
index 8565ecc67e4c4053a7f3790883d0f0c91d3f4cce..4ac9a0f967f1950dbe6e86f98b8a9d2c1f7e5a42 100644 (file)
@@ -1,6 +1,11 @@
 <div class="clearfix diary-comment">
   <%= user_thumbnail diary_comment.user %>
-  <p class="deemphasize comment-heading" id="comment<%= diary_comment.id %>"><%= raw(t('.comment_from', :link_user => (link_to h(diary_comment.user.display_name), user_path(diary_comment.user)), :comment_created_at => link_to(l(diary_comment.created_at, :format => :friendly), :anchor => "comment#{diary_comment.id}"))) %></p>
+  <p class="deemphasize comment-heading" id="comment<%= diary_comment.id %>"><%= raw(t('.comment_from', :link_user => (link_to h(diary_comment.user.display_name), user_path(diary_comment.user)), :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 %>
+      | <%= report_link(t(".report"), diary_comment) %>
+    <% end %>
+  </p>
+
   <div class="richtext"><%= diary_comment.body.to_html %></div>
   <% if current_user && current_user.administrator? %>
     <span>
index 848221a2803ac6d0e6fcff71c18dead3ab696334..930f20e5ecd1e48dbe037889e44187c46c0ae97c 100644 (file)
       <li><%= link_to t('.edit_link'), :action => 'edit', :display_name => diary_entry.user.display_name, :id => diary_entry.id %></li>
     <% end %>
 
+    <% if current_user and diary_entry.user != current_user %>
+      <li>
+        <%= report_link(t(".report"), diary_entry) %>
+      </li>
+    <% end %>
+
     <% if current_user && current_user.administrator? %>
-      <li><%= link_to t('.hide_link'), hide_diary_entry_path(:display_name => diary_entry.user.display_name, :id => diary_entry.id), :method => :post, :data => { :confirm => t('.confirm') } %></li>
+      <li>
+        <%= link_to t('.hide_link'), hide_diary_entry_path(:display_name => diary_entry.user.display_name, :id => diary_entry.id), :method => :post, :data => { :confirm => t('.confirm') } %>
+      </li>
     <% end %>
   </ul>
 </div>
diff --git a/app/views/issues/_comments.html.erb b/app/views/issues/_comments.html.erb
new file mode 100644 (file)
index 0000000..4b5ae4a
--- /dev/null
@@ -0,0 +1,25 @@
+<div class="issue-comments">
+  <% comments.each do |comment| %>
+    <div class="comment">
+      <div style="float:left">
+        <%= link_to user_thumbnail(comment.user), user_path(comment.user.display_name) %>
+      </div>
+      <b> <%= link_to comment.user.display_name, user_path(comment.user.display_name) %> </b> <br/>
+      <%= comment.body %>
+    </div>
+    <span class="deemphasize">
+      <%= t(".created_at", :datetime => l(comment.created_at.to_datetime, :format => :friendly)) %>
+    </span>
+    <hr>
+  <% end %>
+</div>
+<br/>
+<div class="comment">
+  <%= 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('.reassign_param') %> <%= check_box_tag :reassign, true %>
+  <br/>
+  <br/>
+  <%= submit_tag 'Submit' %>
+  <% end %>
+</div>
diff --git a/app/views/issues/_reports.html.erb b/app/views/issues/_reports.html.erb
new file mode 100644 (file)
index 0000000..b499288
--- /dev/null
@@ -0,0 +1,16 @@
+<% reports.each do |report| %>
+  <div class="report">
+    <div style="float:left">
+      <%= link_to user_thumbnail(report.user), user_path(report.user.display_name) %>
+    </div>
+    <%= t ".reported_by_html", :category => report.category, :user => link_to(report.user.display_name, user_path(report.user.display_name)) %>
+    <br/>
+    <span class="deemphasize">
+      <%= t(".updated_at", :datetime => l(report.updated_at.to_datetime, :format => :friendly)) %>
+    </span>
+    <br/>
+    <%= report.details %>
+    <br/>
+  </div>
+  <hr>
+<% end %>
diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb
new file mode 100644 (file)
index 0000000..e5e7ebc
--- /dev/null
@@ -0,0 +1,51 @@
+<% content_for :heading do %>
+  <h1><%= t ".title" %></h1>
+<% end %>
+
+<%= form_tag(issues_path, :method => :get) do %>
+<p><%= t ".search_guidance" %></p>
+<%= select_tag :status, options_for_select(Issue.aasm.states.map(&:name).map{|state| [t(".states.#{state}"), state]}, params[:status]), :include_blank => t(".select_status"), :data => { :behavior => 'category_dropdown' } %>
+<%= select_tag :issue_type, options_for_select(@issue_types, params[:issue_type]), :include_blank => t(".select_type"), :data => { :behavior => 'category_dropdown' } %>
+<%= text_field_tag :search_by_user, params[:search_by_user], placeholder: t(".reported_user") %>
+<%= select_tag :last_updated_by, options_for_select(@users.all.collect{|f| [f.display_name, f.id]} << [ t(".not_updated"), "nil"], params[:last_updated_by]), :include_blank => t(".select_last_updated_by"), :data => { :behavior => 'category_dropdown' } %>
+<%= submit_tag t(".search"), :name => nil %>
+<% end %>
+<br/>
+
+<% if @issues.length == 0 %>
+  <p><%= t ".issues_not_found" %></p>
+<% end %>
+
+<br/>
+
+<table class="issues-list">
+  <thead>
+    <tr>
+      <th><%= t ".status" %></th>
+      <th><%= t ".reports" %></th>
+      <th><%= t ".reported_item" %></th>
+      <th><%= t ".reported_user" %></th>
+      <th><%= t ".last_updated" %></th>
+    </tr>
+  </thead>
+  <tbody>
+    <% @issues.each do |issue| %>
+      <tr>
+        <td><%= t ".states.#{issue.status}" %></td>
+        <td><%= link_to t(".reports_count", :count => issue.reports_count), issue %></td>
+        <td><%= link_to reportable_title(issue.reportable), reportable_url(issue.reportable) %></td>
+        <td><%= link_to issue.reported_user.display_name, user_path(issue.reported_user.display_name) if issue.reported_user %></td>
+        <td>
+          <% if issue.user_updated %>
+            <%= t ".last_updated_time_user_html", :user => link_to(issue.user_updated.display_name, user_path(issue.user_updated.display_name)),
+                                                  :time => distance_of_time_in_words_to_now(issue.updated_at),
+                                                  :title => l(issue.updated_at) %>
+          <% else %>
+            <%= t ".last_updated_time_html", :time => distance_of_time_in_words_to_now(issue.updated_at),
+                                             :title => l(issue.updated_at) %>
+          <% end %>
+        </td>
+      </tr>
+    <% end %>
+  </tbody>
+</table>
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb
new file mode 100644 (file)
index 0000000..6612f06
--- /dev/null
@@ -0,0 +1,62 @@
+<% content_for :heading do %>
+<h2><%= t ".title", :status => @issue.status.humanize, :issue_id => @issue.id %></h2>
+<p><%= @issue.reportable.model_name.human %> : <%= link_to reportable_title(@issue.reportable), reportable_url(@issue.reportable) %></p>
+<p class="deemphasize">
+  <small>
+    <%= @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 %>
+  </small>
+</p>
+<p>
+  <%= link_to t('.resolve'), resolve_issue_url(@issue), :method => :post if @issue.may_resolve? %>
+  <% if @issue.may_ignore? %>
+  | <%= link_to t('.ignore'), ignore_issue_url(@issue), :method => :post %>
+  <% end %>
+</p>
+<p><%= link_to t('.reopen'), reopen_issue_url(@issue), :method => :post if @issue.may_reopen? %></p>
+<% end %>
+
+<div class="report-related-block">
+
+  <div class="report-block">
+    <h3><%= t ".reports_of_this_issue" %></h3>
+
+    <% if @read_reports.present? %>
+    <div class="read-reports">
+      <h4><%= t ".read_reports" %></h4>
+      <%= render 'reports', reports: @read_reports %>
+    </div>
+    <% end %>
+
+    <% if @unread_reports.any? %>
+    <div class="unread-reports">
+      <h4><%= t ".new_reports" %></h4>
+      <%= render 'reports', reports: @unread_reports %>
+    </div>
+    <% end %>
+    <br/>
+  </div>
+
+  <% if @issue.reported_user %>
+    <div class="related-reports">
+      <h3><%= t ".other_issues_against_this_user" %></h3>
+      <% if @related_issues.count > 1 %>
+        <ul>
+          <% @related_issues.each do |issue| %>
+            <% if issue.id != @issue.id %>
+              <li><%= link_to reportable_title(issue.reportable), issue %></li>
+            <% end %>
+          <% end %>
+        </ul>
+      <% else %>
+        <p><%= t ".no_other_issues" %></p>
+      <% end %>
+    </div>
+  <% end %>
+</div>
+
+<h3><%= t ".comments_on_this_issue" %></h3>
+<%= render 'comments', comments: @comments %>
index 8411fefdb71a8f5316c6d5b0adad0a9df9ceeea0..c9107e7a83d16111d8308f7cda6c0e5d30b38d66 100644 (file)
@@ -38,6 +38,9 @@
   </nav>
   <nav class='secondary'>
     <ul>
+      <% if current_user and ( current_user.administrator? or current_user.moderator? ) %>
+        <li class="compact-hide <%= current_page_class(issues_path) %>"><%= link_to t('layouts.issues'), issues_path %></li>
+      <% end %>
       <li class="compact-hide <%= current_page_class(traces_path) %>"><%= link_to t('layouts.gps_traces'), traces_path %></li>
       <li class="compact-hide <%= current_page_class(diary_path) %>"><%= link_to t('layouts.user_diaries'), diary_path %></li>
       <li class="compact-hide <%= current_page_class(copyright_path) %>"><%= link_to t('layouts.copyright'), copyright_path %></li>
diff --git a/app/views/reports/new.html.erb b/app/views/reports/new.html.erb
new file mode 100644 (file)
index 0000000..7023667
--- /dev/null
@@ -0,0 +1,42 @@
+<% content_for :heading do %>
+  <h1><%= t ".title_html", :link => link_to(reportable_title(@report.issue.reportable), reportable_url(@report.issue.reportable)) %></h1>
+<% end %>
+
+<div class="report-disclaimer">
+  <%= t('.disclaimer.intro') %>
+  <ul>
+    <li> <%= t('.disclaimer.not_just_mistake') %> </li>
+    <li> <%= t('.disclaimer.unable_to_fix') %> </li>
+    <li> <%= t('.disclaimer.resolve_with_user') %> </li>
+  </ul>
+</div>
+
+<%= form_for(@report) do |f| %>
+  <%= f.error_messages %>
+  <fieldset>
+    <%= f.fields_for @report.issue do |issue_form| %>
+      <%= issue_form.hidden_field :reportable_id %>
+      <%= issue_form.hidden_field :reportable_type %>
+    <% end %>
+
+    <div class='form-row'>
+      <p><%= t('.select') %></p>
+      <ul>
+      <% Report.categories_for(@report.issue.reportable).each do |c| %>
+        <li>
+          <%= radio_button :report, :category, c %>
+          <%= label_tag "report_category_#{c}", t(".categories.#{@report.issue.reportable.class.name.underscore}.#{c}") %> <br/>
+        </li>
+      <% end %>
+      </ul>
+    </div>
+
+    <div class='form-row'>
+      <%= text_area :report, :details, :cols => 20, :rows => 5, placeholder: t('.details'), required: true %>
+    </div>
+
+    <div class='buttons'>
+      <%= f.submit %>
+    </div>
+  </fieldset>
+<% end %>
index 3a09165b9d34ba8eafadfb0a5ddc96becc2352b4..0acb3f0145a622e05207d1bb109c18083064e503 100644 (file)
             </li>
           <% end %>
 
+          <% if current_user and @user.id != current_user.id %>
+            <li>
+              <%= report_link(t(".report"), @user) %>
+            </li>
+          <% end %>
         </ul>
 
       <% end %>
index a7efaf98566a9426c432ed7a34b63f3b836fc017..17549940a59b0a24cef04964173e4028f2350c9d 100644 (file)
@@ -207,6 +207,7 @@ en:
       reopened_by: "Reactivated by %{user} <abbr title='%{exact_time}'>%{when} ago</abbr>"
       reopened_by_anonymous: "Reactivated by anonymous <abbr title='%{exact_time}'>%{when} ago</abbr>"
       hidden_by: "Hidden by %{user} <abbr title='%{exact_time}'>%{when} ago</abbr>"
+      report: Report this note
     query:
       title: "Query Features"
       introduction: "Click on the map to find nearby features."
@@ -297,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"
@@ -918,6 +921,106 @@ en:
     results:
       no_results: "No results found"
       more_results: "More results"
+  issues:
+    index:
+      title: Issues
+      select_status: Select Status
+      select_type: Select Type
+      select_last_updated_by: Select Last Updated By
+      reported_user: Reported User
+      not_updated: Not Updated
+      search: Search
+      search_guidance: "Search Issues:"
+      user_not_found: User does not exist
+      issues_not_found: No such issues found
+      status: Status
+      reports: Reports
+      last_updated: Last Updated
+      last_updated_time_html: "<abbr title='%{title}'>%{time} ago</abbr>"
+      last_updated_time_user_html: "<abbr title='%{title}'>%{time} ago</abbr> by %{user}"
+      link_to_reports: View Reports
+      reported_user: Reported User
+      reports_count:
+        one: "1 Report"
+        other: "%{count} Reports"
+      reported_item: Reported Item
+      states:
+        ignored: Ignored
+        open: Open
+        resolved: Resolved
+    update:
+      new_report: Your report has been registered sucessfully
+      successful_update: Your report has been updated successfully
+      provide_details: Please provide the required details
+    show:
+      title: "%{status} Issue #%{issue_id}"
+      reports:
+        zero: No reports
+        one: 1 report
+        other: "%{count} reports"
+      report_created_at: "First reported at %{datetime}"
+      last_resolved_at: "Last resolved at %{datetime}"
+      last_updated_at: "Last updated at %{datetime} by %{displayname}"
+      resolve: Resolve
+      ignore: Ignore
+      reopen: Reopen
+      reports_of_this_issue: Reports of this Issue
+      read_reports: Read Reports
+      new_reports: New Reports
+      other_issues_against_this_user: Other issues against this user
+      no_other_issues: No other issues against this user.
+      comments_on_this_issue: Comments on this issue
+    resolve:
+      resolved: Issue status has been set to 'Resolved'
+    ignore:
+      ignored: Issue status has been set to 'Ignored'
+    reopen:
+      reopened: Issue status has been set to 'Open'
+    comments:
+      created_at: "On %{datetime}"
+      reassign_param: Reassign Issue?
+    reports:
+      updated_at: "On %{datetime}"
+      reported_by_html: "Reported as %{category} by %{user}"
+  issue_comments:
+    create:
+      comment_created: Your comment was successfully created
+  reports:
+    new:
+      title_html: "Report %{link}"
+      missing_params: "Cannot create a new report"
+      details: Please provide some more details about the problem (required).
+      select: "Select a reason for your report:"
+      disclaimer:
+        intro: "Before sending your report to the site moderators, please ensure that:"
+        not_just_mistake: You are certain that the problem is not just a mistake
+        unable_to_fix: You are unable to fix the problem yourself or with the help of your fellow community members
+        resolve_with_user: You have already tried to resolve the problem with the user concerned
+      categories:
+        diary_entry:
+          spam: This diary entry is/contains spam
+          offensive: This diary entry is obscene/offensive
+          threat: This diary entry contains a threat
+          other: Other
+        diary_comment:
+          spam: This diary comment is/contains spam
+          offensive: This diary comment is obscene/offensive
+          threat: This diary comment contains a threat
+          other: Other
+        user:
+          spam: This user profile is/contains spam
+          offensive: This user profile is obscene/offensive
+          threat: This user profile contains a threat
+          vandal: This user is a vandal
+          other: Other
+        note:
+          spam: This note is spam
+          personal: This note contains personal data
+          abusive: This note is abusive
+          other: Other
+    create:
+      successful_report: Your report has been registered sucessfully
+      provide_details: Please provide the required details      
   layouts:
     project_name:
       # in <title>
@@ -936,6 +1039,7 @@ en:
     edit: Edit
     history: History
     export: Export
+    issues: Issues
     data: Data
     export_data: Export Data
     gps_traces: GPS Traces
@@ -1684,8 +1788,12 @@ 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."
+    require_moderator_or_admin:
+      not_a_moderator_or_admin: You need to be a moderator or an admin to perform that action
     setup_user_auth:
       blocked_zero_hour: "You have an urgent message on the OpenStreetMap web site. You need to read the message before you will be able to save your edits."
       blocked: "Your access to the API has been blocked. Please log-in to the web interface to find out more."
@@ -1955,6 +2063,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"
index 741268e4623ee6202d6791d58e682ef729f72fa7..16ba85754f3684b6ea7c5835b48694b4d4568832 100644 (file)
@@ -297,6 +297,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 (file)
index 0000000..544ddbd
--- /dev/null
@@ -0,0 +1,64 @@
+require "migrate"
+
+class CreateIssuesAndReports < ActiveRecord::Migration[5.0]
+  def up
+    create_enumeration :issue_status_enum, %w[open ignored resolved]
+
+    create_table :issues do |t|
+      t.string :reportable_type, :null => false
+      t.integer :reportable_id, :null => false
+      t.integer :reported_user_id
+      t.column :status, :issue_status_enum, :null => false, :default => "open"
+      t.column :assigned_role, :user_role_enum, :null => false
+      t.datetime :resolved_at
+      t.integer :resolved_by
+      t.integer :updated_by
+      t.integer :reports_count, :default => 0
+      t.timestamps :null => false
+    end
+
+    add_foreign_key :issues, :users, :column => :reported_user_id, :name => "issues_reported_user_id_fkey"
+    add_foreign_key :issues, :users, :column => :resolved_by, :name => "issues_resolved_by_fkey"
+    add_foreign_key :issues, :users, :column => :updated_by, :name => "issues_updated_by_fkey"
+
+    add_index :issues, [:reportable_type, :reportable_id]
+    add_index :issues, [:reported_user_id]
+    add_index :issues, [:status]
+    add_index :issues, [:assigned_role]
+    add_index :issues, [:updated_by]
+
+    create_table :reports do |t|
+      t.integer :issue_id, :null => false
+      t.integer :user_id, :null => false
+      t.text :details, :null => false
+      t.string :category, :null => false
+      t.timestamps :null => false
+    end
+
+    add_foreign_key :reports, :issues, :name => "reports_issue_id_fkey"
+    add_foreign_key :reports, :users, :column => :user_id, :name => "reports_user_id_fkey"
+
+    add_index :reports, :issue_id
+    add_index :reports, :user_id
+
+    create_table :issue_comments do |t|
+      t.integer :issue_id, :null => false
+      t.integer :user_id, :null => false
+      t.text :body, :null => false
+      t.timestamps :null => false
+    end
+
+    add_foreign_key :issue_comments, :issues, :name => "issue_comments_issue_id_fkey"
+    add_foreign_key :issue_comments, :users, :column => :user_id, :name => "issue_comments_user_id_fkey"
+
+    add_index :issue_comments, :issue_id
+    add_index :issue_comments, :user_id
+  end
+
+  def down
+    drop_table :issue_comments
+    drop_table :reports
+    drop_table :issues
+    drop_enumeration :issue_status_enum
+  end
+end
index 8803c0c02a41088209668ab15854a679a44332e0..48fedeba56b65d1247762f7a35eb29f9a5a1b98b 100644 (file)
@@ -60,6 +60,17 @@ CREATE TYPE gpx_visibility_enum AS ENUM (
 );
 
 
+--
+-- Name: issue_status_enum; Type: TYPE; Schema: public; Owner: -
+--
+
+CREATE TYPE issue_status_enum AS ENUM (
+    'open',
+    'ignored',
+    'resolved'
+);
+
+
 --
 -- Name: note_event_enum; Type: TYPE; Schema: public; Owner: -
 --
@@ -679,6 +690,78 @@ 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,
+    user_id integer NOT NULL,
+    body text NOT NULL,
+    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,
+    status issue_status_enum DEFAULT 'open'::public.issue_status_enum NOT NULL,
+    assigned_role user_role_enum NOT NULL,
+    resolved_at timestamp without time zone,
+    resolved_by integer,
+    updated_by integer,
+    reports_count integer DEFAULT 0,
+    created_at timestamp without time zone NOT NULL,
+    updated_at timestamp without time zone NOT NULL
+);
+
+
+--
+-- 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: -
 --
@@ -980,6 +1063,40 @@ CREATE TABLE relations (
 );
 
 
+--
+-- Name: reports; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE reports (
+    id integer NOT NULL,
+    issue_id integer NOT NULL,
+    user_id integer NOT NULL,
+    details text NOT NULL,
+    category character varying NOT NULL,
+    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: -
 --
@@ -1283,6 +1400,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: issue_comments id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issue_comments ALTER COLUMN id SET DEFAULT nextval('issue_comments_id_seq'::regclass);
+
+
+--
+-- Name: issues id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issues ALTER COLUMN id SET DEFAULT nextval('issues_id_seq'::regclass);
+
+
 --
 -- Name: messages id; Type: DEFAULT; Schema: public; Owner: -
 --
@@ -1325,6 +1456,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: reports id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY reports ALTER COLUMN id SET DEFAULT nextval('reports_id_seq'::regclass);
+
+
 --
 -- Name: user_blocks id; Type: DEFAULT; Schema: public; Owner: -
 --
@@ -1505,6 +1643,22 @@ ALTER TABLE ONLY gpx_files
     ADD CONSTRAINT gpx_files_pkey PRIMARY KEY (id);
 
 
+--
+-- Name: issue_comments issue_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issue_comments
+    ADD CONSTRAINT issue_comments_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: issues issues_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issues
+    ADD CONSTRAINT issues_pkey PRIMARY KEY (id);
+
+
 --
 -- Name: languages languages_pkey; Type: CONSTRAINT; Schema: public; Owner: -
 --
@@ -1601,6 +1755,14 @@ ALTER TABLE ONLY relations
     ADD CONSTRAINT relations_pkey PRIMARY KEY (relation_id, version);
 
 
+--
+-- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY reports
+    ADD CONSTRAINT reports_pkey PRIMARY KEY (id);
+
+
 --
 -- Name: user_blocks user_blocks_pkey; Type: CONSTRAINT; Schema: public; Owner: -
 --
@@ -1875,6 +2037,55 @@ 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_issue_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_issue_comments_on_issue_id ON issue_comments USING btree (issue_id);
+
+
+--
+-- Name: index_issue_comments_on_user_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_issue_comments_on_user_id ON issue_comments USING btree (user_id);
+
+
+--
+-- Name: index_issues_on_assigned_role; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_issues_on_assigned_role ON issues USING btree (assigned_role);
+
+
+--
+-- Name: index_issues_on_reportable_type_and_reportable_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_issues_on_reportable_type_and_reportable_id ON issues USING btree (reportable_type, reportable_id);
+
+
+--
+-- 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_status; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_issues_on_status ON issues USING btree (status);
+
+
+--
+-- 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: -
 --
@@ -1910,6 +2121,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_user_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_reports_on_user_id ON reports USING btree (user_id);
+
+
 --
 -- Name: index_user_blocks_on_user_id; Type: INDEX; Schema: public; Owner: -
 --
@@ -2329,6 +2554,46 @@ ALTER TABLE ONLY gpx_files
     ADD CONSTRAINT gpx_files_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
 
 
+--
+-- Name: issue_comments 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);
+
+
+--
+-- Name: issue_comments issue_comments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issue_comments
+    ADD CONSTRAINT issue_comments_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
+
+
+--
+-- Name: issues 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);
+
+
+--
+-- Name: issues issues_resolved_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY issues
+    ADD CONSTRAINT issues_resolved_by_fkey FOREIGN KEY (resolved_by) REFERENCES users(id);
+
+
+--
+-- Name: issues 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);
+
+
 --
 -- Name: messages messages_from_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
@@ -2441,6 +2706,22 @@ ALTER TABLE ONLY relations
     ADD CONSTRAINT relations_redaction_id_fkey FOREIGN KEY (redaction_id) REFERENCES redactions(id);
 
 
+--
+-- Name: reports 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);
+
+
+--
+-- Name: reports reports_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY reports
+    ADD CONSTRAINT reports_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
+
+
 --
 -- Name: user_blocks user_blocks_moderator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
@@ -2584,6 +2865,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20150111192335'),
 ('20150222101847'),
 ('20150818224516'),
+('20160822153055'),
 ('20161002153425'),
 ('20161011010929'),
 ('20170222134109'),
@@ -2632,5 +2914,3 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('7'),
 ('8'),
 ('9');
-
-
diff --git a/test/controllers/issue_comments_controller_test.rb b/test/controllers/issue_comments_controller_test.rb
new file mode 100644 (file)
index 0000000..00c80f3
--- /dev/null
@@ -0,0 +1,26 @@
+require "test_helper"
+
+class IssueCommentsControllerTest < ActionController::TestCase
+  def test_comment_by_normal_user
+    issue = create(:issue)
+
+    # Login as normal user
+    session[:user] = create(:user).id
+
+    post :create, :params => { :issue_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
+
+    post :create, :params => { :issue_id => issue.id, :issue_comment => { :body => "test comment" } }
+    assert_response :redirect
+    assert_redirected_to issue
+    assert_equal 1, issue.comments.length
+  end
+end
diff --git a/test/controllers/issues_controller_test.rb b/test/controllers/issues_controller_test.rb
new file mode 100644 (file)
index 0000000..ff99b0c
--- /dev/null
@@ -0,0 +1,69 @@
+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
+    assert_response :success
+
+    # Access issues_path by moderator
+    session[:user] = create(:moderator_user).id
+    get :index
+    assert_response :success
+  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 1, Issue.count
+
+    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 true, Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").resolved?
+    assert_response :redirect
+
+    # Test 'Reopen'
+    get :reopen, :params => { :id => issue.id }
+    assert_equal true, Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").open?
+    assert_response :redirect
+
+    # Test 'Ignored'
+    get :ignore, :params => { :id => issue.id }
+    assert_equal true, Issue.find_by(:reportable_id => target_user, :reportable_type => "User").ignored?
+    assert_response :redirect
+  end
+end
diff --git a/test/controllers/reports_controller_test.rb b/test/controllers/reports_controller_test.rb
new file mode 100644 (file)
index 0000000..d9bc190
--- /dev/null
@@ -0,0 +1,131 @@
+require "test_helper"
+
+class ReportsControllerTest < ActionController::TestCase
+  def test_new_report_without_login
+    target_user = create(:user)
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => new_report_path(:reportable_id => target_user.id, :reportable_type => "User"))
+  end
+
+  def test_new_report_after_login
+    target_user = create(:user)
+
+    session[:user] = create(:user).id
+
+    assert_equal 0, Issue.count
+
+    # Create an Issue and a report
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :success
+    assert_difference "Issue.count", 1 do
+      details = "Details of a report"
+      category = "other"
+      post :create,
+           :params => {
+             :report => {
+               :details => details,
+               :category => category,
+               :issue => { :reportable_id => target_user.id, :reportable_type => "User" }
+             }
+           }
+    end
+    assert_equal 1, Issue.count
+    assert_response :redirect
+    assert_redirected_to user_path(target_user.display_name)
+  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 0, Issue.count
+
+    # Create an Issue and a report
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :success
+    assert_difference "Issue.count", 1 do
+      details = "Details of a report"
+      category = "other"
+      post :create,
+           :params => {
+             :report => {
+               :details => details,
+               :category => category,
+               :issue => { :reportable_id => target_user.id, :reportable_type => "User" }
+             }
+           }
+    end
+    assert_equal 1, Issue.count
+    assert_response :redirect
+    assert_redirected_to user_path(target_user.display_name)
+
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :success
+
+    # Report without details
+    assert_no_difference "Issue.count" do
+      category = "other"
+      post :create,
+           :params => {
+             :report => {
+               :category => category,
+               :issue => { :reportable_id => 1, :reportable_type => "User" }
+             }
+           }
+    end
+    assert_response :redirect
+    assert_equal 1, Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").reports.count
+  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 0, Issue.count
+
+    # Create an Issue and a report
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :success
+    assert_difference "Issue.count", 1 do
+      details = "Details of a report"
+      category = "other"
+      post :create,
+           :params => {
+             :report => {
+               :details => details,
+               :category => category,
+               :issue => { :reportable_id => target_user.id, :reportable_type => "User" }
+             }
+           }
+    end
+    assert_equal 1, Issue.count
+    assert_response :redirect
+    assert_redirected_to user_path(target_user.display_name)
+
+    # Create a report for an existing Issue
+    get :new, :params => { :reportable_id => target_user.id, :reportable_type => "User" }
+    assert_response :success
+    assert_no_difference "Issue.count" do
+      details = "Details of another report under the same issue"
+      category = "other"
+      post :create,
+           :params => {
+             :report => {
+               :details => details,
+               :category => category,
+               :issue => { :reportable_id => target_user.id, :reportable_type => "User" }
+             }
+           }
+    end
+    assert_response :redirect
+    report_count = Issue.find_by(:reportable_id => target_user.id, :reportable_type => "User").reports.count
+    assert_equal 2, report_count
+  end
+end
diff --git a/test/factories/issues.rb b/test/factories/issues.rb
new file mode 100644 (file)
index 0000000..913c47f
--- /dev/null
@@ -0,0 +1,7 @@
+FactoryBot.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 (file)
index 0000000..7936d46
--- /dev/null
@@ -0,0 +1,8 @@
+FactoryBot.define do
+  factory :report do
+    sequence(:details) { |n| "Report details #{n}" }
+    category "other"
+    issue
+    user
+  end
+end
diff --git a/test/models/issue_comment_test.rb b/test/models/issue_comment_test.rb
new file mode 100644 (file)
index 0000000..53ba358
--- /dev/null
@@ -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 (file)
index 0000000..2572129
--- /dev/null
@@ -0,0 +1,58 @@
+require "test_helper"
+
+class IssueTest < ActiveSupport::TestCase
+  def test_assigned_role
+    issue = create(:issue)
+
+    assert issue.valid?
+    issue.assigned_role = "bogus"
+    assert_not issue.valid?
+  end
+
+  def test_reported_user
+    note = create(:note_comment, :author => create(:user)).note
+    anonymous_note = create(:note_comment, :author => nil).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
+
+    issue.reportable = note
+    issue.save!
+    assert_equal issue.reported_user, note.author
+
+    issue.reportable = anonymous_note
+    issue.save!
+    assert_nil issue.reported_user
+
+    issue.reportable = diary_entry
+    issue.save!
+    assert_equal issue.reported_user, diary_entry.user
+  end
+
+  def test_default_assigned_role
+    create(:language, :code => "en")
+    diary_entry = create(:diary_entry)
+    note = create(:note_with_comments)
+
+    issue = Issue.new
+    issue.reportable = diary_entry
+    issue.save!
+    assert_equal "administrator", issue.assigned_role
+
+    issue = Issue.new
+    issue.reportable = note
+    issue.save!
+    assert_equal "moderator", issue.assigned_role
+
+    # check the callback doesn't override an explicitly set role
+    issue.assigned_role = "administrator"
+    issue.save!
+    issue.reload
+    assert_equal "administrator", issue.assigned_role
+  end
+end
diff --git a/test/models/report_test.rb b/test/models/report_test.rb
new file mode 100644 (file)
index 0000000..b65753b
--- /dev/null
@@ -0,0 +1,35 @@
+require "test_helper"
+
+class ReportTest < ActiveSupport::TestCase
+  def test_issue_required
+    report = create(:report)
+
+    assert report.valid?
+    report.issue = nil
+    assert_not report.valid?
+  end
+
+  def test_user_required
+    report = create(:report)
+
+    assert report.valid?
+    report.user = nil
+    assert_not report.valid?
+  end
+
+  def test_details_required
+    report = create(:report)
+
+    assert report.valid?
+    report.details = ""
+    assert_not report.valid?
+  end
+
+  def test_category_required
+    report = create(:report)
+
+    assert report.valid?
+    report.category = ""
+    assert_not report.valid?
+  end
+end
diff --git a/test/system/issues_test.rb b/test/system/issues_test.rb
new file mode 100644 (file)
index 0000000..b77908f
--- /dev/null
@@ -0,0 +1,116 @@
+require "application_system_test_case"
+
+class IssuesTest < ApplicationSystemTestCase
+  include IssuesHelper
+
+  def test_view_issues_not_logged_in
+    visit issues_path
+    assert page.has_content?(I18n.t("user.login.title"))
+  end
+
+  def test_view_issues_normal_user
+    sign_in_as(create(:user))
+
+    visit issues_path
+    assert page.has_content?(I18n.t("application.require_moderator_or_admin.not_a_moderator_or_admin"))
+  end
+
+  def test_view_no_issues
+    sign_in_as(create(:moderator_user))
+
+    visit issues_path
+    assert page.has_content?(I18n.t("issues.index.issues_not_found"))
+  end
+
+  def test_view_issues
+    sign_in_as(create(:moderator_user))
+    issues = create_list(:issue, 3, :assigned_role => "moderator")
+
+    visit issues_path
+    assert page.has_content?(issues.first.reported_user.display_name)
+  end
+
+  def test_view_issues_with_no_reported_user
+    sign_in_as(create(:moderator_user))
+    anonymous_note = create(:note_with_comments)
+    issue = create(:issue, :reportable => anonymous_note)
+
+    visit issues_path
+    assert page.has_content?(reportable_title(anonymous_note))
+
+    visit issue_path(issue)
+    assert page.has_content?(reportable_title(anonymous_note))
+  end
+
+  def test_search_issues_by_user
+    good_user = create(:user)
+    bad_user = create(:user)
+    create(:issue, :reportable => bad_user, :reported_user => bad_user, :assigned_role => "administrator")
+
+    sign_in_as(create(:administrator_user))
+
+    # No issues against the user
+    visit issues_path
+    fill_in "search_by_user", :with => good_user.display_name
+    click_on "Search"
+    assert page.has_content?(I18n.t("issues.index.issues_not_found"))
+
+    # User doesn't exist
+    visit issues_path
+    fill_in "search_by_user", :with => "Nonexistant User"
+    click_on "Search"
+    assert page.has_content?(I18n.t("issues.index.user_not_found"))
+
+    # Find Issue against bad_user
+    visit issues_path
+    fill_in "search_by_user", :with => bad_user.display_name
+    click_on "Search"
+    assert_not page.has_content?(I18n.t("issues.index.issues_not_found"))
+  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("issue_comments.create.comment_created"))
+    assert page.has_content?("test comment")
+
+    issue.reload
+    assert_equal issue.comments.first.body, "test comment"
+  end
+
+  def test_reassign_issue
+    issue = create(:issue)
+    assert_equal "administrator", issue.assigned_role
+    sign_in_as(create(:administrator_user))
+
+    visit issue_path(issue)
+
+    fill_in :issue_comment_body, :with => "reassigning to moderators"
+    check :reassign
+    click_on "Submit"
+
+    issue.reload
+    assert_equal "moderator", issue.assigned_role
+  end
+
+  def test_issue_index_with_multiple_roles
+    user1 = create(:user)
+    user2 = create(:user)
+    issue1 = create(:issue, :reportable => user1, :assigned_role => "administrator")
+    issue2 = create(:issue, :reportable => user2, :assigned_role => "moderator")
+
+    user = create(:administrator_user)
+    create(:user_role, :user => user, :role => "moderator")
+    sign_in_as(user)
+
+    visit issues_path
+
+    assert page.has_link?(I18n.t("issues.index.reports_count", :count => issue1.reports_count), :href => issue_path(issue1))
+    assert page.has_link?(I18n.t("issues.index.reports_count", :count => issue2.reports_count), :href => issue_path(issue2))
+  end
+end
diff --git a/test/system/report_diary_comment_test.rb b/test/system/report_diary_comment_test.rb
new file mode 100644 (file)
index 0000000..852bcfa
--- /dev/null
@@ -0,0 +1,35 @@
+require "application_system_test_case"
+
+class ReportDiaryCommentTest < ApplicationSystemTestCase
+  def setup
+    create(:language, :code => "en")
+    @diary_entry = create(:diary_entry)
+    @comment = create(:diary_comment, :diary_entry => @diary_entry)
+  end
+
+  def test_no_link_when_not_logged_in
+    visit diary_entry_path(@diary_entry.user.display_name, @diary_entry)
+    assert page.has_content?(@comment.body)
+
+    assert_not page.has_content?(I18n.t("diary_entry.diary_comment.report"))
+  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 I18n.t("diary_entry.diary_comment.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.diary_comment.spam")
+    fill_in "report_details", :with => "This comment is spam"
+    click_on "Create Report"
+
+    assert page.has_content? "Your report has been registered sucessfully"
+
+    assert_equal 1, Issue.count
+    assert Issue.last.reportable == @comment
+  end
+end
diff --git a/test/system/report_diary_entry_test.rb b/test/system/report_diary_entry_test.rb
new file mode 100644 (file)
index 0000000..1575277
--- /dev/null
@@ -0,0 +1,58 @@
+require "application_system_test_case"
+
+class ReportDiaryEntryTest < ApplicationSystemTestCase
+  def setup
+    create(:language, :code => "en")
+    @diary_entry = create(:diary_entry)
+  end
+
+  def test_no_link_when_not_logged_in
+    visit diary_entry_path(@diary_entry.user.display_name, @diary_entry)
+    assert page.has_content?(@diary_entry.title)
+
+    assert_not page.has_content?(I18n.t("diary_entry.diary_entry.report"))
+  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 I18n.t("diary_entry.diary_entry.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.diary_entry.spam")
+    fill_in "report_details", :with => "This is advertising"
+    click_on "Create Report"
+
+    assert page.has_content? "Your report has been registered sucessfully"
+  end
+
+  def test_it_reopens_issue
+    issue = create(:issue, :reportable => @diary_entry)
+    issue.resolve!
+
+    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 I18n.t("diary_entry.diary_entry.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.diary_entry.spam")
+    fill_in "report_details", :with => "This is advertising"
+    click_on "Create Report"
+
+    issue.reload
+    assert_not issue.resolved?
+    assert issue.open?
+  end
+
+  def test_missing_report_params
+    sign_in_as(create(:user))
+    visit new_report_path
+    assert page.has_content? I18n.t("reports.new.missing_params")
+  end
+end
diff --git a/test/system/report_note_test.rb b/test/system/report_note_test.rb
new file mode 100644 (file)
index 0000000..759306e
--- /dev/null
@@ -0,0 +1,49 @@
+require "application_system_test_case"
+
+class ReportNoteTest < ApplicationSystemTestCase
+  def test_no_link_when_not_logged_in
+    note = create(:note_with_comments)
+    visit browse_note_path(note)
+    assert page.has_content?(note.comments.first.body)
+
+    assert_not page.has_content?(I18n.t("browse.note.report"))
+  end
+
+  def test_can_report_anonymous_notes
+    note = create(:note_with_comments)
+    sign_in_as(create(:user))
+    visit browse_note_path(note)
+
+    click_on I18n.t("browse.note.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.note.spam")
+    fill_in "report_details", :with => "This is spam"
+    click_on "Create Report"
+
+    assert page.has_content? "Your report has been registered sucessfully"
+
+    assert_equal 1, Issue.count
+    assert Issue.last.reportable == note
+  end
+
+  def test_can_report_notes_with_author
+    note = create(:note_comment, :author => create(:user)).note
+    sign_in_as(create(:user))
+    visit browse_note_path(note)
+
+    click_on I18n.t("browse.note.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.note.spam")
+    fill_in "report_details", :with => "This is spam"
+    click_on "Create Report"
+
+    assert page.has_content? "Your report has been registered sucessfully"
+
+    assert_equal 1, Issue.count
+    assert Issue.last.reportable == note
+  end
+end
diff --git a/test/system/report_user_test.rb b/test/system/report_user_test.rb
new file mode 100644 (file)
index 0000000..8bb7d9d
--- /dev/null
@@ -0,0 +1,30 @@
+require "application_system_test_case"
+
+class ReportUserTest < ApplicationSystemTestCase
+  def test_no_link_when_not_logged_in
+    note = create(:note_with_comments)
+    visit browse_note_path(note)
+    assert page.has_content?(note.comments.first.body)
+
+    assert_not page.has_content?(I18n.t("user.view.report"))
+  end
+
+  def test_can_report_user
+    user = create(:user)
+    sign_in_as(create(:user))
+    visit user_path(user.display_name)
+
+    click_on I18n.t("user.view.report")
+    assert page.has_content? "Report"
+    assert page.has_content? I18n.t("reports.new.disclaimer.intro")
+
+    choose I18n.t("reports.new.categories.user.vandal")
+    fill_in "report_details", :with => "This user is a vandal"
+    click_on "Create Report"
+
+    assert page.has_content? "Your report has been registered sucessfully"
+
+    assert_equal 1, Issue.count
+    assert Issue.last.reportable == user
+  end
+end