V2 of Reports System (#441)

* Fix weird breakpoint top-nav issue

* Fix error handling when submitting blank note

* Change status closed to invalid

* Add ability to sort between status

* WIP

* Add working views for new report page

* Add mailer WIP

* Add MVP of new reports system

* Move collapse to the right & add show page link

* Fix buttons for index page

* Change collapse to collapse/expand

* Remove email functionality from new report

* Update copy of report abuse responses

* WIP of showing emails in reports

* Fix notes for user_role_service.rb

* Remove unused scripts from internal.html.erb

* Update tests for reports dashboard

* Add missing migration file

* Add finishing touches to reports dashboard

* Remove deprecated methods

* Update view with better timestamps

* Update and write new tests

* Add .codeclimate.yml

* Remove commented out editor thing

* Undo commented out code for recaptcha method

* Undo comment out code for development

* Use new comment path instead of old comment path
This commit is contained in:
Andy Zhao 2018-09-04 17:08:41 -04:00 committed by Ben Halpern
parent 832e892373
commit 22c9714919
34 changed files with 692 additions and 219 deletions

0
.codeclimate.yml Normal file
View file

View file

@ -2,7 +2,7 @@ module Admin
class UsersController < Admin::ApplicationController
def update
user = User.find(params[:id])
UserRoleService.new(user).check_for_roles(params[:user])
UserRoleService.new(user, current_user.id).check_for_roles(params[:user])
if user.errors.messages.blank? && user.update(user_params)
flash[:notice] = "User successfully updated"
redirect_to "/admin/users/#{params[:id]}"

View file

@ -102,7 +102,7 @@ class CommentsController < ApplicationController
authorize @comment
if @comment.update(permitted_attributes(@comment).merge(edited_at: Time.zone.now))
Mention.create_all(@comment)
redirect_to "#{@comment.commentable.path}/comments/#{@comment.id_code_generated}", notice: "Comment was successfully updated."
redirect_to @comment.path, notice: "Comment was successfully updated."
else
@commentable = @comment.commentable
render :edit

View file

@ -9,7 +9,6 @@ class FeedbackMessagesController < ApplicationController
)
if recaptcha_verified? && @feedback_message.save
send_slack_message
# NotifyMailer.new_report_email(@feedback_message).deliver if @feedback_message.reporter_id?
redirect_to "/feedback_messages"
elsif feedback_message_params[:feedback_type] == "bug-reports"
flash[:notice] = "Make sure the forms are filled 🤖 "
@ -21,10 +20,6 @@ class FeedbackMessagesController < ApplicationController
end
end
def show
@feedback_message = FeedbackMessage.find_by(slug: params[:slug])
end
private
def recaptcha_verified?

View file

@ -3,26 +3,50 @@ class Internal::FeedbackMessagesController < Internal::ApplicationController
def index
@feedback_type = params[:state] || "abuse-reports"
@status = params[:status] || "Open"
@feedback_messages = FeedbackMessage.
where(feedback_type: @feedback_type).
where(feedback_type: @feedback_type, status: @status).
order("created_at DESC").
page(params[:page] || 1).per(25)
end
def update
@feedback_message = FeedbackMessage.find(params[:id])
@feedback_message.status = feedback_message_params[:status]
@feedback_message.reviewer_id = feedback_message_params[:reviewer_id]
note = @feedback_message.find_or_create_note(@feedback_message.feedback_type)
note.content = feedback_message_params[:note][:content]
if @feedback_message.save! && note.save!
@feedback_message.touch(:last_reviewed_at)
flash[:success] = "Report ##{@feedback_message.id} saved. Remember to send emails!"
redirect_to "/internal/reports?state=#{@feedback_message.feedback_type}"
def save_status
feedback_message = FeedbackMessage.find(params[:id])
if feedback_message.update(status: params[:status])
render json: { outcome: "Success" }
else
@feedback_messages = FeedbackMessage.where(feedback_type: @feedback_type)
flash[:error] = @feedback_message.errors.full_messages
render "index.html.erb", state: @feedback_message.feedback_type
render json: { outcome: feedback_message.errors.full_messages }
end
end
def show
@feedback_message = FeedbackMessage.find_by(id: params[:id])
end
def send_email
if NotifyMailer.feedback_message_resolution_email(params).deliver
render json: { outcome: "Success" }
else
render json: { outcome: "Failure" }
end
end
def create_note
note = Note.new(
noteable_id: params["noteable_id"],
noteable_type: params["noteable_type"],
author_id: params["author_id"],
content: params["content"],
reason: params["reason"],
)
if note.save
render json: {
outcome: "Success",
content: params["content"],
author_name: note.author.name,
}
else
render json: { outcome: note.errors.full_messages }
end
end
@ -30,8 +54,8 @@ class Internal::FeedbackMessagesController < Internal::ApplicationController
def feedback_message_params
params[:feedback_message].permit(
:status, :reviewer_id,
note: %i[content reason]
:id, :status, :reviewer_id,
note: %i[content reason noteable_id noteable_type author_id]
)
end
end

View file

@ -0,0 +1,52 @@
module FeedbackMessagesHelper
def offender_email_details
body = <<~HEREDOC
Hi [*USERNAME*],
All dev.to members are expected to help foster a welcoming environment for the community and abide by our terms and conditions of use. It's been brought to our attention that you may have violated our code of conduct. If this behavior continues, we will need to ban your posting privileges on dev.to.
If you think there's been a mistake, please reply to this email and we'll sort it out.
Thanks,
dev.to team
HEREDOC
{
subject: "dev.to Status Update",
body: body,
}.freeze
end
def reporter_email_details
body = <<~HEREDOC
Hi [*USERNAME*],
We wanted to say thank you for flagging a [*comment/post*] that was in violation of the dev.to code of conduct and terms of service. Your action has helped us continue our work of fostering an open and welcoming community.
We've also removed the offending posts and reached out to the offender(s).
Thanks again for being a great part of the community.
PBJ
HEREDOC
{
subject: "dev.to Status Update",
body: body,
}.freeze
end
def victim_email_details
body = <<~HEREDOC
Hi [*USERNAME*],
We noticed some comments (made by others) on your [*post/comment*] that violated the dev.to code of conduct. We want you to know that we have zero tolerance for such behavior, and have removed the offending posts and reached out to the offender(s).
Thanks for being awesome and please don't hesitate to email us with any questions. We welcome all feedback and ideas as we continue our work of fostering an open and welcoming community.
PBJ
HEREDOC
{
subject: "Courtesy Notice from dev.to",
body: body,
}.freeze
end
end

View file

@ -49,6 +49,14 @@ class NotifyMailer < ApplicationMailer
mail(to: @user.email, subject: "You just got a badge")
end
def feedback_message_resolution_email(params)
@user = User.find_by(email: params[:email_to])
@email_body = params[:email_body]
track utm_campaign: params[:email_type]
track extra: { feedback_message_id: params[:feedback_message_id] }
mail(to: params[:email_to], subject: params[:email_subject])
end
def new_report_email(report)
@feedback_message = report
@user = report.reporter

View file

@ -2,4 +2,9 @@ class EmailMessage < Ahoy::Message
# So far this is mostly used to be compatible with administrate gem,
# which doesn't seem to play nicely with namespaces. But there could be other
# reasons to define behavior here, similar to how we use the Tag model.
def body_html_content
doctype_index = content.index("<!DOCTYPE")
closing_html_index = content.index("</html>") + 6
content[doctype_index..closing_html_index]
end
end

View file

@ -3,7 +3,7 @@ class FeedbackMessage < ApplicationRecord
belongs_to :reviewer, foreign_key: "reviewer_id", class_name: "User", optional: true
belongs_to :reporter, foreign_key: "reporter_id", class_name: "User", optional: true
belongs_to :victim, foreign_key: "victim_id", class_name: "User", optional: true
has_one :note, as: :noteable, dependent: :destroy
has_many :notes, as: :noteable, dependent: :destroy
validates_presence_of :feedback_type, :message
validates_presence_of :reported_url, :category, if: :abuse_report?
@ -11,31 +11,20 @@ class FeedbackMessage < ApplicationRecord
inclusion: {
in: ["spam", "other", "rude or vulgar", "harassment", "bug"],
}
before_validation :generate_slug
validates :status,
inclusion: {
in: ["Open", "Invalid", "Resolved"],
}
def abuse_report?
feedback_type == "abuse-reports"
end
def generate_slug
self.slug = SecureRandom.hex(10) unless slug?
end
def capitalize_status
self.status = status.capitalize unless status.blank?
end
def to_eastern_strftime(time)
return if time.nil?
time.in_time_zone("America/New_York").strftime("%A, %b %d %Y - %I:%M %p %Z")
end
def path
"/reports/#{slug}"
end
def find_or_create_note(reason)
note || Note.new(reason: reason, noteable_id: id, noteable_type: "FeedbackMessage")
def email_messages
EmailMessage.where(feedback_message_id: id)
end
end

View file

@ -1,6 +1,5 @@
class Note < ApplicationRecord
belongs_to :noteable, polymorphic: true
belongs_to :author, class_name: "User"
validates :reason, :content, presence: true
validates :noteable_id, uniqueness:
{ scope: :reason, message: "limited to one note per noteable per reason" }
end

View file

@ -51,7 +51,7 @@ class Notification < ApplicationRecord
if notifiable.class.name == "Broadcast" || action == "Moderation"
User.find(ApplicationConfig["DEVTO_USER_ID"])
else
notifiable.user
notifiable&.user
end
end

View file

@ -23,6 +23,7 @@ class User < ApplicationRecord
has_many :mentions, dependent: :destroy
has_many :messages
has_many :notes, as: :noteable
has_many :authored_notes, as: :author, class_name: "Note"
has_many :notifications, dependent: :destroy
has_many :reactions, dependent: :destroy
has_many :tweets, dependent: :destroy

View file

@ -1,6 +1,7 @@
class UserRoleService
def initialize(user)
def initialize(user, current_user_id)
@user = user
@current_user_id = current_user_id
end
def check_for_roles(params)
@ -34,7 +35,11 @@ class UserRoleService
def new_roles?(params)
params[:trusted] == "1" ? @user.add_role(:trusted) : @user.remove_role(:trusted)
params[:analytics] == "1" ? @user.add_role(:analytics_beta_tester) : @user.remove_role(:analytics_beta_tester)
if params[:analytics] == "1"
@user.add_role(:analytics_beta_tester)
else
@user.remove_role(:analytics_beta_tester)
end
if params[:scholar] == "1"
@user.add_role(:workshop_pass)
@user.update(workshop_expiration: params[:workshop_expiration])
@ -70,6 +75,7 @@ class UserRoleService
note = Note.find_by(noteable_id: @user.id, noteable_type: "User", reason: reason)
if note.nil?
Note.create(
author_id: @current_user_id,
noteable_id: @user.id,
noteable_type: "User",
reason: reason,

View file

@ -2,5 +2,8 @@
<div>
<h2>Thank you for your report.</a></h2>
<h3><a href="/">Return to home page</a></h3>
<p>
Questions? Send an email to <a href="mailto:yo@dev.to">yo@dev.to</a>
</p>
</div>
</div>

View file

@ -1,30 +0,0 @@
<meta name="robots" content="noindex" />
<div class="dialog">
<h1>Thank you for your report. You can use this link to check for any updates on your report:</h1>
<h3>https://dev.to<%= @feedback_message.path %></h3>
<p style="color: white;background-color:#d10000;">Note that the link above is public, but secret.</p>
<div class="report-body">
<h3>Current Status: <%= @feedback_message.status %></h3>
<% if @feedback_message.feedback_type == "abuse-reports" %>
<h3>
Reported URL: <%= link_to @feedback_message.reported_url, @feedback_message.reported_url, target: "_blank" %>
<br>
<small>(Opens in a new tab)</small>
</h3>
<hr>
<h4>Category: <%= @feedback_message.category %></h4>
<% end %>
<h4>Message:</h4>
<p>
<% if @feedback_message.message.blank? %>
<span class="blankmessage">No message was left.</span>
<% else %>
<%= @feedback_message.message %>
<% end %>
</p>
</div>
<hr>
<p>
Questions? Send an email to <a href="mailto:yo@dev.to">yo@dev.to</a>
</p>
</div>

View file

@ -3,58 +3,355 @@
width: 100%;
resize: none;
font-size: 18px;
height: 100px;
height: 50px;
border-radius: 3px;
padding: 5px;
margin: 10px 0px;
}
.blankmessage{
font-style: italic;
}
.note-content{
border: 1px dashed black;
border-radius: 5px;
width: 100%;
padding: 5px;
padding-top: 25px;
word-wrap: break-word;
}
.email__alert{
display: none;
margin: 0;
}
.panel-right-elements{
float: right;
}
.email__container{
border: 1px solid gray;
margin: 10px;
padding-left: 50px;
padding-top: 5px;
}
.to__subject{
margin: 5px;
font-size: 20px;
color: black;
text-align: center;
}
.time__badge{
float: right;
margin-top: 7px;
margin-right: 7px;
}
.single__note{
width: 60%;
}
</style>
<div class="panel-group" id="accordion-<%= feedback_message.id %>" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading<%= feedback_message.id %>">
<h2 class="panel-title">
<a href="#collapse<%= feedback_message.id %>" role="button" data-toggle="collapse" data-parent="#accordion" aria-expanded="true" aria-controls="collapse<%= feedback_message.id %>">
Report #<%= feedback_message.id %> - Submitted: <%= time_ago_in_words feedback_message.created_at %> ago
</a>
<div class="panel-right-elements">
✉️ Emails Sent: <%= feedback_message.email_messages.count %> |
<a href="/internal/reports/<%= feedback_message.id %>">Link to Report #<%= feedback_message.id %></a>
</div>
</h2>
</div>
<div class="panel-collapse collapse in" id="collapse<%= feedback_message.id %>" role="tabpanel" aria-labelledby="heading<%= %>">
<div class="panel-body">
<h2>
Reporter:
<% if feedback_message.reporter_id? %>
<%= feedback_message.reporter.name %>
<a href="<%= feedback_message.reporter.path %>">@<%= feedback_message.reporter.username %></a>
<% else %>
Anonymous
<% end %>
</h2>
<h3>
Reported URL (new tab):
<br>
<a href="<%= feedback_message.reported_url %>" target="_blank"><%= feedback_message.reported_url %></a>
</h3>
<h3>Category: <%= feedback_message.category %></h3>
<h3>
Message:
</h3>
<p>
<% if feedback_message.message.blank? %>
<span class="blankmessage">No message was left.</span>
<% else %>
<%= feedback_message.message %>
<% end %>
</p>
<h3>
Status: <%= f.select :status, ['Open', 'Invalid', 'Resolved'], {}, id: "status__#{feedback_message.id}" %>
<button class="btn btn-primary" role="button" id="save__status__<%= feedback_message.id %>">SAVE STATUS</button>
</h3>
<hr>
<h3>Previous Emails:</h3>
<div class="previous__emails__container">
<% EmailMessage.where(feedback_message_id: feedback_message.id).each do |email| %>
<div class="email__container">
<p class="to__subject">Type: <%= email.utm_campaign.capitalize %></p>
<p class="to__subject">To: <%= email.to %></p>
<p class="to__subject">Subject: <%= email.subject %></p>
<%= email.body_html_content.html_safe %>
</div>
<% end %>
</div>
<h3>Email Form:</h3>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#reporter-<%= feedback_message.id %>" aria-controls="reporter-<%= feedback_message.id %>" role="tab" data-toggle="tab">Reporter</a></li>
<li role="presentation"><a href="#offender-<%= feedback_message.id %>" aria-controls="offender-<%= feedback_message.id %>" role="tab" data-toggle="tab">Offender</a></li>
<li role="presentation"><a href="#victim-<%= feedback_message.id %>" aria-controls="victim-<%= feedback_message.id %>" role="tab" data-toggle="tab">Victim</a></li>
</ul>
<h2>
Report #<%= feedback_message.id %> - Submitted on: <%= feedback_message.to_eastern_strftime(feedback_message.created_at) %>
<hr>
Reporter:
<% if feedback_message.reporter_id? %>
<%= feedback_message.reporter.name %>
<a href="<%= feedback_message.reporter.path %>">@<%= feedback_message.reporter.username %></a>
--
Email: <a href="mailto:<%= feedback_message.reporter.email %>"><%= feedback_message.reporter.email %></a>
<% else %>
Anonymous
<% end %>
</h2>
<h3>
Reported URL (new tab):
<br>
<a href="<%= feedback_message.reported_url %>" target="_blank"><%= feedback_message.reported_url %></a>
</h3>
<h3>Category: <%= feedback_message.category %></h3>
<h3>
Message:
</h3>
<p>
<% if feedback_message.message.blank? %>
<span class="blankmessage">No message was left.</span>
<% else %>
<%= feedback_message.message %>
<% end %>
</p>
<hr>
<h3>
Status: <%= f.select :status, ['Open', 'Closed', 'Resolved'] %>
</h3>
<h3>Note:</h3>
<%= f.fields_for :note do |note| %>
<%= note.hidden_field :reason, value: feedback_message.feedback_type %>
<%= note.text_area :content, value: feedback_message.note&.content, placeholder: "Leave some notes about the status and context of this report.", class: "notefield" %>
<% end %>
<%= f.hidden_field :reviewer_id, value: current_user.id %>
<% if feedback_message.reviewer_id %>
<h4>Last reviewed by: <%= feedback_message.reviewer.name %></h4>
<h4>Last reviewed on: <%= feedback_message.to_eastern_strftime(feedback_message.last_reviewed_at) %></h4>
<% else %>
<h4>Last reviewed by: No one yet</h4>
<% end %>
<br>
<button class="btn btn-primary" type="submit">SUBMIT</button>
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" data-id="<%= feedback_message.id %>" data-userType="reporter" id="reporter-<%= feedback_message.id %>">
<h3>
Send to:
<br>
<%= email_field_tag :reporter_email_to, feedback_message.reporter&.email, class: "form-control", id: "reporter__emailto__#{feedback_message.id}", required: true %>
</h3>
<h3>Subject:</h3>
<%= text_field_tag :reporter_email_subject, reporter_email_details[:subject], class: "form-control", id: "reporter__subject__#{feedback_message.id}" %>
<h3>Body:</h3>
<%= text_area_tag (:reporter_email_body), reporter_email_details[:body], class: "form-control", style: "height: 300px;", id: "reporter__body__#{feedback_message.id}" %>
</div>
<div role="tabpanel" class="tab-pane fade" data-id="<%= feedback_message.id %>" data-userType="offender" id="offender-<%= feedback_message.id %>">
<h3>
Send to:
<br>
<%= email_field_tag :offender_email_to, feedback_message.offender&.email, class: "form-control", id: "offender__emailto__#{feedback_message.id}", required: true %>
</h3>
<h3>Subject:</h3>
<%= text_field_tag :offender_email_subject, offender_email_details[:subject], class: "form-control", id: "offender__subject__#{feedback_message.id}" %>
<h3>Body:</h3>
<%= text_area_tag (:offender_email_body), offender_email_details[:body], class: "form-control", style: "height: 300px;", id: "offender__body__#{feedback_message.id}" %>
</div>
<div role="tabpanel" class="tab-pane fade" data-id="<%= feedback_message.id %>" data-userType="victim" id="victim-<%= feedback_message.id %>">
<h3>
Send to:
<br>
<%= email_field_tag :victim_email_to, feedback_message.victim&.email, class: "form-control", id: "victim__emailto__#{feedback_message.id}", required: true %>
</h3>
<h3>Subject:</h3>
<%= text_field_tag :victim_email_subject, victim_email_details[:subject], class: "form-control", id: "victim__subject__#{feedback_message.id}" %>
<h3>Body:</h3>
<%= text_area_tag (:victim_email_body), victim_email_details[:body], class: "form-control", style: "height: 300px;", id: "victim__body__#{feedback_message.id}" %>
</div>
</div>
<br>
<button class="btn btn-primary" type="submit" id="send__email__btn__<%= feedback_message.id %>">SEND EMAIL ✉️ </button>
<div class="email__alert alert fade in" id="email__alert__<%= feedback_message.id %>">
</div>
<h3>Notes:</h3>
<div class="notes__container" id="notes__<%= feedback_message.id %>">
<% feedback_message.notes&.order("created_at").each do |note| %>
<div class="single__note">
<span class="badge time__badge">
<%= time_ago_in_words note.created_at %> ago
</span>
<p class="note-content">
<%= note.author.name %>: <%= note.content %>
</p>
</div>
<% end %>
</div>
<input type="hidden" name="reason" value="<%= feedback_message.feedback_type %>" id="note__reason__<%= feedback_message.id %>">
<input type="hidden" name="noteable_id" value="<%= feedback_message.id %>" id="note__noteable-id__<%= feedback_message.id %>">
<input type="hidden" name="noteable_type" value="FeedbackMessage" id="note__noteable-type__<%= feedback_message.id %>">
<input type="hidden" name="author_id" value="<%= current_user.id %>" id="note__author-id__<%= feedback_message.id %>">
<input
type="textarea"
name="content"
placeholder="Leave some notes about the status and context of this report."
class="notefield"
id="note__content__<%= feedback_message.id %>"
required
>
<br>
<button class="btn btn-primary" type="submit" id="note__submit__<%= feedback_message.id %>">SUBMIT NOTE 📝 </button>
</div>
</div>
</div>
</div>
<script>
function saveStatus(id) {
var formData = new FormData();
var statusSelectTag = document.getElementById('status__' + id);
var statusBtn = document.getElementById('save__status__' + id);
formData.append('id', id);
formData.append('status', statusSelectTag.options[statusSelectTag.selectedIndex].value);
fetch('/internal/reports/save_status', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").content,
},
body: formData,
credentials: 'same-origin'
})
.then(response => response.json()
.then(json => {
if (json.outcome === 'Success') {
console.log(json.outcome)
statusBtn.innerText = "Saved!";
setTimeout(function() {
statusBtn.innerText = "SAVE STATUS"
}, 1000);
} else {
}
}))
}
document.getElementById('save__status__' + <%= feedback_message.id %>).addEventListener('click', function(event) {
event.preventDefault();
var reportId = <%= feedback_message.id %>
saveStatus(reportId);
});
function successfulEmail(alert) {
alert.style.display = 'inline-block';
alert.classList.add('alert-success');
alert.innerHTML = "Email sent successfully! Refresh to see";
}
function failedEmail(alert) {
alert.style.display = 'inline-block';
alert.classList.add('alert-danger');
alert.innerHTML = "Email failed to send, see console for errors";
}
function sendEmail(id) {
var activeTab = document.querySelector(".tab-pane.fade.in.active[data-id='" + id + "']");
var alert = document.getElementById('email__alert__' + id);
var sendEmailBtn = document.getElementById('send__email__btn__' + id);
var userType = activeTab.dataset.usertype;
var emailToAddress = activeTab.querySelector("#" + userType + "__emailto__" + id).value;
var subjectLine = activeTab.querySelector("#" + userType + "__subject__" + id).value;
var emailBody = activeTab.querySelector("#" + userType + "__body__" + id).value;
alert.style.display = "none";
alert.classList.remove("alert-warning");
alert.classList.remove("alert-success");
alert.classList.remove("alert-danger");
if(emailToAddress === "") {
alert.style.display = 'inline-block';
alert.classList.add("alert-warning");
alert.innerHTML = "Email can't be blank!"
return
}
if(emailBody.includes("[*") || emailBody.includes("*]")) {
alert.style.display = 'inline-block';
alert.classList.add("alert-warning");
alert.innerHTML = "Username or (comment/post) wasn't changed!"
return
}
var formData = new FormData();
formData.append('feedback_message_id', id);
formData.append('email_to', emailToAddress);
formData.append('email_body', emailBody);
formData.append('email_subject', subjectLine);
formData.append('email_type', userType);
fetch('/internal/reports/send_email', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").content,
},
body: formData,
credentials: 'same-origin'
})
.then(response => response.json()
.then(json => {
if (json.outcome === "Success") {
successfulEmail(alert);
} else {
console.log('email failed')
failedEmail(alert);
}
}))
.catch(error => {
failedEmail(alert);
console.log(error);
})
}
document.getElementById('send__email__btn__' + <%= feedback_message.id %>).addEventListener('click', function(event) {
event.preventDefault();
var reportId = <%= feedback_message.id %>;
sendEmail(reportId);
});
function addNoteToPage(noteParams, id) {
var notesDiv = document.getElementById('notes__' + id);
var singleNoteDiv = document.createElement('div');
singleNoteDiv.setAttribute('class', 'single__note');
var timeBadge = document.createElement('span');
timeBadge.setAttribute('class', 'time__badge badge');
timeBadge.innerText = "just now";
singleNoteDiv.append(timeBadge);
var newNote = document.createElement('p');
newNote.className = 'note-content';
var noteContent = document.createTextNode(noteParams.author_name + ': ' + noteParams.content);
newNote.appendChild(noteContent);
singleNoteDiv.append(newNote);
notesDiv.append(singleNoteDiv);
}
function clearNote(id) {
document.getElementById('note__content__' + id).value = '';
}
function submitNote(id) {
var formData = new FormData();
formData.append('content', document.getElementById('note__content__' + id).value)
formData.append('reason', document.getElementById('note__reason__' + id).value)
formData.append('noteable_id', document.getElementById('note__noteable-id__' + id).value)
formData.append('noteable_type', document.getElementById('note__noteable-type__' + id).value)
formData.append('author_id', document.getElementById('note__author-id__' + id).value)
fetch('/internal/reports/create_note', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").content,
},
body: formData,
credentials: 'same-origin'
})
.then(response => response.json()
.then(json => {
if (json.outcome === 'Success') {
addNoteToPage(json, id);
clearNote(id);
} else {
// failedNote();
}
})
.catch(error => {
// failedNote();
console.log(error);
}))
}
document.getElementById('note__submit__<%= feedback_message.id %>').addEventListener('click', function() {
var reportId = <%= feedback_message.id %>;
submitNote(reportId);
});
</script>

View file

@ -2,6 +2,14 @@
.top-nav{
line-height: 1.4em;
}
@media (max-width:991px) and (min-width:768px) {
.top-nav{
margin-top: 100px;
}
.alert{
margin-top: 100px;
}
}
.top-nav a {
display: inline-block;
padding: 10px;
@ -12,8 +20,12 @@
</style>
<h3 class="top-nav">
<a href="/internal/reports?state=abuse-reports" class="<%= "active-state" if @feedback_type == "abuse-reports" %>">Abuse Reports</a> |
<a href="/internal/reports?state=bug-reports" class="<%= "active-state" if @feedback_type == "bug-reports" %>">Bug Reports</a>
<a href="/internal/reports?state=abuse-reports&status=Open" class="<%= "active-state" if @feedback_type == "abuse-reports" %>">Abuse Reports</a> |
<a href="/internal/reports?state=bug-reports&status=Open" class="<%= "active-state" if @feedback_type == "bug-reports" %>">Bug Reports</a>
<hr>
<a href="/internal/reports?state=<%= @feedback_type %>&status=Open" class="<%= "active-state" if @status == "Open" %>">Open/Unresolved</a> |
<a href="/internal/reports?state=<%= @feedback_type %>&status=Resolved" class="<%= "active-state" if @status == "Resolved" %>">Resolved</a> |
<a href="/internal/reports?state=<%= @feedback_type %>&status=Invalid" class="<%= "active-state" if @status == "Invalid" %>">Invalid</a>
</h3>
<h1><%= @feedback_type.titleize %></h1>
@ -21,11 +33,9 @@
<%= paginate @feedback_messages %>
<% @feedback_messages.each do |feedback_message| %>
<div class="row main-group">
<%= form_for [:internal, feedback_message] do |f| %>
<%= render "feedback_message", f: f, feedback_message: feedback_message %>
<% end %>
</div>
<%= form_for [:internal, feedback_message] do |f| %>
<%= render "feedback_message", f: f, feedback_message: feedback_message %>
<% end %>
<% end %>
<br>
<br>

View file

@ -0,0 +1,10 @@
<div class="blank_space" style="height: 50px;">
</div>
<div class="blank_space">
</div>
<div class="blank_space">
</div>
<%= form_for [:internal, @feedback_message] do |f| %>
<%= render "feedback_message", f: f, feedback_message: @feedback_message %>
<% end %>

View file

@ -8,6 +8,7 @@
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<%= csrf_meta_tags %>
<%= favicon_link_tag %>
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
@ -112,14 +113,5 @@
</div><!-- /.container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="../../dist/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../../assets/js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>

View file

@ -0,0 +1,3 @@
<p>
<%= @email_body %>
</p>

View file

@ -0,0 +1 @@
<%= @email_body %>

View file

@ -1,6 +0,0 @@
Thank you for your report. You can check the status of your report here:
<br>
<a href="https://dev.to<%= @feedback_message.path %>">https://dev.to<%= @feedback_message.path %></a>
<br>
<br>
If you have any questions, feel free to reply to this email, or send an email to <a href="mailto:yo@dev.to">yo@dev.to</a>.

View file

@ -1,4 +0,0 @@
Thank you for your report. You can check the status of your report here:
https://dev.to<%= @feedback_message.path %>
If you have any questions, feel free to reply to this email, or send an email to yo@dev.to.

View file

@ -36,8 +36,12 @@ Rails.application.routes.draw do
end
resources :members, only: [:index]
resources :events
resources :feedback_messages, only: [:update]
resources :reports, only: %i[index update], controller: "feedback_messages"
resources :feedback_messages, only: [:update, :show]
resources :reports, only: %i[index update show], controller: "feedback_messages" do
post "send_email", to: :send_email, on: :collection
post "create_note", to: :create_note, on: :collection
post "save_status", to: :save_status, on: :collection
end
mount Flipflop::Engine => "/features"
end

View file

@ -0,0 +1,6 @@
class UpdateFeedbackMessageNoteSystem < ActiveRecord::Migration[5.1]
def change
remove_column :feedback_messages, :reviewer_id
add_column :notes, :author_id, :integer
end
end

View file

@ -0,0 +1,5 @@
class AddFeedbackMessageIdToEmail < ActiveRecord::Migration[5.1]
def change
add_column :ahoy_messages, :feedback_message_id, :integer
end
end

View file

@ -0,0 +1,5 @@
class RemoveSlugFromFeedbackMessages < ActiveRecord::Migration[5.1]
def change
remove_column :feedback_messages, :slug
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@ -17,6 +19,7 @@ ActiveRecord::Schema.define(version: 20180826174411) do
create_table "ahoy_messages", id: :serial, force: :cascade do |t|
t.datetime "clicked_at"
t.text "content"
t.integer "feedback_message_id"
t.string "mailer"
t.datetime "opened_at"
t.datetime "sent_at"
@ -286,8 +289,6 @@ ActiveRecord::Schema.define(version: 20180826174411) do
t.string "reported_url"
t.boolean "reporter_email_sent?", default: false
t.integer "reporter_id"
t.integer "reviewer_id"
t.string "slug"
t.string "status", default: "Open"
t.datetime "updated_at"
t.boolean "victim_email_sent?", default: false
@ -387,6 +388,7 @@ ActiveRecord::Schema.define(version: 20180826174411) do
end
create_table "notes", id: :serial, force: :cascade do |t|
t.integer "author_id"
t.text "content"
t.datetime "created_at", null: false
t.integer "noteable_id"

View file

@ -72,17 +72,29 @@ RSpec.describe NotifyMailer, type: :mailer do
end
end
describe "#new_report_email" do
describe "#new_feedback_message_resolution_email" do
def params(user_email, feedback_message_id)
{
email_to: user_email,
email_subject: "dev.to Status Update",
email_body: "You've violated our code of conduct",
email_type: "Reporter",
feedback_message_id: feedback_message_id,
}
end
it "renders proper subject" do
feedback_message = create(:feedback_message, :abuse_report, reporter_id: user.id)
new_report_email = described_class.new_report_email(feedback_message)
expect(new_report_email.subject).to eq("Thank you for your report")
feedback_message_resolution_email = described_class.
feedback_message_resolution_email(params(user.email, feedback_message.id))
expect(feedback_message_resolution_email.subject).to eq("dev.to Status Update")
end
it "renders proper receiver" do
feedback_message = create(:feedback_message, :abuse_report, reporter_id: user.id)
new_report_email = described_class.new_report_email(feedback_message)
expect(new_report_email.to).to eq([user.email])
feedback_message_resolution_email = described_class.
feedback_message_resolution_email(params(user.email, feedback_message.id))
expect(feedback_message_resolution_email.to).to eq([user.email])
end
end
end

View file

@ -28,6 +28,27 @@ class NotifyMailerPreview < ActionMailer::Preview
NotifyMailer.new_report_email(FeedbackMessage.first)
end
def feedback_message_resolution_email
# change email_body text when you need to see a different version
email_body = <<~HEREDOC
Hi [*USERNAME*],
We wanted to say thank you for flagging a [comment/post] that was in violation of the dev.to code of conduct and terms of service. Your action has helped us continue our work of fostering an open and welcoming community.
We've also removed the offending posts and reached out to the offender(s).
Thanks again for being a great part of the community.
PBJ
HEREDOC
params = {
email_to: "andy@dev.to",
email_subject: "Courtesy notice from dev.to",
email_body: email_body,
}
NotifyMailer.feedback_message_resolution_email(params)
end
def new_message_email
NotifyMailer.new_message_email(Message.last)
end

View file

@ -42,8 +42,4 @@ RSpec.describe FeedbackMessage, type: :model do
in_array(["spam", "other", "rude or vulgar", "harassment", "bug"])
end
end
it "always generates a slug" do
expect(abuse_report.slug).not_to be_nil
end
end

View file

@ -0,0 +1,97 @@
require "rails_helper"
RSpec.describe "/internal/reports", type: :request do
let(:feedback_message) { create(:feedback_message, :abuse_report) }
let(:feedback_message2) { create(:feedback_message, :abuse_report) }
let(:feedback_message3) { create(:feedback_message, :abuse_report) }
let(:user) { create(:user) }
let(:admin) { create(:user, :super_admin) }
describe "GET "
describe "POST /save_status" do
context "when a valid request is made" do
before do
login_as admin
valid_save_status_params = { status: "Resolved" }
valid_save_status_params[:id] = feedback_message.id
post "/internal/reports/save_status", params:
valid_save_status_params
end
it "returns a JSON with an outcome key and Success value" do
expect(JSON.parse(response.body)).to eq("outcome" => "Success")
end
it "updates the status of the feedback message" do
expect(FeedbackMessage.last.status).to eq("Resolved")
end
end
end
describe "POST /send_email" do
context "when a valid request is made" do
valid_send_email_params = {
"feedback_message_id" => 1,
"email_subject" => "Thank you for your report",
"email_body" => "Thanks for your report and being an awesome member!",
"email_type" => "reporter",
}
email_message_attributes = {
"feedback_message_id" => 1,
"subject" => "Thank you for your report",
"utm_campaign" => "reporter",
}
before do
feedback_message
login_as admin
valid_send_email_params["email_to"] = user.email
post "/internal/reports/send_email", params:
valid_send_email_params
end
it "returns a JSON with an outcome key and Success value" do
expect(JSON.parse(response.body)).to eq("outcome" => "Success")
end
it "creates a new email message with the same params" do
email_message_attributes["to"] = user.email
expect(EmailMessage.last.attributes).to include(email_message_attributes)
end
end
end
describe "POST /internal/reports/create_note" do
context "when a valid request is made" do
note_params = {
"content" => "test note",
"reason" => "abuse-reports",
"noteable_type" => "FeedbackMessage",
}
json_response = {
outcome: "Success",
content: "test note",
}
before do
feedback_message
login_as admin
note_params["noteable_id"] = feedback_message.id
note_params["author_id"] = admin.id
post "/internal/reports/create_note", params: note_params
end
it "renders the proper JSON response" do
json_response["author_name"] = admin.name
expect(response.body).to eq(json_response.to_json)
end
it "creates a note with the correct params" do
expect(Note.last.attributes).to include(note_params)
end
end
end
end

View file

@ -1,42 +0,0 @@
require "rails_helper"
RSpec.describe "/internal/feedback_messages", type: :request do
describe "PUTS /internal/feedback_messages" do
context "with a valid request" do
let(:feedback_message) { create(:feedback_message, :abuse_report) }
let(:admin) { create(:user, :super_admin) }
valid_abuse_report_params = {
feedback_message: {
id: 1,
status: "Resolved",
note: {
content: "this is valid",
reason: "abuse-reports",
},
},
}
before do
login_as(admin)
valid_abuse_report_params[:feedback_message][:reviewer_id] = admin.id
patch "/internal/feedback_messages/#{feedback_message.id}", params:
valid_abuse_report_params
end
it "adds a note to a report" do
expect(FeedbackMessage.last.note.content).to eq(
valid_abuse_report_params[:feedback_message][:note][:content],
)
end
it "adds the current user as the reviewer" do
expect(FeedbackMessage.find_by(reviewer_id: admin.id)).not_to eq(nil)
end
it "updates the last_reviewed_at timestamp" do
expect(FeedbackMessage.last.last_reviewed_at).not_to eq(nil)
end
end
end
end

View file

@ -2,44 +2,49 @@ require "rails_helper"
RSpec.describe UserRoleService do
let(:user) { create(:user) }
let(:admin) { create(:user, :super_admin) }
let(:banned_user) { create(:user, :banned) }
describe "#check_for_roles" do
it "unbans a user if they were previously banned" do
described_class.new(banned_user).check_for_roles(banned: "0")
described_class.new(banned_user, admin.id).check_for_roles(banned: "0")
expect(banned_user.has_role?(:banned)).to eq(false)
end
it "bans a user when there is a reason included" do
described_class.new(user).check_for_roles(banned: "1", reason_for_ban: "some reason")
described_class.new(user, admin.id).
check_for_roles(banned: "1", reason_for_ban: "some reason")
expect(user.has_role?(:banned)).to eq(true)
end
it "gives an error if there was no reason for ban included" do
described_class.new(user).check_for_roles(banned: "1")
described_class.new(user, admin.id).check_for_roles(banned: "1")
expect(user.errors.messages[:reason_for_ban].first).
to eq("can't be blank if banned is checked")
end
it "gives an error if there was a reason for ban but banned was not checked" do
described_class.new(user).check_for_roles(banned: "0", reason_for_ban: "some reason")
described_class.new(user, admin.id).
check_for_roles(banned: "0", reason_for_ban: "some reason")
expect(user.errors.messages[:banned].first).
to eq("was not checked but had the reason filled out")
end
it "warns a user when there is a reason included" do
described_class.new(user).check_for_roles(warned: "1", reason_for_warning: "some_reason")
described_class.new(user, admin.id).
check_for_roles(warned: "1", reason_for_warning: "some_reason")
expect(user.has_role?(:warned)).to eq(true)
end
it "gives an error if there was no reason for warning included" do
described_class.new(user).check_for_roles(warned: "1")
described_class.new(user, admin.id).check_for_roles(warned: "1")
expect(user.errors.messages[:reason_for_warning].first).
to eq("can't be blank if warned is checked")
end
it "gives an error if there was a reason for warning but warned was not checked" do
described_class.new(user).check_for_roles(warned: "0", reason_for_warning: "some reason")
described_class.new(user, admin.id).
check_for_roles(warned: "0", reason_for_warning: "some reason")
expect(user.errors.messages[:warned].first).
to eq("was not checked but had the reason filled out")
end
@ -47,39 +52,46 @@ RSpec.describe UserRoleService do
describe "#new_roles?" do
it "adds the trusted role to a user with valid params" do
described_class.new(user).send(:new_roles?, trusted: "1")
described_class.new(user, admin.id).send(:new_roles?, trusted: "1")
expect(user.has_role?(:trusted)).to eq(true)
end
it "adds the analytics role to a user with valid params" do
described_class.new(user).send(:new_roles?, analytics: "1")
described_class.new(user, admin.id).send(:new_roles?, analytics: "1")
expect(user.has_role?(:analytics_beta_tester)).to eq(true)
end
it "adds the scholar role to a user with valid params" do
described_class.new(user).send(:new_roles?, scholar: "1")
described_class.new(user, admin.id).send(:new_roles?, scholar: "1")
expect(user.has_role?(:workshop_pass)).to eq(true)
end
it "adds workshop_expiration date with valid params" do
expiration_date = Time.now + 1.year
described_class.new(user).
described_class.new(user, admin.id).
send(:new_roles?, scholar: "1", workshop_expiration: expiration_date)
expect(user.workshop_expiration).to eq(expiration_date)
end
it "doesn't add a workshop_expiration date if scholar is not checked" do
expiration_date = Time.now + 1.year
described_class.new(user).
described_class.new(user, admin.id).
send(:new_roles?, scholar: "0", workshop_expiration: expiration_date)
expect(user.workshop_expiration).to eq(nil)
end
end
describe "#create_or_update_note" do
note_params = {
reason: "banned",
content: "some reason",
noteable_type: "User",
}
it "updates a user's previous note" do
user.notes.create(reason: "banned", content: "some reason")
described_class.new(user).send(:create_or_update_note, "banned", "a more specific reason")
user.notes.create(note_params.merge(noteable_id: user.id, author_id: user.id))
described_class.new(user, admin.id).
send(:create_or_update_note, "banned", "a more specific reason")
expect(user.notes.count).to eq(1)
end
end