Merge branch 'master' into moderation
authorAndy Allan <git@gravitystorm.co.uk>
Wed, 29 Nov 2017 12:18:39 +0000 (12:18 +0000)
committerAndy Allan <git@gravitystorm.co.uk>
Wed, 29 Nov 2017 12:18:39 +0000 (12:18 +0000)
43 files changed:
Gemfile
Gemfile.lock
app/assets/stylesheets/common.scss
app/controllers/diary_entry_controller.rb
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/models/issue.rb [new file with mode: 0644]
app/models/issue_comment.rb [new file with mode: 0644]
app/models/notifier.rb
app/models/report.rb [new file with mode: 0644]
app/models/user.rb
app/views/browse/changeset.html.erb
app/views/browse/note.html.erb
app/views/diary_entry/_diary_comment.html.erb
app/views/diary_entry/_diary_entry.html.erb
app/views/diary_entry/view.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/notifier/new_issue_notification.html.erb [new file with mode: 0644]
app/views/reports/new.html.erb [new file with mode: 0644]
app/views/user/view.html.erb
config/locales/en-GB.yml
config/locales/en.yml
config/routes.rb
db/migrate/20160822153055_create_issues_and_reports.rb [new file with mode: 0644]
db/migrate/20160822153115_create_issue_comments.rb [new file with mode: 0644]
db/migrate/20160822153153_add_reports_count_to_issues.rb [new file with mode: 0644]
db/structure.sql
test/controllers/issues_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/features/can_access_home_test.rb [new file with mode: 0644]
test/features/issues_test.rb [new file with mode: 0644]
test/features/report_diary_entry_test.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/test_helper.rb

diff --git a/Gemfile b/Gemfile
index 79d95f9d5af3e7bc91376002e0e3574997c28aa8..43f39340a812d78ad5ad660ffada06872d2a2fc0 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"
 
@@ -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"
index b65ec4ed9706fc79e552a0b9a4aa859931275967..d67d34d4723d4a670c57f156350b966e8e0998af 100644 (file)
@@ -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
index 6b99662a42c9a844c06ad32dc2ca11bd8bade397..4e89c04c9b6d1924b301338f1f253fd4cab0eb2d 100644 (file)
@@ -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);
+}
index 9e0fd4991bd4f62b6ec60e295efc44cf69aa6b49..88febbe2fe96c520a75dcf04696121be5a627fba 100644 (file)
@@ -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 (file)
index 0000000..ba35b79
--- /dev/null
@@ -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 (file)
index 0000000..e156ea0
--- /dev/null
@@ -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 (file)
index 0000000..6c62deb
--- /dev/null
@@ -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 (file)
index 0000000..7156964
--- /dev/null
@@ -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 (file)
index 0000000..472c860
--- /dev/null
@@ -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 (file)
index 0000000..bbc6261
--- /dev/null
@@ -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
index 8f9e3e2954814e45151632c0ba88ca0db8fa6dfd..54eb9b41852c71622f772a7fb4894558db31172e 100644 (file)
@@ -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 (file)
index 0000000..b857d73
--- /dev/null
@@ -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
index 7a8414ec073c177940ad9601511467d7d89c96e2..678bbad9de34b0466ec916b34c69af5744af0a36 100644 (file)
@@ -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) }
index 86d190680c4a5bb29ba27667c658e273ab2fd724..1fb1e74a43c05b1fc831e4cb7b6ede391761b0ae 100644 (file)
@@ -3,6 +3,11 @@
 <h2>
   <a class="geolink" href="<%= root_path %>"><span class="icon close"></span></a>
   <%= 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 %>
+        &nbsp;&#9872;
+    <% end %>
+  <% end %>
 </h2>
 
 <div class="browse-section">
index 1bacd27d6b2dc3db27b476e2dc06d860a1cf78ef..a7dee697bb5b8cbb7b4a21a1361c69b16fa95579 100644 (file)
@@ -3,6 +3,11 @@
 <h2>
   <a class="geolink" href="<%= root_path %>"><span class="icon close"></span></a>
   <%= 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 %>
+        &nbsp;&#9872;
+    <% end %>
+  <% end %>
 </h2>
 
 <div class="browse-section">
index c651c294304e3e6805009ad3a38b69ab5f468839..a6cf08888565f5cca75c3626a65eb32d912758b2 100644 (file)
@@ -1,6 +1,13 @@
 <div class="clearfix diary-comment">
   <%= user_thumbnail diary_comment.user %>
-  <p class="deemphasize comment-heading" id="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}"))) %></p>
+  <p class="deemphasize comment-heading" id="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 %>
+              &nbsp;&#9872;
+      <% end %>
+    <% end %>
+  </p>
+
   <div class="richtext"><%= diary_comment.body.to_html %></div>
   <%= 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') } %>
index 410e13047663bb32439a90da611a646906216a5c..872e31b3aa347c7e27c69b314ea62bbbeb5d347b 100644 (file)
@@ -6,6 +6,12 @@
 
     <h2><%= link_to h(diary_entry.title), :action => 'view', :display_name => diary_entry.user.display_name, :id => diary_entry.id %></h2>
 
+    <% 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 %>
+            &nbsp;&#9872;
+          <% end %>
+    <% end %>
+
     <small class='deemphasize'>
       <%= 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)) %>
     </small>
index 3c2264d3e58fb849a8793ef1e0aee9da806cd315..5079074e844fbc745a60304f15185af581fbe0bc 100644 (file)
 
 <a id="comments"></a>
 <div class='comments'>
-<%= 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 %>
 </div>
 <%= if_logged_in(:div) do %>
   <h3 id="newcomment"><%= t 'diary_entry.view.leave_a_comment' %></h3>
diff --git a/app/views/issues/_comments.html.erb b/app/views/issues/_comments.html.erb
new file mode 100644 (file)
index 0000000..36a5ec4
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="issue-comments">
+  <% comments.each do |comment| %>
+    <div class="comment">
+      <div style="float:left">
+        <%= link_to user_thumbnail(comment.user), :controller => :user, :action =>:view, :display_name => comment.user.display_name %>
+      </div>
+      <b> <%= link_to comment.user.display_name, :controller => :user, :action =>:view, :display_name => comment.user.display_name %> </b> <br/>
+      <%= comment.body %>
+
+      <% if comment.reassign %>
+      <br/>
+      <i><%= t('issues.show.comments.reassign') %></i>
+      <% end %>
+    </div>
+    <span class="deemphasize">
+      On <%= 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 t('issues.show.comments.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..68e69ee
--- /dev/null
@@ -0,0 +1,15 @@
+<% reports.each do |report| %>
+  <div class="reports">
+    <div style="float:left">
+      <%= link_to user_thumbnail(report.user), :controller => :user, :action =>:view, :display_name => report.user.display_name %>
+    </div>
+    Reported by <b><%= link_to report.user.display_name, :controller => :user, :action =>:view, :display_name => report.user.display_name %></b> <br/>
+    <span class="deemphasize">
+      On <%= 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..a490223
--- /dev/null
@@ -0,0 +1,46 @@
+<% content_for :heading do %>
+  <h1>List of <%= @user_role %> issues:</h1>
+<% end %>
+
+<%= form_tag(issues_path, :method => :get) do %>
+Search for a particular issue(s):  <br/>
+<%= 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 %>
+<br/>
+
+<% if @issues.length == 0 %>
+  <p><%= t ".search.issues_not_found" %></p>
+<% end %>
+
+<br/>
+
+<table>
+  <thead>
+    <tr>
+      <td style="width:40px;"><b> <%= sortable("status") %></b></td>
+      <td style="width:160px;"><b> <%= sortable("reports_count", "Number of Reports") %></b></td>
+      <td style="width:141px;"><b> <%= sortable("updated_at","Last updated at") %></b></td>
+      <td style="width:140px;"><b> <%= sortable("updated_by","Last updated by") %></b></td>
+      <td style="width:203px;"><b> Link to reports </b></td>
+      <td style="width:128px;"><b> <%= sortable("reported_user_id","Reported User") %> </b></td>
+      <td style="width:67px;"><b> Link to reported instance</b></td>
+    </tr>
+  </thead>
+  <tbody>
+    <% @issues.each do |issue| %>
+      <tr>
+        <td><%= issue.status.humanize %></td>
+        <td style="text-align:center;"><%= issue.reports_count %></td>
+        <td><%= l(issue.updated_at.to_datetime, :format => :friendly) %></td>
+        <td><% if issue.user_updated %> <%= issue.user_updated.display_name %> <% else %> - <% end %></td>
+        <td><%= reports_url(issue) %></td>
+        <td><%= link_to issue.reported_user.display_name , :controller => :user, :action => :view, :display_name => issue.reported_user.display_name %></td>
+        <td><%= instance_url(issue.reportable) %></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..e9d68d2
--- /dev/null
@@ -0,0 +1,60 @@
+<% content_for :heading do %>
+<h2> <%= @issue.status.humanize %> Issue #<%= @issue.id %> <br/></h2>
+<p><%= report_type(@issue.reportable_type) %> : <%= reportable_url(@issue.reportable) %></p>
+<p class="deemphasize">
+  <small>
+    <%= @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 %>
+  </small>
+</p>
+<p>
+  <%= 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 %>
+</p>
+<p><%= link_to t('issues.reopen'), reopen_issue_url(@issue), :method => :post if @issue.may_reopen? %></p>
+<% end %>
+
+<div class="report-related-block">
+
+  <div class="report-block">
+    <h3>Reports under this issue:</h3>
+
+    <% if @read_reports.present? %>
+    <div class="read-reports">
+      <h4>Read Reports:</h4>
+      <br/>
+      <%= render 'reports',reports: @read_reports %>
+    </div>
+    <% end %>
+
+    <% if @unread_reports.any? %>
+    <div class="unread-reports">
+      <h4>New Reports:</h4>
+      <br/>
+      <%= render 'reports',reports: @unread_reports %>
+    </div>
+    <% end %>
+    <br/>
+  </div>
+
+  <div class="related-block">
+    <h3> Other issues against this user: </h3>
+    <div class="unread-reports">
+      <% if @related_issues.count > 1 %>
+        <% @related_issues.each do |issue| %>
+          <% if issue.id != @issue.id %>
+            <%= reports_url(issue) %> <br/>
+          <% end %>
+        <% end %>
+      <% else %>
+        <p>No other reports against this user!</p>
+      <% end %>
+    </div>
+  </div>
+</div>
+
+<h3>Comments on this issue:</h3>
+<div class="unread-reports">
+  <%= render 'comments', comments: @comments %>
+</div>
index a5ab460ce67924f3d294639d179238535decf26d..d8e5443b1384406b876739000453b0282a8f4606 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/notifier/new_issue_notification.html.erb b/app/views/notifier/new_issue_notification.html.erb
new file mode 100644 (file)
index 0000000..b38dca4
--- /dev/null
@@ -0,0 +1,5 @@
+<p><%= t("notifier.new_issue_notification.greeting") %></p>
+
+<p><%= t("notifier.new_issue_notification.new_issue") %></p>
+
+<p><%= link_to t("notifier.new_issue_notification.url"), @url %></p>
\ No newline at end of file
diff --git a/app/views/reports/new.html.erb b/app/views/reports/new.html.erb
new file mode 100644 (file)
index 0000000..6dad7aa
--- /dev/null
@@ -0,0 +1,42 @@
+<% content_for :heading do %>
+  <h1>Report <%= reportable_url(@report.issue.reportable) %></h1>
+<% end %>
+
+<div class="disclaimer">
+  <ul>
+    <%= t('issues.new.disclaimer.intro') %>:
+      <li> <%= t('issues.new.disclaimer.not_just_mistake') %> </li>
+      <li> <%= t('issues.new.disclaimer.unable_to_fix') %> </li>
+      <li> <%= t('issues.new.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' style='width:600px'>
+      <p><%= t('issues.new.select') %>:</p>
+
+      <div class="new-report-form">
+        <% @report_strings_yaml.each do |k,v| %>
+          <div style="padding-left:5px">
+            <%= radio_button_tag :report_type, v[:type].to_s %>
+            <%= label_tag v[:details].to_s %> <br/>
+          </div>
+        <% end %>
+      </div>
+
+      <br/>
+      <%= text_area :report, :details, :cols => 20, :rows => 3, placeholder: t('issues.new.details'), required: true %>
+    </div>
+
+    <div class='buttons'>
+      <%= submit_tag %>
+    </div>
+  </fieldset>
+<% end %>
index 137a390ba1fe6437b3280e7fafa1da6f21135eaf..68f27ca38fcb50d13f9678b8f331df2ca446db6a 100644 (file)
       </p>
     </div>
 
+    <% if current_user and @this_user.id != current_user.id %>
+      <div class="report-button">
+         <%= 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%>
+            &nbsp;&#9872;
+         <% end %>
+      </div>
+    <% end %>
+
     <div class="user-description richtext"><%= @this_user.description.to_html %></div>
 
   </div>
index 07f8eaeec4fe7131d90060989806c14fcd1b1b0d..e6ebbb6a87b4ceef301ca49e869701909ddfb69c 100644 (file)
@@ -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 <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.
@@ -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
index 062fd95d48ef75d6b7d86819788e38ede82ca7f4..7393d6efd95cff68efed6ad39d17482aba3d5a0d 100644 (file)
@@ -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} <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."
@@ -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 <title>
@@ -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"
index 98bb332f23cb01bba04f2a149695b16e82bbeb67..8a5dca166756ef71663af5fc5a84a5687720a8f5 100644 (file)
@@ -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 (file)
index 0000000..a25523c
--- /dev/null
@@ -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 (file)
index 0000000..b41dde8
--- /dev/null
@@ -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 (file)
index 0000000..a7ccd22
--- /dev/null
@@ -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
index 08aafa8d7ccea6d24ceb06927b777d1aee9ee7df..7888d8a9cfde10976355e1862ae1f5b422fb9381 100644 (file)
@@ -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 (file)
index 0000000..161fd6d
--- /dev/null
@@ -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 (file)
index 0000000..b6dec44
--- /dev/null
@@ -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 (file)
index 0000000..7c00766
--- /dev/null
@@ -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 (file)
index 0000000..396ffc9
--- /dev/null
@@ -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 (file)
index 0000000..04ff7f5
--- /dev/null
@@ -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 (file)
index 0000000..820b5f7
--- /dev/null
@@ -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 (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..7ee7001
--- /dev/null
@@ -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 (file)
index 0000000..a0de944
--- /dev/null
@@ -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
index 70f69a3ae94a590140eebd1a97fa15f46437775c..552bda7d538c02af9e24509f138cd4152d87c0da 100644 (file)
@@ -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