Merge pull request #128 from openHPI/rfc_run_assess_messages
Show test results and program output on RfC page
This commit is contained in:
@ -59,8 +59,6 @@ CodeOceanEditorCodePilot = {
|
||||
}
|
||||
};
|
||||
|
||||
//Request for comments does currently not work on staging platform (no relative root_url used here).
|
||||
//To fix this rely on ruby routes
|
||||
CodeOceanEditorRequestForComments = {
|
||||
requestComments: function () {
|
||||
var user_id = $('#editor').data('user-id');
|
||||
@ -83,6 +81,8 @@ CodeOceanEditorRequestForComments = {
|
||||
}).done(function () {
|
||||
this.hideSpinner();
|
||||
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
|
||||
// trigger a run
|
||||
this.runSubmission.call(this, submission);
|
||||
}.bind(this)).error(this.ajaxError.bind(this));
|
||||
};
|
||||
|
||||
|
@ -142,19 +142,21 @@ CodeOceanEditorSubmissions = {
|
||||
* Execution-Logic
|
||||
*/
|
||||
runCode: function(event) {
|
||||
event.preventDefault();
|
||||
if ($('#run').is(':visible')) {
|
||||
this.createSubmission('#run', null, function(response) {
|
||||
//Run part starts here
|
||||
$('#stop').data('url', response.stop_url);
|
||||
this.running = true;
|
||||
this.showSpinner($('#run'));
|
||||
$('#score_div').addClass('hidden');
|
||||
this.toggleButtonStates();
|
||||
var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
|
||||
this.initializeSocketForRunning(url);
|
||||
}.bind(this));
|
||||
}
|
||||
event.preventDefault();
|
||||
if ($('#run').is(':visible')) {
|
||||
this.createSubmission('#run', null, this.runSubmission.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
runSubmission: function (submission) {
|
||||
//Run part starts here
|
||||
$('#stop').data('url', submission.stop_url);
|
||||
this.running = true;
|
||||
this.showSpinner($('#run'));
|
||||
$('#score_div').addClass('hidden');
|
||||
this.toggleButtonStates();
|
||||
var url = submission.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
|
||||
this.initializeSocketForRunning(url);
|
||||
},
|
||||
|
||||
saveCode: function(event) {
|
||||
|
@ -1,7 +1,98 @@
|
||||
#commentitor {
|
||||
margin-bottom: 2rem;
|
||||
height: 600px;
|
||||
background-color:#f9f9f9
|
||||
.rfc {
|
||||
|
||||
h5 {
|
||||
color: #008CBA;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.text.collapsed {
|
||||
max-height: 50px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.collapse-button {
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
.text {
|
||||
padding: 5px;
|
||||
background-color: #FAFAFA;
|
||||
border: 1px solid #CCCCCC;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.question {
|
||||
|
||||
.text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.testruns {
|
||||
|
||||
.text {
|
||||
padding: 5px;
|
||||
background-color: #FAFAFA;
|
||||
border: 1px solid #CCCCCC;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.testrun-assess-results {
|
||||
|
||||
display: flex;
|
||||
|
||||
.result {
|
||||
margin-right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.passed {
|
||||
border-radius: 50%;
|
||||
background-color: #8efa00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-radius: 50%;
|
||||
background-color: #ffca00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
-moz-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-radius: 50%;
|
||||
background-color: #ff2600;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#mark-as-solved-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#thank-you-container {
|
||||
@ -11,6 +102,10 @@
|
||||
border: solid lightgrey 1px;
|
||||
background-color: rgba(20, 180, 20, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#thank-you-note {
|
||||
@ -18,6 +113,12 @@
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#commentitor {
|
||||
margin-bottom: 2rem;
|
||||
height: 600px;
|
||||
background-color:#f9f9f9
|
||||
}
|
||||
|
||||
.ace_tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -42,6 +42,14 @@ div.positive-result {
|
||||
box-shadow: 0px 0px 11px 1px rgba(44,222,0,1);
|
||||
}
|
||||
|
||||
div.unknown-result {
|
||||
border-radius: 50%;
|
||||
background-color: #ffca00;
|
||||
-webkit-box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
|
||||
-moz-box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
|
||||
box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
|
||||
}
|
||||
|
||||
div.negative-result {
|
||||
border-radius: 50%;
|
||||
background-color: #ff2600;
|
||||
|
@ -9,7 +9,7 @@ module SubmissionScoring
|
||||
assessment = assessor.assess(output)
|
||||
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
|
||||
testrun_output = passed ? nil : output[:stderr]
|
||||
Testrun.new(submission: submission, file: file, passed: passed, output: testrun_output).save
|
||||
Testrun.new(submission: submission, cause: 'assess', file: file, passed: passed, output: testrun_output).save
|
||||
output.merge!(assessment)
|
||||
output.merge!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight)
|
||||
end
|
||||
|
@ -1,4 +1,5 @@
|
||||
class RequestForCommentsController < ApplicationController
|
||||
include SubmissionScoring
|
||||
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved, :set_thank_you_note]
|
||||
|
||||
skip_after_action :verify_authorized
|
||||
@ -107,6 +108,10 @@ class RequestForCommentsController < ApplicationController
|
||||
@request_for_comment = RequestForComment.new(request_for_comment_params)
|
||||
respond_to do |format|
|
||||
if @request_for_comment.save
|
||||
# create thread here and execute tests. A run is triggered from the frontend and does not need to be handled here.
|
||||
Thread.new do
|
||||
score_submission(@request_for_comment.submission)
|
||||
end
|
||||
format.json { render :show, status: :created, location: @request_for_comment }
|
||||
else
|
||||
format.html { render :new }
|
||||
|
@ -13,8 +13,12 @@ class SubmissionsController < ApplicationController
|
||||
before_action :set_mime_type, only: [:download_file, :render_file]
|
||||
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
|
||||
|
||||
def max_message_buffer_size
|
||||
500
|
||||
def max_run_output_buffer_size
|
||||
if(@submission.cause == 'requestComments')
|
||||
5000
|
||||
else
|
||||
500
|
||||
end
|
||||
end
|
||||
|
||||
def authorize!
|
||||
@ -196,7 +200,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def handle_message(message, tubesock, container)
|
||||
@message_buffer ||= ""
|
||||
@run_output ||= ""
|
||||
# Handle special commands first
|
||||
if (/^#exit/.match(message))
|
||||
# Just call exit_container on the docker_client.
|
||||
@ -205,19 +209,19 @@ class SubmissionsController < ApplicationController
|
||||
# kill_socket is called in the "on close handler" of the websocket to the container
|
||||
@docker_client.exit_container(container)
|
||||
elsif /^#timeout/.match(message)
|
||||
@message_buffer = 'timeout: ' + @message_buffer # add information that this run timed out to the buffer
|
||||
@run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer
|
||||
else
|
||||
# Filter out information about run_command, test_command, user or working directory
|
||||
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
|
||||
test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename])
|
||||
if !(/root|workspace|#{run_command}|#{test_command}/.match(message))
|
||||
@message_buffer += message if @message_buffer.size <= max_message_buffer_size
|
||||
parse_message(message, 'stdout', tubesock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_message(message, output_stream, socket, recursive = true)
|
||||
parsed = '';
|
||||
begin
|
||||
parsed = JSON.parse(message)
|
||||
if(parsed.class == Hash && parsed.key?('cmd'))
|
||||
@ -256,13 +260,16 @@ class SubmissionsController < ApplicationController
|
||||
socket.send_data JSON.dump(parsed)
|
||||
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
|
||||
end
|
||||
ensure
|
||||
# save the data that was send to the run_output if there is enough space left. this will be persisted as a testrun with cause "run"
|
||||
@run_output += JSON.dump(parsed) if @run_output.size <= max_run_output_buffer_size
|
||||
end
|
||||
end
|
||||
|
||||
def save_run_output
|
||||
if !@message_buffer.blank?
|
||||
@message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars
|
||||
Testrun.create(file: @file, submission: @submission, output: @message_buffer)
|
||||
if !@run_output.blank?
|
||||
@run_output = @run_output[(0..max_run_output_buffer_size-1)] # trim the string to max_message_buffer_size chars
|
||||
Testrun.create(file: @file, cause: 'run', submission: @submission, output: @run_output)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -54,8 +54,10 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
-submission_or_intervention.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
- elsif run.failed
|
||||
.unit-test-result.negative-result title=run.output
|
||||
- else
|
||||
.unit-test-result.unknown-result title=run.output
|
||||
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
|
||||
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
- elsif submission_or_intervention.is_a? UserExerciseIntervention
|
||||
|
9
app/views/request_for_comments/_admin_menu.html.slim
Normal file
9
app/views/request_for_comments/_admin_menu.html.slim
Normal file
@ -0,0 +1,9 @@
|
||||
br
|
||||
h4 Admin Menu
|
||||
h5
|
||||
ul
|
||||
li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id)
|
||||
li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id)
|
||||
ul
|
||||
li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id)
|
||||
li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id)
|
7
app/views/request_for_comments/_mark_as_solved.html.slim
Normal file
7
app/views/request_for_comments/_mark_as_solved.html.slim
Normal file
@ -0,0 +1,7 @@
|
||||
button.btn.btn-primary#mark-as-solved-button = t('request_for_comments.mark_as_solved')
|
||||
|
||||
#thank-you-container
|
||||
p = t('request_for_comments.write_a_thank_you_node')
|
||||
textarea#thank-you-note
|
||||
button.btn.btn-primary#send-thank-you-note = t('request_for_comments.send_thank_you_note')
|
||||
button.btn.btn-default#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note')
|
@ -1,6 +1,6 @@
|
||||
<div class="list-group">
|
||||
<h4 id ="exercise_caption" class="list-group-item-heading" data-exercise-id="<%=@request_for_comment.exercise.id%>" data-comment-exercise-url="<%=create_comment_exercise_request_for_comment_path%>" data-rfc-id = "<%= @request_for_comment.id %>" >
|
||||
<% if (@request_for_comment.solved?) %>
|
||||
<% if @request_for_comment.solved? %>
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
<% end %>
|
||||
<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>
|
||||
@ -9,61 +9,75 @@
|
||||
<%
|
||||
user = @request_for_comment.user
|
||||
submission = @request_for_comment.submission
|
||||
testruns = Testrun.where(:submission_id => @request_for_comment.submission)
|
||||
%>
|
||||
<%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
|
||||
|
||||
</p>
|
||||
|
||||
<h5>
|
||||
<u><%= t('activerecord.attributes.exercise.description') %>:</u> <%= render_markdown(@request_for_comment.exercise.description) %>
|
||||
</h5>
|
||||
|
||||
<h5>
|
||||
<% if @request_for_comment.question and not @request_for_comment.question == '' %>
|
||||
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> "<%= @request_for_comment.question %>"
|
||||
<% else %>
|
||||
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
|
||||
<% end %>
|
||||
</h5>
|
||||
|
||||
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
|
||||
<button class="btn btn-primary" id="mark-as-solved-button">
|
||||
<%= t('request_for_comments.mark_as_solved') %>
|
||||
</button>
|
||||
<div id="thank-you-container">
|
||||
<p>
|
||||
<%= t('request_for_comments.write_a_thank_you_node') %>
|
||||
</p>
|
||||
<textarea id="thank-you-note"></textarea>
|
||||
<button class="btn btn-primary" id="send-thank-you-note">
|
||||
<%= t('request_for_comments.send_thank_you_note') %>
|
||||
</button>
|
||||
<button class="btn btn-default" id="cancel-thank-you-note">
|
||||
<%= t('request_for_comments.cancel_thank_you_note') %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
|
||||
<br>
|
||||
<br>
|
||||
<h4>Admin Menu</h4>
|
||||
<div class="rfc">
|
||||
<div class="description">
|
||||
<h5>
|
||||
<ul>
|
||||
<li><%= link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) %></li>
|
||||
<li><%= link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) %></li> <br>
|
||||
<li><%= link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) %> </li>
|
||||
<li><%= link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) %> </li>
|
||||
</ul>
|
||||
<%= t('activerecord.attributes.exercise.description') %>
|
||||
</h5>
|
||||
<% end %>
|
||||
<h5>
|
||||
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %>
|
||||
</h5>
|
||||
<div class="text">
|
||||
<span class="fa fa-chevron-up collapse-button"></span>
|
||||
<%= render_markdown(@request_for_comment.exercise.description) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="question">
|
||||
<h5>
|
||||
<%= t('activerecord.attributes.request_for_comments.question')%>
|
||||
</h5>
|
||||
<div class="text">
|
||||
<%= @request_for_comment.question or t('request_for_comments.no_question')%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved? %>
|
||||
<%= render('mark_as_solved') %>
|
||||
<% end %>
|
||||
|
||||
<% if testruns.size > 0 %>
|
||||
<div class="testruns">
|
||||
<% output_runs = testruns.select { |run| run.cause == 'run' } %>
|
||||
<% if output_runs.size > 0 %>
|
||||
<h5><%= t('request_for_comments.runtime_output') %></h5>
|
||||
<div class="testrun-output text">
|
||||
<span class="fa fa-chevron-up collapse-button"></span>
|
||||
<% output_runs.each do |testrun| %>
|
||||
<pre><%= testrun.try(:output) or t('request_for_comments.no_output') %></pre>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% assess_runs = testruns.select { |run| run.cause == 'assess' } %>
|
||||
<% if assess_runs.size > 0 %>
|
||||
<h5><%= t('request_for_comments.test_results') %></h5>
|
||||
<div class="testrun-assess-results">
|
||||
<% assess_runs.each do |testrun| %>
|
||||
<div class="result <%= testrun.passed ? 'passed' : 'failed' %>" title="<%= testrun.output %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
|
||||
<%= render('admin_menu') %>
|
||||
<% end %>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="howto">
|
||||
<h5>
|
||||
<%= t('request_for_comments.howto_title') %>
|
||||
</h5>
|
||||
<div class="text">
|
||||
<%= render_markdown(t('request_for_comments.howto')) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="hidden sanitizer"></div>
|
||||
<!--
|
||||
@ -128,6 +142,12 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
thankYouContainer.hide();
|
||||
});
|
||||
|
||||
$('.text > .collapse-button').on('click', function(e) {
|
||||
$(this).toggleClass('fa-chevron-down');
|
||||
$(this).toggleClass('fa-chevron-up');
|
||||
$(this).parent().toggleClass('collapsed');
|
||||
});
|
||||
|
||||
// set file paths for ace
|
||||
var ACE_FILES_PATH = '/assets/ace/';
|
||||
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
|
||||
|
@ -497,7 +497,7 @@ de:
|
||||
Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br>
|
||||
Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br>
|
||||
Mit "Kommentar abschicken" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
howto_title: 'Anleitung:'
|
||||
howto_title: 'Anleitung'
|
||||
index:
|
||||
get_my_comment_requests: Meine Kommentaranfragen
|
||||
all: "Alle Kommentaranfragen"
|
||||
@ -517,6 +517,9 @@ de:
|
||||
modal_title: "Einen Kommentar in Zeile ${line} hinzufügen"
|
||||
click_for_more_comments: "Klicken um ${numComments} weitere Kommentare zu sehen..."
|
||||
subscribe_to_author: "Bei neuen Kommentaren des Autors per E-Mail benachrichtigt werden"
|
||||
no_output: "Keine Ausgabe."
|
||||
runtime_output: "Programmausgabe"
|
||||
test_results: "Testergebnisse"
|
||||
sessions:
|
||||
create:
|
||||
failure: Fehlerhafte E-Mail oder Passwort.
|
||||
|
@ -497,7 +497,7 @@ en:
|
||||
To leave comments to a specific code line, click on the respective line number. <br>
|
||||
Enter your comment in the popup and save it by clicking "Comment this". <br>
|
||||
Your comment will show up next to the line number as a speech bubble symbol.
|
||||
howto_title: 'How to comment:'
|
||||
howto_title: 'How to comment'
|
||||
index:
|
||||
all: All Requests for Comments
|
||||
get_my_comment_requests: My Requests for Comments
|
||||
@ -517,6 +517,9 @@ en:
|
||||
modal_title: "Add a comment to line ${line}"
|
||||
click_for_more_comments: "Click to view ${numComments} more comments..."
|
||||
subscribe_to_author: "Receive E-Mail notifications for new comments of the original author"
|
||||
no_output: "No output."
|
||||
runtime_output: "Runtime Output"
|
||||
test_results: "Test Results"
|
||||
sessions:
|
||||
create:
|
||||
failure: Invalid email or password.
|
||||
|
18
db/migrate/20170830083601_add_cause_to_testruns.rb
Normal file
18
db/migrate/20170830083601_add_cause_to_testruns.rb
Normal file
@ -0,0 +1,18 @@
|
||||
class AddCauseToTestruns < ActiveRecord::Migration
|
||||
def up
|
||||
add_column :testruns, :cause, :string
|
||||
Testrun.reset_column_information
|
||||
Testrun.all.each{ |testrun|
|
||||
if(testrun.submission.nil?)
|
||||
say_with_time "#{testrun.id} has no submission" do end
|
||||
else
|
||||
testrun.cause = testrun.submission.cause
|
||||
testrun.save
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :testruns, :cause
|
||||
end
|
||||
end
|
@ -304,6 +304,7 @@ ActiveRecord::Schema.define(version: 20170920145852) do
|
||||
t.integer "submission_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "cause"
|
||||
end
|
||||
|
||||
create_table "user_exercise_feedbacks", force: :cascade do |t|
|
||||
|
Reference in New Issue
Block a user