Add new file role teacher_defined_linter

This commit is contained in:
Sebastian Serth
2020-10-15 00:43:57 +02:00
parent a5416758eb
commit be3ec82bd4
17 changed files with 41 additions and 28 deletions

View File

@ -413,7 +413,7 @@ var CodeOceanEditor = {
}, },
isActiveFileTestable: function () { isActiveFileTestable: function () {
return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test'].includes(this.active_frame.data('role')); return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test', 'teacher_defined_linter'].includes(this.active_frame.data('role'));
}, },
isBrowserSupported: function () { isBrowserSupported: function () {

View File

@ -195,7 +195,7 @@ $(document).on('turbolinks:load', function () {
var observeFileRoleChanges = function () { var observeFileRoleChanges = function () {
$(document).on('change', 'select[name$="[role]"]', function () { $(document).on('change', 'select[name$="[role]"]', function () {
var is_test_file = $(this).val() === 'teacher_defined_test'; var is_test_file = $(this).val() === 'teacher_defined_test' || $(this).val() === 'teacher_defined_linter';
var parent = $(this).parents('.card'); var parent = $(this).parents('.card');
var fields = parent.find('.test-related-fields'); var fields = parent.find('.test-related-fields');
if (is_test_file) { if (is_test_file) {

View File

@ -3,7 +3,7 @@ require 'concurrent/future'
module SubmissionScoring module SubmissionScoring
def collect_test_results(submission) def collect_test_results(submission)
# Mnemosyne.trace 'custom.codeocean.collect_test_results', meta: { submission: submission.id } do # Mnemosyne.trace 'custom.codeocean.collect_test_results', meta: { submission: submission.id } do
submission.collect_files.select(&:teacher_defined_test?).map do |file| submission.collect_files.select(&:teacher_defined_assessment?).map do |file|
future = Concurrent::Future.execute do future = Concurrent::Future.execute do
# Mnemosyne.trace 'custom.codeocean.collect_test_results_block', meta: { file: file.id, submission: submission.id } do # Mnemosyne.trace 'custom.codeocean.collect_test_results_block', meta: { file: file.id, submission: submission.id } do
assessor = Assessor.new(execution_environment: submission.execution_environment) assessor = Assessor.new(execution_environment: submission.execution_environment)

View File

@ -19,11 +19,11 @@ module CodeOcean
include DefaultValues include DefaultValues
DEFAULT_WEIGHT = 1.0 DEFAULT_WEIGHT = 1.0
ROLES = %w(main_file reference_implementation regular_file executable_file teacher_defined_test user_defined_file user_defined_test) ROLES = %w[main_file reference_implementation regular_file executable_file teacher_defined_test user_defined_file user_defined_test teacher_defined_linter].freeze
TEACHER_DEFINED_ROLES = ROLES - %w(user_defined_file) TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
after_initialize :set_default_values after_initialize :set_default_values
before_validation :clear_weight, unless: :teacher_defined_test? before_validation :clear_weight, unless: :teacher_defined_assessment?
before_validation :hash_content, if: :content_present? before_validation :hash_content, if: :content_present?
before_validation :set_ancestor_values, if: :incomplete_descendent? before_validation :set_ancestor_values, if: :incomplete_descendent?
@ -45,19 +45,20 @@ module CodeOcean
ROLES.each do |role| ROLES.each do |role|
scope :"#{role}s", -> { where(role: role) } scope :"#{role}s", -> { where(role: role) }
end end
scope :teacher_defined_assessments, -> { where(role: %w[teacher_defined_test teacher_defined_linter]) }
default_scope { order(name: :asc) } default_scope { order(name: :asc) }
validates :feedback_message, if: :teacher_defined_test?, presence: true validates :feedback_message, if: :teacher_defined_assessment?, presence: true
validates :feedback_message, absence: true, unless: :teacher_defined_test? validates :feedback_message, absence: true, unless: :teacher_defined_assessment?
validates :file_type_id, presence: true validates :file_type_id, presence: true
validates :hashed_content, if: :content_present?, presence: true validates :hashed_content, if: :content_present?, presence: true
validates :hidden, boolean_presence: true validates :hidden, boolean_presence: true
validates :name, presence: true validates :name, presence: true
validates :read_only, boolean_presence: true validates :read_only, boolean_presence: true
validates :role, inclusion: {in: ROLES} validates :role, inclusion: {in: ROLES}
validates :weight, if: :teacher_defined_test?, numericality: true, presence: true validates :weight, if: :teacher_defined_assessment?, numericality: true, presence: true
validates :weight, absence: true, unless: :teacher_defined_test? validates :weight, absence: true, unless: :teacher_defined_assessment?
validates :file, presence: true if :context.is_a?(Submission) validates :file, presence: true if :context.is_a?(Submission)
validates_with FileNameValidator, fields: [:name, :path, :file_type_id] validates_with FileNameValidator, fields: [:name, :path, :file_type_id]
@ -75,6 +76,10 @@ module CodeOcean
end end
private :clear_weight private :clear_weight
def teacher_defined_assessment?
teacher_defined_test? || teacher_defined_linter?
end
def content_present? def content_present?
content? || native_file? content? || native_file?
end end
@ -111,7 +116,7 @@ module CodeOcean
def set_default_values def set_default_values
set_default_values_if_present(content: '', hidden: false, read_only: false) set_default_values_if_present(content: '', hidden: false, read_only: false)
set_default_values_if_present(weight: DEFAULT_WEIGHT) if teacher_defined_test? set_default_values_if_present(weight: DEFAULT_WEIGHT) if teacher_defined_assessment?
end end
private :set_default_values private :set_default_values

View File

@ -263,7 +263,7 @@ class Exercise < ApplicationRecord
FROM files FROM files
WHERE context_type = 'Exercise' WHERE context_type = 'Exercise'
AND context_id = #{id} AND context_id = #{id}
AND role = 'teacher_defined_test' AND role IN ('teacher_defined_test', 'teacher_defined_linter')
GROUP BY context_id), GROUP BY context_id),
-- filter for rows containing max points -- filter for rows containing max points
time_max_score AS time_max_score AS
@ -392,7 +392,7 @@ class Exercise < ApplicationRecord
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}' WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
GROUP BY user_id, id, exercise_id), GROUP BY user_id, id, exercise_id),
MAX_POINTS AS MAX_POINTS AS
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id), (SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role IN ('teacher_defined_test', 'teacher_defined_linter') GROUP BY context_id),
-- filter for rows containing max points -- filter for rows containing max points
TIME_MAX_SCORE AS TIME_MAX_SCORE AS
@ -506,7 +506,7 @@ class Exercise < ApplicationRecord
0 0
end end
else else
files.teacher_defined_tests.sum(:weight) files.teacher_defined_assessments.sum(:weight)
end end
end end

View File

@ -55,7 +55,7 @@ module ProformaService
end end
def tests def tests
@exercise.files.filter { |file| file.role == 'teacher_defined_test' }.map do |file| @exercise.files.filter { |file| file.role == 'teacher_defined_test' || file.role == 'teacher_defined_linter' }.map do |file|
Proforma::Test.new( Proforma::Test.new(
id: file.id, id: file.id,
title: file.name, title: file.name,
@ -78,7 +78,7 @@ module ProformaService
def task_files def task_files
@exercise.files @exercise.files
.filter { |file| !file.role.in? %w[reference_implementation teacher_defined_test] }.map do |file| .filter { |file| !file.role.in? %w[reference_implementation teacher_defined_test teacher_defined_linter] }.map do |file|
task_file(file) task_file(file)
end end
end end

View File

@ -37,7 +37,7 @@ li.card.mt-2
label.form-check-label label.form-check-label
= f.check_box(:read_only, class: 'form-check-input') = f.check_box(:read_only, class: 'form-check-input')
= t('activerecord.attributes.file.read_only') = t('activerecord.attributes.file.read_only')
.test-related-fields style="display: #{f.object.teacher_defined_test? ? 'initial' : 'none'};" .test-related-fields style="display: #{f.object.teacher_defined_assessment? ? 'initial' : 'none'};"
.form-group .form-group
= f.label(:name, t('activerecord.attributes.file.feedback_message')) = f.label(:name, t('activerecord.attributes.file.feedback_message'))
= f.text_area(:feedback_message, class: 'form-control', maxlength: 255) = f.text_area(:feedback_message, class: 'form-control', maxlength: 255)

View File

@ -30,7 +30,7 @@ h1 = Exercise.model_name.human(count: 2)
tr data-id=exercise.id tr data-id=exercise.id
td.p-1.pt-2 = link_to_if(policy(exercise).show?, exercise.title, exercise, 'data-turbolinks' => "false") td.p-1.pt-2 = link_to_if(policy(exercise).show?, exercise.title, exercise, 'data-turbolinks' => "false")
td.p-1.pt-2 = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td.p-1.pt-2 = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td.p-1.pt-2 = exercise.files.teacher_defined_tests.count td.p-1.pt-2 = exercise.files.teacher_defined_assessments.count
td.p-1.pt-2 = exercise.maximum_score td.p-1.pt-2 = exercise.maximum_score
td.p-1.pt-2 = exercise.exercise_tags.count td.p-1.pt-2 = exercise.exercise_tags.count
td.p-1.pt-2 = exercise.expected_difficulty td.p-1.pt-2 = exercise.expected_difficulty

View File

@ -4,7 +4,7 @@
= row(label: 'file.role', value: file.role? ? t("files.roles.#{file.role}") : '') = row(label: 'file.role', value: file.role? ? t("files.roles.#{file.role}") : '')
= row(label: 'file.hidden', value: file.hidden) = row(label: 'file.hidden', value: file.hidden)
= row(label: 'file.read_only', value: file.read_only) = row(label: 'file.read_only', value: file.read_only)
- if file.teacher_defined_test? - if file.teacher_defined_assessment?
= row(label: 'file.feedback_message', value: render_markdown(file.feedback_message), class: 'm-0') = row(label: 'file.feedback_message', value: render_markdown(file.feedback_message), class: 'm-0')
= row(label: 'file.weight', value: file.weight) = row(label: 'file.weight', value: file.weight)
= row(label: 'file.content', value: file.native_file? ? link_to_if(policy(file).show?, file.native_file.file.filename, file.native_file.url) : code_tag(file.content)) = row(label: 'file.content', value: file.native_file? ? link_to_if(policy(file).show?, file.native_file.file.filename, file.native_file.url) : code_tag(file.content))

View File

@ -481,6 +481,7 @@ de:
teacher_defined_test: Test als Bewertungsgrundlage teacher_defined_test: Test als Bewertungsgrundlage
user_defined_file: Benutzerdefinierte Datei user_defined_file: Benutzerdefinierte Datei
user_defined_test: Benutzerdefinierter Test user_defined_test: Benutzerdefinierter Test
teacher_defined_linter: Linter als Bewertungsgrundlage
error: error:
filename: "Die Datei konnte nicht gespeichert werden, da eine Datei mit dem Namen '%{name}' bereits existiert." filename: "Die Datei konnte nicht gespeichert werden, da eine Datei mit dem Namen '%{name}' bereits existiert."
hints: hints:

View File

@ -481,6 +481,7 @@ en:
teacher_defined_test: Test for Assessment teacher_defined_test: Test for Assessment
user_defined_file: User-defined File user_defined_file: User-defined File
user_defined_test: User-defined Test user_defined_test: User-defined Test
teacher_defined_linter: Linter for Assessment
error: error:
filename: "The file could not be saved, because another file with the name '%{name}' already exists." filename: "The file could not be saved, because another file with the name '%{name}' already exists."
hints: hints:

View File

@ -376,10 +376,13 @@ class DockerClient
""" """
Stick to existing Docker API with exec command. Stick to existing Docker API with exec command.
""" """
filepath = submission.collect_files.find { |f| f.name_with_extension == filename }.filepath file = submission.collect_files.find { |f| f.name_with_extension == filename }
filepath = file.filepath
command = submission.execution_environment.test_command % command_substitutions(filepath) command = submission.execution_environment.test_command % command_substitutions(filepath)
create_workspace_files = proc { create_workspace_files(container, submission) } create_workspace_files = proc { create_workspace_files(container, submission) }
execute_command(command, create_workspace_files, block) test_result = execute_command(command, create_workspace_files, block)
test_result.merge!(file_role: file.role)
test_result
end end
def self.find_image_by_tag(tag) def self.find_image_by_tag(tag)

View File

@ -5,9 +5,10 @@ class PyUnitAndPyLintAdapter < TestingFrameworkAdapter
end end
def parse_output(output) def parse_output(output)
PyLintAdapter.new.parse_output(output) if output[:file_role] == 'teacher_defined_linter'
rescue NoMethodError PyLintAdapter.new.parse_output(output)
# The regex for PyLint failed and did not return any matches else
PyUnitAdapter.new.parse_output(output) PyUnitAdapter.new.parse_output(output)
end
end end
end end

View File

@ -13,7 +13,7 @@ describe SubmissionScoring do
after(:each) { controller.send(:collect_test_results, @submission) } after(:each) { controller.send(:collect_test_results, @submission) }
it 'executes every teacher-defined test file' do it 'executes every teacher-defined test file' do
@submission.collect_files.select(&:teacher_defined_test?).each do |file| @submission.collect_files.select(&:teacher_defined_assessment?).each do |file|
expect(controller).to receive(:execute_test_file).with(file, @submission).and_return({}) expect(controller).to receive(:execute_test_file).with(file, @submission).and_return({})
end end
end end

View File

@ -241,7 +241,7 @@ describe SubmissionsController do
end end
describe 'GET #test' do describe 'GET #test' do
let(:filename) { submission.collect_files.detect(&:teacher_defined_test?).name_with_extension } let(:filename) { submission.collect_files.detect(&:teacher_defined_assessment?).name_with_extension }
let(:output) { {} } let(:output) { {} }
before(:each) do before(:each) do

View File

@ -263,7 +263,7 @@ describe DockerClient, docker: true do
end end
describe '#execute_test_command' do describe '#execute_test_command' do
let(:filename) { submission.exercise.files.detect { |file| file.role == 'teacher_defined_test' }.name_with_extension } let(:filename) { submission.exercise.files.detect { |file| file.role == 'teacher_defined_test' || file.role == 'teacher_defined_linter' }.name_with_extension }
after(:each) { docker_client.send(:execute_test_command, submission, filename) } after(:each) { docker_client.send(:execute_test_command, submission, filename) }
it 'takes a container from the pool' do it 'takes a container from the pool' do

View File

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
describe ProformaService::ConvertTaskToExercise do describe ProformaService::ConvertTaskToExercise do
# ToDo: Add teacher_defined_linter for tests
describe '.new' do describe '.new' do
subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) } subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) }