diff --git a/app/assets/stylesheets/backstage.css b/app/assets/stylesheets/backstage.css index 4a97b3f2..7abfddef 100644 --- a/app/assets/stylesheets/backstage.css +++ b/app/assets/stylesheets/backstage.css @@ -17,7 +17,7 @@ border: solid 5px var(--black-color); -moz-box-sizing: border-box; -webkit-box-sizing: border-box; - box-sizing: border-box; + box-sizing: border-box; } #backstage-container .panel-title { @@ -74,7 +74,7 @@ #backstage-container .panel-menu a:focus { cursor: pointer; - background-color: var(--bright-blue-color); + background-color: var(--bright-blue-color); outline: none; } @@ -92,7 +92,7 @@ #backstage-container .panel-filters a.selected { font-weight: 700; - color: var(--light-black-color); + color: var(--light-black-color); } #backstage-container .panel-filters a:hover { @@ -119,12 +119,21 @@ #backstage-container .record-row:hover { cursor: pointer; - background: var(--orange-overlay); + background: var(--orange-overlay); } #backstage-container .record-row:active { cursor: pointer; - background: var(--blue-overlay); + background: var(--blue-overlay); +} + +#backstage-container .record-row .icons-space { + width: 5.5em; +} + +#backstage-container .record-row .icons-space .icon { + font-family: var(--icon-font); + margin-right: 0.3em; } #backstage-container .record-row .row-title { @@ -160,12 +169,12 @@ width: auto; padding: 2em 2em 2em 2em; } - + #backstage-container .control-panel { min-width: 16em; padding: 2em 2em 2em 2em; } - + #backstage-container .panel-descriptor { text-align: left; } diff --git a/app/assets/stylesheets/reports.css b/app/assets/stylesheets/reports.css index 182a1cf4..ee5d7f15 100644 --- a/app/assets/stylesheets/reports.css +++ b/app/assets/stylesheets/reports.css @@ -54,6 +54,24 @@ font-size: 0.9em; } +#backstage-container .record-row .row-status { + font-weight: 500; + width: 7em; + text-align: right; +} + +#backstage-container .record-row .row-status.revoked { + color: var(--alert-red-color); +} + +#backstage-container .record-row .row-status.warned { + color: var(--bright-orange-color); +} + +#backstage-container .record-row .row-status.dismissed { + color: var(--bright-green-color); +} + #backstage-container .report-buttons { margin-top: 2em; } @@ -99,7 +117,7 @@ #backstage-container .report-buttons .button:focus { cursor: pointer; - background-color: var(--bright-blue-color); + background-color: var(--bright-blue-color); outline: none; } @@ -118,7 +136,7 @@ margin-left: 0em; margin-top: 2em; } - + #backstage-container .report-buttons a.stealth { margin-left: auto; margin-right: auto; diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 396dcacb..b0679e3d 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -1,58 +1,62 @@ class ReportsController < ApplicationController - + layout "backstage", only: [ :index, :show ] - + before_action :authenticate_user!, only: [ :index, :show, :dismiss, :undismiss ] before_action :ensure_staff, only: [ :index, :show, :dismiss, :undismiss ] before_action :find_report, only: [ :show, :dismiss, :undismiss ] - before_action :find_reported_twitch_user, only: [ :show ] around_action :display_timezone - + def index # f is used to filter reports by scope if params[:f].present? && Report::AVAILABLE_SCOPES.key?(params[:f].to_sym) - @reports = eval("Report."+params[:f]+".all.order(created_at: :desc)") + @reports = eval("Report.includes(:pledge)."+params[:f]+".all.order(created_at: :desc)") # TODO add: paginate(page: params[:page], per_page: 30) @filter_category = params[:f] else - @reports = Report.unresolved.all.order(created_at: :desc) + @reports = Report.includes(:pledge).unresolved.all.order(created_at: :desc) @filter_category = "unresolved" end end - + def show # Create keybot advice message - if @reported_twitch_user == nil + if !@report.twitch_id @message = "The reported Twitch user does not exist." - elsif @pledge = Pledge.find_by(twitch_id: @reported_twitch_user) + elsif @pledge = Pledge.find_by(twitch_id: @report.twitch_id) @message = "The reported Twitch user signed the pledge as " + @pledge.twitch_display_name + " on " + @pledge.signed_on.strftime('%b. %-d, %Y.') else @message = "The reported Twitch user did not sign the pledge." end - + if @report.twitch_id + @other_reports = Report.where(twitch_id: @report.twitch_id).where.not(id: @report.id) + else + @other_reports = nil + end # TODO: check if reporter has pledged (lookup email/Twitch name) and add info to keybot message # TODO: check if incident stream owner has pledged (Twitch name) and add info to keybot message end - + def new @report = Report.new end def create @report = Report.new(report_params) - + if @report.save # Email notification to staff StaffMailer.notify_staff_new_report(@report).deliver_now - + set_twitch_id + flash[:notice] = "You've successfully submitted the report. Thank you." redirect_to root_path - else + else flash.now[:alert] ||= "" @report.errors.full_messages.each do |message| flash.now[:alert] << message + ". " - end + end render(action: :new) end end @@ -65,14 +69,14 @@ def dismiss end redirect_to reports_path end - + def undismiss @report.dismissed = false @report.reviewer = nil if @report.save flash[:notice] = "You undismissed the report about #{@report.reported_twitch_name}. It can now be reviewed again." redirect_to report_path(@report) - else + else redirect_to reports_path end end @@ -83,32 +87,32 @@ def find_report rescue ActiveRecord::RecordNotFound redirect_to staff_index_path end - - def find_reported_twitch_user + + def set_twitch_id # Check if reported_twitch_name exists on Twitch response = HTTParty.get(URI.escape("#{ENV['TWITCH_API_BASE_URL']}/users?login=#{@report.reported_twitch_name}"), headers: {"Client-ID": ENV['TWITCH_CLIENT_ID'], "Authorization": "Bearer #{TwitchToken.first.valid_token!}"}) - + if response["data"].blank? - @reported_twitch_user = nil + @report.update_attribute(:twitch_id, nil) else - @reported_twitch_user = response["data"][0]["id"] + @report.update_attribute(:twitch_id, response["data"][0]["id"]) end end - private + private def ensure_staff unless current_user.is_moderator? || current_user.is_admin? redirect_to root_url end end - + def display_timezone timezone = Time.find_zone( cookies[:browser_timezone] ) Time.use_zone(timezone) { yield } end - + def report_params params.require(:report).permit(:reporter_email, :reporter_twitch_name, :reported_twitch_name, :incident_stream, :incident_occurred, :incident_description, :recommended_response, :image) end - + end diff --git a/app/controllers/revocations_controller.rb b/app/controllers/revocations_controller.rb index fe7db4ed..f6a088b9 100644 --- a/app/controllers/revocations_controller.rb +++ b/app/controllers/revocations_controller.rb @@ -1,52 +1,51 @@ class RevocationsController < ApplicationController layout "backstage" - + before_action :authenticate_user! before_action :ensure_staff before_action :find_report before_action :ensure_sane_review - before_action :find_reported_twitch_user around_action :display_timezone - + def new - if @reported_twitch_user == nil + if @report.twitch_id == nil redirect_to staff_index_path - elsif @pledge = Pledge.find_by(twitch_id: @reported_twitch_user) + elsif @pledge = Pledge.find_by(twitch_id: @report.twitch_id) @revocation = Revocation.new else redirect_to staff_index_path end end - + def create - if @reported_twitch_user == nil + if @report.twitch_id == nil redirect_to staff_index_path - elsif @pledge = Pledge.find_by(twitch_id: @reported_twitch_user) + elsif @pledge = Pledge.find_by(twitch_id: @report.twitch_id) @revocation = Revocation.new(revocation_params) @revocation.report = @report @revocation.pledge = @pledge @revocation.reviewer = current_user - + if @revocation.save # Email revocation to pledger PledgeMailer.revoke_pledger(@revocation).deliver_now - + # Email reporter that action has been taken PledgeMailer.notify_reporter_revocation(@revocation).deliver_now - + # Revoke badge on Twitch # TODO: Roll over to Helix v6 API endpoint when they are built badge_result = HTTParty.delete(URI.escape("#{ENV['TWITCH_API_V5_BASE_URL']}/users/#{@pledge.twitch_id}/chat/badges/pledge?secret=#{ENV['TWITCH_PLEDGE_SECRET']}"), headers: {Accept: 'application/vnd.twitchtv.v5+json', "Client-ID": ENV['TWITCH_CLIENT_ID']}) - + @pledge.badge_revoked = true @pledge.revoked_on = Time.now @pledge.save - + @report.revoked = true @report.reviewer = current_user @report.save - + flash[:notice] = "You revoked the badge from #{@report.reported_twitch_name} and sent them a notification at #{@pledge.email}." redirect_to reports_path else @@ -60,24 +59,13 @@ def create redirect_to staff_index_path end end - + protected def find_report @report = Report.find(params[:report_id]) rescue ActiveRecord::RecordNotFound redirect_to staff_index_path end - - def find_reported_twitch_user - # Check if reported_twitch_name exists on Twitch - response = HTTParty.get(URI.escape("#{ENV['TWITCH_API_BASE_URL']}/users?login=#{@report.reported_twitch_name}"), headers: {"Client-ID": ENV['TWITCH_CLIENT_ID'], "Authorization": "Bearer #{TwitchToken.first.valid_token!}"}) - - if response["data"].blank? - @reported_twitch_user = nil - else - @reported_twitch_user = response["data"][0]["id"] - end - end private def ensure_staff @@ -85,24 +73,24 @@ def ensure_staff redirect_to root_url end end - + def ensure_sane_review unless !@report.dismissed && !@report.warned && !@report.revoked redirect_to staff_index_path end end - + def display_timezone timezone = Time.find_zone( cookies[:browser_timezone] ) Time.use_zone(timezone) { yield } end - + def conduct_warning_params params.require(:conduct_warning).permit(:reason) end - + def revocation_params params.require(:revocation).permit(:reason) end - + end diff --git a/app/controllers/warnings_controller.rb b/app/controllers/warnings_controller.rb index 37ca2b2b..53063b39 100644 --- a/app/controllers/warnings_controller.rb +++ b/app/controllers/warnings_controller.rb @@ -1,44 +1,43 @@ class WarningsController < ApplicationController - + layout "backstage" - + before_action :authenticate_user! before_action :ensure_staff before_action :find_report before_action :ensure_sane_review - before_action :find_reported_twitch_user around_action :display_timezone - + def new - if @reported_twitch_user == nil + if @report.twitch_id == nil redirect_to staff_index_path - elsif @pledge = Pledge.find_by(twitch_id: @reported_twitch_user) + elsif @pledge = Pledge.find_by(twitch_id: @report.twitch_id) @warning = ConductWarning.new else redirect_to staff_index_path end end - + def create - if @reported_twitch_user == nil + if @report.twitch_id == nil redirect_to staff_index_path - elsif @pledge = Pledge.find_by(twitch_id: @reported_twitch_user) + elsif @pledge = Pledge.find_by(twitch_id: @report.twitch_id) @warning = ConductWarning.new(conduct_warning_params) @warning.report = @report @warning.pledge = @pledge @warning.reviewer = current_user - + if @warning.save # Email warning to pledger PledgeMailer.warn_pledger(@warning).deliver_now - + # Email reporter that action has been taken PledgeMailer.notify_reporter_warning(@warning).deliver_now - + @report.warned = true @report.reviewer = current_user @report.save - + flash[:notice] = "You sent a warning to #{@pledge.email} (#{@report.reported_twitch_name})." redirect_to reports_path else @@ -59,17 +58,6 @@ def find_report rescue ActiveRecord::RecordNotFound redirect_to staff_index_path end - - def find_reported_twitch_user - # Check if reported_twitch_name exists on Twitch - response = HTTParty.get(URI.escape("#{ENV['TWITCH_API_BASE_URL']}/users?login=#{@report.reported_twitch_name}"), headers: {"Client-ID": ENV['TWITCH_CLIENT_ID'], "Authorization": "Bearer #{TwitchToken.first.valid_token!}"}) - - if response["data"].blank? - @reported_twitch_user = nil - else - @reported_twitch_user = response["data"][0]["id"] - end - end private def ensure_staff @@ -77,20 +65,20 @@ def ensure_staff redirect_to root_url end end - + def ensure_sane_review unless !@report.dismissed && !@report.warned && !@report.revoked redirect_to staff_index_path end end - + def display_timezone timezone = Time.find_zone( cookies[:browser_timezone] ) Time.use_zone(timezone) { yield } end - + def conduct_warning_params params.require(:conduct_warning).permit(:reason) end - + end diff --git a/app/models/pledge.rb b/app/models/pledge.rb index 6fae70c3..1ae65f68 100644 --- a/app/models/pledge.rb +++ b/app/models/pledge.rb @@ -1,7 +1,7 @@ class Pledge < ApplicationRecord - + before_create :ensure_signed_on_set - + validates_presence_of :first_name, :last_name, :email @@ -9,19 +9,20 @@ class Pledge < ApplicationRecord case_sensitive: false validates_format_of :email, with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/, - if: lambda { |x| x.email.present? } - - + if: lambda { |x| x.email.present? } + + belongs_to :referrer, class_name: :Pledge, foreign_key: :referrer_id, optional: true has_many :referrals, class_name: :Pledge, foreign_key: :referrer_id + has_many :reports, foreign_key: :twitch_id, primary_key: :twitch_id - # Non-sequential identifier scheme + # Non-sequential identifier scheme uniquify :identifier, length: 8, chars: ('A'..'Z').to_a + ('0'..'9').to_a def to_param identifier end - + def display_name if !self.twitch_display_name.blank? return self.twitch_display_name @@ -29,12 +30,12 @@ def display_name return self.first_name + ' ' + self.last_name.first + '.' end end - + private def ensure_signed_on_set if !self.signed_on.present? self.signed_on = Time.now end end - + end diff --git a/app/models/report.rb b/app/models/report.rb index a36a0acb..6e037c75 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1,5 +1,5 @@ class Report < ApplicationRecord - + AVAILABLE_SCOPES = { unresolved: "Unresolved", dismissed: "Dismissed", @@ -7,12 +7,12 @@ class Report < ApplicationRecord revoked: "Revoked", all: "All" }.freeze - + IMAGE_STYLES = { thumb: { resize: "120x120" }, preview: { resize: "240x240" } }.freeze - + validates_presence_of :reported_twitch_name, :incident_stream, :incident_description, @@ -24,22 +24,23 @@ class Report < ApplicationRecord validates :incident_description, length: { maximum: 1000 } - + validates :recommended_response, length: { maximum: 500 } validate :ensure_sane_review belongs_to :reviewer, class_name: :User, foreign_key: :reviewer_id, optional: true - + belongs_to :pledge, counter_cache: true, foreign_key: :twitch_id, primary_key: :twitch_id, optional: true + image_accessor :image - + scope :dismissed, lambda { where("#{table_name}.dismissed IS TRUE") } scope :warned, lambda { where("#{table_name}.warned IS TRUE") } scope :revoked, lambda { where("#{table_name}.revoked IS TRUE") } scope :unresolved, lambda { where("#{table_name}.dismissed IS FALSE AND #{table_name}.warned IS FALSE AND #{table_name}.revoked IS FALSE") } - - + + def image_url(style = :thumb) if style == :original self.image.remote_url @@ -47,7 +48,7 @@ def image_url(style = :thumb) process_image(style).url end end - + protected def ensure_sane_review if (self.dismissed || self.warned || self.revoked) && !(self.dismissed ^ self.warned ^ self.revoked) @@ -62,10 +63,10 @@ def ensure_sane_review end end end - + private - def process_image(style) + def process_image(style) self.image.process(:auto_orient).thumb(Report::IMAGE_STYLES[style][:resize]) end - + end diff --git a/app/views/reports/_other_report_row.html.erb b/app/views/reports/_other_report_row.html.erb new file mode 100644 index 00000000..150f0196 --- /dev/null +++ b/app/views/reports/_other_report_row.html.erb @@ -0,0 +1,17 @@ +
flexbox vertical stretch" data-url="<%= report_path(report, back: request.fullpath) %>"> +
+
<%= report.reported_twitch_name %>
+
+
<%= l(report.created_at, format: "%b. %-d, %Y · %-l:%M%P ") %>
+ <% if report.revoked %> +
Revoked
+ <% elsif report.warned %> +
Warned
+ <% elsif report.dismissed %> +
Dismissed
+ <% else %> +
Unresolved
+ <% end %> +
+
+
diff --git a/app/views/reports/_row.html.erb b/app/views/reports/_row.html.erb index 7e1935f7..1fb38f14 100644 --- a/app/views/reports/_row.html.erb +++ b/app/views/reports/_row.html.erb @@ -1,6 +1,15 @@
flexbox vertical stretch" data-url="<%= report_path(report, back: request.fullpath) %>">
-
<%= report.reported_twitch_name %>
+
+
+ <% if report.pledge %> + <% if report.pledge.reports.size > 1 %> + <%= report.pledge.reports.size %> + <% end %> + <% end %> +
+
<%= report.reported_twitch_name %>
+
<%= l(report.created_at, format: "%b. %-d, %Y · %-l:%M%P ") %>
-
\ No newline at end of file + diff --git a/app/views/reports/show.html.erb b/app/views/reports/show.html.erb index b8e6e098..e05a52ec 100644 --- a/app/views/reports/show.html.erb +++ b/app/views/reports/show.html.erb @@ -11,7 +11,15 @@ <%= render(partial: "shared/report_body") %>

- Keybot says: <%= @message %> + <% if @other_reports.present? %> +
+
Other reports<%= @other_reports.present? ? "(#{@other_reports.count})" : ''%>
+
<%= render(partial: "reports/other_report_row", collection: @other_reports, as: :report) %>
+
+ <% end %> +
+ Keybot says: <%= @message %> +
<% if @report.dismissed %> @@ -19,7 +27,7 @@ <% elsif @report.warned %>
Warned
<% elsif @report.revoked %> -
Revoked
+
Revoked
<% else %> <% if @pledge %> <%= link_to("Revoke", new_report_revocation_path(@report, back: request.fullpath), class: 'revoke-button button') %> diff --git a/db/migrate/20190904053235_change_desired_outcome_to_recommended_response.rb b/db/migrate/20190904053235_change_desired_outcome_to_recommended_response.rb deleted file mode 100644 index 7715639f..00000000 --- a/db/migrate/20190904053235_change_desired_outcome_to_recommended_response.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ChangeDesiredOutcomeToRecommendedResponse < ActiveRecord::Migration[6.0] - def change - rename_column :reports, :desired_outcome, :recommended_response - end -end diff --git a/db/migrate/20220608232140_add_twitch_id_to_reports.rb b/db/migrate/20220608232140_add_twitch_id_to_reports.rb new file mode 100644 index 00000000..823e27f3 --- /dev/null +++ b/db/migrate/20220608232140_add_twitch_id_to_reports.rb @@ -0,0 +1,5 @@ +class AddTwitchIdToReports < ActiveRecord::Migration[6.0] + def change + add_column :reports, :twitch_id, :integer + end +end diff --git a/db/migrate/20220610194809_add_reports_count_to_pledge.rb b/db/migrate/20220610194809_add_reports_count_to_pledge.rb new file mode 100644 index 00000000..80beb155 --- /dev/null +++ b/db/migrate/20220610194809_add_reports_count_to_pledge.rb @@ -0,0 +1,5 @@ +class AddReportsCountToPledge < ActiveRecord::Migration[6.0] + def change + add_column :pledges, :reports_count, :integer + end +end diff --git a/db/migrate/20220610201940_reset_all_pledge_cache_counters.rb b/db/migrate/20220610201940_reset_all_pledge_cache_counters.rb new file mode 100644 index 00000000..d76834dc --- /dev/null +++ b/db/migrate/20220610201940_reset_all_pledge_cache_counters.rb @@ -0,0 +1,12 @@ +class ResetAllPledgeCacheCounters < ActiveRecord::Migration[6.0] + def up + Pledge.all.each do |pledge| + Pledge.reset_counters(pledge.id, :reports) + end + end + + def down + # no rollback needed + end + +end diff --git a/db/schema.rb b/db/schema.rb index baa309b8..a7594843 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_02_23_190839) do +ActiveRecord::Schema.define(version: 2022_06_10_201940) do create_table "affiliates", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci", force: :cascade do |t| t.string "name" @@ -55,6 +55,7 @@ t.datetime "twitch_authed_on" t.integer "referrer_id" t.integer "referrals_count", default: 0 + t.integer "reports_count" end create_table "reports", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci", force: :cascade do |t| @@ -72,6 +73,7 @@ t.boolean "dismissed", default: false t.boolean "warned", default: false t.boolean "revoked", default: false + t.integer "twitch_id" t.index ["reviewer_id"], name: "index_reports_on_reviewer_id" end @@ -96,7 +98,7 @@ t.datetime "published_on" end - create_table "twitch_tokens", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t| + create_table "twitch_tokens", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci", force: :cascade do |t| t.string "access_token" t.integer "expires_in" t.datetime "created_at", precision: 6, null: false diff --git a/lib/tasks/reports.rake b/lib/tasks/reports.rake new file mode 100644 index 00000000..f16da7c0 --- /dev/null +++ b/lib/tasks/reports.rake @@ -0,0 +1,34 @@ +# This task looks up twitch ID for all of +# the existing reports. This task should be +# ran once after staging implementation of twitch_id +# lookup at the time of report submittion. +# The search is thorough twitch API and +# is based on the twitch username +# that was reported. This task will ignore +# reports that have twitch_id already. + +namespace :reports do + + desc "Looks up twtich ID for all existing reports" + + task :twitch_id_lookup => :environment do + + puts "Looking up twtich ID for existing reports..." + + Report.where(twitch_id: nil).each do |report| #.where(twitch_id: nil) + # Check if reported_twitch_name exists on Twitch + response = HTTParty.get(URI.escape("#{ENV['TWITCH_API_BASE_URL']}/users?login=#{report.reported_twitch_name}"), headers: {"Client-ID": ENV['TWITCH_CLIENT_ID'], "Authorization": "Bearer #{TwitchToken.first.valid_token!}"}) + + puts response.headers["ratelimit-remaining"] + puts "Updating ID:#{report.id} (#{report.reported_twitch_name})" + + if response["data"].blank? + report.update_attribute(:twitch_id, nil) + else + report.update_attribute(:twitch_id, response["data"][0]["id"]) + end + end + + end + +end diff --git a/lib/tasks/reports_counter.rake b/lib/tasks/reports_counter.rake new file mode 100644 index 00000000..58363cd0 --- /dev/null +++ b/lib/tasks/reports_counter.rake @@ -0,0 +1,19 @@ +# This task resets the reports_count of existing pledges by reflecting +# the existing reports in the database. This task should be run +# after the rake task "reports:twitch_id_lookup" is run, +# because it looks up using twitch_id as a key + +namespace :pledges do + + desc "Reset the reports_count for existing pledges" + + task :reset_reports_count => :environment do + + puts "Resetting reports_coun for existing pledges..." + + Pledge.where.not(twitch_id: nil).each do |pledge| + puts "Updating ID:#{pledge.id} (#{pledge.twitch_display_name})" + Pledge.reset_counters(pledge.id, :reports) + end + end +end