Merge remote-tracking branch 'origin/master' into error-info
# Conflicts: # app/controllers/concerns/submission_scoring.rb # app/views/application/_navigation.html.slim # config/locales/de.yml # config/locales/en.yml # db/schema.rb
This commit is contained in:
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -4,7 +4,7 @@
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "ubuntu/trusty64"
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.memory = 1024
|
||||
v.memory = 8192
|
||||
end
|
||||
config.vm.network "private_network", ip: "192.168.59.104"
|
||||
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
//= require ace/ace
|
||||
//= require chosen.jquery.min
|
||||
//= require jquery-ui.min
|
||||
//= require d3
|
||||
//= require jquery.turbolinks
|
||||
//= require jquery_ujs
|
||||
|
@ -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) {
|
||||
|
@ -62,7 +62,9 @@ a.file-heading {
|
||||
fill: #ffd897;
|
||||
}
|
||||
|
||||
|
||||
.container > form > .actions {
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
|
@ -1,7 +1,98 @@
|
||||
#commentitor {
|
||||
margin-top: 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,49 +113,142 @@
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#commentitor {
|
||||
margin-bottom: 2rem;
|
||||
height: 600px;
|
||||
background-color:#f9f9f9
|
||||
}
|
||||
|
||||
.ace_tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
p.comment {
|
||||
width: 400px;
|
||||
.modal-content {
|
||||
min-height: 512px;
|
||||
min-width: 360px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.modal-body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#otherComments {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
.comment {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
min-width: 200px;
|
||||
|
||||
.comment-header {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
|
||||
.comment-username {
|
||||
font-weight: bold;
|
||||
width: 60%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
text-align: right;
|
||||
color: #008cba;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.comment-updated {
|
||||
text-align: right;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-username {
|
||||
font-weight: bold;
|
||||
width: 60%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.popover-date {
|
||||
text-align: right;
|
||||
color: #008cba;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.popover-updated {
|
||||
text-align: right;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.popover-comment {
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popover-divider {
|
||||
.comment-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #008cba;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#otherComments {
|
||||
h5 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #cccccc;
|
||||
padding: 15px;
|
||||
|
||||
.comment-removed {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input#subscribe {
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#myComment {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-footer {
|
||||
color: #008cba;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
|
||||
def current_user
|
||||
::NewRelic::Agent.add_custom_attributes({ external_user_id: session[:external_user_id], session_user_id: session[:user_id] })
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
class CommentsController < ApplicationController
|
||||
before_action :set_comment, only: [:show, :edit, :update, :destroy_by_id]
|
||||
before_action :set_comment, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
# to disable authorization check: comment the line below back in
|
||||
# skip_after_action :verify_authorized
|
||||
@ -21,6 +21,7 @@ class CommentsController < ApplicationController
|
||||
comment.username = comment.user.displayname
|
||||
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
|
||||
comment.updated = (comment.created_at != comment.updated_at)
|
||||
comment.editable = comment.user == current_user
|
||||
}
|
||||
else
|
||||
@comments = []
|
||||
@ -50,12 +51,14 @@ class CommentsController < ApplicationController
|
||||
def create
|
||||
@comment = Comment.new(comment_params_without_request_id)
|
||||
|
||||
if comment_params[:request_id]
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user).deliver_now
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if @comment.save
|
||||
if comment_params[:request_id]
|
||||
request_for_comment = RequestForComment.find(comment_params[:request_id])
|
||||
send_mail_to_author @comment, request_for_comment
|
||||
send_mail_to_subscribers @comment, request_for_comment
|
||||
end
|
||||
|
||||
format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
|
||||
format.json { render :show, status: :created, location: @comment }
|
||||
else
|
||||
@ -83,7 +86,8 @@ class CommentsController < ApplicationController
|
||||
|
||||
# DELETE /comments/1
|
||||
# DELETE /comments/1.json
|
||||
def destroy_by_id
|
||||
def destroy
|
||||
authorize!
|
||||
@comment.destroy
|
||||
respond_to do |format|
|
||||
format.html { head :no_content, notice: 'Comment was successfully destroyed.' }
|
||||
@ -91,30 +95,45 @@ class CommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@comments = Comment.where(file_id: params[:file_id], row: params[:row], user: current_user)
|
||||
@comments.each { |comment| authorize comment; comment.destroy }
|
||||
respond_to do |format|
|
||||
#format.html { redirect_to comments_url, notice: 'Comments were successfully destroyed.' }
|
||||
format.html { head :no_content, notice: 'Comments were successfully destroyed.' }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def comment_params_without_request_id
|
||||
comment_params.except :request_id
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
|
||||
# fuer production mode, damit böse menschen keine falsche user_id uebergeben:
|
||||
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
|
||||
# fuer production mode, damit böse menschen keine falsche user_id uebergeben:
|
||||
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
def send_mail_to_author(comment, request_for_comment)
|
||||
if current_user != request_for_comment.user
|
||||
UserMailer.got_new_comment(comment, request_for_comment, current_user).deliver_now
|
||||
end
|
||||
end
|
||||
|
||||
def send_mail_to_subscribers(comment, request_for_comment)
|
||||
request_for_comment.commenters.each do |commenter|
|
||||
already_sent_mail = false
|
||||
subscriptions = Subscription.where(
|
||||
:request_for_comment_id => request_for_comment.id,
|
||||
:user_id => commenter.id, :user_type => commenter.class.name,
|
||||
:deleted => false)
|
||||
subscriptions.each do |subscription|
|
||||
if (subscription.subscription_type == 'author' and current_user == request_for_comment.user) or subscription.subscription_type == 'all'
|
||||
unless subscription.user == current_user or already_sent_mail
|
||||
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now
|
||||
already_sent_mail = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -42,12 +42,12 @@ module Lti
|
||||
private :external_user_email
|
||||
|
||||
def external_user_name(provider)
|
||||
# save person_name_full if supplied. this is the display_name, if it is set.
|
||||
# else only save the firstname, we don't want lastnames (family names)
|
||||
if provider.lis_person_name_full
|
||||
provider.lis_person_name_full
|
||||
elsif provider.lis_person_name_given && provider.lis_person_name_family
|
||||
"#{provider.lis_person_name_given} #{provider.lis_person_name_family}"
|
||||
else
|
||||
provider.lis_person_name_given || provider.lis_person_name_family
|
||||
provider.lis_person_name_given
|
||||
end
|
||||
end
|
||||
private :external_user_name
|
||||
@ -104,7 +104,7 @@ module Lti
|
||||
private :return_to_consumer
|
||||
|
||||
def send_score(exercise_id, score, user_id)
|
||||
::NewRelic::Agent.add_custom_parameters({ score: score, session: session })
|
||||
::NewRelic::Agent.add_custom_attributes({ score: score, session: session })
|
||||
fail(Error, "Score #{score} must be between 0 and #{MAXIMUM_SCORE}!") unless (0..MAXIMUM_SCORE).include?(score)
|
||||
|
||||
if session[:consumer_id]
|
||||
|
@ -8,7 +8,7 @@ module SubmissionScoring
|
||||
output = execute_test_file(file, submission)
|
||||
assessment = assessor.assess(output)
|
||||
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
|
||||
testrun_output = passed ? nil : output[:stderr]
|
||||
testrun_output = passed ? nil : 'message: ' + output[:message].to_s + "\n stdout: " + output[:stdout].to_s + "\n stderr: " + output[:stderr].to_s
|
||||
if !testrun_output.blank?
|
||||
submission.exercise.execution_environment.error_templates.each do |template|
|
||||
pattern = Regexp.new(template.signature).freeze
|
||||
@ -17,7 +17,7 @@ module SubmissionScoring
|
||||
end
|
||||
end
|
||||
end
|
||||
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
|
||||
|
51
app/controllers/exercise_collections_controller.rb
Normal file
51
app/controllers/exercise_collections_controller.rb
Normal file
@ -0,0 +1,51 @@
|
||||
class ExerciseCollectionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@exercise_collections = ExerciseCollection.all.paginate(:page => params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@exercise_collection = ExerciseCollection.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def create
|
||||
@exercise_collection = ExerciseCollection.new(exercise_collection_params)
|
||||
authorize!
|
||||
create_and_respond(object: @exercise_collection)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!
|
||||
destroy_and_respond(object: @exercise_collection)
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
update_and_respond(object: @exercise_collection, params: exercise_collection_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_exercise_collection
|
||||
@exercise_collection = ExerciseCollection.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise_collection || @exercise_collections)
|
||||
end
|
||||
|
||||
def exercise_collection_params
|
||||
params[:exercise_collection].permit(:name, :exercise_ids => [])
|
||||
end
|
||||
end
|
@ -20,7 +20,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def max_intervention_count
|
||||
def max_intervention_count_per_day
|
||||
3
|
||||
end
|
||||
|
||||
@ -166,7 +166,7 @@ class ExercisesController < ApplicationController
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.has_user_solved(current_user)
|
||||
user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count
|
||||
user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count_per_day
|
||||
is_java_course = @course_token && @course_token.eql?(java_course_token)
|
||||
|
||||
user_intervention_group = UserGroupSeparator.getInterventionGroup(current_user)
|
||||
@ -203,7 +203,7 @@ class ExercisesController < ApplicationController
|
||||
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
|
||||
match.captures.first
|
||||
else
|
||||
java_course_token
|
||||
""
|
||||
end
|
||||
else
|
||||
""
|
||||
@ -344,7 +344,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def transmit_lti_score
|
||||
::NewRelic::Agent.add_custom_parameters({ submission: @submission.id, normalized_score: @submission.normalized_score })
|
||||
::NewRelic::Agent.add_custom_attributes({ submission: @submission.id, normalized_score: @submission.normalized_score })
|
||||
response = send_score(@submission.exercise_id, @submission.normalized_score, @submission.user_id)
|
||||
|
||||
if response[:status] == 'success'
|
||||
|
@ -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
|
||||
@ -22,7 +23,7 @@ class RequestForCommentsController < ApplicationController
|
||||
request_for_comments.submission_id, request_for_comments.row_number') # ugly, but rails wants it this way
|
||||
.select('request_for_comments.*, max(comments.updated_at) as last_comment')
|
||||
.search(params[:q])
|
||||
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page])
|
||||
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page], total_entries: @search.result.length)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -68,11 +69,8 @@ class RequestForCommentsController < ApplicationController
|
||||
def set_thank_you_note
|
||||
authorize!
|
||||
@request_for_comment.thank_you_note = params[:note]
|
||||
commenters = []
|
||||
@request_for_comment.comments.distinct.to_a.each {|comment|
|
||||
commenters.append comment.user
|
||||
}
|
||||
commenters = commenters.uniq {|user| user.id}
|
||||
|
||||
commenters = @request_for_comment.commenters
|
||||
commenters.each {|commenter| UserMailer.send_thank_you_note(@request_for_comment, commenter).deliver_now}
|
||||
|
||||
respond_to do |format|
|
||||
@ -110,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!
|
||||
@ -210,7 +214,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.
|
||||
@ -219,19 +223,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'))
|
||||
@ -270,13 +274,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
|
||||
|
||||
|
62
app/controllers/subscriptions_controller.rb
Normal file
62
app/controllers/subscriptions_controller.rb
Normal file
@ -0,0 +1,62 @@
|
||||
class SubscriptionsController < ApplicationController
|
||||
|
||||
def authorize!
|
||||
authorize(@subscription || @subscriptions)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
# POST /subscriptions.json
|
||||
def create
|
||||
@subscription = Subscription.new(subscription_params)
|
||||
respond_to do |format|
|
||||
if @subscription.save
|
||||
format.json { render json: @subscription, status: :created }
|
||||
else
|
||||
format.json { render json: @subscription.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
authorize!
|
||||
end
|
||||
|
||||
# DELETE /subscriptions/1
|
||||
# DELETE /subscriptions/1.json
|
||||
def destroy
|
||||
begin
|
||||
@subscription = Subscription.find(params[:id])
|
||||
rescue
|
||||
skip_authorization
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') }
|
||||
format.json { render json: {message: t('subscriptions.subscription_not_existent')}, status: :not_found }
|
||||
end
|
||||
else
|
||||
authorize!
|
||||
rfc = @subscription.try(:request_for_comment)
|
||||
@subscription.deleted = true
|
||||
if @subscription.save
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comment_url(rfc), notice: t('subscriptions.successfully_unsubscribed') }
|
||||
format.json { render json: {message: t('subscriptions.successfully_unsubscribed')}, status: :ok}
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comment_url(rfc), :flash => { :danger => t('shared.message_failure') } }
|
||||
format.json { render json: {message: t('shared.message_failure')}, status: :internal_server_error}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_subscription
|
||||
@subscription = Subscription.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_subscription
|
||||
|
||||
def subscription_params
|
||||
current_user_id = current_user.try(:id)
|
||||
current_user_class_name = current_user.try(:class).try(:name)
|
||||
params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id, user_type: current_user_class_name, deleted: false)
|
||||
end
|
||||
private :subscription_params
|
||||
end
|
@ -21,6 +21,15 @@ class UserMailer < ActionMailer::Base
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email)
|
||||
end
|
||||
|
||||
def got_new_comment_for_subscription(comment, subscription, from_user)
|
||||
@receiver_displayname = subscription.user.displayname
|
||||
@author_displayname = from_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(subscription.request_for_comment)
|
||||
@unsubscribe_link = unsubscribe_subscription_url(subscription)
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment_for_subscription.subject', author_displayname: @author_displayname), to: subscription.user.email)
|
||||
end
|
||||
|
||||
def send_thank_you_note(request_for_comments, receiver)
|
||||
@receiver_displayname = receiver.displayname
|
||||
@author = request_for_comments.user.displayname
|
||||
|
@ -1,7 +1,7 @@
|
||||
class Comment < ActiveRecord::Base
|
||||
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
|
||||
include Creation
|
||||
attr_accessor :username, :date, :updated
|
||||
attr_accessor :username, :date, :updated, :editable
|
||||
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
belongs_to :user, polymorphic: true
|
||||
|
@ -2,4 +2,8 @@ class ExerciseCollection < ActiveRecord::Base
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
|
||||
def to_s
|
||||
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
|
||||
end
|
||||
|
||||
end
|
@ -5,11 +5,9 @@ class ExternalUser < ActiveRecord::Base
|
||||
validates :external_id, presence: true
|
||||
|
||||
def displayname
|
||||
result = "User " + id.to_s
|
||||
if(!consumer.nil? && consumer.name == 'openHPI')
|
||||
result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
|
||||
Xikolo::UserClient.get(external_id.to_s)[:display_name]
|
||||
end
|
||||
result = name
|
||||
if(result == nil || result == "")
|
||||
result = "User " + id.to_s
|
||||
end
|
||||
result
|
||||
end
|
||||
|
@ -37,35 +37,17 @@ class ProxyExercise < ActiveRecord::Base
|
||||
assigned_user_proxy_exercise.exercise
|
||||
else
|
||||
matching_exercise =
|
||||
if (token.eql? "47f4c736")
|
||||
Rails.logger.debug("Proxy exercise with token 47f4c736, split user in groups..")
|
||||
group = UserGroupSeparator.getGroupWeek2Testing(user)
|
||||
Rails.logger.debug("user assigned to group #{group}")
|
||||
case group
|
||||
when :group_a
|
||||
exercises.where(id: 348).first
|
||||
when :group_b
|
||||
exercises.where(id: 349).first
|
||||
when :group_c
|
||||
exercises.where(id: 350).first
|
||||
when :group_d
|
||||
exercises.where(id: 351).first
|
||||
end
|
||||
else
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue #fallback
|
||||
rescue => e #fallback
|
||||
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
|
||||
@reason[:reason] = "fallback because of error"
|
||||
@reason[:error] = "#{$!}"
|
||||
exercises.shuffle.first
|
||||
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
|
||||
exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
|
||||
end
|
||||
end
|
||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||
matching_exercise
|
||||
|
||||
|
||||
end
|
||||
recommended_exercise
|
||||
end
|
||||
@ -136,6 +118,7 @@ class ProxyExercise < ActiveRecord::Base
|
||||
relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag
|
||||
end
|
||||
end
|
||||
|
||||
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0
|
||||
best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
|
||||
@reason[:reason] = "best matching exercise"
|
||||
|
@ -5,6 +5,7 @@ class RequestForComment < ActiveRecord::Base
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
|
||||
has_many :comments, through: :submission
|
||||
has_many :subscriptions
|
||||
|
||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||
|
||||
@ -37,6 +38,14 @@ class RequestForComment < ActiveRecord::Base
|
||||
submission.files.map { |file| file.comments.size}.sum
|
||||
end
|
||||
|
||||
def commenters
|
||||
commenters = []
|
||||
comments.distinct.to_a.each {|comment|
|
||||
commenters.append comment.user
|
||||
}
|
||||
commenters.uniq {|user| user.id}
|
||||
end
|
||||
|
||||
def to_s
|
||||
"RFC-" + self.id.to_s
|
||||
end
|
||||
|
@ -34,7 +34,7 @@ class Submission < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def normalized_score
|
||||
::NewRelic::Agent.add_custom_parameters({ unnormalized_score: score })
|
||||
::NewRelic::Agent.add_custom_attributes({ unnormalized_score: score })
|
||||
if !score.nil? && !exercise.maximum_score.nil? && (exercise.maximum_score > 0)
|
||||
score / exercise.maximum_score
|
||||
else
|
||||
|
4
app/models/subscription.rb
Normal file
4
app/models/subscription.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Subscription < ActiveRecord::Base
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :request_for_comment
|
||||
end
|
@ -12,14 +12,10 @@ class CommentPolicy < ApplicationPolicy
|
||||
everyone
|
||||
end
|
||||
|
||||
[:new?, :destroy?, :update?].each do |action|
|
||||
[:new?, :destroy?, :update?, :edit?].each do |action|
|
||||
define_method(action) { admin? || author? }
|
||||
end
|
||||
|
||||
def edit?
|
||||
admin?
|
||||
end
|
||||
|
||||
def index?
|
||||
everyone
|
||||
end
|
||||
|
3
app/policies/exercise_collection_policy.rb
Normal file
3
app/policies/exercise_collection_policy.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class ExerciseCollectionPolicy < AdminOnlyPolicy
|
||||
|
||||
end
|
18
app/policies/subscription_policy.rb
Normal file
18
app/policies/subscription_policy.rb
Normal file
@ -0,0 +1,18 @@
|
||||
class SubscriptionPolicy < ApplicationPolicy
|
||||
def create?
|
||||
everyone
|
||||
end
|
||||
|
||||
def destroy?
|
||||
author? || admin?
|
||||
end
|
||||
|
||||
def show_error?
|
||||
everyone
|
||||
end
|
||||
|
||||
def author?
|
||||
@user == @record.user
|
||||
end
|
||||
private :author?
|
||||
end
|
@ -1,5 +1,5 @@
|
||||
#flash.fixed_error_messages data-message-failure=t('shared.message_failure')
|
||||
- %w[alert danger info notice success warning].each do |severity|
|
||||
div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" id="flash-#{severity}"
|
||||
p = flash[severity]
|
||||
div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}"
|
||||
p id="flash-#{severity}" = flash[severity]
|
||||
span.fa.fa-times
|
@ -8,8 +8,8 @@
|
||||
- if current_user.admin?
|
||||
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
|
||||
li.divider
|
||||
- models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate,
|
||||
ErrorTemplate, ErrorTemplateAttribute, InternalUser].sort_by { |model| model.model_name.human(count: 2) }
|
||||
- models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink,
|
||||
ErrorTemplate, ErrorTemplateAttribute, ExternalUser, FileType, FileTemplate, InternalUser].sort_by {|model| model.model_name.human(count: 2) }
|
||||
- models.each do |model|
|
||||
- if policy(model).index?
|
||||
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
||||
|
@ -1,4 +1,4 @@
|
||||
json.array!(@comments) do |comment|
|
||||
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated
|
||||
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated, :editable
|
||||
json.url comment_url(comment, format: :json)
|
||||
end
|
||||
|
11
app/views/exercise_collections/_form.html.slim
Normal file
11
app/views/exercise_collections/_form.html.slim
Normal file
@ -0,0 +1,11 @@
|
||||
- exercises = Exercise.order(:title)
|
||||
|
||||
= form_for(@exercise_collection, data: {exercises: exercises}, multipart: true) do |f|
|
||||
= render('shared/form_errors', object: @exercise_collection)
|
||||
.form-group
|
||||
= f.label(:name)
|
||||
= f.text_field(:name, class: 'form-control', required: true)
|
||||
.form-group
|
||||
= f.label(:exercises)
|
||||
= f.collection_select(:exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true})
|
||||
.actions = render('shared/submit_button', f: f, object: @exercise_collection)
|
3
app/views/exercise_collections/edit.html.slim
Normal file
3
app/views/exercise_collections/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = @exercise_collection
|
||||
|
||||
= render('form')
|
24
app/views/exercise_collections/index.html.slim
Normal file
24
app/views/exercise_collections/index.html.slim
Normal file
@ -0,0 +1,24 @@
|
||||
h1 = ExerciseCollection.model_name.human(count: 2)
|
||||
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise_collections.id')
|
||||
th = t('activerecord.attributes.exercise_collections.name')
|
||||
th = t('activerecord.attributes.exercise_collections.updated_at')
|
||||
th = t('activerecord.attributes.exercise_collections.exercises')
|
||||
th colspan=3 = t('shared.actions')
|
||||
tbody
|
||||
- @exercise_collections.each do |collection|
|
||||
tr
|
||||
td = collection.id
|
||||
td = link_to(collection.name, collection)
|
||||
td = collection.updated_at
|
||||
td = collection.exercises.size
|
||||
td = link_to(t('shared.show'), collection)
|
||||
td = link_to(t('shared.edit'), edit_exercise_collection_path(collection))
|
||||
td = link_to(t('shared.destroy'), collection, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
|
||||
|
||||
= render('shared/pagination', collection: @exercise_collections)
|
||||
p = render('shared/new_button', model: ExerciseCollection)
|
3
app/views/exercise_collections/new.html.slim
Normal file
3
app/views/exercise_collections/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: ExerciseCollection.model_name.human)
|
||||
|
||||
= render('form')
|
11
app/views/exercise_collections/show.html.slim
Normal file
11
app/views/exercise_collections/show.html.slim
Normal file
@ -0,0 +1,11 @@
|
||||
h1
|
||||
= @exercise_collection
|
||||
= render('shared/edit_button', object: @exercise_collection)
|
||||
|
||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||
|
||||
h4 = t('activerecord.attributes.exercise_collections.exercises')
|
||||
ul.list-unstyled
|
||||
- @exercise_collection.exercises.sort_by{|c| c.title}.each do |exercise|
|
||||
li = link_to(exercise, exercise)
|
@ -1,9 +1,12 @@
|
||||
h5 =t('exercises.implement.comment.addyours')
|
||||
|
||||
textarea.form-control(style='resize:none;')
|
||||
#otherComments
|
||||
h5 =t('exercises.implement.comment.others')
|
||||
pre#otherCommentsTextfield
|
||||
p = ''
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
||||
button#removeAllButton.btn.btn-block.btn-warning(type='button') =t('exercises.implement.comment.removeAllOnLine')
|
||||
.container
|
||||
|
||||
label
|
||||
input#subscribe type='checkbox' title=t('request_for_comments.subscribe_to_author') data-subscription=Subscription.where(user: current_user, request_for_comment_id: @request_for_comment.id, subscription_type: 'author', deleted: false).try(:first).try(:id)
|
||||
= t('request_for_comments.subscribe_to_author')
|
||||
|
||||
#myComment
|
||||
h5 =t('exercises.implement.comment.addyours')
|
||||
textarea.form-control
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
@ -2,10 +2,10 @@
|
||||
|
||||
li.panel.panel-default
|
||||
.panel-heading role="tab" id="heading"
|
||||
a.file-heading data-toggle="collapse" data-parent="#files" href="#collapse#{id}"
|
||||
a.file-heading data-toggle="collapse" href="#collapse#{id}"
|
||||
div.clearfix role="button"
|
||||
span = f.object.name
|
||||
.panel-collapse.collapse-in id="collapse#{id}" role="tabpanel"
|
||||
.panel-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{id}" role="tabpanel"
|
||||
.panel-body
|
||||
.clearfix = link_to(t('shared.destroy'), '#', class:'btn btn-warning btn-sm discard-file pull-right')
|
||||
.form-group
|
||||
|
@ -38,19 +38,28 @@
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise.worktime'))
|
||||
= f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1
|
||||
h2 Tags
|
||||
.table-responsive
|
||||
table.table#tags-table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.selection')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.tag.name'))
|
||||
th = t('activerecord.attributes.tag.difficulty')
|
||||
= collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b|
|
||||
tr
|
||||
td = b.check_box
|
||||
td = b.object.tag.name
|
||||
td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1
|
||||
|
||||
h2 = t('exercises.form.tags')
|
||||
ul.list-unstyled.panel-group
|
||||
li.panel.panel-default
|
||||
.panel-heading role="tab" id="heading"
|
||||
a.file-heading data-toggle="collapse" href="#tag-collapse"
|
||||
div.clearfix role="button"
|
||||
span = t('exercises.form.click_to_collapse')
|
||||
.panel-collapse.collapse id="tag-collapse" role="tabpanel"
|
||||
.table-responsive
|
||||
table.table#tags-table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.selection')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.tag.name'))
|
||||
th = t('activerecord.attributes.tag.difficulty')
|
||||
= collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b|
|
||||
tr
|
||||
td = b.check_box
|
||||
td = b.object.tag.name
|
||||
td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1
|
||||
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
ul#files.list-unstyled.panel-group
|
||||
= f.fields_for :files do |files_form|
|
||||
|
@ -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
|
||||
|
@ -35,7 +35,7 @@ h1 = @exercise
|
||||
tbody
|
||||
- @exercise.send(symbol).distinct().each do |user|
|
||||
- if user_statistics[user.id] then us = user_statistics[user.id] else us = {"maximum_score" => nil, "runs" => nil}
|
||||
- label = current_user.teacher? ? "#{user.name}" : "#{user.name} (#{user.email})"
|
||||
- label = "#{user.displayname}"
|
||||
tr
|
||||
td = link_to_if symbol==:external_users, label, {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id}
|
||||
td = us['maximum_score'] or 0
|
||||
|
@ -10,7 +10,7 @@ h1 = ProxyExercise.model_name.human(count: 2)
|
||||
thead
|
||||
tr
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title'))
|
||||
th = "Token"
|
||||
th = t('activerecord.attributes.exercise.token')
|
||||
th = t('activerecord.attributes.proxy_exercise.files_count')
|
||||
th colspan=6 = t('shared.actions')
|
||||
tbody
|
||||
|
@ -10,6 +10,7 @@ h1
|
||||
= row(label: 'exercise.title', value: @proxy_exercise.title)
|
||||
= row(label: 'proxy_exercise.files_count', value: @exercises.count)
|
||||
= row(label: 'exercise.description', value: @proxy_exercise.description)
|
||||
= row(label: 'exercise.token', value: @proxy_exercise.token)
|
||||
h3 Exercises
|
||||
.table-responsive
|
||||
table.table
|
||||
|
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,76 @@
|
||||
<%
|
||||
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">
|
||||
<% question = @request_for_comment.question %>
|
||||
<%= question.nil? or question.empty? ? t('request_for_comments.no_question') : 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>
|
||||
<!--
|
||||
@ -81,8 +96,13 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$('.modal-content').draggable({
|
||||
handle: '.modal-header'
|
||||
}).resizable({
|
||||
autoHide: true
|
||||
});
|
||||
|
||||
var solvedButton = $('#mark-as-solved-button');
|
||||
var commentOnExerciseButton = $('#comment-exercise-button');
|
||||
var addCommentExerciseButton = $('#addCommentExerciseButton');
|
||||
|
||||
var thankYouContainer = $('#thank-you-container');
|
||||
@ -123,6 +143,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) {
|
||||
@ -138,102 +164,82 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
currentEditor.getSession().setMode($(editor).data('mode'));
|
||||
currentEditor.getSession().setOption("useWorker", false);
|
||||
|
||||
currentEditor.commentVisualsByLine = {};
|
||||
setAnnotations(currentEditor, $(editor).data('file-id'));
|
||||
currentEditor.on("guttermousedown", handleSidebarClick);
|
||||
currentEditor.on("guttermousemove", showPopover);
|
||||
});
|
||||
|
||||
function cleanupPopovers() {
|
||||
// remove all possible popovers
|
||||
$('.editor > .ace_gutter > .ace_gutter-layer > .ace_gutter-cell').popover('destroy');
|
||||
function preprocess(commentText) {
|
||||
// sanitize comments to deal with XSS attacks:
|
||||
commentText = $('div.sanitizer').text(commentText).html();
|
||||
// display original line breaks:
|
||||
return commentText.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function preprocess(commentText) {
|
||||
// sanitize comments to deal with XSS attacks:
|
||||
commentText = $('div.sanitizer').text(commentText).html();
|
||||
// display original line breaks:
|
||||
return commentText.replace(/\n/g, '<br>');
|
||||
function generateCommentHtmlContent(comments) {
|
||||
var htmlContent = '';
|
||||
comments.forEach(function(comment, index) {
|
||||
var commentText = preprocess(comment.text);
|
||||
if (index !== 0) {
|
||||
htmlContent += '<div class="comment-divider"></div>'
|
||||
}
|
||||
htmlContent += '\
|
||||
<div class="comment" data-comment-id=' + comment.id + '> \
|
||||
<div class="comment-header"> \
|
||||
<div class="comment-username">' + preprocess(comment.username) + '</div> \
|
||||
<div class="comment-date">' + comment.date + '</div> \
|
||||
<div class="comment-updated' + (comment.updated ? '' : ' hidden') + '"> \
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i> \
|
||||
<%= t('request_for_comments.comment_edited') %> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="comment-content">' + commentText + '</div> \
|
||||
<textarea class="comment-editor">' + commentText + '</textarea> \
|
||||
<div class="comment-actions' + (comment.editable ? '' : ' hidden') + '"> \
|
||||
<button class="action-edit btn btn-xs btn-warning"><%= t('shared.edit') %></button> \
|
||||
<button class="action-delete btn btn-xs btn-danger"><%= t('shared.destroy') %></button> \
|
||||
</div> \
|
||||
</div>';
|
||||
});
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
function buildPopover(comments, where) {
|
||||
// only display the newest three comments in preview
|
||||
var maxComments = 3;
|
||||
var htmlContent = generateCommentHtmlContent(comments.reverse().slice(0, maxComments));
|
||||
if (comments.length > maxComments) {
|
||||
// add a hint that there are more comments than shown here
|
||||
htmlContent += '<div class="popover-footer"><%= t('request_for_comments.click_for_more_comments') %></div>'
|
||||
.replace('${numComments}', String(comments.length - maxComments));
|
||||
}
|
||||
where.popover({
|
||||
content: htmlContent,
|
||||
html: true, // necessary to style comments. XSS is not possible due to comment pre-processing (sanitizing)
|
||||
trigger: 'manual', // can only be triggered by $(where).popover('show' | 'hide')
|
||||
container: 'body'
|
||||
});
|
||||
}
|
||||
|
||||
function setAnnotations(editor, fileid) {
|
||||
var session = editor.getSession();
|
||||
|
||||
var jqrequest = $.ajax({
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
url: '/comments',
|
||||
data: {
|
||||
file_id: fileid
|
||||
}
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
url: '/comments',
|
||||
data: {
|
||||
file_id: fileid
|
||||
}
|
||||
});
|
||||
|
||||
jqrequest.done(function(response){
|
||||
// comments need to be sorted to cluster them per line
|
||||
var comments = response.slice().sort(function (a, b) {
|
||||
return a.row - b.row;
|
||||
});
|
||||
while (comments.length > 0) {
|
||||
// new cluster of comments
|
||||
var cluster = [];
|
||||
var clusterRow = comments[0].row;
|
||||
// now collect all comments on this line
|
||||
while (comments.length > 0 && comments[0].row === clusterRow) {
|
||||
cluster.push(comments.shift());
|
||||
}
|
||||
// sort the comments by creation date
|
||||
cluster = cluster.sort(function (a, b) {
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
// build the markup for the current line's popover
|
||||
var popupContent = '';
|
||||
cluster.forEach(function(comment, index) {
|
||||
if (index !== 0) {
|
||||
popupContent += '<div class="popover-divider"></div>'
|
||||
}
|
||||
popupContent += '<p class="comment">';
|
||||
popupContent += '<div class="popover-header">' +
|
||||
'<div class="popover-username">' + preprocess(comment.username) + '</div>' +
|
||||
'<div class="popover-date">' + comment.date + '</div>';
|
||||
if (comment.updated) {
|
||||
popupContent += '<div class="popover-updated">' +
|
||||
'<i class="fa fa-pencil" aria-hidden="true"></i>' +
|
||||
'<%= t('request_for_comments.comment_edited') %>' +
|
||||
'</div>'
|
||||
}
|
||||
popupContent += '</div>';
|
||||
popupContent += '<div class="popover-comment">' + preprocess(comment.text) + '</div>';
|
||||
popupContent += '</p>';
|
||||
});
|
||||
|
||||
// attach the popover to the ace sidebar (where the comment icon is displayed)
|
||||
var icon = $('*[data-file-id="' + fileid + '"]') // the editor for this file
|
||||
.find('.ace_gutter > .ace_gutter-layer') // the sidebar
|
||||
.find('div:nth-child(' + (clusterRow + 1) + ')'); // the correct line
|
||||
icon.popover({
|
||||
content: popupContent,
|
||||
html: true, // necessary to style comments. XSS is not possible due to comment pre-processing (sanitizing)
|
||||
trigger: 'hover',
|
||||
container: 'body'
|
||||
});
|
||||
}
|
||||
|
||||
$.each(response, function(index, comment) {
|
||||
comment.className = 'code-ocean_comment';
|
||||
// if we have tabs or carriage returns in the comment, just add the name and leave it as it is. otherwise: format!
|
||||
if(comment.text.includes("\n") || comment.text.includes("\t")){
|
||||
comment.text = comment.username + ": " + comment.text;
|
||||
} else {
|
||||
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n");
|
||||
}
|
||||
});
|
||||
session.setAnnotations(response);
|
||||
})
|
||||
}
|
||||
|
||||
function hasCommentsInRow(editor, row){
|
||||
return editor.getSession().getAnnotations().some(function(element) {
|
||||
return element.row === row;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getCommentsForRow(editor, row){
|
||||
@ -242,23 +248,36 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
})
|
||||
}
|
||||
|
||||
function deleteComment(file_id, row, editor) {
|
||||
cleanupPopovers();
|
||||
function deleteComment(commentId, editor, file_id, callback) {
|
||||
var jqxhr = $.ajax({
|
||||
type: 'DELETE',
|
||||
url: "/comments",
|
||||
data: {
|
||||
row: row,
|
||||
file_id: file_id }
|
||||
url: "/comments/" + commentId
|
||||
});
|
||||
jqxhr.done(function (response) {
|
||||
jqxhr.done(function () {
|
||||
setAnnotations(editor, file_id);
|
||||
callback();
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function updateComment(commentId, text, editor, file_id, callback) {
|
||||
var jqxhr = $.ajax({
|
||||
type: 'PATCH',
|
||||
url: "/comments/" + commentId,
|
||||
data: {
|
||||
comment: {
|
||||
text: text
|
||||
}
|
||||
}
|
||||
});
|
||||
jqxhr.done(function () {
|
||||
setAnnotations(editor, file_id);
|
||||
callback();
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createComment(file_id, row, editor, commenttext){
|
||||
cleanupPopovers();
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
@ -273,71 +292,173 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
jqxhr.done(function(){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createCommentOnExercise(file_id, row, editor, commenttext){
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
file_id: file_id,
|
||||
row: row,
|
||||
column: 0,
|
||||
text: commenttext,
|
||||
request_id: $('h4#exercise_caption').data('rfc-id')
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
function subscribeToRFC(subscriptionType, checkbox){
|
||||
checkbox.attr("disabled", true);
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
subscription: {
|
||||
request_for_comment_id: $('h4#exercise_caption').data('rfc-id'),
|
||||
subscription_type: subscriptionType
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/subscriptions.json"
|
||||
});
|
||||
jqxhr.done(function(subscription) {
|
||||
checkbox.data('subscription', subscription.id);
|
||||
checkbox.attr("disabled", false);
|
||||
});
|
||||
jqxhr.fail(function(response) {
|
||||
checkbox.prop('checked', false);
|
||||
checkbox.attr("disabled", false);
|
||||
ajaxError(response);
|
||||
});
|
||||
}
|
||||
|
||||
function unsubscribeFromRFC(checkbox) {
|
||||
checkbox.attr("disabled", true);
|
||||
var subscriptionId = checkbox.data('subscription');
|
||||
var jqxhr = $.ajax({
|
||||
url: '/subscriptions/' + subscriptionId + '/unsubscribe.json'
|
||||
});
|
||||
jqxhr.done(function(response) {
|
||||
checkbox.prop('checked', false);
|
||||
checkbox.data('subscription', null);
|
||||
checkbox.attr("disabled", false);
|
||||
$.flash.success({text: response.message});
|
||||
});
|
||||
jqxhr.fail(function(response) {
|
||||
checkbox.prop('checked', true);
|
||||
checkbox.attr("disabled", false);
|
||||
ajaxError(response);
|
||||
});
|
||||
}
|
||||
|
||||
var lastRow = null;
|
||||
var lastTarget = null;
|
||||
function showPopover(e) {
|
||||
var target = e.domEvent.target;
|
||||
var row = e.getDocumentPosition().row;
|
||||
|
||||
if (target.className.indexOf('ace_gutter-cell') === -1 || lastRow === row) {
|
||||
return;
|
||||
}
|
||||
if (lastTarget === target) {
|
||||
// sometimes the row gets updated before the DOM event target, so we need to wait for it to change
|
||||
return;
|
||||
}
|
||||
lastRow = row;
|
||||
|
||||
var editor = e.editor;
|
||||
var comments = getCommentsForRow(editor, row);
|
||||
buildPopover(comments, $(target));
|
||||
lastTarget = target;
|
||||
|
||||
$(target).popover('show');
|
||||
$(target).on('mouseleave', function () {
|
||||
$(this).off('mouseleave');
|
||||
$(this).popover('destroy');
|
||||
});
|
||||
}
|
||||
|
||||
$('.ace_gutter').on('mouseleave', function () {
|
||||
lastRow = null;
|
||||
lastTarget = null;
|
||||
});
|
||||
|
||||
function handleSidebarClick(e) {
|
||||
var target = e.domEvent.target;
|
||||
var editor = e.editor;
|
||||
if (target.className.indexOf('ace_gutter-cell') === -1) return;
|
||||
|
||||
if (target.className.indexOf("ace_gutter-cell") == -1) return;
|
||||
var editor = e.editor;
|
||||
var fileid = $(editor.container).data('file-id');
|
||||
|
||||
var row = e.getDocumentPosition().row;
|
||||
e.stop();
|
||||
$('.modal-title').text('<%= t('request_for_comments.modal_title') %>'.replace('${line}', row + 1));
|
||||
|
||||
var commentModal = $('#comment-modal');
|
||||
|
||||
if (hasCommentsInRow(editor, row)) {
|
||||
var rowComments = getCommentsForRow(editor, row);
|
||||
var comments = _.pluck(rowComments, 'text').join('\n');
|
||||
commentModal.find('#otherComments').show();
|
||||
commentModal.find('#otherCommentsTextfield').text(comments);
|
||||
var otherComments = commentModal.find('#otherComments');
|
||||
var htmlContent = generateCommentHtmlContent(getCommentsForRow(editor, row));
|
||||
if (htmlContent) {
|
||||
otherComments.show();
|
||||
var container = otherComments.find('.container');
|
||||
container.html(htmlContent);
|
||||
|
||||
var deleteButtons = container.find('.action-delete');
|
||||
deleteButtons.on('click', function (event) {
|
||||
var button = $(event.target);
|
||||
var parent = $(button).parent().parent();
|
||||
var commentId = parent.data('comment-id');
|
||||
|
||||
deleteComment(commentId, editor, fileid, function () {
|
||||
parent.html('<div class="comment-removed"><%= t('comments.deleted') %></div>');
|
||||
});
|
||||
});
|
||||
|
||||
var editButtons = container.find('.action-edit');
|
||||
editButtons.on('click', function (event) {
|
||||
var button = $(event.target);
|
||||
var parent = $(button).parent().parent();
|
||||
var commentId = parent.data('comment-id');
|
||||
var currentlyEditing = button.data('editing');
|
||||
|
||||
var deleteButton = parent.find('.action-delete');
|
||||
var commentContent = parent.find('.comment-content');
|
||||
var commentEditor = parent.find('textarea.comment-editor');
|
||||
var commentUpdated = parent.find('.comment-updated');
|
||||
|
||||
if (currentlyEditing) {
|
||||
updateComment(commentId, commentEditor.val(), editor, fileid, function () {
|
||||
button.text('<%= t('shared.edit') %>');
|
||||
button.data('editing', false);
|
||||
commentContent.text(commentEditor.val());
|
||||
deleteButton.show();
|
||||
commentContent.show();
|
||||
commentEditor.hide();
|
||||
commentUpdated.removeClass('hidden');
|
||||
});
|
||||
} else {
|
||||
button.text('<%= t('comments.save_update') %>');
|
||||
button.data('editing', true);
|
||||
deleteButton.hide();
|
||||
commentContent.hide();
|
||||
commentEditor.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
commentModal.find('#otherComments').hide();
|
||||
otherComments.hide();
|
||||
}
|
||||
|
||||
commentModal.find('#addCommentButton').off('click');
|
||||
commentModal.find('#removeAllButton').off('click');
|
||||
|
||||
commentModal.find('#addCommentButton').on('click', function(e){
|
||||
var commenttext = commentModal.find('textarea').val();
|
||||
var file_id = $(editor.container).data('file-id');
|
||||
|
||||
if (commenttext !== "") {
|
||||
createComment(file_id, row, editor, commenttext);
|
||||
commentModal.find('textarea').val('') ;
|
||||
commentModal.modal('hide');
|
||||
var subscribeCheckbox = commentModal.find('#subscribe');
|
||||
subscribeCheckbox.prop('checked', subscribeCheckbox.data('subscription'));
|
||||
subscribeCheckbox.off('change');
|
||||
subscribeCheckbox.on('change', function() {
|
||||
if (this.checked) {
|
||||
subscribeToRFC('author', $(this));
|
||||
} else {
|
||||
unsubscribeFromRFC($(this));
|
||||
}
|
||||
});
|
||||
|
||||
commentModal.find('#removeAllButton').on('click', function(e){
|
||||
var file_id = $(editor.container).data('file-id');
|
||||
deleteComment(file_id, row, editor);
|
||||
commentModal.modal('hide');
|
||||
var addCommentButton = commentModal.find('#addCommentButton');
|
||||
addCommentButton.off('click');
|
||||
addCommentButton.on('click', function(){
|
||||
var commentTextarea = commentModal.find('#myComment > textarea');
|
||||
var commenttext = commentTextarea.val();
|
||||
if (commenttext !== "") {
|
||||
createComment(fileid, row, editor, commenttext);
|
||||
commentTextarea.val('') ;
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
commentModal.modal('show');
|
||||
@ -351,19 +472,4 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function stringDivider(str, width, spaceReplacer) {
|
||||
if (str.length>width) {
|
||||
var p=width;
|
||||
for (;p>0 && str[p]!=' ';p--) {
|
||||
}
|
||||
if (p>0) {
|
||||
var left = str.substring(0, p);
|
||||
var right = str.substring(p+1);
|
||||
return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -0,0 +1,7 @@
|
||||
== t('mailers.user_mailer.got_new_comment_for_subscription.body',
|
||||
receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link),
|
||||
unsubscribe_link: link_to(@unsubscribe_link, @unsubscribe_link),
|
||||
author_displayname: @author_displayname,
|
||||
comment_text: @comment_text,
|
||||
link_my_comments: link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_url),
|
||||
link_all_comments: link_to(t('request_for_comments.index.all'), request_for_comments_url) )
|
@ -42,6 +42,7 @@ de:
|
||||
allow_file_creation: "Dateierstellung erlauben"
|
||||
difficulty: Schwierigkeitsgrad
|
||||
worktime: "vermutete Arbeitszeit in Minuten"
|
||||
token: "Aufgaben-Token"
|
||||
proxy_exercise:
|
||||
title: Title
|
||||
files_count: Anzahl der Aufgaben
|
||||
@ -120,6 +121,11 @@ de:
|
||||
key: "Name"
|
||||
description: "Beschreibung"
|
||||
regex: "Regulärer Ausdruck"
|
||||
exercise_collections:
|
||||
id: "ID"
|
||||
name: "Name"
|
||||
updated_at: "Letzte Änderung"
|
||||
exercises: "Aufgaben"
|
||||
models:
|
||||
code_harbor_link:
|
||||
one: CodeHarbor-Link
|
||||
@ -142,6 +148,9 @@ de:
|
||||
exercise:
|
||||
one: Aufgabe
|
||||
other: Aufgaben
|
||||
exercise_collection:
|
||||
one: Aufgabesammlung
|
||||
other: Aufgabensammlungen
|
||||
proxy_exercise:
|
||||
one: Proxy Aufgabe
|
||||
other: Proxy Aufgaben
|
||||
@ -169,9 +178,15 @@ de:
|
||||
submission:
|
||||
one: Abgabe
|
||||
other: Abgaben
|
||||
tag:
|
||||
one: Tag
|
||||
other: Tags
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
comment:
|
||||
one: Kommentar
|
||||
other: Kommentare
|
||||
errors:
|
||||
messages:
|
||||
together: 'muss zusammen mit %{attribute} definiert werden'
|
||||
@ -270,6 +285,8 @@ de:
|
||||
path: 'Pfad der Datei im Projektverzeichnis. Kann auch leer gelassen werden.'
|
||||
form:
|
||||
add_file: Datei hinzufügen
|
||||
tags: "Tags"
|
||||
click_to_collapse: "Zum Aus-/Einklappen hier klicken..."
|
||||
implement:
|
||||
alert:
|
||||
text: 'Ihr Browser unterstützt nicht alle Funktionalitäten, die %{application_name} benötigt. Bitte nutzen Sie einen modernen Browser, um %{application_name} zu besuchen.'
|
||||
@ -462,14 +479,49 @@ de:
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author} sagt Danke!"
|
||||
got_new_comment_for_subscription:
|
||||
body: |
|
||||
English version below <br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{author_displayname} zu einer Kommentaranfrage auf CodeOcean, die Sie abonniert haben. <br>
|
||||
<br>
|
||||
%{author_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden die Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Wenn Sie keine weiteren Benachrichtigungen zu dieser Anfrage erhalten möchten, klicken Sie bitte hier: %{unsubscribe_link}
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{author_displayname} to a request for comments on CodeOcean that you have subscribed to. <br>
|
||||
<br>
|
||||
%{author_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find the request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
If you don't want to be notified about further comments, please click here: %{unsubscribe_link}
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author_displayname} hat einen neuen Kommentar in einer Diskussion veröffentlicht, die Sie abonniert haben."
|
||||
request_for_comments:
|
||||
click_here: Zum Kommentieren auf die Seitenleiste klicken!
|
||||
comments: Kommentare
|
||||
howto: |
|
||||
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 "Kommentieren" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
howto_title: 'Anleitung:'
|
||||
Mit "Kommentar abschicken" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
howto_title: 'Anleitung'
|
||||
index:
|
||||
get_my_comment_requests: Meine Kommentaranfragen
|
||||
all: "Alle Kommentaranfragen"
|
||||
@ -486,6 +538,12 @@ de:
|
||||
send_thank_you_note: "Senden"
|
||||
cancel_thank_you_note: "Nichts senden"
|
||||
comment_edited: "bearbeitet"
|
||||
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.
|
||||
@ -605,3 +663,9 @@ de:
|
||||
signature: "Ein regulärer Ausdruck in Ruby-Syntax und ohne führende und schließende \"/\""
|
||||
attributes: "Attribute"
|
||||
add_attribute: "Attribut hinzufügen"
|
||||
comments:
|
||||
deleted: "Gelöscht"
|
||||
save_update: "Speichern"
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "Ihr Abonnement für weitere Kommentare auf dieser Kommentaranfrage wurde erfolgreich beendet."
|
||||
subscription_not_existent: "Das Abonnement, von dem Sie sich abmelden wollen, existiert nicht."
|
||||
|
@ -1,24 +1,3 @@
|
||||
# Files in the config/locales directory are used for internationalization
|
||||
# and are automatically loaded by Rails. If you want to use locales other
|
||||
# than English, add the necessary files in this directory.
|
||||
#
|
||||
# To use the locales, use `I18n.t`:
|
||||
#
|
||||
# I18n.t 'hello'
|
||||
#
|
||||
# In views, this is aliased to just `t`:
|
||||
#
|
||||
# <%= t('hello') %>
|
||||
#
|
||||
# To use a different locale, set it with `I18n.locale`:
|
||||
#
|
||||
# I18n.locale = :es
|
||||
#
|
||||
# This would use the information in config/locales/es.yml.
|
||||
#
|
||||
# To learn more, please read the Rails Internationalization guide
|
||||
# available at http://guides.rubyonrails.org/i18n.html.
|
||||
|
||||
en:
|
||||
activerecord:
|
||||
attributes:
|
||||
@ -63,6 +42,7 @@ en:
|
||||
allow_file_creation: "Allow file creation"
|
||||
difficulty: Difficulty
|
||||
worktime: "Expected worktime in minutes"
|
||||
token: "Exercise Token"
|
||||
proxy_exercise:
|
||||
title: Title
|
||||
files_count: Exercises Count
|
||||
@ -141,6 +121,11 @@ en:
|
||||
key: "Identifier"
|
||||
description: "Description"
|
||||
regex: "Regular Expression"
|
||||
exercise_collections:
|
||||
id: "ID"
|
||||
name: "Name"
|
||||
updated_at: "Last Update"
|
||||
exercises: "Exercises"
|
||||
models:
|
||||
code_harbor_link:
|
||||
one: CodeHarbor Link
|
||||
@ -163,6 +148,9 @@ en:
|
||||
exercise:
|
||||
one: Exercise
|
||||
other: Exercises
|
||||
exercise_collection:
|
||||
one: Exercise Collection
|
||||
other: Exercise Collections
|
||||
proxy_exercise:
|
||||
one: Proxy Exercise
|
||||
other: Proxy Exercises
|
||||
@ -190,9 +178,15 @@ en:
|
||||
submission:
|
||||
one: Submission
|
||||
other: Submissions
|
||||
tag:
|
||||
one: Tag
|
||||
other: Tags
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
comment:
|
||||
one: Comment
|
||||
other: Comments
|
||||
errors:
|
||||
messages:
|
||||
together: 'has to be set along with %{attribute}'
|
||||
@ -291,6 +285,8 @@ en:
|
||||
path: "The file's path in the project tree. Can be left blank."
|
||||
form:
|
||||
add_file: Add file
|
||||
tags: "Tags"
|
||||
click_to_collapse: "Click to expand/collapse..."
|
||||
implement:
|
||||
alert:
|
||||
text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.'
|
||||
@ -483,6 +479,41 @@ en:
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author} says thank you!"
|
||||
got_new_comment_for_subscription:
|
||||
body: |
|
||||
English version below <br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{author_displayname} zu einer Kommentaranfrage auf CodeOcean, die Sie abonniert haben. <br>
|
||||
<br>
|
||||
%{author_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden die Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Wenn Sie keine weiteren Benachrichtigungen zu dieser Anfrage erhalten möchten, klicken Sie bitte hier: %{unsubscribe_link}
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{author_displayname} to a request for comments on CodeOcean that you have subscribed to. <br>
|
||||
<br>
|
||||
%{author_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find the request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
If you don't want to be notified about further comments, please click here: %{unsubscribe_link}
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author_displayname} has posted a new comment to a discussion you subscribed to on CodeOcean."
|
||||
request_for_comments:
|
||||
click_here: Click on this sidebar to comment!
|
||||
comments: Comments
|
||||
@ -490,7 +521,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
|
||||
@ -507,6 +538,12 @@ en:
|
||||
send_thank_you_note: "Send"
|
||||
cancel_thank_you_note: "Don't send"
|
||||
comment_edited: "edited"
|
||||
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.
|
||||
@ -626,3 +663,9 @@ en:
|
||||
signature: "A regular expression in Ruby syntax without leading and trailing \"/\""
|
||||
attributes: "Attributes"
|
||||
add_attribute: "Add attribute"
|
||||
comments:
|
||||
deleted: "Deleted"
|
||||
save_update: "Save"
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "You successfully unsubscribed from this Request for Comment"
|
||||
subscription_not_existent: "The subscription you want to unsubscribe from does not exist."
|
||||
|
@ -21,17 +21,19 @@ Rails.application.routes.draw do
|
||||
post :set_thank_you_note
|
||||
end
|
||||
end
|
||||
resources :comments, except: [:destroy] do
|
||||
collection do
|
||||
delete :destroy
|
||||
end
|
||||
end
|
||||
resources :comments
|
||||
get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests'
|
||||
get '/my_rfc_activity', as: 'my_rfc_activity', to: 'request_for_comments#get_rfcs_with_my_comments'
|
||||
|
||||
delete '/comment_by_id', to: 'comments#destroy_by_id'
|
||||
put '/comments', to: 'comments#update'
|
||||
|
||||
resources :subscriptions do
|
||||
member do
|
||||
get :unsubscribe, to: 'subscriptions#destroy'
|
||||
end
|
||||
end
|
||||
|
||||
root to: 'application#welcome'
|
||||
|
||||
namespace :admin do
|
||||
@ -79,6 +81,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :exercise_collections
|
||||
|
||||
resources :proxy_exercises do
|
||||
member do
|
||||
post :clone
|
||||
|
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
|
11
db/migrate/20170906124500_create_subscriptions.rb
Normal file
11
db/migrate/20170906124500_create_subscriptions.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class CreateSubscriptions < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :subscriptions do |t|
|
||||
t.belongs_to :user, polymorphic: true
|
||||
t.references :request_for_comment
|
||||
t.string :type
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
5
db/migrate/20170913054203_rename_subscription_type.rb
Normal file
5
db/migrate/20170913054203_rename_subscription_type.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class RenameSubscriptionType < ActiveRecord::Migration
|
||||
def change
|
||||
rename_column :subscriptions, :type, :subscription_type
|
||||
end
|
||||
end
|
5
db/migrate/20170920145852_add_deleted_to_subscription.rb
Normal file
5
db/migrate/20170920145852_add_deleted_to_subscription.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddDeletedToSubscription < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :subscriptions, :deleted, :boolean
|
||||
end
|
||||
end
|
13
db/schema.rb
13
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20170719133351) do
|
||||
ActiveRecord::Schema.define(version: 20170920145852) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -321,6 +321,16 @@ ActiveRecord::Schema.define(version: 20170719133351) do
|
||||
add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree
|
||||
add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree
|
||||
|
||||
create_table "subscriptions", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "user_type"
|
||||
t.integer "request_for_comment_id"
|
||||
t.string "subscription_type"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "deleted"
|
||||
end
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at"
|
||||
@ -334,6 +344,7 @@ ActiveRecord::Schema.define(version: 20170719133351) 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|
|
||||
|
@ -25,8 +25,8 @@
|
||||
var showFlashes = function() {
|
||||
$('.flash').each(function() {
|
||||
var container = $(this);
|
||||
var message = container.children().first();
|
||||
var button = container.children().last();
|
||||
var message = container.find('p');
|
||||
var button = container.find('span.fa-times');
|
||||
|
||||
var hide = function() {
|
||||
container.slideUp(function () {
|
||||
|
@ -72,7 +72,7 @@ class DockerClient
|
||||
# Headers are required by Docker
|
||||
headers = {'Origin' => 'http://localhost'}
|
||||
|
||||
socket_url = DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params
|
||||
socket_url = DockerClient.config['ws_host'] + '/v1.27/containers/' + @container.id + '/attach/ws?' + query_params
|
||||
socket = Faye::WebSocket::Client.new(socket_url, [], :headers => headers)
|
||||
|
||||
Rails.logger.debug "Opening Websocket on URL " + socket_url
|
||||
@ -399,6 +399,9 @@ class DockerClient
|
||||
output = container.exec(['bash', '-c', command])
|
||||
Rails.logger.debug "output from container.exec"
|
||||
Rails.logger.debug output
|
||||
if(output == nil)
|
||||
kill_container(container)
|
||||
end
|
||||
result = {status: output[2] == 0 ? :ok : :failed, stdout: output[0].join.force_encoding('utf-8'), stderr: output[1].join.force_encoding('utf-8')}
|
||||
end
|
||||
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
|
||||
|
17
lib/tasks/write_displaynames.rake
Normal file
17
lib/tasks/write_displaynames.rake
Normal file
@ -0,0 +1,17 @@
|
||||
namespace :user do
|
||||
require 'csv'
|
||||
|
||||
desc 'write displaynames retrieved from the account service as csv into the codeocean database'
|
||||
|
||||
task :write_displaynames, [:file_path_read] => [ :environment ] do |t, args|
|
||||
|
||||
csv_input = CSV.read(args[:file_path_read], headers:true)
|
||||
|
||||
csv_input.each do |row|
|
||||
user = ExternalUser.find_by(:external_id => row[0])
|
||||
puts "Change name from #{user.name} to #{row[1]}"
|
||||
user.update(name: row[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -24,17 +24,4 @@ class UserGroupSeparator
|
||||
end
|
||||
end
|
||||
|
||||
def self.getGroupWeek2Testing(user)
|
||||
groupById = user.id % 4
|
||||
if groupById == 0
|
||||
:group_a
|
||||
elsif groupById == 1
|
||||
:group_b
|
||||
elsif groupById == 2
|
||||
:group_c
|
||||
else # 3
|
||||
:group_d
|
||||
end
|
||||
end
|
||||
|
||||
end
|
14
test/controllers/exercise_collections_controller_test.rb
Normal file
14
test/controllers/exercise_collections_controller_test.rb
Normal file
@ -0,0 +1,14 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ExerciseCollectionsControllerTest < ActionController::TestCase
|
||||
test "should get index" do
|
||||
get :index
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get show" do
|
||||
get :show
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
end
|
7
test/controllers/subscription_controller_test.rb
Normal file
7
test/controllers/subscription_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class SubscriptionControllerTest < ActionController::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
7
test/factories/subscriptions.rb
Normal file
7
test/factories/subscriptions.rb
Normal file
@ -0,0 +1,7 @@
|
||||
FactoryGirl.define do
|
||||
factory :subscription do
|
||||
user nil
|
||||
request_for_comments nil
|
||||
type ""
|
||||
end
|
||||
end
|
7
test/models/subscription_test.rb
Normal file
7
test/models/subscription_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class SubscriptionTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
8
vendor/assets/javascripts/jquery-ui.min.js
vendored
Normal file
8
vendor/assets/javascripts/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
vendor/assets/stylesheets/jquery-ui.min.css
vendored
Normal file
6
vendor/assets/stylesheets/jquery-ui.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*! jQuery UI - v1.12.1 - 2017-09-05
|
||||
* http://jqueryui.com
|
||||
* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}
|
5
vendor/assets/stylesheets/jquery-ui.structure.min.css
vendored
Normal file
5
vendor/assets/stylesheets/jquery-ui.structure.min.css
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/*! jQuery UI - v1.12.1 - 2017-09-05
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}
|
Reference in New Issue
Block a user