Merge branch 'master' into user_exercise_feedback_backend

# Conflicts:
#	app/views/application/_navigation.html.slim
This commit is contained in:
Ralf Teusner
2017-11-01 10:29:43 +01:00
56 changed files with 798 additions and 72 deletions

View File

@@ -0,0 +1,18 @@
$(function() {
if ($.isController('error_templates')) {
$('#add-attribute').find('button').on('click', function () {
$.ajax(location + '/attribute.json', {
method: 'POST',
data: {
_method: 'PUT',
dataType: 'json',
error_template_attribute_id: $('#add-attribute').find('select').val()
}
}).success(function () {
location.reload();
}).error(function (error) {
$.flash.danger({text: error.statusText});
});
});
}
});

View File

@@ -0,0 +1,9 @@
#add-attribute {
display: flex;
max-width: 400px;
margin-top: 30px;
button {
margin-left: 10px;
}
}

View File

@@ -9,6 +9,14 @@ module SubmissionScoring
assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
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
if pattern.match(testrun_output)
StructuredError.create_from_template(template, testrun_output)
end
end
end
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)

View File

@@ -0,0 +1,86 @@
class ErrorTemplateAttributesController < ApplicationController
before_action :set_error_template_attribute, only: [:show, :edit, :update, :destroy]
def authorize!
authorize(@error_template_attributes || @error_template_attribute)
end
private :authorize!
# GET /error_template_attributes
# GET /error_template_attributes.json
def index
@error_template_attributes = ErrorTemplateAttribute.all.order('important DESC', :key, :id).paginate(page: params[:page])
authorize!
end
# GET /error_template_attributes/1
# GET /error_template_attributes/1.json
def show
authorize!
end
# GET /error_template_attributes/new
def new
@error_template_attribute = ErrorTemplateAttribute.new
authorize!
end
# GET /error_template_attributes/1/edit
def edit
authorize!
end
# POST /error_template_attributes
# POST /error_template_attributes.json
def create
@error_template_attribute = ErrorTemplateAttribute.new(error_template_attribute_params)
authorize!
respond_to do |format|
if @error_template_attribute.save
format.html { redirect_to @error_template_attribute, notice: 'Error template attribute was successfully created.' }
format.json { render :show, status: :created, location: @error_template_attribute }
else
format.html { render :new }
format.json { render json: @error_template_attribute.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /error_template_attributes/1
# PATCH/PUT /error_template_attributes/1.json
def update
authorize!
respond_to do |format|
if @error_template_attribute.update(error_template_attribute_params)
format.html { redirect_to @error_template_attribute, notice: 'Error template attribute was successfully updated.' }
format.json { render :show, status: :ok, location: @error_template_attribute }
else
format.html { render :edit }
format.json { render json: @error_template_attribute.errors, status: :unprocessable_entity }
end
end
end
# DELETE /error_template_attributes/1
# DELETE /error_template_attributes/1.json
def destroy
authorize!
@error_template_attribute.destroy
respond_to do |format|
format.html { redirect_to error_template_attributes_url, notice: 'Error template attribute was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_error_template_attribute
@error_template_attribute = ErrorTemplateAttribute.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_attribute_params
params[:error_template_attribute].permit(:key, :description, :regex, :important)
end
end

View File

@@ -0,0 +1,104 @@
class ErrorTemplatesController < ApplicationController
before_action :set_error_template, only: [:show, :edit, :update, :destroy, :add_attribute, :remove_attribute]
def authorize!
authorize(@error_templates || @error_template)
end
private :authorize!
# GET /error_templates
# GET /error_templates.json
def index
@error_templates = ErrorTemplate.all.order(:execution_environment_id, :name).paginate(page: params[:page])
authorize!
end
# GET /error_templates/1
# GET /error_templates/1.json
def show
authorize!
end
# GET /error_templates/new
def new
@error_template = ErrorTemplate.new
authorize!
end
# GET /error_templates/1/edit
def edit
authorize!
end
# POST /error_templates
# POST /error_templates.json
def create
@error_template = ErrorTemplate.new(error_template_params)
authorize!
respond_to do |format|
if @error_template.save
format.html { redirect_to @error_template, notice: 'Error template was successfully created.' }
format.json { render :show, status: :created, location: @error_template }
else
format.html { render :new }
format.json { render json: @error_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /error_templates/1
# PATCH/PUT /error_templates/1.json
def update
authorize!
respond_to do |format|
if @error_template.update(error_template_params)
format.html { redirect_to @error_template, notice: 'Error template was successfully updated.' }
format.json { render :show, status: :ok, location: @error_template }
else
format.html { render :edit }
format.json { render json: @error_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /error_templates/1
# DELETE /error_templates/1.json
def destroy
authorize!
@error_template.destroy
respond_to do |format|
format.html { redirect_to error_templates_url, notice: 'Error template was successfully destroyed.' }
format.json { head :no_content }
end
end
def add_attribute
authorize!
@error_template.error_template_attributes << ErrorTemplateAttribute.find(params['error_template_attribute_id'])
respond_to do |format|
format.html { redirect_to @error_template }
format.json { head :no_content }
end
end
def remove_attribute
authorize!
@error_template.error_template_attributes.delete(ErrorTemplateAttribute.find(params['error_template_attribute_id']))
respond_to do |format|
format.html { redirect_to @error_template }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_error_template
@error_template = ErrorTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_params
params[:error_template].permit(:name, :execution_environment_id, :signature, :description, :hint)
end
end

View File

@@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
include SubmissionScoring
include Tubesock::Hijack
before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :extract_errors, :show, :statistics, :stop, :test]
before_action :set_docker_client, only: [:run, :test]
before_action :set_files, only: [:download, :download_file, :render_file, :show]
before_action :set_file, only: [:download_file, :render_file]
@@ -191,6 +191,9 @@ class SubmissionsController < ApplicationController
end
def kill_socket(tubesock)
# search for errors and save them as StructuredError (for scoring runs see submission_scoring.rb)
extract_errors
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
save_run_output
@@ -199,6 +202,17 @@ class SubmissionsController < ApplicationController
tubesock.close
end
def extract_errors
if !@message_buffer.blank?
@submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze
if pattern.match(@message_buffer)
StructuredError.create_from_template(template, @message_buffer)
end
end
end
end
def handle_message(message, tubesock, container)
@run_output ||= ""
# Handle special commands first

View File

@@ -0,0 +1,2 @@
module ErrorTemplateAttributesHelper
end

View File

@@ -0,0 +1,2 @@
module ErrorTemplatesHelper
end

View File

@@ -0,0 +1,8 @@
class ErrorTemplate < ActiveRecord::Base
belongs_to :execution_environment
has_and_belongs_to_many :error_template_attributes
def to_s
"#{id} [#{name}]"
end
end

View File

@@ -0,0 +1,7 @@
class ErrorTemplateAttribute < ActiveRecord::Base
has_and_belongs_to_many :error_template
def to_s
"#{id} [#{key}]"
end
end

View File

@@ -11,6 +11,7 @@ class ExecutionEnvironment < ActiveRecord::Base
has_many :exercises
belongs_to :file_type
has_many :hints
has_many :error_templates
scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') }

View File

@@ -36,8 +36,8 @@ class ProxyExercise < ActiveRecord::Base
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
assigned_user_proxy_exercise.exercise
else
Rails.logger.debug("find new matching exercise for user #{user.id}" )
matching_exercise =
Rails.logger.debug("find new matching exercise for user #{user.id}" )
begin
find_matching_exercise(user)
rescue => e #fallback
@@ -72,7 +72,7 @@ class ProxyExercise < ActiveRecord::Base
# find exercises
potential_recommended_exercises = []
exercises.where("expected_difficulty > 1").each do |ex|
exercises.where("expected_difficulty >= 1").each do |ex|
## find exercises which have only tags the user has already seen
if (ex.tags - tags_user_has_seen).empty?
potential_recommended_exercises << ex
@@ -85,8 +85,7 @@ class ProxyExercise < ActiveRecord::Base
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
select_easiest_exercise(exercises)
else
recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
recommended_exercise
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
end
end
@@ -238,4 +237,4 @@ class ProxyExercise < ActiveRecord::Base
exercises.order(:expected_difficulty).first
end
end
end

View File

@@ -0,0 +1,12 @@
class StructuredError < ActiveRecord::Base
belongs_to :error_template
belongs_to :file, class_name: 'CodeOcean::File'
def self.create_from_template(template, message_buffer)
instance = self.create(error_template: template)
template.error_template_attributes.each do |attribute|
StructuredErrorAttribute.create_from_template(attribute, instance, message_buffer)
end
instance
end
end

View File

@@ -0,0 +1,17 @@
class StructuredErrorAttribute < ActiveRecord::Base
belongs_to :structured_error
belongs_to :error_template_attribute
def self.create_from_template(attribute, structured_error, message_buffer)
match = false
value = nil
result = message_buffer.match(attribute.regex)
if result != nil
match = true
if result.captures.size > 0
value = result.captures[0]
end
end
self.create(structured_error: structured_error, error_template_attribute: attribute, value: value, match: match)
end
end

View File

@@ -0,0 +1,3 @@
class ErrorTemplateAttributePolicy < AdminOnlyPolicy
end

View File

@@ -0,0 +1,9 @@
class ErrorTemplatePolicy < AdminOnlyPolicy
def add_attribute?
admin?
end
def remove_attribute?
admin?
end
end

View File

@@ -8,7 +8,8 @@
- if current_user.admin?
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
li.divider
- models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate, InternalUser, UserExerciseFeedback].sort_by { |model| model.model_name.human(count: 2) }
- models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink, UserExerciseFeedback,
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"))

View File

@@ -0,0 +1,16 @@
= form_for(@error_template_attribute) do |f|
= render('shared/form_errors', object: @error_template_attribute)
.form-group
= f.label(:key)
= f.text_field(:key, class: 'form-control', required: true)
.form-group
= f.label(:description)
= f.text_field(:description, class: 'form-control')
.form-group
= f.label(:regex)
= f.text_field(:regex, class: 'form-control', required: true)
.help-block == t('error_templates.hints.signature')
.form-group
= f.check_box(:important)
= t('activerecord.attributes.error_template_attribute.important')
.actions = render('shared/submit_button', f: f, object: @error_template_attribute)

View File

@@ -0,0 +1,3 @@
h1 = @error_template_attribute
= render('form')

View File

@@ -0,0 +1,28 @@
h1 = ErrorTemplateAttribute.model_name.human(count: 2)
.table-responsive
table.sortable.table
thead
tr
th
th = t('activerecord.attributes.error_template_attribute.key')
th = t('activerecord.attributes.error_template_attribute.description')
th = t('activerecord.attributes.error_template_attribute.regex')
th colspan=5 = t('shared.actions')
tbody
- @error_template_attributes.each do |error_template_attribute|
tr
td
- if error_template_attribute.important
span class="fa fa-star" aria-hidden="true"
- else
span class="fa fa-star-o" aria-hidden="true"
td = error_template_attribute.key
td = error_template_attribute.description
td = error_template_attribute.regex
td = link_to(t('shared.show'), error_template_attribute)
td = link_to(t('shared.edit'), edit_error_template_attribute_path(error_template_attribute))
td = link_to(t('shared.destroy'), error_template_attribute, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @error_template_attributes)
p = render('shared/new_button', model: ErrorTemplateAttribute)

View File

@@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: ErrorTemplateAttribute.model_name.human)
= render('form')

View File

@@ -0,0 +1,8 @@
h1
= @error_template_attribute
= render('shared/edit_button', object: @error_template_attribute)
- [:key, :description, :regex, :important].each do |attribute|
= row(label: "error_template_attribute.#{attribute}", value: @error_template_attribute.send(attribute))
// todo: used by

View File

@@ -0,0 +1,19 @@
= form_for(@error_template) do |f|
= render('shared/form_errors', object: @error_template)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:execution_environment_id)
= f.collection_select(:execution_environment_id, ExecutionEnvironment.all.order(:name), :id, :name, {include_blank: false}, class: 'form-control')
.form-group
= f.label(:signature)
= f.text_field(:signature, class: 'form-control')
.help-block == t('error_templates.hints.signature')
.form-group
= f.label(:description)
= f.text_field(:description, class: 'form-control')
.form-group
= f.label(:hint)
= f.text_field(:hint, class: 'form-control')
.actions = render('shared/submit_button', f: f, object: @error_template)

View File

@@ -0,0 +1,3 @@
h1 = @error_template
= render('form')

View File

@@ -0,0 +1,22 @@
h1 = ErrorTemplate.model_name.human(count: 2)
.table-responsive
table.sortable.table
thead
tr
th = t('activerecord.attributes.error_template.name')
th = t('activerecord.attributes.error_template.description')
th = t('activerecord.attributes.exercise.execution_environment')
th colspan=3 = t('shared.actions')
tbody
- @error_templates.each do |error_template|
tr
td = error_template.name
td = error_template.description
td = link_to(error_template.execution_environment)
td = link_to(t('shared.show'), error_template)
td = link_to(t('shared.edit'), edit_error_template_path(error_template))
td = link_to(t('shared.destroy'), error_template, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @error_templates)
p = render('shared/new_button', model: ErrorTemplate)

View File

@@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: ErrorTemplate.model_name.human)
= render('form')

View File

@@ -0,0 +1,40 @@
h1
= @error_template
= render('shared/edit_button', object: @error_template)
= row(label: 'error_template.name', value: @error_template.name)
= row(label: 'exercise.execution_environment', value: link_to(@error_template.execution_environment))
- [:signature, :description, :hint].each do |attribute|
= row(label: "error_template.#{attribute}", value: @error_template.send(attribute))
h3
= t 'error_templates.attributes'
.table-responsive
table.sortable.table
thead
tr
th
th = t('activerecord.attributes.error_template_attribute.key')
th = t('activerecord.attributes.error_template_attribute.description')
th = t('activerecord.attributes.error_template_attribute.regex')
th colspan=3 = t('shared.actions')
tbody
- @error_template.error_template_attributes.order('important DESC', :key).each do |attribute|
tr
td
- if attribute.important
span class="fa fa-star" aria-hidden="true"
- else
span class="fa fa-star-o" aria-hidden="true"
td = attribute.key
td = attribute.description
td = attribute.regex
td = link_to(t('shared.show'), attribute)
td = link_to(t('shared.destroy'), attribute_error_template_url(:error_template_attribute_id => attribute.id), :method => :delete)
#add-attribute
= collection_select({}, :error_template_attribute_id,
ErrorTemplateAttribute.where.not(id: @error_template.error_template_attributes.select(:id).to_a).order('important DESC', :key),
:id, :key, {include_blank: false}, class: '')
button.btn.btn-default = t('error_templates.add_attribute')

View File

@@ -3,7 +3,7 @@
- consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
- show_break_interventions = @show_break_interventions || "false"
- show_rfc_interventions = @show_rfc_interventions || "false"
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise)
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
div id='frames' class='editor-col'
@@ -24,4 +24,4 @@
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')