diff --git a/.gitignore b/.gitignore index 5866d40c..1d57184e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ yarn.lock .idea/ passenger.3000.pid passenger.3000.pid.lock +.vscode diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d63c492f..10550c95 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -46,15 +46,20 @@ class ApplicationController < ActionController::Base end def logged_in? - user_id = session[:user_id] - begin - user_id && User.find(user_id) ? true : false + current_user ? true : false rescue StandardError false end end + def logged_in_as(roles, action) + unless current_user && roles.any? {|role| current_user.role == role } + flash[:error] = "Only #{roles.collect {|role| role.pluralize}.join(' and ')} can #{action}." + redirect_to('/' + '?_=' + Time.now.to_i.to_s) + end + end + def save_tags(map) return unless params[:tags].present? diff --git a/app/controllers/spam_controller.rb b/app/controllers/spam_controller.rb new file mode 100644 index 00000000..db05d881 --- /dev/null +++ b/app/controllers/spam_controller.rb @@ -0,0 +1,46 @@ +class SpamController < ApplicationController + module ModerationGuards + def spam_check(map) + #check and spam only unspammed maps + map.spam unless map.status == Map::Status::BANNED + end + + def ban_check(map) + #check and ban only unbanned non-anonymous authors + map.user.ban unless map.anonymous? || map.user.status == User::Status::BANNED + end + end + + include ModerationGuards + + require 'set' + + before_action :require_login + before_action { logged_in_as(['admin', 'moderator'], 'moderate maps and users') } + + def spam_map + @map = Map.find(params[:id]) + if spam_check(@map) + notice_text = 'Map marked as spam.' + notice_text.chop! << ' and author banned.' if ban_check(@map) + else + notice_text = 'Map already marked as spam.' + end + flash[:notice] = notice_text + redirect_back(fallback_location: root_path) + end + + def batch_spam_map + spammed_maps = 0 + banned_authors = Set.new + params[:ids].split(',').uniq.each do |id| + map = Map.find(id) + if spam_check(map) + spammed_maps += 1 + banned_authors << map.user.id if ban_check(map) + end + end + flash[:notice] = helpers.pluralize(spammed_maps, 'map') + ' spammed and ' + helpers.pluralize(banned_authors.size, 'author') + ' banned.' + redirect_back(fallback_location: root_path) + end +end diff --git a/app/models/map.rb b/app/models/map.rb index 36c7294d..9eff6902 100755 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -3,6 +3,14 @@ class Map < ApplicationRecord extend FriendlyId friendly_id :name, use: %i(slugged static) + module Status + VALUES = [ + BANNED = 0, # Usage: Status::BANNED + NORMAL = 1, # Usage: Status::NORMAL + MODERATED = 4 # Usage: Status::MODERATED + ].freeze + end + attr_accessor :image_urls validates_presence_of :name, :slug, :author, :lat, :lon @@ -287,4 +295,8 @@ class Map < ApplicationRecord end User.where(id: user_ids.flatten.uniq).where.not(id: user_id) end + + def spam + self.update!(status: Status::BANNED) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 510fb543..35349236 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,14 @@ require 'digest/sha1' class User < ApplicationRecord + module Status + VALUES = [ + BANNED = 0, # Usage: Status::BANNED + NORMAL = 1, # Usage: Status::NORMAL + MODERATED = 5 # Usage: Status::MODERATED + ].freeze + end + has_many :maps has_many :tags has_many :comments @@ -55,4 +63,8 @@ class User < ApplicationRecord def can_edit?(resource) owns?(resource) end + + def ban + self.update!(status: Status::BANNED) + end end diff --git a/config/routes.rb b/config/routes.rb index 19930209..ed4d775e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -121,7 +121,11 @@ Mapknitter::Application.routes.draw do end end + get '/warps/:map/:file(.:format)', to: redirect('https://archive.publiclab.org/warps/%{map}/%{file}.%{format}') + + patch 'moderate/spam_map/:id' => 'spam#spam_map', as: 'spam_map' + patch 'moderate/batch_spam_map/:ids' => 'spam#batch_spam_map', as: 'batch_spam_map' + # See how all your routes lay out with 'rails routes' - get '/warps/:map/:file(.:format)', to: redirect('https://archive.publiclab.org/warps/%{map}/%{file}.%{format}') end diff --git a/db/migrate/20220619221926_make_status_columns_not_nullable.rb b/db/migrate/20220619221926_make_status_columns_not_nullable.rb new file mode 100644 index 00000000..b833013b --- /dev/null +++ b/db/migrate/20220619221926_make_status_columns_not_nullable.rb @@ -0,0 +1,6 @@ +class MakeStatusColumnsNotNullable < ActiveRecord::Migration[5.2] + def change + change_column_null :users, :status, false + change_column_null :maps, :status, false + end +end diff --git a/db/schema.rb b/db/schema.rb index a4221b0b..3b3cd48d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_06_08_115906) do +ActiveRecord::Schema.define(version: 2022_06_19_221926) do create_table "annotations", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.integer "map_id" @@ -77,7 +77,7 @@ ActiveRecord::Schema.define(version: 2022_06_08_115906) do t.boolean "anon_annotatable", default: false t.string "slug" t.boolean "display_welcome", default: true - t.integer "status", default: 1 + t.integer "status", default: 1, null: false t.index ["author"], name: "index_maps_on_author" t.index ["slug"], name: "index_maps_on_slug", unique: true t.index ["user_id"], name: "index_maps_on_user_id" @@ -123,7 +123,7 @@ ActiveRecord::Schema.define(version: 2022_06_08_115906) do t.datetime "updated_at" t.string "remember_token", limit: 40 t.datetime "remember_token_expires_at" - t.integer "status", default: 1 + t.integer "status", default: 1, null: false t.index ["login"], name: "index_users_on_login", unique: true end diff --git a/test/controllers/spam_controller_test.rb b/test/controllers/spam_controller_test.rb new file mode 100644 index 00000000..17361d0b --- /dev/null +++ b/test/controllers/spam_controller_test.rb @@ -0,0 +1,155 @@ +require 'test_helper' + +class SpamControllerTest < ActionController::TestCase + def setup #called before every single test + @map = maps(:saugus) + @maps = [maps(:cubbon)] + end + + def custom_setup + @map_ids = @maps.collect { |map| map['id'] }.join(',') + end + + test 'should not moderate a map if user not logged in' do + patch(:spam_map, params: { id: @map.id }) + @map.reload + + assert_equal 'You must be logged in to access this section', flash[:warning] + assert_redirected_to "/login?back_to=/moderate/spam_map/#{@map.id}" + assert_equal 1, @map.status + end + + test 'should not moderate maps if user is not an admin or a moderator' do + custom_setup + session[:user_id] = 1 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal 'Only admins and moderators can moderate maps and users.', flash[:error] + assert_redirected_to ('/' + '?_=' + Time.now.to_i.to_s) + end + + test 'should spam a map owned by an anonymous author and not ban the author' do + anon_map = maps(:yaya) + session[:user_id] = 2 + patch(:spam_map, params: { id: anon_map.id }) + anon_map.reload + + assert_equal 'Map marked as spam.', flash[:notice] + assert_redirected_to root_path + assert_equal 0, anon_map.status + end + + test 'should spam a map owned by a non-anonymous author and ban the author' do + session[:user_id] = 2 + patch(:spam_map, params: { id: @map.id }) + @map.reload + + assert_equal 'Map marked as spam and author banned.', flash[:notice] + assert_redirected_to root_path + assert_equal 0, @map.status + assert_equal 0, @map.user.status + end + + test 'should spam a map owned by a banned author and not ban the author again' do + session[:user_id] = 2 + @map.spam + @map.user.ban + + second_map = maps(:cubbon) + patch(:spam_map, params: { id: second_map.id }) + second_map.reload + + assert_equal 'Map marked as spam.', flash[:notice] + assert_redirected_to root_path + assert_equal 0, @map.status + assert_equal 0, @map.user.status + assert_equal 0, second_map.status + assert_equal 0, second_map.user.status + end + + test 'should not spam an already spammed map' do + session[:user_id] = 2 + @map.spam + @map.user.ban + + patch(:spam_map, params: { id: @map.id }) + + assert_equal 'Map already marked as spam.', flash[:notice] + assert_redirected_to root_path + assert_equal 0, @map.status + assert_equal 0, @map.user.status + end + + test 'should batch-spam maps and ban non-anonymous authors' do + @maps << maps(:village) + custom_setup + session[:user_id] = 2 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal @maps.length, 2 + assert_equal @maps.uniq.length, 2 + assert_equal '2 maps spammed and 2 authors banned.', flash[:notice] + assert @maps.all? { |map| map.reload.status == 0 } + assert @maps.all? { |map| map.user.status == 0 } + assert_redirected_to root_path + end + + test 'should batch-spam maps and not ban anonymous authors' do + @maps << maps(:yaya) + custom_setup + session[:user_id] = 2 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal @maps.length, 2 + assert_equal @maps.uniq.length, 2 + assert_equal '2 maps spammed and 1 author banned.', flash[:notice] + assert_redirected_to root_path + assert @maps.all? { |map| map.reload.status == 0 } + assert @maps.one? { |map| map.user.nil? } + end + + test 'should not batch-spam a duplicate map' do + @maps << maps(:cubbon) + custom_setup + session[:user_id] = 2 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal @maps.length, 2 + assert_equal @maps.uniq.length, 1 + assert_equal '1 map spammed and 1 author banned.', flash[:notice] + assert @maps.uniq.one? { |map| map.reload.status == 0 } + assert @maps.uniq.one? { |map| map.user.status == 0 } + assert_redirected_to root_path + end + + test 'should not batch-spam already-spammed maps' do + @maps[0].spam + @maps[0].user.ban + + assert_equal 0, @maps[0].status + assert_equal 0, @maps[0].user.status + + custom_setup + session[:user_id] = 2 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal @maps.length, 1 + assert_equal @maps.uniq.length, 1 + assert_equal '0 maps spammed and 0 authors banned.', flash[:notice] + assert_redirected_to root_path + end + + test 'should batch-spam maps and skip banning of authors already banned' do + @maps << @map + custom_setup + session[:user_id] = 2 + patch(:batch_spam_map, params: { ids: @map_ids }) + + assert_equal @maps.length, 2 + assert_equal @maps.uniq.length, 2 + assert_equal '2 maps spammed and 1 author banned.', flash[:notice] + assert @maps.all? { |map| map.reload.status == 0 } + assert @maps.all? { |map| map.user.status == 0 } + assert_redirected_to root_path + end +end diff --git a/test/fixtures/maps.yml b/test/fixtures/maps.yml index 3b9cd509..9623d014 100644 --- a/test/fixtures/maps.yml +++ b/test/fixtures/maps.yml @@ -24,7 +24,7 @@ cubbon: created_at: <%= Time.now %> license: publicdomain user_id: 1 - author: aaron + author: quentin nairobi: id: 3 @@ -49,8 +49,8 @@ village: description: A mall in Nairobi created_at: <%= Time.now %> license: publicdomain - user_id: 2 - author: aaron + user_id: 3 + author: chris yaya: id: 5 diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml index c51f2025..964a44dc 100644 --- a/test/fixtures/tags.yml +++ b/test/fixtures/tags.yml @@ -9,5 +9,5 @@ nice: featured: name: featured created_at: <%= Time.now %> - map_id: 2 + map_id: 3 user_id: 1 diff --git a/test/integration/routes_test.rb b/test/integration/routes_test.rb new file mode 100644 index 00000000..1bf74539 --- /dev/null +++ b/test/integration/routes_test.rb @@ -0,0 +1,12 @@ +require 'test_helper' + +class RoutesTest < ActionDispatch::IntegrationTest + + test "test single-spam-map route" do + assert_routing({ path: '/moderate/spam_map/1', method: :patch }, { controller: 'spam', action: 'spam_map', id: '1' }) + end + + test "test batch-spam-maps route" do + assert_routing({ path: '/moderate/batch_spam_map/1,2', method: :patch }, { controller: 'spam', action: 'batch_spam_map', ids: '1,2' }) + end +end diff --git a/test/models/map_test.rb b/test/models/map_test.rb index 5ccac60f..0c0101bc 100644 --- a/test/models/map_test.rb +++ b/test/models/map_test.rb @@ -89,12 +89,19 @@ class MapTest < ActiveSupport::TestCase end test 'filter bbox with tag if present' do - maps = Map.bbox(10,60,30,80,'featured') - assert maps.collect(&:name).include?('Cubbon Park') + maps = Map.bbox(-5,35,0,40,'featured') + assert maps.collect(&:name).include?('Nairobi City') end test 'bbox without tag returns results' do maps = Map.bbox(40,-80,50,-60) assert maps.collect(&:name).include?('Saugus Landfill Incinerator') end + + test 'should spam map' do + map = maps(:saugus) + assert_equal Map::Status::NORMAL, map.status + map.spam + assert_equal Map::Status::BANNED, map.status + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 4821c931..5f915f03 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -52,6 +52,13 @@ class UserTest < ActiveSupport::TestCase assert_equal map_images.flatten, user.warpables end + test 'should ban user' do + user = users(:quentin) + assert_equal User::Status::NORMAL, user.status + user.ban + assert_equal User::Status::BANNED, user.status + end + # def test_should_authenticate_user # assert_equal users(:quentin), User.authenticate('quentin', 'monkey') # end