transferred Code Ocean from original repository to GitHub
This commit is contained in:
0
app/models/.keep
Normal file
0
app/models/.keep
Normal file
84
app/models/code_ocean/file.rb
Normal file
84
app/models/code_ocean/file.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
require File.expand_path('../../../uploaders/file_uploader', __FILE__)
|
||||
|
||||
module CodeOcean
|
||||
class File < ActiveRecord::Base
|
||||
DEFAULT_WEIGHT = 1.0
|
||||
ROLES = %w[main_file reference_implementation regular_file teacher_defined_test user_defined_file user_defined_test]
|
||||
TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
|
||||
|
||||
after_initialize :set_default_values
|
||||
before_validation :set_ancestor_values, if: :incomplete_descendent?
|
||||
before_validation :hash_content, if: :content_present?
|
||||
|
||||
belongs_to :context, polymorphic: true
|
||||
belongs_to :execution_environment
|
||||
belongs_to :file
|
||||
alias_method :ancestor, :file
|
||||
belongs_to :file_type
|
||||
|
||||
has_many :files
|
||||
alias_method :descendants, :files
|
||||
|
||||
mount_uploader :native_file, FileUploader
|
||||
|
||||
scope :editable, -> { where(read_only: false) }
|
||||
scope :visible, -> { where(hidden: false) }
|
||||
|
||||
validates :feedback_message, if: :teacher_defined_test?, presence: true
|
||||
validates :feedback_message, absence: true, unless: :teacher_defined_test?
|
||||
validates :file_type_id, presence: true
|
||||
validates :hashed_content, if: :content_present?, presence: true
|
||||
validates :hidden, inclusion: {in: [true, false]}
|
||||
validates :name, presence: true
|
||||
validates :read_only, inclusion: {in: [true, false]}
|
||||
validates :role, inclusion: {in: ROLES}
|
||||
validates :weight, if: :teacher_defined_test?, numericality: true, presence: true
|
||||
validates :weight, absence: true, unless: :teacher_defined_test?
|
||||
|
||||
ROLES.each do |role|
|
||||
define_method("#{role}?") { self.role == role }
|
||||
end
|
||||
|
||||
def ancestor_id
|
||||
file_id || id
|
||||
end
|
||||
|
||||
def content_present?
|
||||
content? || native_file?
|
||||
end
|
||||
private :content_present?
|
||||
|
||||
def hash_content
|
||||
self.hashed_content = Digest::MD5.new.hexdigest(file_type.binary? ? ::File.new(native_file.file.path, 'r').read : content)
|
||||
end
|
||||
private :hash_content
|
||||
|
||||
def incomplete_descendent?
|
||||
file_id.present? && file_type_id.blank?
|
||||
end
|
||||
private :incomplete_descendent?
|
||||
|
||||
def name_with_extension
|
||||
name + (file_type.file_extension || '')
|
||||
end
|
||||
|
||||
def set_ancestor_values
|
||||
[:feedback_message, :file_type_id, :hidden, :name, :path, :read_only, :role, :weight].each do |attribute|
|
||||
send(:"#{attribute}=", ancestor.send(attribute))
|
||||
end
|
||||
end
|
||||
private :set_ancestor_values
|
||||
|
||||
def set_default_values
|
||||
self.content ||= ''
|
||||
self.hidden ||= false
|
||||
self.read_only ||= false
|
||||
self.weight ||= DEFAULT_WEIGHT if teacher_defined_test?
|
||||
end
|
||||
private :set_default_values
|
||||
|
||||
def visible
|
||||
!hidden
|
||||
end
|
||||
end
|
||||
end
|
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
20
app/models/concerns/context.rb
Normal file
20
app/models/concerns/context.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
module Context
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :files, as: :context, class: CodeOcean::File
|
||||
accepts_nested_attributes_for :files
|
||||
end
|
||||
|
||||
def add_file(file_attributes)
|
||||
file = files.create(file_attributes)
|
||||
save
|
||||
file
|
||||
end
|
||||
|
||||
def add_file!(file_attributes)
|
||||
file = files.create!(file_attributes)
|
||||
save!
|
||||
file
|
||||
end
|
||||
end
|
12
app/models/concerns/creation.rb
Normal file
12
app/models/concerns/creation.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Creation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :user, polymorphic: true
|
||||
alias_method :author, :user
|
||||
alias_method :creator, :user
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :user_type, presence: true
|
||||
end
|
||||
end
|
30
app/models/concerns/user.rb
Normal file
30
app/models/concerns/user.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
module User
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ROLES = %w[admin teacher]
|
||||
|
||||
included do
|
||||
belongs_to :consumer
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||
end
|
||||
|
||||
ROLES.each do |role|
|
||||
define_method("#{role}?") { self.try(:role) == role }
|
||||
end
|
||||
|
||||
def external?
|
||||
is_a?(ExternalUser)
|
||||
end
|
||||
|
||||
def internal?
|
||||
is_a?(InternalUser)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
13
app/models/consumer.rb
Normal file
13
app/models/consumer.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class Consumer < ActiveRecord::Base
|
||||
has_many :users
|
||||
|
||||
scope :with_users, -> { where('id IN (SELECT consumer_id FROM internal_users)') }
|
||||
|
||||
validates :name, presence: true
|
||||
validates :oauth_key, presence: true, uniqueness: true
|
||||
validates :oauth_secret, presence: true
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
14
app/models/error.rb
Normal file
14
app/models/error.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Error < ActiveRecord::Base
|
||||
belongs_to :execution_environment
|
||||
|
||||
scope :for_execution_environment, ->(execution_environment) do
|
||||
Error.find_by_sql("SELECT MAX(created_at) AS created_at, MAX(id) AS id, message, COUNT(*) AS count FROM errors WHERE #{sanitize_sql_hash_for_conditions(execution_environment_id: execution_environment.id)} GROUP BY message ORDER BY count DESC")
|
||||
end
|
||||
|
||||
validates :execution_environment_id, presence: true
|
||||
validates :message, presence: true
|
||||
|
||||
def self.nested_resource?
|
||||
true
|
||||
end
|
||||
end
|
42
app/models/execution_environment.rb
Normal file
42
app/models/execution_environment.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
class ExecutionEnvironment < ActiveRecord::Base
|
||||
include Creation
|
||||
|
||||
VALIDATION_COMMAND = 'whoami'
|
||||
|
||||
has_many :exercises
|
||||
has_many :hints
|
||||
|
||||
scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') }
|
||||
|
||||
validate :valid_test_setup?
|
||||
validate :working_docker_image?, if: :validate_docker_image?
|
||||
validates :docker_image, presence: true
|
||||
validates :name, presence: true
|
||||
validates :permitted_execution_time, numericality: {only_integer: true}, presence: true
|
||||
validates :run_command, presence: true
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def valid_test_setup?
|
||||
if test_command? ^ testing_framework?
|
||||
errors.add(:test_command, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
|
||||
end
|
||||
end
|
||||
private :valid_test_setup?
|
||||
|
||||
def validate_docker_image?
|
||||
docker_image.present? && Rails.env != 'test'
|
||||
end
|
||||
private :validate_docker_image?
|
||||
|
||||
def working_docker_image?
|
||||
DockerClient.pull(docker_image) unless DockerClient.image_tags.include?(docker_image)
|
||||
output = DockerClient.new(execution_environment: self).execute_command(VALIDATION_COMMAND)
|
||||
errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present?
|
||||
rescue DockerClient::Error => error
|
||||
errors.add(:docker_image, "error: #{error}")
|
||||
end
|
||||
private :working_docker_image?
|
||||
end
|
60
app/models/exercise.rb
Normal file
60
app/models/exercise.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
class Exercise < ActiveRecord::Base
|
||||
include Context
|
||||
include Creation
|
||||
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_default_values
|
||||
|
||||
belongs_to :execution_environment
|
||||
has_many :submissions
|
||||
has_many :users, source_type: ExternalUser, through: :submissions
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
|
||||
validate :valid_main_file?
|
||||
validates :description, presence: true
|
||||
validates :execution_environment_id, presence: true
|
||||
validates :public, inclusion: {in: [true, false]}
|
||||
validates :title, presence: true
|
||||
validates :token, presence: true, uniqueness: true
|
||||
|
||||
def average_percentage
|
||||
(average_score / maximum_score * 100).round if average_score
|
||||
end
|
||||
|
||||
def average_score
|
||||
ActiveRecord::Base.connection.execute("SELECT AVG(score) AS average_score FROM (SELECT MAX(score) AS score FROM submissions WHERE cause = 'submit' AND exercise_id = '#{id}' GROUP BY user_id) AS maximum_scores")[0]['average_score'].to_f.round(2)
|
||||
end
|
||||
|
||||
def duplicate(attributes)
|
||||
exercise = dup
|
||||
exercise.attributes = attributes
|
||||
files.each { |file| exercise.files << file.dup }
|
||||
exercise
|
||||
end
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.hex(4)
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def maximum_score
|
||||
files.where(role: 'teacher_defined_test').sum(:weight)
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
self.public ||= false
|
||||
end
|
||||
private :set_default_values
|
||||
|
||||
def to_s
|
||||
title
|
||||
end
|
||||
|
||||
def valid_main_file?
|
||||
if files.where(role: 'main_file').count > 1
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
|
||||
end
|
||||
end
|
||||
private :valid_main_file?
|
||||
end
|
6
app/models/external_user.rb
Normal file
6
app/models/external_user.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class ExternalUser < ActiveRecord::Base
|
||||
include User
|
||||
|
||||
validates :consumer_id, presence: true
|
||||
validates :external_id, presence: true
|
||||
end
|
35
app/models/file_type.rb
Normal file
35
app/models/file_type.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
class FileType < ActiveRecord::Base
|
||||
include Creation
|
||||
|
||||
AUDIO_FILE_EXTENSIONS = %w[.aac .flac .m4a .mp3 .ogg .wav .wma]
|
||||
IMAGE_FILE_EXTENSIONS = %w[.bmp .gif .jpeg .jpg .png]
|
||||
VIDEO_FILE_EXTENSIONS = %w[.avi .flv .mkv .mp4 .m4v .ogv .webm]
|
||||
|
||||
after_initialize :set_default_values
|
||||
|
||||
has_many :files
|
||||
|
||||
validates :binary, inclusion: {in: [true, false]}
|
||||
validates :editor_mode, presence: true, unless: :binary?
|
||||
validates :executable, inclusion: {in: [true, false]}
|
||||
validates :indent_size, presence: true, unless: :binary?
|
||||
validates :name, presence: true
|
||||
validates :renderable, inclusion: {in: [true, false]}
|
||||
|
||||
[:audio, :image, :video].each do |type|
|
||||
define_method("#{type}?") do
|
||||
self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension)
|
||||
end
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
self.binary ||= false
|
||||
self.executable ||= false
|
||||
self.renderable ||= false
|
||||
end
|
||||
private :set_default_values
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
17
app/models/hint.rb
Normal file
17
app/models/hint.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class Hint < ActiveRecord::Base
|
||||
belongs_to :execution_environment
|
||||
|
||||
validates :execution_environment_id, presence: true
|
||||
validates :locale, presence: true
|
||||
validates :message, presence: true
|
||||
validates :name, presence: true
|
||||
validates :regular_expression, presence: true
|
||||
|
||||
def self.nested_resource?
|
||||
true
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
13
app/models/internal_user.rb
Normal file
13
app/models/internal_user.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class InternalUser < ActiveRecord::Base
|
||||
include User
|
||||
|
||||
authenticates_with_sorcery!
|
||||
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validates :password, confirmation: true, on: :update, presence: true, unless: :activated?
|
||||
validates :role, inclusion: {in: ROLES}
|
||||
|
||||
def activated?
|
||||
activation_state == 'active'
|
||||
end
|
||||
end
|
58
app/models/submission.rb
Normal file
58
app/models/submission.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
class Submission < ActiveRecord::Base
|
||||
include Context
|
||||
include Creation
|
||||
|
||||
CAUSES = %w[assess download file render run save submit test]
|
||||
FILENAME_URL_PLACEHOLDER = '{filename}'
|
||||
|
||||
belongs_to :exercise
|
||||
|
||||
scope :final, -> { where(cause: 'submit') }
|
||||
scope :intermediate, -> { where.not(cause: 'submit') }
|
||||
|
||||
validates :cause, inclusion: {in: CAUSES}
|
||||
validates :exercise_id, presence: true
|
||||
|
||||
def collect_files
|
||||
ancestors = exercise.files.map(&:id).zip(exercise.files)
|
||||
descendants = files.map(&:file_id).zip(files)
|
||||
(ancestors + descendants).to_h.values
|
||||
end
|
||||
|
||||
def execution_environment
|
||||
exercise.execution_environment
|
||||
end
|
||||
|
||||
[:download, :render, :run, :test].each do |action|
|
||||
filename = FILENAME_URL_PLACEHOLDER.gsub(/\W/, '')
|
||||
define_method("#{action}_url") do
|
||||
Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self, filename).sub(filename, FILENAME_URL_PLACEHOLDER)
|
||||
end
|
||||
end
|
||||
|
||||
def main_file
|
||||
collect_files.detect(&:main_file?)
|
||||
end
|
||||
|
||||
def normalized_score
|
||||
score / exercise.maximum_score
|
||||
end
|
||||
|
||||
def percentage
|
||||
(normalized_score * 100).round
|
||||
end
|
||||
|
||||
[:score, :stop].each do |action|
|
||||
define_method("#{action}_url") do
|
||||
Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self)
|
||||
end
|
||||
end
|
||||
|
||||
def siblings
|
||||
Submission.where(exercise_id: exercise_id, user_id: user_id, user_type: user_type)
|
||||
end
|
||||
|
||||
def to_s
|
||||
Submission.model_name.human
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user