merged with current master

This commit is contained in:
Thomas Hille
2017-11-16 17:16:22 +01:00
144 changed files with 1406 additions and 349 deletions

5
.gitignore vendored
View File

@ -4,9 +4,11 @@
/config/secrets.yml /config/secrets.yml
/config/sendmail.yml /config/sendmail.yml
/config/smtp.yml /config/smtp.yml
/config/docker.yml.erb /config/docker.yml*.erb
/config/*.production.yml /config/*.production.yml
/config/*.staging.yml /config/*.staging.yml
/config/*.staging-epic.yml
/config/deploy/staging-epic.rb
/coverage /coverage
/log /log
/public/assets /public/assets
@ -14,6 +16,7 @@
/rubocop.html /rubocop.html
/tmp /tmp
/vagrant/ /vagrant/
/.capistrano
/.vagrant /.vagrant
*.sublime-* *.sublime-*
/.idea /.idea

View File

@ -8,7 +8,7 @@ gem 'coffee-rails', '~> 4.0.0'
gem 'concurrent-ruby', '~> 1.0.1' gem 'concurrent-ruby', '~> 1.0.1'
gem 'concurrent-ruby-ext', '~> 1.0.1', platform: :ruby gem 'concurrent-ruby-ext', '~> 1.0.1', platform: :ruby
gem 'docker-api','~> 1.25.0', require: 'docker' gem 'docker-api','~> 1.25.0', require: 'docker'
gem 'factory_girl_rails', '~> 4.0' gem 'factory_bot_rails', '~> 4.8.2'
gem 'forgery' gem 'forgery'
gem 'highline' gem 'highline'
gem 'jbuilder', '~> 2.0' gem 'jbuilder', '~> 2.0'

View File

@ -125,10 +125,10 @@ GEM
eventmachine (1.0.9.1-java) eventmachine (1.0.9.1-java)
excon (0.54.0) excon (0.54.0)
execjs (2.6.0) execjs (2.6.0)
factory_girl (4.5.0) factory_bot (4.8.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.6.0) factory_bot_rails (4.8.2)
factory_girl (~> 4.5.0) factory_bot (~> 4.8.2)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.9.2) faraday (0.9.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -401,7 +401,7 @@ DEPENDENCIES
d3-rails d3-rails
database_cleaner database_cleaner
docker-api (~> 1.25.0) docker-api (~> 1.25.0)
factory_girl_rails (~> 4.0) factory_bot_rails (~> 4.8.2)
faye-websocket faye-websocket
forgery forgery
highline highline

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

@ -94,3 +94,15 @@ a.file-heading {
top: 100%; top: 100%;
left: 0; left: 0;
} }
.feedback {
.text {
margin-bottom: 10px;
}
.difficulty {
font-weight: bold;
}
.worktime {
font-weight: bold;
}
}

View File

@ -8,7 +8,15 @@ module SubmissionScoring
output = execute_test_file(file, submission) output = execute_test_file(file, submission)
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 : 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
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 Testrun.new(submission: submission, cause: 'assess', file: file, passed: passed, output: testrun_output).save
output.merge!(assessment) output.merge!(assessment)
output.merge!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight) 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

@ -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

View File

@ -6,7 +6,7 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: [:create, :update] before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload] before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback]
before_action :set_external_user, only: [:statistics] before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_course_token, only: [:implement] before_action :set_course_token, only: [:implement]
@ -28,7 +28,6 @@ class ExercisesController < ApplicationController
1 1
end end
def java_course_token def java_course_token
"702cbd2a-c84c-4b37-923a-692d7d1532d0" "702cbd2a-c84c-4b37-923a-692d7d1532d0"
end end
@ -94,6 +93,11 @@ class ExercisesController < ApplicationController
collect_set_and_unset_exercise_tags collect_set_and_unset_exercise_tags
end end
def feedback
authorize!
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page])
end
def import_proforma_xml def import_proforma_xml
begin begin
user = user_for_oauth2_request() user = user_for_oauth2_request()
@ -147,8 +151,7 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token private :user_by_code_harbor_token
def exercise_params def exercise_params
params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60 params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
private :exercise_params private :exercise_params
@ -388,10 +391,13 @@ class ExercisesController < ApplicationController
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.) # otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
# redirect 10 percent pseudorandomly to the feedback page # redirect 10 percent pseudorandomly to the feedback page
if current_user.respond_to? :external_id if current_user.respond_to? :external_id
if ((current_user.id + @submission.exercise.created_at.to_i) % 10 == 1) if @submission.redirect_to_feedback?
redirect_to_user_feedback redirect_to_user_feedback
return return
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first end
rfc = @submission.own_unsolved_rfc
if rfc
# set a message that informs the user that his own RFC should be closed. # set a message that informs the user that his own RFC should be closed.
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc') flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
flash.keep(:notice) flash.keep(:notice)
@ -401,9 +407,11 @@ class ExercisesController < ApplicationController
format.json { render(json: {redirect: url_for(rfc)}) } format.json { render(json: {redirect: url_for(rfc)}) }
end end
return return
end
# else: show open rfc for same exercise if available # else: show open rfc for same exercise if available
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < 5) } rfc = @submission.unsolved_rfc
unless rfc.nil?
# set a message that informs the user that his score was perfect and help in RFC is greatly appreciated. # set a message that informs the user that his score was perfect and help in RFC is greatly appreciated.
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)
@ -417,7 +425,11 @@ class ExercisesController < ApplicationController
end end
else else
# redirect to feedback page if score is less than 100 percent # redirect to feedback page if score is less than 100 percent
if @exercise.needs_more_feedback?
redirect_to_user_feedback redirect_to_user_feedback
else
redirect_to_lti_return_path
end
return return
end end
redirect_to_lti_return_path redirect_to_lti_return_path

View File

@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
include SubmissionScoring include SubmissionScoring
include Tubesock::Hijack 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_docker_client, only: [:run, :test]
before_action :set_files, only: [:download, :download_file, :render_file, :show] before_action :set_files, only: [:download, :download_file, :render_file, :show]
before_action :set_file, only: [:download_file, :render_file] before_action :set_file, only: [:download_file, :render_file]
@ -191,6 +191,9 @@ class SubmissionsController < ApplicationController
end end
def kill_socket(tubesock) 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 the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
save_run_output save_run_output
@ -199,6 +202,17 @@ 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 ||= "" @run_output ||= ""
# Handle special commands first # Handle special commands first

View File

@ -2,6 +2,7 @@ class UserExerciseFeedbacksController < ApplicationController
include CommonBehavior include CommonBehavior
before_action :set_user_exercise_feedback, only: [:edit, :update] before_action :set_user_exercise_feedback, only: [:edit, :update]
before_action :set_user_exercise_feedback_by_id, only: [:show, :destroy]
def comment_presets def comment_presets
[[0,t('user_exercise_feedback.difficulty_easy')], [[0,t('user_exercise_feedback.difficulty_easy')],
@ -19,10 +20,15 @@ class UserExerciseFeedbacksController < ApplicationController
[4,t('user_exercise_feedback.estimated_time_more_30')]] [4,t('user_exercise_feedback.estimated_time_more_30')]]
end end
def authorize! def index
authorize(@uef) @search = UserExerciseFeedback.all.search params[:q]
@uefs = @search.result.includes(:execution_environment).order(:id).paginate(page: params[:page])
authorize!
end
def show
authorize!
end end
private :authorize!
def create def create
@exercise = Exercise.find(uef_params[:exercise_id]) @exercise = Exercise.find(uef_params[:exercise_id])
@ -49,7 +55,8 @@ class UserExerciseFeedbacksController < ApplicationController
end end
def destroy def destroy
destroy_and_respond(object: @tag) authorize!
destroy_and_respond(object: @uef)
end end
def edit def edit
@ -58,11 +65,6 @@ class UserExerciseFeedbacksController < ApplicationController
authorize! authorize!
end end
def uef_params
params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :uef_params
def new def new
@texts = comment_presets.to_a @texts = comment_presets.to_a
@times = time_presets.to_a @times = time_presets.to_a
@ -89,6 +91,12 @@ class UserExerciseFeedbacksController < ApplicationController
end end
end end
private
def authorize!
authorize(@uef || @uefs)
end
def to_s def to_s
name name
end end
@ -98,6 +106,14 @@ class UserExerciseFeedbacksController < ApplicationController
@uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user) @uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user)
end end
def set_user_exercise_feedback_by_id
@uef = UserExerciseFeedback.find(params[:id])
end
def uef_params
params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name)
end
def validate_inputs(uef_params) def validate_inputs(uef_params)
begin begin
if uef_params[:difficulty].to_i < 0 || uef_params[:difficulty].to_i >= comment_presets.size if uef_params[:difficulty].to_i < 0 || uef_params[:difficulty].to_i >= comment_presets.size

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 has_many :exercises
belongs_to :file_type belongs_to :file_type
has_many :hints has_many :hints
has_many :error_templates
scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') } scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') }

View File

@ -20,6 +20,7 @@ class Exercise < ActiveRecord::Base
has_many :exercise_tags has_many :exercise_tags
has_many :tags, through: :exercise_tags has_many :tags, through: :exercise_tags
accepts_nested_attributes_for :exercise_tags accepts_nested_attributes_for :exercise_tags
has_many :user_exercise_feedbacks
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
@ -36,6 +37,8 @@ class Exercise < ActiveRecord::Base
@working_time_statistics = nil @working_time_statistics = nil
MAX_EXERCISE_FEEDBACKS = 20
def average_percentage def average_percentage
if average_score and maximum_score != 0.0 and submissions.exists?(cause: 'submit') if average_score and maximum_score != 0.0 and submissions.exists?(cause: 'submit')
@ -361,4 +364,8 @@ class Exercise < ActiveRecord::Base
end end
private :valid_main_file? private :valid_main_file?
def needs_more_feedback
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
end
end end

View File

@ -2,4 +2,8 @@ class ExerciseCollection < ActiveRecord::Base
has_and_belongs_to_many :exercises has_and_belongs_to_many :exercises
def to_s
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
end
end end

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

@ -18,6 +18,8 @@ class Submission < ActiveRecord::Base
validates :cause, inclusion: {in: CAUSES} validates :cause, inclusion: {in: CAUSES}
validates :exercise_id, presence: true validates :exercise_id, presence: true
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
def build_files_hash(files, attribute) def build_files_hash(files, attribute)
files.map(&attribute.to_proc).zip(files).to_h files.map(&attribute.to_proc).zip(files).to_h
end end
@ -53,4 +55,16 @@ class Submission < ActiveRecord::Base
def to_s def to_s
Submission.model_name.human Submission.model_name.human
end end
def redirect_to_feedback?
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback
end
def own_unsolved_rfc
RequestForComment.unsolved.where(exercise_id: exercise, user_id: user_id).first
end
def unsolved_rfc
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
end
end end

View File

@ -2,6 +2,7 @@ class UserExerciseFeedback < ActiveRecord::Base
include Creation include Creation
belongs_to :exercise belongs_to :exercise
has_one :execution_environment, through: :exercise
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] } validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }

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

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

View File

@ -12,7 +12,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
@user.internal_user? @user.internal_user?
end end
[:clone?, :destroy?, :edit?, :statistics?, :update?].each do |action| [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?].each do |action|
define_method(action) { admin? || author?} define_method(action) { admin? || author?}
end end

View File

@ -1,8 +1,4 @@
class UserExerciseFeedbackPolicy < ApplicationPolicy class UserExerciseFeedbackPolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
def create? def create?
everyone everyone
@ -12,8 +8,4 @@ class UserExerciseFeedbackPolicy < ApplicationPolicy
everyone everyone
end end
[:show? ,:destroy?, :edit?, :update?].each do |action|
define_method(action) { admin? || author?}
end
end end

View File

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

@ -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)

View File

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

View 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)

View File

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

View 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)

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 : '') - 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_break_interventions = @show_break_interventions || "false"
- show_rfc_interventions = @show_rfc_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="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='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' div id='frames' class='editor-col'

View File

@ -35,9 +35,6 @@
.form-group .form-group
= f.label(t('activerecord.attributes.exercise.difficulty')) = f.label(t('activerecord.attributes.exercise.difficulty'))
= f.number_field :expected_difficulty, in: 1..10, step: 1 = f.number_field :expected_difficulty, in: 1..10, step: 1
.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 = t('exercises.form.tags') h2 = t('exercises.form.tags')
ul.list-unstyled.panel-group ul.list-unstyled.panel-group

View File

@ -0,0 +1,15 @@
h1 = @exercise
ul.list-unstyled.panel-group#files
- @feedbacks.each do |feedback|
li.panel.panel-default
.panel-heading role="tab" id="heading"
div.clearfix
span = feedback.user.name
.panel-collapse role="tabpanel"
.panel-body.feedback
.text = feedback.feedback_text
.difficulty = "#{t('user_exercise_feedback.difficulty')} #{feedback.difficulty}" if feedback.difficulty
.worktime = "#{t('user_exercise_feedback.working_time')} #{feedback.user_estimated_worktime}" if feedback.user_estimated_worktime
= render('shared/pagination', collection: @feedbacks)

View File

@ -18,7 +18,6 @@ h1 = Exercise.model_name.human(count: 2)
th = t('activerecord.attributes.exercise.maximum_score') th = t('activerecord.attributes.exercise.maximum_score')
th = t('activerecord.attributes.exercise.tags') th = t('activerecord.attributes.exercise.tags')
th = t('activerecord.attributes.exercise.difficulty') th = t('activerecord.attributes.exercise.difficulty')
th = t('activerecord.attributes.exercise.worktime')
th th
= t('activerecord.attributes.exercise.public') = t('activerecord.attributes.exercise.public')
- if policy(Exercise).batch_update? - if policy(Exercise).batch_update?
@ -34,7 +33,6 @@ h1 = Exercise.model_name.human(count: 2)
td = exercise.maximum_score td = exercise.maximum_score
td = exercise.exercise_tags.count td = exercise.exercise_tags.count
td = exercise.expected_difficulty td = exercise.expected_difficulty
td = (exercise.expected_worktime_seconds / 60).ceil
td.public data-value=exercise.public? = symbol_for(exercise.public?) td.public data-value=exercise.public? = symbol_for(exercise.public?)
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
@ -47,6 +45,7 @@ h1 = Exercise.model_name.human(count: 2)
span.sr-only Toggle Dropdown span.sr-only Toggle Dropdown
ul.dropdown-menu.pull-right role="menu" ul.dropdown-menu.pull-right role="menu"
li = link_to(t('shared.show'), exercise) if policy(exercise).show? li = link_to(t('shared.show'), exercise) if policy(exercise).show?
li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise)) if policy(exercise).feedback?
li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy? li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy?
li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone? li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone?

View File

@ -20,7 +20,6 @@ h1
= row(label: 'exercise.embedding_parameters') do = row(label: 'exercise.embedding_parameters') do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
= row(label: 'exercise.difficulty', value: @exercise.expected_difficulty) = row(label: 'exercise.difficulty', value: @exercise.expected_difficulty)
= row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min")
= row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", "))
h2 = t('activerecord.attributes.exercise.files') h2 = t('activerecord.attributes.exercise.files')

View File

@ -29,7 +29,8 @@
<%= t('activerecord.attributes.request_for_comments.question')%> <%= t('activerecord.attributes.request_for_comments.question')%>
</h5> </h5>
<div class="text"> <div class="text">
<%= @request_for_comment.question or t('request_for_comments.no_question')%> <% question = @request_for_comment.question %>
<%= question.nil? or question.empty? ? t('request_for_comments.no_question') : question %>
</div> </div>
</div> </div>

View File

@ -0,0 +1,27 @@
h1 = UserExerciseFeedback.model_name.human(count: 2)
= render(layout: 'shared/form_filters') do |f|
.form-group
= f.label(:execution_environment_id_eq, t('activerecord.attributes.exercise.execution_environment'), class: 'sr-only')
= f.collection_select(:execution_environment_id_eq, ExecutionEnvironment.with_exercises, :id, :name, class: 'form-control', prompt: t('activerecord.attributes.exercise.execution_environment'))
.form-group
= f.label(:exercise_title_cont, t('activerecord.attributes.request_for_comments.exercise'), class: 'sr-only')
= f.search_field(:exercise_title_cont, class: 'form-control', placeholder: t('activerecord.attributes.request_for_comments.exercise'))
.table-responsive
table.table
thead
tr
th colspan=2 = t('activerecord.attributes.user_exercise_feedback.user')
th = t('activerecord.attributes.user_exercise_feedback.exercise')
th colspan=2 = t('shared.actions')
tbody
- @uefs.each do |uef|
tr
td = uef.user.id
td = uef.user.name
td = link_to(uef.exercise.title, uef.exercise)
td = link_to(t('shared.show'), uef)
td = link_to(t('shared.destroy'), uef, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @uefs)

View File

@ -0,0 +1,7 @@
h2 = @uef
= row(label: 'activerecord.attributes.user_exercise_feedback.exercise', value: link_to(@uef.exercise.title, @uef.exercise))
= row(label: 'user_exercise_feedback.user', value: @uef.user)
= row(label: 'activerecord.attributes.user_exercise_feedback.feedback_text', value: @uef.feedback_text)
= row(label: 'user_exercise_feedback.difficulty', value: @uef.difficulty)
= row(label: 'user_exercise_feedback.working_time', value: @uef.user_estimated_worktime)

View File

@ -39,4 +39,8 @@ Rails.application.configure do
# Raises error for missing translations # Raises error for missing translations
# config.action_view.raise_on_missing_translations = true # config.action_view.raise_on_missing_translations = true
#config.logger = Logger.new(STDOUT)
# Set log level
#config.log_level = :DEBUG
end end

View File

@ -41,7 +41,6 @@ de:
allow_auto_completion: "Autovervollständigung aktivieren" allow_auto_completion: "Autovervollständigung aktivieren"
allow_file_creation: "Dateierstellung erlauben" allow_file_creation: "Dateierstellung erlauben"
difficulty: Schwierigkeitsgrad difficulty: Schwierigkeitsgrad
worktime: "vermutete Arbeitszeit in Minuten"
token: "Aufgaben-Token" token: "Aufgaben-Token"
proxy_exercise: proxy_exercise:
title: Title title: Title
@ -111,6 +110,25 @@ de:
name: "Name" name: "Name"
file_type: "Dateityp" file_type: "Dateityp"
content: "Code" content: "Code"
error_template:
name: Name
signature: Regulärer Ausdruck
description: Beschreibung
hint: Hinweis
error_template_attribute:
important: "Wichtig"
key: "Name"
description: "Beschreibung"
regex: "Regulärer Ausdruck"
exercise_collections:
id: "ID"
name: "Name"
updated_at: "Letzte Änderung"
exercises: "Aufgaben"
user_exercise_feedback:
user: "Autor"
exercise: "Aufgabe"
feedback_text: "Feedback Text"
models: models:
code_harbor_link: code_harbor_link:
one: CodeHarbor-Link one: CodeHarbor-Link
@ -121,12 +139,21 @@ de:
error: error:
one: Fehler one: Fehler
other: Fehler other: Fehler
error_template:
one: Fehlertemplate
other: Fehlertemplates
error_template_attribute:
one: Fehlertemplatettribut
other: Fehlertemplatettribute
execution_environment: execution_environment:
one: Ausführungsumgebung one: Ausführungsumgebung
other: Ausführungsumgebungen other: Ausführungsumgebungen
exercise: exercise:
one: Aufgabe one: Aufgabe
other: Aufgaben other: Aufgaben
exercise_collection:
one: Aufgabesammlung
other: Aufgabensammlungen
proxy_exercise: proxy_exercise:
one: Proxy Aufgabe one: Proxy Aufgabe
other: Proxy Aufgaben other: Proxy Aufgaben
@ -304,6 +331,7 @@ de:
clone: Duplizieren clone: Duplizieren
implement: Implementieren implement: Implementieren
test_files: Test-Dateien test_files: Test-Dateien
feedback: Feedback
statistics: statistics:
average_score: Durchschnittliche Punktzahl average_score: Durchschnittliche Punktzahl
final_submissions: Finale Abgaben final_submissions: Finale Abgaben
@ -634,10 +662,14 @@ de:
estimated_time_20_to_30: "zwischen 20 und 30 Minuten" estimated_time_20_to_30: "zwischen 20 und 30 Minuten"
estimated_time_more_30: "mehr als 30 Minuten" estimated_time_more_30: "mehr als 30 Minuten"
working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:" working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:"
error_templates:
hints:
signature: "Ein regulärer Ausdruck in Ruby-Syntax und ohne führende und schließende \"/\""
attributes: "Attribute"
add_attribute: "Attribut hinzufügen"
comments: comments:
deleted: "Gelöscht" deleted: "Gelöscht"
save_update: "Speichern" save_update: "Speichern"
subscriptions: subscriptions:
successfully_unsubscribed: "Ihr Abonnement für weitere Kommentare auf dieser Kommentaranfrage wurde erfolgreich beendet." 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." subscription_not_existent: "Das Abonnement, von dem Sie sich abmelden wollen, existiert nicht."

View File

@ -41,7 +41,6 @@ en:
allow_auto_completion: "Allow auto completion" allow_auto_completion: "Allow auto completion"
allow_file_creation: "Allow file creation" allow_file_creation: "Allow file creation"
difficulty: Difficulty difficulty: Difficulty
worktime: "Expected worktime in minutes"
token: "Exercise Token" token: "Exercise Token"
proxy_exercise: proxy_exercise:
title: Title title: Title
@ -111,6 +110,25 @@ en:
name: "Name" name: "Name"
file_type: "File Type" file_type: "File Type"
content: "Content" content: "Content"
error_template:
name: Name
signature: Signature Regular Expression
description: Description
hint: Hint
error_template_attribute:
important: "Important"
key: "Identifier"
description: "Description"
regex: "Regular Expression"
exercise_collections:
id: "ID"
name: "Name"
updated_at: "Last Update"
exercises: "Exercises"
user_exercise_feedback:
user: "Author"
exercise: "Exercise"
feedback_text: "Feedback Text"
models: models:
code_harbor_link: code_harbor_link:
one: CodeHarbor Link one: CodeHarbor Link
@ -121,12 +139,21 @@ en:
error: error:
one: Error one: Error
other: Errors other: Errors
error_template:
one: Error Template
other: Error Templates
error_template_attribute:
one: Error Template Attribute
other: Error Template Attributes
execution_environment: execution_environment:
one: Execution Environment one: Execution Environment
other: Execution Environments other: Execution Environments
exercise: exercise:
one: Exercise one: Exercise
other: Exercises other: Exercises
exercise_collection:
one: Exercise Collection
other: Exercise Collections
proxy_exercise: proxy_exercise:
one: Proxy Exercise one: Proxy Exercise
other: Proxy Exercises other: Proxy Exercises
@ -304,6 +331,7 @@ en:
clone: Duplicate clone: Duplicate
implement: Implement implement: Implement
test_files: Test Files test_files: Test Files
feedback: Feedback
statistics: statistics:
average_score: Average Score average_score: Average Score
final_submissions: Final Submissions final_submissions: Final Submissions
@ -634,6 +662,11 @@ en:
estimated_time_20_to_30: "between 20 and 30 minutes" estimated_time_20_to_30: "between 20 and 30 minutes"
estimated_time_more_30: "more than 30 minutes" estimated_time_more_30: "more than 30 minutes"
working_time: "Estimated time working on this exercise:" working_time: "Estimated time working on this exercise:"
error_templates:
hints:
signature: "A regular expression in Ruby syntax without leading and trailing \"/\""
attributes: "Attributes"
add_attribute: "Add attribute"
comments: comments:
deleted: "Deleted" deleted: "Deleted"
save_update: "Save" save_update: "Save"

View File

@ -1,6 +1,13 @@
FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP) FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP)
Rails.application.routes.draw do Rails.application.routes.draw do
resources :error_template_attributes
resources :error_templates do
member do
put 'attribute', to: 'error_templates#add_attribute'
delete 'attribute', to: 'error_templates#remove_attribute'
end
end
resources :file_templates do resources :file_templates do
collection do collection do
get 'by_file_type/:file_type_id', as: :by_file_type, action: :by_file_type get 'by_file_type/:file_type_id', as: :by_file_type, action: :by_file_type
@ -69,11 +76,14 @@ Rails.application.routes.draw do
post :intervention post :intervention
post :search post :search
get :statistics get :statistics
get :feedback
get :reload get :reload
post :submit post :submit
end end
end end
resources :exercise_collections
resources :proxy_exercises do resources :proxy_exercises do
member do member do
post :clone post :clone

View File

@ -1,5 +1,5 @@
class AddUserToCodeHarborLink < ActiveRecord::Migration class AddUserToCodeHarborLink < ActiveRecord::Migration
def change def change
add_reference :code_harbor_links, :user, index: true, foreign_key: true add_reference :code_harbor_links, :user, polymorphic: true, index: true
end end
end end

View File

@ -0,0 +1,11 @@
class CreateErrorTemplates < ActiveRecord::Migration
def change
create_table :error_templates do |t|
t.belongs_to :execution_environment
t.string :name
t.string :signature
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,11 @@
class CreateErrorTemplateAttributes < ActiveRecord::Migration
def change
create_table :error_template_attributes do |t|
t.belongs_to :error_template
t.string :key
t.string :regex
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,10 @@
class CreateStructuredErrors < ActiveRecord::Migration
def change
create_table :structured_errors do |t|
t.references :error_template
t.belongs_to :file
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,11 @@
class CreateStructuredErrorAttributes < ActiveRecord::Migration
def change
create_table :structured_error_attributes do |t|
t.belongs_to :structured_error
t.references :error_template_attribute
t.string :value
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,9 @@
class AddDescriptionAndHintToErrorTemplate < ActiveRecord::Migration
def change
add_column :error_templates, :description, :text
add_column :error_templates, :hint, :text
add_column :error_template_attributes, :description, :text
add_column :error_template_attributes, :important, :boolean
end
end

View File

@ -0,0 +1,6 @@
class ChangeErrorTemplateAttributeRelationshipToNToM < ActiveRecord::Migration
def change
remove_belongs_to :error_template_attributes, :error_template
create_join_table :error_templates, :error_template_attributes
end
end

View File

@ -0,0 +1,5 @@
class AddMatchToStructuredErrorAttribute < ActiveRecord::Migration
def change
add_column :structured_error_attributes, :match, :boolean
end
end

View File

@ -0,0 +1,5 @@
class RemoveExpectedWorkingTime < ActiveRecord::Migration
def change
remove_column :exercises, :expected_worktime_seconds
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: 20170920145852) do ActiveRecord::Schema.define(version: 20171002131135) 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"
@ -47,6 +47,30 @@ ActiveRecord::Schema.define(version: 20170920145852) do
t.string "oauth_secret", limit: 255 t.string "oauth_secret", limit: 255
end end
create_table "error_template_attributes", force: :cascade do |t|
t.string "key"
t.string "regex"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description"
t.boolean "important"
end
create_table "error_template_attributes_templates", id: false, force: :cascade do |t|
t.integer "error_template_id", null: false
t.integer "error_template_attribute_id", null: false
end
create_table "error_templates", force: :cascade do |t|
t.integer "execution_environment_id"
t.string "name"
t.string "signature"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description"
t.text "hint"
end
create_table "errors", force: :cascade do |t| create_table "errors", force: :cascade do |t|
t.integer "execution_environment_id" t.integer "execution_environment_id"
t.text "message" t.text "message"
@ -110,7 +134,6 @@ ActiveRecord::Schema.define(version: 20170920145852) do
t.boolean "hide_file_tree" t.boolean "hide_file_tree"
t.boolean "allow_file_creation" t.boolean "allow_file_creation"
t.boolean "allow_auto_completion", default: false t.boolean "allow_auto_completion", default: false
t.integer "expected_worktime_seconds", default: 60
t.integer "expected_difficulty", default: 1 t.integer "expected_difficulty", default: 1
end end
@ -268,6 +291,22 @@ ActiveRecord::Schema.define(version: 20170920145852) do
t.datetime "updated_at" t.datetime "updated_at"
end end
create_table "structured_error_attributes", force: :cascade do |t|
t.integer "structured_error_id"
t.integer "error_template_attribute_id"
t.string "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "match"
end
create_table "structured_errors", force: :cascade do |t|
t.integer "error_template_id"
t.integer "file_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
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"

View File

@ -1,5 +1,5 @@
def find_factories_by_class(klass) def find_factories_by_class(klass)
FactoryGirl.factories.select do |factory| FactoryBot.factories.select do |factory|
factory.instance_variable_get(:@class_name) == klass || factory.instance_variable_get(:@name) == klass.model_name.singular.to_sym factory.instance_variable_get(:@class_name) == klass || factory.instance_variable_get(:@name) == klass.model_name.singular.to_sym
end end
end end
@ -9,7 +9,7 @@ module ActiveRecord
[:build, :create].each do |strategy| [:build, :create].each do |strategy|
define_singleton_method("#{strategy}_factories") do |attributes = {}| define_singleton_method("#{strategy}_factories") do |attributes = {}|
find_factories_by_class(self).map(&:name).map do |factory_name| find_factories_by_class(self).map(&:name).map do |factory_name|
FactoryGirl.send(strategy, factory_name, attributes) FactoryBot.send(strategy, factory_name, attributes)
end end
end end
end end

View File

@ -1,9 +1,9 @@
# consumers # consumers
FactoryGirl.create(:consumer) FactoryBot.create(:consumer)
FactoryGirl.create(:consumer, name: 'openSAP') FactoryBot.create(:consumer, name: 'openSAP')
# users # users
[:admin, :external_user, :teacher].each { |factory_name| FactoryGirl.create(factory_name) } [:admin, :external_user, :teacher].each { |factory_name| FactoryBot.create(factory_name) }
# execution environments # execution environments
ExecutionEnvironment.create_factories ExecutionEnvironment.create_factories
@ -12,7 +12,7 @@ ExecutionEnvironment.create_factories
Error.create_factories Error.create_factories
# exercises # exercises
@exercises = find_factories_by_class(Exercise).map(&:name).map { |factory_name| [factory_name, FactoryGirl.create(factory_name)] }.to_h @exercises = find_factories_by_class(Exercise).map(&:name).map { |factory_name| [factory_name, FactoryBot.create(factory_name)] }.to_h
# file types # file types
FileType.create_factories FileType.create_factories
@ -21,4 +21,4 @@ FileType.create_factories
Hint.create_factories Hint.create_factories
# submissions # submissions
FactoryGirl.create(:submission, exercise: @exercises[:fibonacci]) FactoryBot.create(:submission, exercise: @exercises[:fibonacci])

View File

@ -1,7 +1,7 @@
require 'highline/import' require 'highline/import'
# consumers # consumers
FactoryGirl.create(:consumer) FactoryBot.create(:consumer)
# users # users
email = ask('Enter admin email: ') email = ask('Enter admin email: ')
@ -11,7 +11,7 @@ passwords = ['password', 'password confirmation'].map do |attribute|
end end
if passwords.uniq.length == 1 if passwords.uniq.length == 1
FactoryGirl.create(:admin, email: email, name: 'Administrator', password: passwords.first) FactoryBot.create(:admin, email: email, name: 'Administrator', password: passwords.first)
else else
abort('Passwords do not match!') abort('Passwords do not match!')
end end

View File

@ -11,7 +11,7 @@ describe Lti do
describe '#build_tool_provider' do describe '#build_tool_provider' do
it 'instantiates a tool provider' do it 'instantiates a tool provider' do
expect(IMS::LTI::ToolProvider).to receive(:new) expect(IMS::LTI::ToolProvider).to receive(:new)
controller.send(:build_tool_provider, consumer: FactoryGirl.build(:consumer), parameters: {}) controller.send(:build_tool_provider, consumer: FactoryBot.build(:consumer), parameters: {})
end end
end end
@ -25,31 +25,23 @@ describe Lti do
describe '#external_user_name' do describe '#external_user_name' do
let(:first_name) { 'Jane' } let(:first_name) { 'Jane' }
let(:full_name) { 'John Doe' }
let(:last_name) { 'Doe' } let(:last_name) { 'Doe' }
let(:full_name) { 'John Doe' }
let(:provider) { double } let(:provider) { double }
let(:provider_full) { double(:lis_person_name_full => full_name) }
context 'when a full name is provided' do context 'when a full name is provided' do
it 'returns the full name' do it 'returns the full name' do
expect(provider).to receive(:lis_person_name_full).twice.and_return(full_name) expect(provider_full).to receive(:lis_person_name_full).twice.and_return(full_name)
expect(controller.send(:external_user_name, provider)).to eq(full_name) expect(controller.send(:external_user_name, provider_full)).to eq(full_name)
end
end
context 'when first and last name are provided' do
it 'returns the concatenated names' do
expect(provider).to receive(:lis_person_name_full)
expect(provider).to receive(:lis_person_name_given).twice.and_return(first_name)
expect(provider).to receive(:lis_person_name_family).twice.and_return(last_name)
expect(controller.send(:external_user_name, provider)).to eq("#{first_name} #{last_name}")
end end
end end
context 'when only partial information is provided' do context 'when only partial information is provided' do
it 'returns the first available name' do it 'returns the first available name' do
expect(provider).to receive(:lis_person_name_full) expect(provider).to receive(:lis_person_name_full)
expect(provider).to receive(:lis_person_name_given).twice.and_return(first_name) expect(provider).to receive(:lis_person_name_given).and_return(first_name)
expect(provider).to receive(:lis_person_name_family) expect(provider).not_to receive(:lis_person_name_family)
expect(controller.send(:external_user_name, provider)).to eq(first_name) expect(controller.send(:external_user_name, provider)).to eq(first_name)
end end
end end
@ -103,10 +95,10 @@ describe Lti do
end end
describe '#send_score' do describe '#send_score' do
let(:consumer) { FactoryGirl.create(:consumer) } let(:consumer) { FactoryBot.create(:consumer) }
let(:score) { 0.5 } let(:score) { 0.5 }
let(:submission) { FactoryGirl.create(:submission) } let(:submission) { FactoryBot.create(:submission) }
let!(:lti_parameter) { FactoryGirl.create(:lti_parameter)} let!(:lti_parameter) { FactoryBot.create(:lti_parameter)}
context 'with an invalid score' do context 'with an invalid score' do
it 'raises an exception' do it 'raises an exception' do
@ -122,6 +114,7 @@ describe Lti do
context 'when grading is not supported' do context 'when grading is not supported' do
it 'returns a corresponding status' do it 'returns a corresponding status' do
skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked')
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false) expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false)
expect(controller.send(:send_score, submission.exercise_id, score, submission.user_id)[:status]).to eq('unsupported') expect(controller.send(:send_score, submission.exercise_id, score, submission.user_id)[:status]).to eq('unsupported')
end end
@ -140,10 +133,12 @@ describe Lti do
end end
it 'sends the score' do it 'sends the score' do
skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked')
controller.send(:send_score, submission.exercise_id, score, submission.user_id) controller.send(:send_score, submission.exercise_id, score, submission.user_id)
end end
it 'returns code, message, and status' do it 'returns code, message, and status' do
skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked')
result = controller.send(:send_score, submission.exercise_id, score, submission.user_id) result = controller.send(:send_score, submission.exercise_id, score, submission.user_id)
expect(result[:code]).to eq(response.response_code) expect(result[:code]).to eq(response.response_code)
expect(result[:message]).to eq(response.body) expect(result[:message]).to eq(response.body)
@ -164,18 +159,18 @@ describe Lti do
let(:parameters) { {} } let(:parameters) { {} }
it 'stores data in the session' do it 'stores data in the session' do
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) controller.instance_variable_set(:@current_user, FactoryBot.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci)) controller.instance_variable_set(:@exercise, FactoryBot.create(:fibonacci))
expect(controller.session).to receive(:[]=).with(:consumer_id, anything) expect(controller.session).to receive(:[]=).with(:consumer_id, anything)
expect(controller.session).to receive(:[]=).with(:external_user_id, anything) expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters) controller.send(:store_lti_session_data, consumer: FactoryBot.build(:consumer), parameters: parameters)
end end
it 'it creates an LtiParameter Object' do it 'it creates an LtiParameter Object' do
before_count = LtiParameter.count before_count = LtiParameter.count
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) controller.instance_variable_set(:@current_user, FactoryBot.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci)) controller.instance_variable_set(:@exercise, FactoryBot.create(:fibonacci))
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters) controller.send(:store_lti_session_data, consumer: FactoryBot.build(:consumer), parameters: parameters)
expect(LtiParameter.count).to eq(before_count + 1) expect(LtiParameter.count).to eq(before_count + 1)
end end
end end

View File

@ -6,8 +6,8 @@ end
describe SubmissionScoring do describe SubmissionScoring do
let(:controller) { Controller.new } let(:controller) { Controller.new }
before(:all) { @submission = FactoryGirl.create(:submission, cause: 'submit') } before(:all) { @submission = FactoryBot.create(:submission, cause: 'submit') }
before(:each) { controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) } before(:each) { controller.instance_variable_set(:@current_user, FactoryBot.create(:external_user)) }
describe '#collect_test_results' do describe '#collect_test_results' do
after(:each) { controller.send(:collect_test_results, @submission) } after(:each) { controller.send(:collect_test_results, @submission) }

View File

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
describe Admin::DashboardController do describe Admin::DashboardController do
before(:each) { allow(controller).to receive(:current_user).and_return(FactoryGirl.build(:admin)) } before(:each) { allow(controller).to receive(:current_user).and_return(FactoryBot.build(:admin)) }
describe 'GET #show' do describe 'GET #show' do
describe 'with format HTML' do describe 'with format HTML' do

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe ApplicationController do describe ApplicationController do
describe '#current_user' do describe '#current_user' do
context 'with an external user' do context 'with an external user' do
let(:external_user) { FactoryGirl.create(:external_user) } let(:external_user) { FactoryBot.create(:external_user) }
before(:each) { session[:external_user_id] = external_user.id } before(:each) { session[:external_user_id] = external_user.id }
it 'returns the external user' do it 'returns the external user' do
@ -12,7 +12,7 @@ describe ApplicationController do
end end
context 'without an external user' do context 'without an external user' do
let(:internal_user) { FactoryGirl.create(:teacher) } let(:internal_user) { FactoryBot.create(:teacher) }
before(:each) { login_user(internal_user) } before(:each) { login_user(internal_user) }
it 'returns the internal user' do it 'returns the internal user' do

View File

@ -1,14 +1,14 @@
require 'rails_helper' require 'rails_helper'
describe CodeOcean::FilesController do describe CodeOcean::FilesController do
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
let(:submission) { FactoryGirl.create(:submission, user: user) } let(:submission) { FactoryBot.create(:submission, user: user) }
context 'with a valid file' do context 'with a valid file' do
let(:request) { proc { post :create, code_ocean_file: FactoryGirl.build(:file, context: submission).attributes, format: :json } } let(:request) { proc { post :create, code_ocean_file: FactoryBot.build(:file, context: submission).attributes, format: :json } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(file: CodeOcean::File) expect_assigns(file: CodeOcean::File)
@ -31,14 +31,14 @@ describe CodeOcean::FilesController do
end end
describe 'DELETE #destroy' do describe 'DELETE #destroy' do
let(:exercise) { FactoryGirl.create(:fibonacci) } let(:exercise) { FactoryBot.create(:fibonacci) }
let(:request) { proc { delete :destroy, id: exercise.files.first.id } } let(:request) { proc { delete :destroy, id: exercise.files.first.id } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(file: CodeOcean::File) expect_assigns(file: CodeOcean::File)
it 'destroys the file' do it 'destroys the file' do
FactoryGirl.create(:fibonacci) FactoryBot.create(:fibonacci)
expect { request.call }.to change(CodeOcean::File, :count).by(-1) expect { request.call }.to change(CodeOcean::File, :count).by(-1)
end end

View File

@ -1,13 +1,13 @@
require 'rails_helper' require 'rails_helper'
describe ConsumersController do describe ConsumersController do
let(:consumer) { FactoryGirl.create(:consumer) } let(:consumer) { FactoryBot.create(:consumer) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
context 'with a valid consumer' do context 'with a valid consumer' do
let(:request) { proc { post :create, consumer: FactoryGirl.attributes_for(:consumer) } } let(:request) { proc { post :create, consumer: FactoryBot.attributes_for(:consumer) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(consumer: Consumer) expect_assigns(consumer: Consumer)
@ -34,7 +34,7 @@ describe ConsumersController do
expect_assigns(consumer: Consumer) expect_assigns(consumer: Consumer)
it 'destroys the consumer' do it 'destroys the consumer' do
consumer = FactoryGirl.create(:consumer) consumer = FactoryBot.create(:consumer)
expect { delete :destroy, id: consumer.id }.to change(Consumer, :count).by(-1) expect { delete :destroy, id: consumer.id }.to change(Consumer, :count).by(-1)
end end
@ -50,7 +50,7 @@ describe ConsumersController do
end end
describe 'GET #index' do describe 'GET #index' do
let!(:consumers) { FactoryGirl.create_pair(:consumer) } let!(:consumers) { FactoryBot.create_pair(:consumer) }
before(:each) { get :index } before(:each) { get :index }
expect_assigns(consumers: Consumer.all) expect_assigns(consumers: Consumer.all)
@ -76,7 +76,7 @@ describe ConsumersController do
describe 'PUT #update' do describe 'PUT #update' do
context 'with a valid consumer' do context 'with a valid consumer' do
before(:each) { put :update, consumer: FactoryGirl.attributes_for(:consumer), id: consumer.id } before(:each) { put :update, consumer: FactoryBot.attributes_for(:consumer), id: consumer.id }
expect_assigns(consumer: Consumer) expect_assigns(consumer: Consumer)
expect_redirect(:consumer) expect_redirect(:consumer)

View File

@ -1,15 +1,15 @@
require 'rails_helper' require 'rails_helper'
describe ExecutionEnvironmentsController do describe ExecutionEnvironmentsController do
let(:execution_environment) { FactoryGirl.create(:ruby) } let(:execution_environment) { FactoryBot.create(:ruby) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
before(:each) { expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([]) } before(:each) { expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([]) }
context 'with a valid execution environment' do context 'with a valid execution environment' do
let(:request) { proc { post :create, execution_environment: FactoryGirl.attributes_for(:ruby) } } let(:request) { proc { post :create, execution_environment: FactoryBot.attributes_for(:ruby) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(docker_images: Array) expect_assigns(docker_images: Array)
@ -37,7 +37,7 @@ describe ExecutionEnvironmentsController do
expect_assigns(execution_environment: :execution_environment) expect_assigns(execution_environment: :execution_environment)
it 'destroys the execution environment' do it 'destroys the execution environment' do
execution_environment = FactoryGirl.create(:ruby) execution_environment = FactoryBot.create(:ruby)
expect { delete :destroy, id: execution_environment.id }.to change(ExecutionEnvironment, :count).by(-1) expect { delete :destroy, id: execution_environment.id }.to change(ExecutionEnvironment, :count).by(-1)
end end
@ -72,7 +72,7 @@ describe ExecutionEnvironmentsController do
end end
describe 'GET #index' do describe 'GET #index' do
before(:all) { FactoryGirl.create_pair(:ruby) } before(:all) { FactoryBot.create_pair(:ruby) }
before(:each) { get :index } before(:each) { get :index }
expect_assigns(execution_environments: ExecutionEnvironment.all) expect_assigns(execution_environments: ExecutionEnvironment.all)
@ -150,7 +150,7 @@ describe ExecutionEnvironmentsController do
context 'with a valid execution environment' do context 'with a valid execution environment' do
before(:each) do before(:each) do
expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([]) expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([])
put :update, execution_environment: FactoryGirl.attributes_for(:ruby), id: execution_environment.id put :update, execution_environment: FactoryBot.attributes_for(:ruby), id: execution_environment.id
end end
expect_assigns(docker_images: Array) expect_assigns(docker_images: Array)

View File

@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
describe ExercisesController do describe ExercisesController do
let(:exercise) { FactoryGirl.create(:dummy) } let(:exercise) { FactoryBot.create(:dummy) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'PUT #batch_update' do describe 'PUT #batch_update' do
@ -52,7 +52,7 @@ describe ExercisesController do
end end
describe 'POST #create' do describe 'POST #create' do
let(:exercise_attributes) { FactoryGirl.build(:dummy).attributes } let(:exercise_attributes) { FactoryBot.build(:dummy).attributes }
context 'with a valid exercise' do context 'with a valid exercise' do
let(:request) { proc { post :create, exercise: exercise_attributes } } let(:request) { proc { post :create, exercise: exercise_attributes } }
@ -71,7 +71,7 @@ describe ExercisesController do
let(:request) { proc { post :create, exercise: exercise_attributes.merge(files_attributes: files_attributes) } } let(:request) { proc { post :create, exercise: exercise_attributes.merge(files_attributes: files_attributes) } }
context 'when specifying the file content within the form' do context 'when specifying the file content within the form' do
let(:files_attributes) { {'0' => FactoryGirl.build(:file).attributes} } let(:files_attributes) { {'0' => FactoryBot.build(:file).attributes} }
it 'creates the file' do it 'creates the file' do
expect { request.call }.to change(CodeOcean::File, :count) expect { request.call }.to change(CodeOcean::File, :count)
@ -79,11 +79,11 @@ describe ExercisesController do
end end
context 'when uploading a file' do context 'when uploading a file' do
let(:files_attributes) { {'0' => FactoryGirl.build(:file, file_type: file_type).attributes.merge(content: uploaded_file)} } let(:files_attributes) { {'0' => FactoryBot.build(:file, file_type: file_type).attributes.merge(content: uploaded_file)} }
context 'when uploading a binary file' do context 'when uploading a binary file' do
let(:file_path) { Rails.root.join('db', 'seeds', 'audio_video', 'devstories.mp4') } let(:file_path) { Rails.root.join('db', 'seeds', 'audio_video', 'devstories.mp4') }
let(:file_type) { FactoryGirl.create(:dot_mp4) } let(:file_type) { FactoryBot.create(:dot_mp4) }
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'video/mp4', true) } let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'video/mp4', true) }
it 'creates the file' do it 'creates the file' do
@ -98,7 +98,7 @@ describe ExercisesController do
context 'when uploading a non-binary file' do context 'when uploading a non-binary file' do
let(:file_path) { Rails.root.join('db', 'seeds', 'fibonacci', 'exercise.rb') } let(:file_path) { Rails.root.join('db', 'seeds', 'fibonacci', 'exercise.rb') }
let(:file_type) { FactoryGirl.create(:dot_rb) } let(:file_type) { FactoryBot.create(:dot_rb) }
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'text/x-ruby', false) } let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'text/x-ruby', false) }
it 'creates the file' do it 'creates the file' do
@ -128,7 +128,7 @@ describe ExercisesController do
expect_assigns(exercise: :exercise) expect_assigns(exercise: :exercise)
it 'destroys the exercise' do it 'destroys the exercise' do
exercise = FactoryGirl.create(:dummy) exercise = FactoryBot.create(:dummy)
expect { delete :destroy, id: exercise.id }.to change(Exercise, :count).by(-1) expect { delete :destroy, id: exercise.id }.to change(Exercise, :count).by(-1)
end end
@ -147,13 +147,13 @@ describe ExercisesController do
let(:request) { proc { get :implement, id: exercise.id } } let(:request) { proc { get :implement, id: exercise.id } }
context 'with an exercise with visible files' do context 'with an exercise with visible files' do
let(:exercise) { FactoryGirl.create(:fibonacci) } let(:exercise) { FactoryBot.create(:fibonacci) }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(exercise: :exercise) expect_assigns(exercise: :exercise)
context 'with an existing submission' do context 'with an existing submission' do
let!(:submission) { FactoryGirl.create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: user.class.name) } let!(:submission) { FactoryBot.create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: user.class.name) }
it "populates the editors with the submission's files' content" do it "populates the editors with the submission's files' content" do
request.call request.call
@ -182,7 +182,7 @@ describe ExercisesController do
describe 'GET #index' do describe 'GET #index' do
let(:scope) { Pundit.policy_scope!(user, Exercise) } let(:scope) { Pundit.policy_scope!(user, Exercise) }
before(:all) { FactoryGirl.create_pair(:dummy) } before(:all) { FactoryBot.create_pair(:dummy) }
before(:each) { get :index } before(:each) { get :index }
expect_assigns(exercises: :scope) expect_assigns(exercises: :scope)
@ -230,8 +230,8 @@ describe ExercisesController do
describe 'POST #submit' do describe 'POST #submit' do
let(:output) { {} } let(:output) { {} }
let(:request) { post :submit, format: :json, id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id} } let(:request) { post :submit, format: :json, id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id} }
let!(:external_user) { FactoryGirl.create(:external_user) } let!(:external_user) { FactoryBot.create(:external_user) }
let!(:lti_parameter) { FactoryGirl.create(:lti_parameter, external_user: external_user, exercise: exercise) } let!(:lti_parameter) { FactoryBot.create(:lti_parameter, external_user: external_user, exercise: exercise) }
before(:each) do before(:each) do
allow_any_instance_of(Submission).to receive(:normalized_score).and_return(1) allow_any_instance_of(Submission).to receive(:normalized_score).and_return(1)
@ -298,7 +298,7 @@ describe ExercisesController do
describe 'PUT #update' do describe 'PUT #update' do
context 'with a valid exercise' do context 'with a valid exercise' do
let(:exercise_attributes) { FactoryGirl.build(:dummy).attributes } let(:exercise_attributes) { FactoryBot.build(:dummy).attributes }
before(:each) { put :update, exercise: exercise_attributes, id: exercise.id } before(:each) { put :update, exercise: exercise_attributes, id: exercise.id }
expect_assigns(exercise: Exercise) expect_assigns(exercise: Exercise)

View File

@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
describe ExternalUsersController do describe ExternalUsersController do
let(:user) { FactoryGirl.build(:admin) } let(:user) { FactoryBot.build(:admin) }
let!(:users) { FactoryGirl.create_pair(:external_user) } let!(:users) { FactoryBot.create_pair(:external_user) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'GET #index' do describe 'GET #index' do

View File

@ -1,13 +1,13 @@
require 'rails_helper' require 'rails_helper'
describe FileTypesController do describe FileTypesController do
let(:file_type) { FactoryGirl.create(:dot_rb) } let(:file_type) { FactoryBot.create(:dot_rb) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
context 'with a valid file type' do context 'with a valid file type' do
let(:request) { proc { post :create, file_type: FactoryGirl.attributes_for(:dot_rb) } } let(:request) { proc { post :create, file_type: FactoryBot.attributes_for(:dot_rb) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(editor_modes: Array) expect_assigns(editor_modes: Array)
@ -36,7 +36,7 @@ describe FileTypesController do
expect_assigns(file_type: FileType) expect_assigns(file_type: FileType)
it 'destroys the file type' do it 'destroys the file type' do
file_type = FactoryGirl.create(:dot_rb) file_type = FactoryBot.create(:dot_rb)
expect { delete :destroy, id: file_type.id }.to change(FileType, :count).by(-1) expect { delete :destroy, id: file_type.id }.to change(FileType, :count).by(-1)
end end
@ -53,7 +53,7 @@ describe FileTypesController do
end end
describe 'GET #index' do describe 'GET #index' do
before(:all) { FactoryGirl.create_pair(:dot_rb) } before(:all) { FactoryBot.create_pair(:dot_rb) }
before(:each) { get :index } before(:each) { get :index }
expect_assigns(file_types: FileType.all) expect_assigns(file_types: FileType.all)
@ -80,7 +80,7 @@ describe FileTypesController do
describe 'PUT #update' do describe 'PUT #update' do
context 'with a valid file type' do context 'with a valid file type' do
before(:each) { put :update, file_type: FactoryGirl.attributes_for(:dot_rb), id: file_type.id } before(:each) { put :update, file_type: FactoryBot.attributes_for(:dot_rb), id: file_type.id }
expect_assigns(editor_modes: Array) expect_assigns(editor_modes: Array)
expect_assigns(file_type: FileType) expect_assigns(file_type: FileType)

View File

@ -1,14 +1,14 @@
require 'rails_helper' require 'rails_helper'
describe HintsController do describe HintsController do
let(:execution_environment) { FactoryGirl.create(:ruby) } let(:execution_environment) { FactoryBot.create(:ruby) }
let(:hint) { FactoryGirl.create(:ruby_syntax_error) } let(:hint) { FactoryBot.create(:ruby_syntax_error) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
context 'with a valid hint' do context 'with a valid hint' do
let(:request) { proc { post :create, execution_environment_id: execution_environment.id, hint: FactoryGirl.attributes_for(:ruby_syntax_error) } } let(:request) { proc { post :create, execution_environment_id: execution_environment.id, hint: FactoryBot.attributes_for(:ruby_syntax_error) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(execution_environment: :execution_environment) expect_assigns(execution_environment: :execution_environment)
@ -38,7 +38,7 @@ describe HintsController do
expect_assigns(hint: Hint) expect_assigns(hint: Hint)
it 'destroys the hint' do it 'destroys the hint' do
hint = FactoryGirl.create(:ruby_syntax_error) hint = FactoryBot.create(:ruby_syntax_error)
expect { delete :destroy, execution_environment_id: execution_environment.id, id: hint.id }.to change(Hint, :count).by(-1) expect { delete :destroy, execution_environment_id: execution_environment.id, id: hint.id }.to change(Hint, :count).by(-1)
end end
@ -55,7 +55,7 @@ describe HintsController do
end end
describe 'GET #index' do describe 'GET #index' do
before(:all) { FactoryGirl.create_pair(:ruby_syntax_error) } before(:all) { FactoryBot.create_pair(:ruby_syntax_error) }
before(:each) { get :index, execution_environment_id: execution_environment.id } before(:each) { get :index, execution_environment_id: execution_environment.id }
expect_assigns(execution_environment: :execution_environment) expect_assigns(execution_environment: :execution_environment)
@ -84,7 +84,7 @@ describe HintsController do
describe 'PUT #update' do describe 'PUT #update' do
context 'with a valid hint' do context 'with a valid hint' do
before(:each) { put :update, execution_environment_id: execution_environment.id, hint: FactoryGirl.attributes_for(:ruby_syntax_error), id: hint.id } before(:each) { put :update, execution_environment_id: execution_environment.id, hint: FactoryBot.attributes_for(:ruby_syntax_error), id: hint.id }
expect_assigns(execution_environment: :execution_environment) expect_assigns(execution_environment: :execution_environment)
expect_assigns(hint: Hint) expect_assigns(hint: Hint)

View File

@ -1,11 +1,11 @@
require 'rails_helper' require 'rails_helper'
describe InternalUsersController do describe InternalUsersController do
let(:user) { FactoryGirl.build(:admin) } let(:user) { FactoryBot.build(:admin) }
let!(:users) { FactoryGirl.create_pair(:teacher) } let!(:users) { FactoryBot.create_pair(:teacher) }
describe 'GET #activate' do describe 'GET #activate' do
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher)) } let(:user) { InternalUser.create(FactoryBot.attributes_for(:teacher)) }
before(:each) do before(:each) do
user.send(:setup_activation) user.send(:setup_activation)
@ -37,7 +37,7 @@ describe InternalUsersController do
end end
describe 'PUT #activate' do describe 'PUT #activate' do
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher)) } let(:user) { InternalUser.create(FactoryBot.attributes_for(:teacher)) }
let(:password) { SecureRandom.hex } let(:password) { SecureRandom.hex }
before(:each) do before(:each) do
@ -103,7 +103,7 @@ describe InternalUsersController do
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
context 'with a valid internal user' do context 'with a valid internal user' do
let(:request) { proc { post :create, internal_user: FactoryGirl.attributes_for(:teacher) } } let(:request) { proc { post :create, internal_user: FactoryBot.attributes_for(:teacher) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(user: InternalUser) expect_assigns(user: InternalUser)
@ -303,7 +303,7 @@ describe InternalUsersController do
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
context 'with a valid internal user' do context 'with a valid internal user' do
before(:each) { put :update, internal_user: FactoryGirl.attributes_for(:teacher), id: users.first.id } before(:each) { put :update, internal_user: FactoryBot.attributes_for(:teacher), id: users.first.id }
expect_assigns(user: InternalUser) expect_assigns(user: InternalUser)
expect_redirect { user } expect_redirect { user }

View File

@ -1,12 +1,12 @@
require 'rails_helper' require 'rails_helper'
describe SessionsController do describe SessionsController do
let(:consumer) { FactoryGirl.create(:consumer) } let(:consumer) { FactoryBot.create(:consumer) }
describe 'POST #create' do describe 'POST #create' do
let(:password) { user_attributes[:password] } let(:password) { user_attributes[:password] }
let(:user) { InternalUser.create(user_attributes) } let(:user) { InternalUser.create(user_attributes) }
let(:user_attributes) { FactoryGirl.attributes_for(:teacher) } let(:user_attributes) { FactoryBot.attributes_for(:teacher) }
context 'with valid credentials' do context 'with valid credentials' do
before(:each) do before(:each) do
@ -27,8 +27,8 @@ describe SessionsController do
end end
describe 'POST #create_through_lti' do describe 'POST #create_through_lti' do
let(:exercise) { FactoryGirl.create(:dummy) } let(:exercise) { FactoryBot.create(:dummy) }
let(:exercise2) { FactoryGirl.create(:dummy) } let(:exercise2) { FactoryBot.create(:dummy) }
let(:nonce) { SecureRandom.hex } let(:nonce) { SecureRandom.hex }
before(:each) { I18n.locale = I18n.default_locale } before(:each) { I18n.locale = I18n.default_locale }
@ -73,7 +73,7 @@ describe SessionsController do
context 'with valid launch parameters' do context 'with valid launch parameters' do
let(:locale) { :de } let(:locale) { :de }
let(:request) { post :create_through_lti, custom_locale: locale, custom_token: exercise.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id } let(:request) { post :create_through_lti, custom_locale: locale, custom_token: exercise.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id }
let(:user) { FactoryGirl.create(:external_user, consumer_id: consumer.id) } let(:user) { FactoryBot.create(:external_user, consumer_id: consumer.id) }
before(:each) { expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true) } before(:each) { expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true) }
it 'assigns the current user' do it 'assigns the current user' do
@ -132,14 +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
FactoryGirl.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
# dummy user has no exercises finished, therefore his highest difficulty is 0 # dummy user has no exercises finished, therefore his highest difficulty is 0
FactoryGirl.create(:proxy_exercise, exercises: [exercise, exercise2]) FactoryBot.create(:proxy_exercise, exercises: [exercise, exercise2])
exercise.expected_difficulty = 3 exercise.expected_difficulty = 3
exercise.save exercise.save
exercise2.expected_difficulty = 1 exercise2.expected_difficulty = 1
@ -191,7 +191,7 @@ describe SessionsController do
describe 'GET #destroy_through_lti' do describe 'GET #destroy_through_lti' do
let(:request) { proc { get :destroy_through_lti, consumer_id: consumer.id, submission_id: submission.id } } let(:request) { proc { get :destroy_through_lti, consumer_id: consumer.id, submission_id: submission.id } }
let(:submission) { FactoryGirl.create(:submission, exercise: FactoryGirl.create(:dummy)) } let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:dummy)) }
before(:each) do before(:each) do
#Todo replace session with lti_parameter #Todo replace session with lti_parameter
@ -225,7 +225,7 @@ describe SessionsController do
context 'when a user is already logged in' do context 'when a user is already logged in' do
before(:each) do before(:each) do
expect(controller).to receive(:current_user).and_return(FactoryGirl.build(:teacher)) expect(controller).to receive(:current_user).and_return(FactoryBot.build(:teacher))
get :new get :new
end end

View File

@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
describe SubmissionsController do describe SubmissionsController do
let(:submission) { FactoryGirl.create(:submission) } let(:submission) { FactoryBot.create(:submission) }
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) } before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do describe 'POST #create' do
@ -11,8 +11,8 @@ describe SubmissionsController do
end end
context 'with a valid submission' do context 'with a valid submission' do
let(:exercise) { FactoryGirl.create(:hello_world) } let(:exercise) { FactoryBot.create(:hello_world) }
let(:request) { proc { post :create, format: :json, submission: FactoryGirl.attributes_for(:submission, exercise_id: exercise.id) } } let(:request) { proc { post :create, format: :json, submission: FactoryBot.attributes_for(:submission, exercise_id: exercise.id) } }
before(:each) { request.call } before(:each) { request.call }
expect_assigns(submission: Submission) expect_assigns(submission: Submission)
@ -42,7 +42,7 @@ describe SubmissionsController do
end end
context 'with a valid filename' do context 'with a valid filename' do
let(:submission) { FactoryGirl.create(:submission, exercise: FactoryGirl.create(:audio_video)) } let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:audio_video)) }
before(:each) { get :download_file, filename: file.name_with_extension, id: submission.id } before(:each) { get :download_file, filename: file.name_with_extension, id: submission.id }
context 'for a binary file' do context 'for a binary file' do
@ -74,7 +74,7 @@ describe SubmissionsController do
end end
describe 'GET #index' do describe 'GET #index' do
before(:all) { FactoryGirl.create_pair(:submission) } before(:all) { FactoryBot.create_pair(:submission) }
before(:each) { get :index } before(:each) { get :index }
expect_assigns(submissions: Submission.all) expect_assigns(submissions: Submission.all)
@ -92,7 +92,7 @@ describe SubmissionsController do
end end
context 'with a valid filename' do context 'with a valid filename' do
let(:submission) { FactoryGirl.create(:submission, exercise: FactoryGirl.create(:audio_video)) } let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:audio_video)) }
before(:each) { get :render_file, filename: file.name_with_extension, id: submission.id } before(:each) { get :render_file, filename: file.name_with_extension, id: submission.id }
context 'for a binary file' do context 'for a binary file' do
@ -183,6 +183,41 @@ describe SubmissionsController do
expect_template(:show) expect_template(:show)
end end
describe 'GET #show.json' do
# Render views requested in controller tests in order to get json responses
# https://github.com/rails/jbuilder/issues/32
render_views
before(:each) { get :show, id: submission.id, format: :json }
expect_assigns(submission: :submission)
expect_status(200)
[:render, :run, :test].each do |action|
describe "##{action}_url" do
let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") }
it "starts like the #{action} path" do
filename = File.basename(__FILE__)
expect(url).to start_with(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission, filename).sub(filename, ''))
end
it 'ends with a placeholder' do
expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER)
end
end
end
[:score, :stop].each do |action|
describe "##{action}_url" do
let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") }
it "corresponds to the #{action} path" do
expect(url).to eq(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission))
end
end
end
end
describe 'GET #score' do describe 'GET #score' do
let(:request) { proc { get :score, id: submission.id } } let(:request) { proc { get :score, id: submission.id } }
before(:each) { request.call } before(:each) { request.call }

View File

@ -1,7 +1,7 @@
require 'seeds_helper' require 'seeds_helper'
module CodeOcean module CodeOcean
FactoryGirl.define do FactoryBot.define do
factory :file, class: CodeOcean::File do factory :file, class: CodeOcean::File do
content '' content ''
association :context, factory: :submission association :context, factory: :submission

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :consumer do factory :consumer do
name 'openHPI' name 'openHPI'
oauth_key { SecureRandom.hex } oauth_key { SecureRandom.hex }

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :error, class: Error do factory :error, class: Error do
association :execution_environment, factory: :ruby association :execution_environment, factory: :ruby
message "exercise.rb:4:in `<main>': undefined local variable or method `foo' for main:Object (NameError)" message "exercise.rb:4:in `<main>': undefined local variable or method `foo' for main:Object (NameError)"

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :coffee_script, class: ExecutionEnvironment do factory :coffee_script, class: ExecutionEnvironment do
created_by_teacher created_by_teacher
default_memory_limit default_memory_limit

View File

@ -2,7 +2,7 @@ require 'seeds_helper'
def create_seed_file(exercise, path, file_attributes = {}) def create_seed_file(exercise, path, file_attributes = {})
file_extension = File.extname(path) file_extension = File.extname(path)
file_type = FactoryGirl.create(file_attributes[:file_type] || :"dot_#{file_extension.gsub('.', '')}") file_type = FactoryBot.create(file_attributes[:file_type] || :"dot_#{file_extension.gsub('.', '')}")
name = File.basename(path).gsub(file_extension, '') name = File.basename(path).gsub(file_extension, '')
file_attributes.merge!(file_type: file_type, name: name, path: path.split('/')[1..-2].join('/'), role: file_attributes[:role] || 'regular_file') file_attributes.merge!(file_type: file_type, name: name, path: path.split('/')[1..-2].join('/'), role: file_attributes[:role] || 'regular_file')
if file_type.binary? if file_type.binary?
@ -13,7 +13,7 @@ def create_seed_file(exercise, path, file_attributes = {})
exercise.add_file!(file_attributes) exercise.add_file!(file_attributes)
end end
FactoryGirl.define do FactoryBot.define do
factory :audio_video, class: Exercise do factory :audio_video, class: Exercise do
created_by_teacher created_by_teacher
description "Try HTML's audio and video capabilities." description "Try HTML's audio and video capabilities."
@ -38,6 +38,24 @@ FactoryGirl.define do
association :execution_environment, factory: :ruby association :execution_environment, factory: :ruby
instructions instructions
title 'Dummy' title 'Dummy'
factory :dummy_with_user_feedbacks do
# user_feedbacks_count is declared as a transient attribute and available in
# attributes on the factory, as well as the callback via the evaluator
transient do
user_feedbacks_count 5
end
# the after(:create) yields two values; the exercise instance itself and the
# evaluator, which stores all values from the factory, including transient
# attributes; `create_list`'s second argument is the number of records
# to create and we make sure the user_exercise_feedback is associated properly to the exercise
after(:create) do |exercise, evaluator|
create_list(:user_exercise_feedback, evaluator.user_feedbacks_count, exercise: exercise)
end
end
end end
factory :even_odd, class: Exercise do factory :even_odd, class: Exercise do

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :external_user do factory :external_user do
association :consumer association :consumer
generated_email generated_email

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :dot_coffee, class: FileType do factory :dot_coffee, class: FileType do
created_by_admin created_by_admin
editor_mode 'ace/mode/coffee' editor_mode 'ace/mode/coffee'

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :node_js_invalid_assignment, class: Hint do factory :node_js_invalid_assignment, class: Hint do
association :execution_environment, factory: :node_js association :execution_environment, factory: :node_js
english english

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :admin, class: InternalUser do factory :admin, class: InternalUser do
activated_user activated_user
email 'admin@example.org' email 'admin@example.org'

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
LTI_PARAMETERS = { LTI_PARAMETERS = {
lis_result_sourcedid: "c2db0c7c-4411-4b27-a52b-ddfc3dc32065", lis_result_sourcedid: "c2db0c7c-4411-4b27-a52b-ddfc3dc32065",

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :proxy_exercise, class: ProxyExercise do factory :proxy_exercise, class: ProxyExercise do
token 'dummytoken' token 'dummytoken'
title 'Dummy' title 'Dummy'

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
[:admin, :external_user, :teacher].each do |factory_name| [:admin, :external_user, :teacher].each do |factory_name|
trait :"created_by_#{factory_name}" do trait :"created_by_#{factory_name}" do
association :user, factory: factory_name association :user, factory: factory_name

View File

@ -1,4 +1,4 @@
FactoryGirl.define do FactoryBot.define do
factory :submission do factory :submission do
cause 'save' cause 'save'
created_by_external_user created_by_external_user

View File

@ -0,0 +1,7 @@
FactoryBot.define do
factory :user_exercise_feedback, class: UserExerciseFeedback do
created_by_external_user
feedback_text 'Most suitable exercise ever'
end
end

View File

@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
describe 'Authentication' do describe 'Authentication' do
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryBot.create(:admin) }
let(:password) { FactoryGirl.attributes_for(:admin)[:password] } let(:password) { FactoryBot.attributes_for(:admin)[:password] }
context 'when signed out' do context 'when signed out' do
before(:each) { visit(root_path) } before(:each) { visit(root_path) }

Some files were not shown because too many files have changed in this diff Show More