Add ticket infrastructure.

This commit is contained in:
Kira 2019-03-13 14:04:59 -07:00
parent c5e90023d1
commit 6f1d47c10b
37 changed files with 1411 additions and 8 deletions

View File

@ -0,0 +1,108 @@
class TicketsController < ApplicationController
respond_to :html, :xml, :json
before_action :member_only, except: [:index]
before_action :admin_only, only: [:update, :edit, :destroy, :claim, :unclaim]
def index
@tickets = Ticket.search(search_params).paginate(params[:page], limit: params[:limit])
respond_with(@tickets)
end
def new
@ticket = Ticket.new(qtype: params[:type], disp_id: params[:disp_id])
check_new_permission(@ticket)
end
def create
@ticket = Ticket.create(ticket_params)
if @ticket.valid?
flash[:success] = 'Ticket created'
redirect_back(fallback_location: tickets_path())
else
respond_with(@ticket)
end
end
def show
@ticket = Ticket.find(params[:id])
check_permission(@ticket)
respond_with(@ticket)
end
def update
@ticket = Ticket.find(params[:id])
@ticket.transaction do
if @ticket.claimant_id.present? && @ticket.claimant_id != CurrentUser.id && params[:force_claim] != 'true'
flash[:error] = "Ticket has already been claimed by somebody else, submit again to force."
redirect_to ticket_path(@ticket, force_claim: 'true')
return
end
@ticket.handler_id = CurrentUser.id
@ticket.claimant_id = CurrentUser.id
@ticket.update(update_ticket_params)
end
respond_with(@ticket)
end
def claim
@ticket = Ticket.find(params[:id])
if @ticket.claimant.nil?
@ticket.claim!
redirect_to ticket_path(@ticket)
return
end
flash[:error] = 'Ticket already claimed.'
redirect_to ticket_path(@ticket)
end
def unclaim
@ticket = Ticket.find(params[:id])
if @ticket.claimant.nil?
flash[:error] = 'Ticket not claimed.'
redirect_to ticket_path(@ticket)
return
elsif @ticket.claimant.id != CurrentUser.id
flash[:error] = 'Ticket not claimed by you.'
redirect_to ticket_path(@ticket)
return
end
@ticket.unclaim!
flash[:success] = 'Claim removed.'
redirect_to ticket_path(@ticket)
end
private
def ticket_params
params.require(:ticket).permit(%i[qtype disp_id reason force_claim])
end
def update_ticket_params
params.require(:ticket).permit(%i[response status])
end
def search_params
search_params = params.fetch(:search, {})
allowed = %i[type status order]
allowed = %i[creator_id] if CurrentUser.is_admin? || (search_params[:creator_id].present? && search_params[:creator_id].to_i == CurrentUser.id)
allowed += %i[creator_name accused_name accused_id claimant_id] if CurrentUser.is_admin?
search_params.permit(allowed)
end
def check_new_permission(ticket)
unless ticket.can_create_for?(CurrentUser.user)
raise User::PrivilegeError
end
end
def check_permission(ticket)
unless ticket.can_see_details?(CurrentUser.user)
raise User::PrivilegeError
end
end
end

View File

@ -68,6 +68,10 @@ module ApplicationHelper
raw %{<a href="#{h(url)}" #{attributes}>#{text}</a>}
end
def link_to_function(text, function, **options)
tag.a text, href: '#', onclick: function, **options
end
def format_text(text, **options)
raw DTextRagel.parse(text, **options)
rescue DTextRagel::Error => e

View File

@ -0,0 +1,12 @@
module TicketHelper
def pretty_ticket_status(ticket)
status = ticket.status
if status == "partial"
"Under Investigation"
elsif status == "approved"
"Investigated"
else
status.titleize
end
end
end

View File

@ -35,10 +35,6 @@ class Blip < ApplicationRecord
def method_attributes
super + [:creator_name]
end
def creator_name
User.id_to_name(creator_id)
end
end
module PermissionsMethods
@ -57,8 +53,12 @@ class Blip < ApplicationRecord
end
module SearchMethods
def visible(user = CurrentUser.user)
where('is_hidden = ?', false) unless user.is_moderator?
def visible(user = CurrentUser)
if user.is_moderator?
where('is_hidden = ?', false)
else
where('1=1')
end
end
def for_creator(user_id)

View File

@ -205,6 +205,10 @@ class Comment < ApplicationRecord
creator_id == user.id || user.is_moderator?
end
def visible_to?(user)
is_deleted? == false || (creator_id == user.id || user.is_moderator?)
end
def hidden_attributes
super + [:body_index]
end

View File

@ -249,6 +249,6 @@ class Takedown < ApplicationRecord
include StatusMethods
include ModifyPostMethods
include ProcessMethods
include SearchMethods
extend SearchMethods
include AccessMethods
end

214
app/models/ticket.rb Normal file
View File

@ -0,0 +1,214 @@
require 'ticket/type_methods'
class Ticket < ApplicationRecord
belongs_to_creator
belongs_to :claimant, class_name: "User", optional: true
belongs_to :handler, class_name: "User", optional: true
before_validation :initialize_fields, on: :create
after_initialize :validate_type
after_initialize :classify
validates_presence_of :qtype
validates_presence_of :reason
after_update :log_update, if: :should_send_notification
after_update :send_update_dmail, if: :should_send_notification
validate :validate_can_see_target, on: :create
=begin
Permission truth table.
Type | Field | Access
-----------------------------------------
Any | Username | Admin+ / Current User
Name Change | Old Nme | Any
Any | Created At | Any
Any | Updated At | Any
Any | Claimed By | Admin+
Any | Status | Any
Any | IP Address | Admin+
User Complaint | Reported User | Admin+ / Current User
Dmail | Details | Admin+ / Current User
Comment | Comment Link | Any
Comment | Comment Author| Any
Forum | Forum Post | Forum Visibility / Any
Wiki | Wiki Page | Any
Blip | Blip | Any
Pool | Pool | Any
Set | Set | Any
Other | Any | N/A(No details shown)
Name Change | Desired Name | Any
DMail | Reason | Admin+ / Current User
User Complaint | Reason | Admin+ / Current User
Any | Reason | Any
DMail | Response | Admin+ / Current User
User Complaint | Response | Admin+ / Current User
Any | Response | Any
Any | Handled By | Any
=end
module APIMethods
def hidden_attributes
hidden = []
hidden += %i[claimant_id] unless CurrentUser.is_admin?
hidden += %i[creator_id] unless can_see_username?(CurrentUser)
hidden += %i[disp_id] unless can_see_details?(CurrentUser)
hidden += %i[reason] unless can_see_reason?(CurrentUser)
super + hidden
end
end
VALID_STATUSES = %w(pending partial denied approved)
TYPE_MAP = {
'forum' => TicketTypes::ForumType,
'comment' => TicketTypes::CommentType,
'blip' => TicketTypes::BlipType,
'user' => TicketTypes::UserType,
'dmail' => TicketTypes::DmailType,
'namechange' => TicketTypes::NamechangeType,
'wiki' => TicketTypes::WikiType,
'pool' => TicketTypes::PoolType,
'set' => TicketTypes::SetType,
'post' => TicketTypes::PostType
}
attr_reader :can_see, :type_valid
module ValidationMethods
def validate_type
unless TYPE_MAP.key?(qtype)
errors.add(:qtype, "is not valid")
@type_valid = false
else
@type_valid = true
end
end
def validate_can_see_target(user = CurrentUser.user)
unless can_create_for?(user)
errors.add(:base, "You can not report this content because you can not see it.")
@can_see = false
else
@can_see = true
end
end
def initialize_fields
self.status = 'pending'
end
end
module SearchMethods
def search(params)
q = super.includes(:creator).includes(:claimant)
if params[:creator_id].present?
q = q.where('creator_id = ?', params[:creator_id].to_i)
end
if params[:claimant_id].present?
q = q.where('claimant_id = ?', params[:claimant_id].to_i)
end
if params[:creator_name].present?
q = q.where('tickets.creator_id = (select _.id from users _ where lower(_.name) = ?', params[:reporter_name].tr(' ', '_').downcase)
end
if params[:accused].present?
q = q.where("tickets.disp_id = (select _.id from users _ where lower(_.name) = ? AND tickets.qtype = ?)", params[:accused].tr(' ', '_').downcase, 'user')
end
if params[:type].present?
q = q.where('qtype = ?', params[:type])
end
if params[:status].present?
q = q.where('status = ?', params[:status])
end
q
end
end
module ClassifyMethods
def classify
klass = TYPE_MAP.fetch(qtype, TicketTypes::DefaultType)
self.extend(klass)
klass.after_extended(self)
end
end
def can_see_reason?(user)
true
end
def can_see_details?(user)
true
end
def can_see_username?(user)
user.is_admin? || (user.id == creator_id)
end
def can_see_response?(user)
true
end
def can_create_for?(user)
false
end
def type_title
'Ticket'
end
def subject
if reason.length > 40
"#{reason[0, 38]}..."
else
reason
end
end
module ClaimMethods
def claim!(user = CurrentUser)
transaction do
ModAction.log("Ticket ##{id} claimed by ##{user.id}", :claim_ticket)
update_attribute(:claimant_id, user.id)
end
end
def unclaim!(user = CurrentUser)
transaction do
ModAction.log("Ticket ##{id} unclaimed by ##{user.id}", :unclaim_ticket)
update_attribute(:claimant_id, nil)
end
end
end
module NotificationMethods
def should_send_notification
status_changed?
end
def send_update_dmail
msg = "\"Your ticket\":#{Rails.application.routes.url_helpers.ticket_path(self)} has been updated by" +
" #{handler.pretty_name}.\nTicket Status: #{status}\n\n" +
(qtype == "reason" ? "Reason" : "Response") +
": #{response}"
Dmail.create_automated(
:to_id => user.id,
:title => "Your ticket has been updated to '#{pretty_status}'",
:body => msg
)
end
def log_update
ModAction.log("Ticket ##{id} updated by #{CurrentUser.id}", :update_ticket)
end
end
include ClassifyMethods
include ValidationMethods
include APIMethods
include ClaimMethods
include NotificationMethods
extend SearchMethods
end

View File

@ -0,0 +1,414 @@
module TicketTypes
module DefaultType
def type_title
"Ticket"
end
def self.after_extended(m)
m
end
end
module NamechangeType
def self.after_extended(m)
m.class_eval do
attr_accessor :no_name_change
end
m.oldname = m.user.name if m.oldname.blank?
m.no_name_change = false
end
# Required to override default ("Investigated")
def pretty_status
status.titleize
end
def subject
"Requested Name: #{reason}"
end
def before_save
super
if change_username?
return false unless username_valid?
change_username
end
true
end
def username_valid?
if User.find_by_name(requested_name)
errors.add :requested_name, "is already taken."
return false
end
true
end
def change_username?
status_was == 'pending' and status == 'approved' and not no_name_change
end
def change_username
Ticket.transaction do
user.name = requested_name
user.save
user.errors.each {|k, v| errors.add k, v}
Namechange.create(mod: admin, user_id: user_id,
oldname: oldname, newname: reason)
end
end
def requested_name
reason
end
def type_title
'Change Username'
end
def validate_on_create
errors.add :user, "doesn't even exist" unless user
errors.add :you, "can only create one namechange request per week" if Ticket.first(
order: created_at,
conditions: ["qtype = ? AND user_id = ? AND created_at > ?", "namechange", user.id, 1.week.ago])
errors.add :requested_name, "is taken" if User.find_by_name(requested_name)
if admin.nil? or status == 'approved'
user.name = reason
user.valid?
user.errors.each do |key, value|
errors.add key, value
end
user.reload
end
end
def can_see_username?(user)
true
end
end
module ForumType
def self.after_extended(m)
m
end
def type_title
'Forum Post Complaint'
end
def validate_on_create
if forum.nil?
errors.add :forum, "post does not exist"
end
end
def forum=(new_forum)
@forum = new_forum
self.disp_id = new_forum.id unless new_forum.nil?
end
def forum
@forum ||= begin
::ForumPost.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
forum.visible?(user)
end
def can_see_details?(current_user)
if forum and forum.category
forum.category.can_view <= current_user.level
else
true
end
end
end
module CommentType
def self.after_extended(m)
m
end
def type_title
'Comment Complaint'
end
def validate_on_create
if comment.nil?
errors.add :comment, "does not exist"
end
end
def comment=(new_comment)
@comment = new_comment
self.disp_id = new_comment.id unless new_comment.nil?
end
def comment
@comment ||= begin
::Comment.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
comment.visible_to?(user)
end
end
module DmailType
def self.after_extended(m)
m
end
def type_title
'Dmail Complaint'
end
def validate_on_create
unless dmail and dmail.to_id == user_id
errors.add :dmail, "does not exist"
end
end
def dmail=(new_dmail)
@dmail = new_dmail
self.disp_id = new_dmail.id unless new_dmail.nil?
end
def dmail
@dmail = begin
::Dmail.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
dmail.visible_to?(user, nil)
end
def can_see_details?(current_user)
current_user.is_admin? || (current_user.id == user_id)
end
def can_see_reason?(current_user)
can_see_details?(current_user)
end
def can_see_response?(current_user)
can_see_details?(current_user)
end
end
module WikiType
def self.after_extended(m)
m
end
def type_title
'Wiki Page Complaint'
end
def validate_on_create
if wiki.nil?
errors.add :wiki, "page does not exist"
end
end
def wiki=(new_wiki)
@wiki = new_wiki
self.disp_id = new_wiki.id unless new_wiki.nil?
end
def wiki
@wiki ||= begin
::WikiPage.find disp_id unless disp_id.nil?
rescue
end
end
def can_creator_for?(user)
true
end
end
module PoolType
def self.after_extended(m)
m
end
def type_title
'Pool Complaint'
end
def validate_on_create
if pool.nil?
errors.add :pool, "does not exist"
end
end
def pool=(new_pool)
@pool = new_pool
self.disp_id = new_pool.id unless new_pool.nil?
end
def pool
@pool ||= begin
::Pool.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
true
end
end
module SetType
def self.after_extended(m)
m
end
def type_title
'Set Complaint'
end
def validate_on_create
if set.nil?
errors.add :set, "does not exist"
end
end
def set=(new_set)
@set = new_set
self.disp_id = new_set.id unless new_set.nil?
end
def set
@set ||= begin
::PostSet.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
# TODO: When sets go in, fill this in.
end
end
module PostType
def self.after_extended(m)
m
end
def type_title
'Post Complaint'
end
def validate_on_create
if post.nil?
errors.add :post, "does not exist"
end
if report_reason.nil?
errors.add :report_reason, "does not exist"
end
end
def subject
reason.split("\n")[0] || "Unknown Report Type"
end
def post=(new_post)
@post = new_post
self.disp_id = new_post.id unless new_post.nil?
end
def post
@post ||= begin
::Post.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
true
end
end
module BlipType
def self.after_extended(m)
m
end
def type_title
'Blip Complaint'
end
def validate_on_create
if blip.nil?
errors.add :blip, "does not exist"
end
end
def blip=(new_blip)
@blip = new_blip
self.disp_id = new_blip.id unless new_blip.nil?
end
def blip
@blip ||= begin
::Blip.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_create_for?(user)
blip.visible_to?(user)
end
end
module UserType
def self.after_extended(m)
m
end
def type_title
'User Complaint'
end
def validate_on_create
if accused.nil?
errors.add :user, "does not exist"
end
end
def accused=(new_accused)
@accused = new_accused
self.disp_id = new_accused.id unless new_accused.nil?
end
def accused
@accused ||= begin
::User.find(disp_id) unless disp_id.nil?
rescue
end
end
def can_see_details?(current_user)
current_user.is_admin? || current_user.id == creator_id
end
def can_see_reason?(current_user)
can_see_details?(current_user)
end
def can_see_response?(current_user)
can_see_details?(current_user)
end
end
end

View File

@ -0,0 +1,10 @@
<% content_for("subnavbar") do %>
<li><%= link_to "All", action: "index" %></li>
<% unless current_user.anon? %>
<li><%= link_to "Mine", action: "index", user_id: current_user.id %></li>
<% end %>
<% if current_user.is_admin? %>
<li><%= link_to "Claimed", action: "index", claim_id: current_user.id %></li>
<% end %>
<li><%= link_to "Help", controller: "help" %></li>
<% end %>

View File

@ -0,0 +1,35 @@
<div id='searchform_hide'>
<%= link_to_function "Show Search Options", "$('#searchform').fadeIn('fast'); $('#searchform_hide').hide();" %>
</div>
<%= simple_form_for(:search, method: :get, defaults: {required: false}, html: {id: 'searchform', class: "inline-form"}) do |f| %>
<% if CurrentUser.is_admin? %>
<%= f.input :creator_name, label: "Reporter", hint: "Use * for wildcard", input_html: {value: params[:search][:creator_name]} %>
<%= f.input :creator_id, label: "Reporter ID", input_html: {value: params[:search][:creator_id]} %>
<%= f.input :accused_name, label: "Accused", input_html: {value: params[:search][:accused_name]} %>
<% end %>
<%= f.input :type, collection: [
["Any", "any"],
["User complaint", "user"],
["Private message complaint", "dmail"],
["Comment complaint", "comment"],
["Forum post complaint", "forum"],
["Blip complaint", "blip"],
["Wiki complaint", "wiki"],
["Username change", "namechange"],
["Pool complaint", "pool"],
["Set complaint", "set"],
["Post complaint", "post"]
], selected: params[:search][:type] %>
<%= f.input :status, collection: [
["Any", "any"],
["Approved/investigated", "approved"],
["Under investigation", "partial"],
["Pending", "pending"],
["Denied", "denied"]
], selected: params[:search][:status] %>
<%= f.submit "Search" %>
<% end %>
<% if params[:name] || params[:type] || params[:status] %>
<script type='text/javascript'>$('#searchform_hide').hide();
$('#searchform').show();</script>
<% end %>

View File

@ -0,0 +1,11 @@
<% content_for(:secondary_links) do %>
<menu>
<%= subnav_link_to 'List', tickets_path %>
<% unless CurrentUser.is_anonymous? %>
<%= subnav_link_to 'Mine', tickets_path(search: {creator_id: CurrentUser.id}) %>
<% end %>
<% if CurrentUser.is_admin? %>
<%= subnav_link_to 'Claimed', tickets_path(search: {claimant_id: CurrentUser.id}) %>
<% end %>
</menu>
<% end %>

View File

@ -0,0 +1,62 @@
<div id="c-tickets">
<div id="a-index">
</div>
</div>
<%= render partial: 'search' %>
<h2>Ticket Center</h2>
<table class="valign rounded">
<thead>
<tr>
<th style="width:5%">ID</th>
<% if CurrentUser.is_admin? %>
<th style="width:10%">Reporter</th>
<th style="width:10%">Claimed By</th>
<% end %>
<th style="width:15%">Type</th>
<th style="width:25%">Subject</th>
<th style="width:8%">Status</th>
<th style="width:10%">Updated</th>
<th style="width:18%">Created</th>
</tr>
</thead>
<tbody>
<% @tickets.each do |ticket| %>
<tr data-link="<%= ticket_path(ticket) %>">
<td><%= link_to ticket.id, ticket_path(ticket) %></td>
<% if CurrentUser.is_admin? %>
<td>
<%= link_to_user ticket.creator %>
</td>
<td>
<% if ticket.claimant.nil? %>
<span class="redtext">Unclaimed</span>
<% else %>
<%= link_to_user ticket.claimant %>
<% end %>
</td>
<% end %>
<td><%= link_to ticket.type_title, ticket_path(ticket) %></td>
<% if !ticket.can_see_reason?(CurrentUser.user) %>
<td><span style="cursor:help;" class="redtext" title="Due to privacy concerns, this information is confidential">Confidential</span></td>
<% else %>
<td class="ticket-subject" title="<%= h(truncate(strip_dtext(ticket.reason), length: 200)) %>"><%= link_to h(strip_dtext(truncate(ticket.subject, length: 200))), action: "show", id: ticket.id %></td>
<% end %>
<td class="<%= ticket.status %>-ticket"><%= pretty_ticket_status(ticket) %></td>
<td style="cursor:help;" title="<%= ticket.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(ticket.updated_at) %> ago</td>
<td style="cursor:help;" title="<%= ticket.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(ticket.created_at) %> ago</td>
</tr>
<% end %>
</tbody>
</table>
<div id="paginator">
<%= numbered_paginator(@tickets) %>
</div>
<% render partial: 'secondary_links' %>

View File

@ -0,0 +1,66 @@
<div id="c-tickets">
<div id="a-new">
<%= simple_form_for(@ticket) do |f| %>
<% @found_item = true %>
<% if params[:type].nil? %>
<% @found_item = false %>
<div class='section' style='width:80em;'>
To submit a ticket about a problematic comment, click "Report" on the comment itself.<br/>
To submit a ticket about a problematic forum post, click "Report" on the post itself.<br/>
To submit a ticket about a problematic pool, click "Report" on the pool page itself.<br/>
To submit a ticket about a problematic set, click "Report" on the set page itself.<br/>
To submit a ticket about a problematic user, click "Report" on the user's profile page.<br/>
To submit a ticket about a problematic private message, click "Report PM" above the PM itself.<br/>
To submit a username request, click "Request Username Change" on <a href='/user/home'>the user home page</a>.<br/>
</div>
<% elsif CurrentUser.is_anonymous? %>
<% @found_item = false %>
<div class='section' style='width:80em;'>
You must be logged in to submit a ticket. Please <a href='/user/login'>log in</a> and try again.
</div>
<% elsif @ticket.type_valid %>
<%= render partial: "tickets/new_types/#{@ticket.qtype}" %>
<% else %>
<% @found_item = false %>
<div class='section' style='width:80em;'>
Hmm, it seems you tried to report something that doesn't make sense.
</div>
<% end %>
<% if @found_item %>
<%= f.hidden_field :disp_id %>
<%= f.hidden_field :qtype %>
<div class='section' style='width:80em;'>
<label for="ticket_reason">
<% if params[:type] == "namechange" %>
Desired username
<% elsif params[:type] == "post" %>
Additional details.
<% else %>
Reason for complaint
<% end %>
</label><br/>
<% if params[:type] == "namechange" %>
<p class='nomargin'>Note: Multiple name change requests made within a short period of time may not be
honored</p>
<%= f.input :reason, as: :text, maxlength: 20 %><br/>
<% else %>
<p class='nomargin'>Note: Abuse of this system will lead to disciplinary action</p>
<%= dtext_field "ticket", "reason", :classes => "autocomplete-mentions", :value => @ticket.reason, :input_id => "ticket_reason", :preview_id => "dtext-preview-for-ticket-reason" %>
<% end %>
</div>
<% if params[:type] == "namechange" %>
<%= submit_tag "Submit Request" %>
<% else %>
<%= submit_tag "Submit Complaint" %> <%= dtext_preview_button "ticket", "reason", :input_id => "ticket_reason", :preview_id => "dtext-preview-for-ticket-reason" %>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% render partial: 'secondary_links' %>

View File

@ -0,0 +1,12 @@
<% unless @ticket.blip %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That blip does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<h4>You are reporting the following blip:</h4>
<div class="author"><%= link_to_user(@ticket.blip.creator) %></div>
<span class="date" title="Posted on <%= @ticket.blip.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@ticket.blip.created_at) %> ago</span>
<br /><br />
<%= format_text(@ticket.blip.body) %>
</div>
<% end %>

View File

@ -0,0 +1,13 @@
<% unless @ticket.comment %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That comment does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @comment = @ticket.comment %>
<h4>You are reporting the following comment:</h4>
<div class="author"><%= link_to_user @comment.creator %></div>
<span class="date" title="Posted on <%= @comment.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@comment.created_at) %> ago</span>
<br /><br />
<%= format_text(@comment.body) %>
</div>
<% end %>

View File

@ -0,0 +1,16 @@
<% unless Dmail.find_by_id(params[:disp_id]) && Dmail.find_by_id(params[:disp_id]).to_id == current_user.id %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That message does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @dmail = Dmail.find_by_id(params[:disp_id]) %>
<h4>You are reporting the following private message:</h4>
<% if (@dmail.from_id) %>
<div class='author'><%= fast_link_to(@dmail.from_name, controller: "user", action: "show", id: @dmail.from_id) %></div>
<% else %>
<div class='author'><%= h User.find_name(@dmail.user_id) %></div>
<% end %>
<span class="date" title="Sent on <%= @dmail.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@dmail.created_at) %> ago</span>
<%= format_text(@dmail.body, mode: :dmail) %>
</div>
<% end %>

View File

@ -0,0 +1,16 @@
<% unless ForumPost.find_by_id(params[:disp_id]) %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That forum post does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @forum = ForumPost.find_by_id(params[:disp_id]) %>
<h4>You are reporting the following forum post:</h4>
<% if (@forum.creator.id) %>
<div class='author'><%= fast_link_to(@forum.creator.name, controller: "forum", action: "show", id: @forum.creator.id) %></div>
<% else %>
<div class='author'><%= h User.find_name(@forum.user_id) %></div>
<% end %>
<span class="date" title="Posted on <%= @forum.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@forum.created_at) %> ago</span>
<%= format_text(@forum.body, mode: :forum, userlevel: @forum.creator.level) %>
</div>
<% end %>

View File

@ -0,0 +1,18 @@
<% unless (params[:disp_id].to_i == current_user.id) %>
<% @found_item = false %>
<div class='section' style='width:80em;'>You can only change your own username.</div>
<% else %>
<div class='section' style='width:80em;'>
<p>You are requesting to change your username.</p>
<p>Usernames may contain:
<ul>
<li>Non-accented letters (<b>A-Z</b>)</li>
<li>Numbers (<b>0-9</b>)</li>
<li>The following symbols: <b>-_~'</b></li>
</ul>
<p>The name change is not automated, but must be approved by an Administrator.
Requests are usually handled within 24 hours, but it may take up to 3 days to process.
Once the name change has been approved your old username is able to used by other users.
Please ensure that you are willing to risk losing your name!</p>
</div>
<% end %>

View File

@ -0,0 +1,16 @@
<% unless Pool.find_by_id(params[:disp_id]) %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That pool does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @pool = Pool.find_by_id(params[:disp_id]) %>
<h4>You are reporting the following pool:</h4>
<div class='page'><%= fast_link_to(@pool.name, controller: "pool", action: "show", id: @pool.id) %></div>
<% if (@pool.user_id) %>
<div class='author'><%= fast_link_to(@pool.user.name, controller: "user", action: "show", id: @pool.user_id) %></div>
<% else %>
<div class='author'><%= h User.find_name(@pool.user_id) %></div>
<% end %>
<span class="date" title="Created on <%= @pool.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@pool.created_at) %> ago</span>
</div>
<% end %>

View File

@ -0,0 +1,23 @@
<% unless Post.find_by_id params[:disp_id] %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That post does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @post = Post.find_by_id params[:disp_id] %>
You are reporting <%= thumb(@post, dtext: true, blacklist: false) %>
</div>
<div class='section' style='width:80em;'>
<p class="nomargin">Reason:
<%= select :ticket, :report_reason, options_for_select(PostReportReason.for_select) %>
<span id="post_reason_description">You must select a reason.</span>
</p>
<script>
var post_report_reasons = <%= PostReportReason.for_select_descriptions.to_json %>;
$('ticket_report_reason').onchange = function(e) {
var description = post_report_reasons[this.options[this.selectedIndex].value] || "You must select a reason.";
jQuery('#post_reason_description').text(description);
};
</script>
</div>
<% end %>

View File

@ -0,0 +1,16 @@
<% unless PostSet.find_by_id(params[:disp_id]) %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That set does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @set = PostSet.find_by_id(params[:disp_id]) %>
<h4>You are reporting the following set:</h4>
<div class='page'><%= fast_link_to(@set.name, controller: "set", action: "show", id: @set.id) %></div>
<% if (@set.user_id) %>
<div class='author'><%= fast_link_to(@set.user.name, controller: "user", action: "show", id: @set.user_id) %></div>
<% else %>
<div class='author'><%= h User.find_name(@set.user_id) %></div>
<% end %>
<span class="date" title="Created on <%= @set.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@set.created_at) %> ago</span>
</div>
<% end %>

View File

@ -0,0 +1,8 @@
<% unless @ticket.accused %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That user does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
You are reporting <%= link_to_user(@ticket.accused) %>.
</div>
<% end %>

View File

@ -0,0 +1,12 @@
<% unless WikiPage.find_by_id(params[:disp_id]) %>
<% @found_item = false %>
<div class='section' style='width:80em;'>That wiki page does not exist.</div>
<% else %>
<div class='section' style='width:80em;'>
<% @wiki = WikiPage.find_by_id(params[:disp_id]) %>
<h4>You are reporting the following wiki page:</h4>
<div class='page'><%= fast_link_to(@wiki.title, controller: "wiki", action: "show", title: @wiki.title) %></div>
<span class="date" title="Posted on <%= @wiki.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@wiki.created_at) %> ago</span>
<%= format_text(@wiki.body, mode: :wiki, userlevel: '50') %>
</div>
<% end %>

View File

@ -0,0 +1,135 @@
<div id="c-tickets">
<div id="a-show">
<div class='section'>
<h3><%= @ticket.type_title %> Ticket</h3>
<table>
<% if @ticket.qtype == "namechange" %>
<tr>
<td><span class='title'>Original Name</span></td>
<td><%= link_to h(@ticket.oldname), controller: "user", action: "show", id: @ticket.user_id %></td>
</tr>
<% elsif @ticket.can_see_username?(CurrentUser) %>
<tr>
<td><span class='title'>Requested by</span></td>
<td><%= link_to_user @ticket.creator %></td>
</tr>
<% end %>
<tr>
<td><span class="title">Created</span></td>
<td style="cursor:help;" title="<%= @ticket.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@ticket.created_at) %> ago</td>
</tr>
<tr>
<td><span class="title">Updated</span></td>
<td style="cursor:help;" title="<%= @ticket.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@ticket.updated_at) %> ago</td>
</tr>
<% if CurrentUser.is_admin? %>
<tr>
<td><span class="title">Claimed By</span></td>
<% if @ticket.claimant.nil? %>
<td id="claimed_by"><%= link_to 'Claim', claim_ticket_path(@ticket), method: :post %></td>
<% else %>
<td id="claimed_by"><%= @ticket.claimant.name %>
<% if @ticket.claimant.id == CurrentUser.id %> | <%= link_to 'Unclaim', unclaim_ticket_path(@ticket), method: :post %><% end %></td>
<% end %>
</tr>
<% end %>
<tr>
<td><span class='title'>Status</span></td>
<td class="<%= @ticket.status %>-ticket"><%= pretty_ticket_status(@ticket) %></td>
</tr>
<% if CurrentUser.is_admin? %>
<tr>
<td><span class='title'>IP</span></td>
<td><%= link_to_ip(@ticket.creator_ip_addr) %></td>
</tr>
<% end %>
<%= render partial: "tickets/types/#{@ticket.qtype}" %>
<tr>
<% if (@ticket.qtype == "namechange") %>
<td><span class='title'>Desired Name</span></td>
<% else %>
<td><span class='title'>Reason</span></td>
<% end %>
<% if @ticket.can_see_reason?(CurrentUser) %>
<td><%= format_text(@ticket.reason) %></td>
<% else %>
<td><span style="cursor:help;" class="redtext" title="Due to privacy concerns, this information is confidential">Confidential</span></td>
<% end %>
</tr>
<% if(!@ticket.response.blank?) %>
<tr>
<td><span class='title'>Handled by</span></td>
<% if (!@ticket.handler.nil?)%>
<td><%= link_to_user @ticket.handler %></td>
<% else %>
<td>Unknown</td>
<% end %>
</tr>
<tr>
<td><span class='title'>Response</span></td>
<% if @ticket.can_see_response?(CurrentUser) %>
<td><%= format_text(!@ticket.response.blank? ? @ticket.response : "No response.") %></td>
<% else %>
<td><span style="cursor:help;" class="redtext" title="Due to privacy concerns, this information is confidential">Confidential</span></td>
<% end %>
</tr>
<% end %>
</table>
</div>
<% if CurrentUser.is_admin? %>
<%= simple_form_for(@ticket) do |f| %>
<div class='section'>
<% if (@ticket.qtype == "namechange") %>
<table>
<tr>
<% if @ticket.status == "pending" %>
<td colspan="2"><input type="checkbox" name="no_username_change" id="no_username_change"/> <label for="no_username_change">Don't automatically change username to <span class='yellowtext'><%= @ticket.reason %></span> (use <%= fast_link_to("this form", controller: "user", action: "change_username", id: @ticket.user_id, newname: @ticket.reason) %>)</label></td>
<% else %>
<td colspan="2">Ticket has already been approved/denied, so no automatic username change will happen.<br /><%= fast_link_to("Username change form", controller: "user", action: "change_username", id: @ticket.user_id, newname: @ticket.reason) %></td>
<% end %>
</tr>
<tr>
<td><span class='title'>Status</span></td>
<td>
<select name="ticket[status]">
<option value="approved">Approved</option>
<option value="denied">Denied</option>
</select>
</td>
</tr>
<tr>
<td><span class='title'>Reason</span></td>
<td><%= text_field "ticket", "response", size: 50, value: "Username changed" %></td>
</tr>
</table>
<%= submit_tag "Submit" %>
<% else %>
<table>
<%= f.input :status, collection: [['Investigated', 'approved'], ['Under Investigation', 'partial']], selected: @ticket.status || 'approved' %>
<tr>
<td><%= dtext_field "ticket", "response", :classes => "autocomplete-mentions", :value => @ticket.response, :input_id => "ticket_response_for_#{@ticket.id}", :preview_id => "dtext-preview-for-#{@ticket.id}" %></td>
</tr>
</table>
<%= tag.input name: 'force_claim', type: 'hidden', value: params[:force_claim] %>
<%= f.button :submit, "Submit" %>
<%= dtext_preview_button "ticket", "response", :input_id => "ticket_response_for_#{@ticket.id}", :preview_id => "dtext-preview-for-#{@ticket.id}" %>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
<% content_for(:title) do %>
<%= @ticket.type_title %>
<% end %>
<% render partial: 'secondary_links' %>

View File

@ -0,0 +1,13 @@
<% if @ticket.blip %>
<tr>
<td><span class='title'>Reported Blip</span></td>
<td>
<%= link_to "Blip by #{@ticket.blip.creator_name}", blip_path(@ticket.blip) %>
</td>
</tr>
<% else %>
<tr>
<td><span class='title'>Reported Blip</span></td>
<td><span class='redtext'>Blip has been deleted</span></td>
</tr>
<% end %>

View File

@ -0,0 +1,14 @@
<% if @ticket.comment %>
<tr>
<td><span class='title'>Reported Comment</span></td>
<td>
<%= fast_link_to("Comment by #{@ticket.comment.creator_name}", comment_path(@ticket.comment)) %> on
<%= fast_link_to("post ##{@ticket.comment.post_id}", post_path(@ticket.comment.post_id)) %>
</td>
</tr>
<% else %>
<tr>
<td><span class='title'>Reported Comment</span></td>
<td><span class='redtext'>Comment has been deleted</span></td>
</tr>
<% end %>

View File

@ -0,0 +1,13 @@
<tr>
<td><span class='title'>Reported Message</span></td>
<td>
<% if @ticket.can_see_details?(CurrentUser) %>
<% @dmail = @ticket.dmail %>
<%= fast_link_to("Message", controller: "dmail", action: "show", id: @ticket.disp_id) %> sent from
<%= fast_link_to(@dmail.from_name, controller: "user", action: "show", id: @dmail.from_id) %> to
<%= fast_link_to(@dmail.to_name, controller: "user", action: "show", id: @dmail.to_id) %>
<% else %>
<span style="cursor:help;" class="redtext" title="Due to privacy concerns, this information is confidential">Confidential</span>
<% end %>
</td>
</tr>

View File

@ -0,0 +1,16 @@
<% if (@forum = ForumPost.find_by_id(@ticket.disp_id)) %>
<tr>
<td><span class='title'>Reported Forum Post</span></td>
<td>
<% if @forum.parent.nil? %>
<%= fast_link_to("Post by #{@forum.creator.name} in '#{@forum.title}'", controller: "forum", action: "show", id: @ticket.disp_id) %>
<% else %>
<%= fast_link_to("Post by #{@forum.creator.name} in '#{@forum.parent.title}'", controller: "forum", action: "show", id: @ticket.disp_id) %>
<% end %></td>
</tr>
<% else %>
<tr>
<td><span class='title'>Reported Forum Post</span></td>
<td><span class='redtext'>Forum post has been deleted</span></td>
</tr>
<% end %>

View File

@ -0,0 +1,6 @@
<% if (@pool = Pool.find_by_id(@ticket.disp_id)) %>
<tr>
<td><span class='title'>Reported Pool</span></td>
<td><%= fast_link_to(@pool.name, controller: "pool", action: "show", id: @pool.id) %></td>
</tr>
<% end %>

View File

@ -0,0 +1,6 @@
<% if (@post = Post.find_by_id(@ticket.disp_id)) %>
<tr>
<td><span class='title'>Reported Post</span></td>
<td><%= thumb(@post, dtext: true, blacklist: false) %></td>
</tr>
<% end %>

View File

@ -0,0 +1,6 @@
<% if (@set = PostSet.find_by_id(@ticket.disp_id)) %>
<tr>
<td><span class='title'>Reported Set</span></td>
<td><%= fast_link_to(@set.name, controller: "set", action: "show", id: @set.id) %></td>
</tr>
<% end %>

View File

@ -0,0 +1,8 @@
<tr>
<td><span class='title'>Reported User</span></td>
<% if @ticket.can_see_details?(current_user) %>
<td><%= fast_link_to(User.find_by_id(@ticket.disp_id).name, controller: "user", action: "show", id: @ticket.disp_id) %></td>
<% else %>
<td><span style="cursor:help;" class="redtext" title="Due to privacy concerns, this information is confidential">Confidential</span></td>
<% end %>
</tr>

View File

@ -0,0 +1,6 @@
<% if (@wiki = WikiPage.find_by_id(@ticket.disp_id)) %>
<tr>
<td><span class='title'>Reported Wiki Page</span></td>
<td><%= fast_link_to("#{@wiki.title}", controller: "wiki", action: "show", title: @wiki.title) %></td>
</tr>
<% end %>

View File

@ -74,6 +74,13 @@ Rails.application.routes.draw do
end
end
resources :tickets do
member do
post :claim
post :unclaim
end
end
resources :takedowns do
collection do
post :count_matching_posts

View File

@ -0,0 +1,17 @@
class CreateTickets < ActiveRecord::Migration[5.2]
def change
create_table :tickets do |t|
t.integer :creator_id, null: false
t.column :creator_ip_addr, "inet", null: false
t.integer :disp_id, null: false
t.string :qtype, null: false
t.string :status, null: false, default: "pending"
t.string :reason
t.string :response, null: false, default: ''
t.integer :handler_id, null: false, default: 0
t.integer :claimant_id
t.timestamps
end
end
end

View File

@ -1881,6 +1881,45 @@ CREATE SEQUENCE public.takedowns_id_seq
ALTER SEQUENCE public.takedowns_id_seq OWNED BY public.takedowns.id;
--
-- Name: tickets; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.tickets (
id bigint NOT NULL,
creator_id integer NOT NULL,
creator_ip_addr inet NOT NULL,
disp_id integer NOT NULL,
qtype character varying NOT NULL,
status character varying DEFAULT 'pending'::character varying NOT NULL,
reason character varying,
response character varying DEFAULT ''::character varying NOT NULL,
handler_id integer DEFAULT 0 NOT NULL,
claimant_id integer,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
--
-- Name: tickets_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.tickets_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: tickets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.tickets_id_seq OWNED BY public.tickets.id;
--
-- Name: token_buckets; Type: TABLE; Schema: public; Owner: -
--
@ -1981,6 +2020,17 @@ CREATE SEQUENCE public.uploads_id_seq
ALTER SEQUENCE public.uploads_id_seq OWNED BY public.uploads.id;
--
-- Name: user_blacklisted_tags; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.user_blacklisted_tags (
id integer NOT NULL,
user_id integer NOT NULL,
tags text NOT NULL
);
--
-- Name: user_feedback; Type: TABLE; Schema: public; Owner: -
--
@ -2554,6 +2604,13 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id
ALTER TABLE ONLY public.takedowns ALTER COLUMN id SET DEFAULT nextval('public.takedowns_id_seq'::regclass);
--
-- Name: tickets id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.tickets ALTER COLUMN id SET DEFAULT nextval('public.tickets_id_seq'::regclass);
--
-- Name: upload_whitelists id; Type: DEFAULT; Schema: public; Owner: -
--
@ -3002,6 +3059,14 @@ ALTER TABLE ONLY public.takedowns
ADD CONSTRAINT takedowns_pkey PRIMARY KEY (id);
--
-- Name: tickets tickets_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.tickets
ADD CONSTRAINT tickets_pkey PRIMARY KEY (id);
--
-- Name: upload_whitelists upload_whitelists_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -4368,6 +4433,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190220025517'),
('20190220041928'),
('20190222082952'),
('20190228144206');
('20190228144206'),
('20190305165101');