Merge branch 'master' into update-gemfile

This commit is contained in:
Ralf Teusner
2018-02-14 13:31:14 +01:00
18 changed files with 88 additions and 116 deletions

View File

@ -9,7 +9,7 @@ module SubmissionScoring
assessment = assessor.assess(output) assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0)) 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 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? unless testrun_output.blank?
submission.exercise.execution_environment.error_templates.each do |template| submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze pattern = Regexp.new(template.signature).freeze
if pattern.match(testrun_output) if pattern.match(testrun_output)
@ -47,6 +47,14 @@ module SubmissionScoring
end end
end end
submission.update(score: score) submission.update(score: score)
if submission.normalized_score == 1.0
Thread.new do
RequestForComment.where(exercise_id: submission.exercise_id, user_id: submission.user_id, user_type: submission.user_type).each{ |rfc|
rfc.full_score_reached = true
rfc.save
}
end
end
outputs outputs
end end
end end

View File

@ -416,6 +416,9 @@ class ExercisesController < ApplicationController
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc') flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc')
flash.keep(:notice) flash.keep(:notice)
# increase counter 'times_featured' in rfc
rfc.increment!(:times_featured)
respond_to do |format| respond_to do |format|
format.html {redirect_to(rfc)} format.html {redirect_to(rfc)}
format.json {render(json: {redirect: url_for(rfc)})} format.json {render(json: {redirect: url_for(rfc)})}

View File

@ -20,7 +20,7 @@ class RequestForCommentsController < ApplicationController
.group('request_for_comments.id, request_for_comments.user_id, request_for_comments.exercise_id, .group('request_for_comments.id, request_for_comments.user_id, request_for_comments.exercise_id,
request_for_comments.file_id, request_for_comments.question, request_for_comments.created_at, request_for_comments.file_id, request_for_comments.question, request_for_comments.created_at,
request_for_comments.updated_at, request_for_comments.user_type, request_for_comments.solved, request_for_comments.updated_at, request_for_comments.user_type, request_for_comments.solved,
request_for_comments.submission_id, request_for_comments.row_number') # ugly, but rails wants it this way request_for_comments.full_score_reached, 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') .select('request_for_comments.*, max(comments.updated_at) as last_comment')
.search(params[:q]) .search(params[:q])
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page], total_entries: @search.result.length) @request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page], total_entries: @search.result.length)

View File

@ -202,21 +202,11 @@ class SubmissionsController < ApplicationController
tubesock.close tubesock.close
end 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) def handle_message(message, tubesock, container)
@run_output ||= "" @raw_output ||= ''
@run_output ||= ''
# Handle special commands first # Handle special commands first
if (/^#exit/.match(message)) if /^#exit/.match(message)
# Just call exit_container on the docker_client. # Just call exit_container on the docker_client.
# Do not call kill_socket for the websocket to the client here. # Do not call kill_socket for the websocket to the client here.
# @docker_client.exit_container closes the socket to the container, # @docker_client.exit_container closes the socket to the container,
@ -228,17 +218,17 @@ class SubmissionsController < ApplicationController
# Filter out information about run_command, test_command, user or working directory # Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
test_command = @submission.execution_environment.test_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)) unless /root|workspace|#{run_command}|#{test_command}/.match(message)
parse_message(message, 'stdout', tubesock) parse_message(message, 'stdout', tubesock)
end end
end end
end end
def parse_message(message, output_stream, socket, recursive = true) def parse_message(message, output_stream, socket, recursive = true)
parsed = ''; parsed = ''
begin begin
parsed = JSON.parse(message) parsed = JSON.parse(message)
if(parsed.class == Hash && parsed.key?('cmd')) if parsed.class == Hash and parsed.key?('cmd')
socket.send_data message socket.send_data message
Rails.logger.info('parse_message sent: ' + message) Rails.logger.info('parse_message sent: ' + message)
else else
@ -248,24 +238,24 @@ class SubmissionsController < ApplicationController
end end
rescue JSON::ParserError => e rescue JSON::ParserError => e
# Check wether the message contains multiple lines, if true try to parse each line # Check wether the message contains multiple lines, if true try to parse each line
if ((recursive == true) && (message.include? "\n")) if recursive and message.include? "\n"
for part in message.split("\n") for part in message.split("\n")
self.parse_message(part,output_stream,socket,false) self.parse_message(part,output_stream,socket,false)
end end
elsif(message.include? "<img") elsif message.include? '<img'
#Rails.logger.info('img foung') #Rails.logger.info('img foung')
@buffering = true @buffering = true
@buffer = "" @buffer = ''
@buffer += message @buffer += message
#Rails.logger.info('Starting to buffer') #Rails.logger.info('Starting to buffer')
elsif(@buffering && (message.include? "/>")) elsif @buffering and message.include? '/>'
@buffer += message @buffer += message
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>@buffer} parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>@buffer}
socket.send_data JSON.dump(parsed) socket.send_data JSON.dump(parsed)
#socket.send_data @buffer #socket.send_data @buffer
@buffering = false @buffering = false
#Rails.logger.info('Sent complete buffer') #Rails.logger.info('Sent complete buffer')
elsif(@buffering) elsif @buffering
@buffer += message @buffer += message
#Rails.logger.info('Appending to buffer') #Rails.logger.info('Appending to buffer')
else else
@ -275,18 +265,30 @@ class SubmissionsController < ApplicationController
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed)) Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
end end
ensure ensure
@raw_output += parsed['data'] if parsed.class == Hash and parsed.key? 'data'
# 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" # 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 @run_output += JSON.dump(parsed) if @run_output.size <= max_run_output_buffer_size
end end
end end
def save_run_output def save_run_output
if !@run_output.blank? unless @run_output.blank?
@run_output = @run_output[(0..max_run_output_buffer_size-1)] # trim the string to max_message_buffer_size chars @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) Testrun.create(file: @file, cause: 'run', submission: @submission, output: @run_output)
end end
end end
def extract_errors
unless @raw_output.blank?
@submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze
if pattern.match(@raw_output)
StructuredError.create_from_template(template, @raw_output, @submission)
end
end
end
end
def score def score
hijack do |tubesock| hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?

View File

@ -364,7 +364,7 @@ class Exercise < ActiveRecord::Base
end end
private :valid_main_file? private :valid_main_file?
def needs_more_feedback def needs_more_feedback?
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
end end

View File

@ -52,6 +52,6 @@ class RequestForComment < ActiveRecord::Base
private private
def self.row_number_user_sql def self.row_number_user_sql
select("id, user_id, exercise_id, file_id, question, created_at, updated_at, user_type, solved, submission_id, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql select("id, user_id, exercise_id, file_id, question, created_at, updated_at, user_type, solved, full_score_reached, submission_id, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql
end end
end end

View File

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

View File

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

View File

@ -8,6 +8,7 @@ class Submission < ActiveRecord::Base
belongs_to :exercise belongs_to :exercise
has_many :testruns has_many :testruns
has_many :structured_errors
has_many :comments, through: :files has_many :comments, through: :files
delegate :execution_environment, to: :exercise delegate :execution_environment, to: :exercise
@ -57,7 +58,7 @@ class Submission < ActiveRecord::Base
end end
def redirect_to_feedback? def redirect_to_feedback?
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback ((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback?
end end
def own_unsolved_rfc def own_unsolved_rfc

View File

@ -54,8 +54,6 @@ h1 = "#{@exercise} (external user #{@external_user})"
-submission_or_intervention.testruns.each do |run| -submission_or_intervention.testruns.each do |run|
- if run.passed - if run.passed
.unit-test-result.positive-result title=run.output .unit-test-result.positive-result title=run.output
- elsif run.failed
.unit-test-result.negative-result title=run.output
- else - else
.unit-test-result.unknown-result title=run.output .unit-test-result.unknown-result title=run.output
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0

View File

@ -27,6 +27,9 @@ h1 = RequestForComment.model_name.human(count: 2)
- if request_for_comment.solved? - if request_for_comment.solved?
td td
span class="fa fa-check" aria-hidden="true" span class="fa fa-check" aria-hidden="true"
- elsif request_for_comment.full_score_reached
td
span class="fa fa-check" style="color:darkgrey" aria-hidden="true"
- else - else
td = '' td = ''
td = link_to(request_for_comment.exercise.title, request_for_comment) td = link_to(request_for_comment.exercise.title, request_for_comment)

View File

@ -0,0 +1,5 @@
class AddSubmissionToStructuredErrors < ActiveRecord::Migration
def change
add_reference :structured_errors, :submission, index: true
end
end

View File

@ -0,0 +1,15 @@
class AddReachedFullScoreToRequestForComment < ActiveRecord::Migration
def up
add_column :request_for_comments, :full_score_reached, :boolean, default: false
RequestForComment.all.each { |rfc|
if (rfc.submission.present? && rfc.submission.exercise.has_user_solved(rfc.user))
rfc.full_score_reached = true
rfc.save
end
}
end
def down
remove_column :request_for_comments, :full_score_reached
end
end

View File

@ -0,0 +1,5 @@
class AddTimesFeaturedToRequestForComments < ActiveRecord::Migration
def change
add_column :request_for_comments, :times_featured, :integer, default: 0
end
end

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171120153705) do ActiveRecord::Schema.define(version: 20180202132034) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -270,16 +270,18 @@ ActiveRecord::Schema.define(version: 20171120153705) do
end end
create_table "request_for_comments", force: :cascade do |t| create_table "request_for_comments", force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "exercise_id", null: false t.integer "exercise_id", null: false
t.integer "file_id", null: false t.integer "file_id", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "user_type", limit: 255 t.string "user_type", limit: 255
t.text "question" t.text "question"
t.boolean "solved", default: false t.boolean "solved", default: false
t.integer "submission_id" t.integer "submission_id"
t.text "thank_you_note" t.text "thank_you_note"
t.boolean "full_score_reached", default: false
t.integer "times_featured", default: 0
end end
create_table "searches", force: :cascade do |t| create_table "searches", force: :cascade do |t|
@ -305,8 +307,11 @@ ActiveRecord::Schema.define(version: 20171120153705) do
t.integer "file_id" t.integer "file_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "submission_id"
end end
add_index "structured_errors", ["submission_id"], name: "index_structured_errors_on_submission_id", using: :btree
create_table "submissions", force: :cascade do |t| create_table "submissions", force: :cascade do |t|
t.integer "exercise_id" t.integer "exercise_id"
t.float "score" t.float "score"
@ -354,8 +359,8 @@ ActiveRecord::Schema.define(version: 20171120153705) do
t.integer "working_time_seconds" t.integer "working_time_seconds"
t.string "feedback_text" t.string "feedback_text"
t.integer "user_estimated_worktime" t.integer "user_estimated_worktime"
t.datetime "created_at", default: '2017-11-20 18:20:25', null: false t.datetime "created_at", default: '2018-01-30 17:39:22', null: false
t.datetime "updated_at", default: '2017-11-20 18:20:25', null: false t.datetime "updated_at", default: '2018-01-30 17:39:22', null: false
end end
create_table "user_exercise_interventions", force: :cascade do |t| create_table "user_exercise_interventions", force: :cascade do |t|

View File

@ -1,59 +0,0 @@
class Xikolo::Client
def self.get_user(user_id)
params = {:user_id => user_id}
response = get_request(user_profile_url(user_id), params)
if response
return JSON.parse(response)
else
return nil
end
end
def self.user_profile_url(user_id)
return url + 'v2/users/' + user_id
end
def self.post_request(url, params)
begin
return RestClient.post url, params, http_header
rescue
return nil
end
end
def self.get_request(url, params)
begin
return RestClient.get url, {:params => params}.merge(http_header)
rescue
return nil
end
end
def self.http_header
return {:accept => accept, :authorization => token}
end
def self.url
@url ||= CodeOcean::Config.new(:code_ocean).read.fetch(:xikolo_api_url, 'http://localhost:3000/api/') #caches this with ||=, second value of fetch is default value
end
def self.accept
'application/vnd.xikolo.v1, application/vnd.api+json, application/json'
end
def self.token
'Token token='+Rails.application.secrets.openhpi_api_token#+'"'
end
private
def authenticate_with_user
params = {:email => "admin@openhpi.de", :password => "admin"}
response = post_request(authentication_url, params)
@token = 'Token token="'+JSON.parse(response)['token']+'"'
end
def self.authentication_url
return @url + 'authenticate'
end
end

View File

@ -1,15 +0,0 @@
class Xikolo::UserClient
def self.get(user_id)
user = Xikolo::Client.get_user(user_id)
# return default values if user is not found or if there is a server issue:
if user
name = user.dig('data', 'attributes', 'name') || "User " + user_id
user_visual = user.dig('data', 'attributes', 'avatar_url') || ActionController::Base.helpers.image_path('default.png')
language = user.dig('data', 'attributes', 'language') || "DE"
return {display_name: name, user_visual: user_visual, language: language}
else
return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"}
end
end
end

View File

@ -132,12 +132,14 @@ describe SessionsController do
end end
it 'redirects to recommended exercise if requested token of proxy exercise' do it 'redirects to recommended exercise if requested token of proxy exercise' do
skip 'test is currently oscillating'
FactoryBot.create(:proxy_exercise, exercises: [exercise]) FactoryBot.create(:proxy_exercise, exercises: [exercise])
post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id
expect(controller).to redirect_to(implement_exercise_path(exercise.id)) expect(controller).to redirect_to(implement_exercise_path(exercise.id))
end end
it 'recommends only exercises who are 1 degree more complicated than what user has seen' do it 'recommends only exercises who are 1 degree more complicated than what user has seen' do
skip 'test is currently oscillating'
# dummy user has no exercises finished, therefore his highest difficulty is 0 # dummy user has no exercises finished, therefore his highest difficulty is 0
FactoryBot.create(:proxy_exercise, exercises: [exercise, exercise2]) FactoryBot.create(:proxy_exercise, exercises: [exercise, exercise2])
exercise.expected_difficulty = 3 exercise.expected_difficulty = 3