Merge branch 'master' into refactor_proforma_import_export

# Conflicts:
#	spec/controllers/exercises_controller_spec.rb
This commit is contained in:
Karol
2022-01-11 22:20:18 +01:00
122 changed files with 1558 additions and 1424 deletions

View File

@ -77,7 +77,7 @@ jobs:
run: bundle exec rspec --color --format progress --require spec_helper --require rails_helper
- name: Send coverage to CodeClimate
uses: paambaati/codeclimate-action@v2.7.4
uses: paambaati/codeclimate-action@v3.0.0
continue-on-error: true
if: ${{ success() || failure() }}
env:

View File

@ -1,9 +1,9 @@
GIT
remote: https://github.com/evolve75/RubyTree.git
revision: eb045068f73529c66d9d84f0553fdf85fc98bc4f
revision: 6081d0959b706dcefb85e85faa329ebb2dabcf9e
specs:
rubytree (1.0.2)
json (~> 2.3.1)
json (~> 2.6.1)
structured_warnings (~> 0.4.0)
GIT
@ -30,64 +30,64 @@ GEM
remote: https://rubygems.org/
specs:
ZenTest (4.12.0)
actioncable (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
actioncable (6.1.4.4)
actionpack (= 6.1.4.4)
activesupport (= 6.1.4.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionmailbox (6.1.4.4)
actionpack (= 6.1.4.4)
activejob (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
mail (>= 2.7.1)
actionmailer (6.1.4.1)
actionpack (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionmailer (6.1.4.4)
actionpack (= 6.1.4.4)
actionview (= 6.1.4.4)
activejob (= 6.1.4.4)
activesupport (= 6.1.4.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.1)
actionview (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionpack (6.1.4.4)
actionview (= 6.1.4.4)
activesupport (= 6.1.4.4)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.1)
actionpack (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
actiontext (6.1.4.4)
actionpack (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
nokogiri (>= 1.8.5)
actionview (6.1.4.1)
activesupport (= 6.1.4.1)
actionview (6.1.4.4)
activesupport (= 6.1.4.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
activejob (6.1.4.4)
activesupport (= 6.1.4.4)
globalid (>= 0.3.6)
activemodel (6.1.4.1)
activesupport (= 6.1.4.1)
activemodel (6.1.4.4)
activesupport (= 6.1.4.4)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (6.1.4.1)
activemodel (= 6.1.4.1)
activesupport (= 6.1.4.1)
activestorage (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activesupport (= 6.1.4.1)
activerecord (6.1.4.4)
activemodel (= 6.1.4.4)
activesupport (= 6.1.4.4)
activestorage (6.1.4.4)
actionpack (= 6.1.4.4)
activejob (= 6.1.4.4)
activerecord (= 6.1.4.4)
activesupport (= 6.1.4.4)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
activesupport (6.1.4.1)
activesupport (6.1.4.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -109,7 +109,7 @@ GEM
bindex (0.8.1)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.9.1)
bootsnap (1.9.3)
msgpack (~> 1.0)
bootstrap-will_paginate (1.0.0)
will_paginate
@ -151,7 +151,7 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
debug_inspector (1.1.0)
diff-lcs (1.4.4)
diff-lcs (1.5.0)
docile (1.4.0)
docker-api (2.2.0)
excon (>= 0.47.0)
@ -162,37 +162,41 @@ GEM
regexp_parser (~> 2.0)
erubi (1.10.0)
eventmachine (1.2.7)
excon (0.88.0)
excon (0.89.0)
factory_bot (6.2.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faraday (1.8.0)
faraday (1.9.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.2)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faye-websocket (0.11.1)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.4)
forgery (0.8.1)
globalid (0.5.2)
globalid (1.0.0)
activesupport (>= 5.0)
haml (5.2.2)
temple (>= 0.8.0)
@ -215,7 +219,8 @@ GEM
builder (>= 1.0, < 4.0)
oauth (>= 0.4.5, < 0.6)
influxdb (0.8.1)
jbuilder (2.11.3)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
@ -223,33 +228,33 @@ GEM
thor (>= 0.14, < 2.0)
jquery-ui-rails (6.0.1)
railties (>= 3.2.16)
js-routes (2.1.2)
js-routes (2.2.0)
railties (>= 4)
json (2.3.1)
json (2.6.1)
json_schemer (0.2.18)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.3.0)
kaminari (1.2.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
kramdown (2.3.1)
rexml
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.12.0)
loofah (2.13.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -257,16 +262,16 @@ GEM
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0901)
mime-types-data (3.2022.0105)
mimemagic (0.4.3)
nokogiri (~> 1)
rake
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.5.3)
minitest (5.14.4)
minitest (5.15.0)
minitest-autotest (1.1.1)
minitest-server (~> 1.0)
path_expander (~> 1.0)
@ -283,14 +288,14 @@ GEM
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
netrc (0.11.0)
newrelic_rpm (8.1.0)
newrelic_rpm (8.2.0)
nio4r (2.5.8)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nyan-cat-formatter (0.12.0)
rspec (>= 2.99, >= 2.14.2, < 4)
oauth (0.5.7)
oauth (0.5.8)
oauth2 (1.4.7)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
@ -300,11 +305,11 @@ GEM
pagedown-bootstrap-rails (2.1.4)
railties (> 3.1)
parallel (1.21.0)
parser (3.0.2.0)
parser (3.1.0.0)
ast (~> 2.4.1)
path_expander (1.1.0)
pg (1.2.3)
prometheus_exporter (0.8.1)
prometheus_exporter (1.0.1)
webrick
pry (0.13.1)
coderay (~> 1.1)
@ -326,24 +331,24 @@ GEM
rack-pjax (1.1.0)
nokogiri (~> 1.5)
rack (>= 1.1)
rack-proxy (0.7.0)
rack-proxy (0.7.2)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.4.1)
actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4.1)
actionpack (= 6.1.4.1)
actiontext (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activemodel (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
rails (6.1.4.4)
actioncable (= 6.1.4.4)
actionmailbox (= 6.1.4.4)
actionmailer (= 6.1.4.4)
actionpack (= 6.1.4.4)
actiontext (= 6.1.4.4)
actionview (= 6.1.4.4)
activejob (= 6.1.4.4)
activemodel (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
bundler (>= 1.15.0)
railties (= 6.1.4.1)
railties (= 6.1.4.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -354,9 +359,9 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
rails-i18n (6.0.0)
rails-i18n (7.0.1)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (>= 6.0.0, < 8)
rails-timeago (2.19.1)
actionpack (>= 3.1)
activesupport (>= 3.1)
@ -372,15 +377,15 @@ GEM
rails (>= 5.0, < 7)
remotipart (~> 1.3)
sassc-rails (>= 1.3, < 3)
railties (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
railties (6.1.4.4)
actionpack (= 6.1.4.4)
activesupport (= 6.1.4.4)
method_source
rake (>= 0.13)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.6)
ransack (2.4.2)
ransack (2.5.0)
activerecord (>= 5.2.4)
activesupport (>= 5.2.4)
i18n
@ -388,7 +393,7 @@ GEM
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtree (0.4.4)
regexp_parser (2.1.1)
regexp_parser (2.2.0)
remotipart (1.4.4)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
@ -421,28 +426,28 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.3)
rubocop (1.23.0)
rubocop (1.24.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.12.0, < 2.0)
rubocop-ast (>= 1.15.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.13.0)
rubocop-ast (1.15.1)
parser (>= 3.0.1.1)
rubocop-performance (1.12.0)
rubocop-performance (1.13.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.12.4)
rubocop-rails (2.13.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.6.0)
rubocop-rspec (2.7.0)
rubocop (~> 1.19)
ruby-progressbar (1.11.0)
ruby-vips (2.1.3)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@ -461,18 +466,18 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
semantic_range (3.0.0)
sentry-rails (4.8.0)
sentry-rails (4.8.3)
railties (>= 5.0)
sentry-ruby-core (~> 4.8.0)
sentry-ruby (4.8.0)
sentry-ruby-core (~> 4.8.3)
sentry-ruby (4.8.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
faraday (>= 1.0)
sentry-ruby-core (= 4.8.0)
sentry-ruby-core (4.8.0)
faraday (~> 1.0)
sentry-ruby-core (= 4.8.3)
sentry-ruby-core (4.8.3)
concurrent-ruby
faraday
set (1.0.2)
shoulda-matchers (5.0.0)
shoulda-matchers (5.1.0)
activesupport (>= 5.2.0)
simplecov (0.21.2)
docile (~> 1.1)
@ -487,27 +492,27 @@ GEM
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 5.0)
sorcery (0.16.1)
sorcery (0.16.2)
bcrypt (~> 3.1)
oauth (~> 0.5, >= 0.5.5)
oauth2 (~> 1.0, >= 0.8.0)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
spring (3.0.0)
spring (4.0.0)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
ssrf_filter (1.0.7)
structured_warnings (0.4.0)
telegraf (2.0.0)
influxdb
temple (0.8.2)
thor (1.1.0)
thor (1.2.1)
tilt (2.0.10)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
@ -543,7 +548,7 @@ GEM
will_paginate (3.3.1)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.5.1)
zeitwerk (2.5.3)
PLATFORMS
ruby
@ -624,4 +629,4 @@ DEPENDENCIES
whenever
BUNDLED WITH
2.2.29
2.3.4

View File

@ -68,7 +68,7 @@ $(document).on('turbolinks:load', function() {
_.each(response.docker, function(data) {
groups.update({
id: data.id,
visible: data.pool_size > 0
visible: data.prewarmingPoolSize > 0
});
});
};
@ -78,26 +78,27 @@ $(document).on('turbolinks:load', function() {
dataset.add({
group: data.id,
x: vis.moment(),
y: data.quantity
y: data.usedRunners
});
});
};
var updateProgressBar = function(progress_bar, data) {
var percentage = Math.min(Math.round(data.quantity / data.pool_size * 100), 100);
var percentage = Math.min(Math.round(data.idleRunners / data.prewarmingPoolSize * 100), 100);
progress_bar.attr({
'aria-valuemax': data.pool_size,
'aria-valuenow': data.quantity,
'aria-valuemax': data.prewarmingPoolSize,
'aria-valuenow': data.idleRunners,
style: 'width: ' + percentage + '%'
});
progress_bar.html(data.quantity);
progress_bar.html(data.idleRunners);
};
var updateTable = function(response) {
_.each(response.docker, function(data) {
var row = $('tbody tr[data-id=' + data.id + ']');
$('.pool-size', row).html(data.pool_size);
var progress_bar = $('.quantity .progress .progress-bar', row);
$('.prewarming-pool-size', row).html(data.prewarmingPoolSize);
$('.used-runners', row).html(`+ ${data.usedRunners}`);
var progress_bar = $('.idle-runners .progress .progress-bar', row);
updateProgressBar(progress_bar, data);
});
};

View File

@ -17,6 +17,7 @@ var CodeOceanEditor = {
//Request-For-Comments-Configuration
REQUEST_FOR_COMMENTS_DELAY: 0,
REQUEST_TOOLTIP_TIME: 5000,
REQUEST_TOOLTIP_DELAY: 10 * 60 * 1000,
editors: [],
editor_for_file: new Map(),
@ -170,7 +171,7 @@ var CodeOceanEditor = {
this.active_frame = frame;
frame.show();
this.resizeParentOfAceEditor(frame.find('.ace_editor.ace-tm'));
this.resizeParentOfAceEditor(frame.find('.ace_editor'));
},
getProgressBarClass: function (percentage) {
@ -406,7 +407,7 @@ var CodeOceanEditor = {
initializeRegexes: function () {
// These RegEx are run on the HTML escaped output!
this.regex_for_language.set("ace/mode/python", /File &quot;(.+?)&quot;, line (\d+)/g);
this.regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g);
this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
},
initializeTooltips: function () {
@ -444,10 +445,12 @@ var CodeOceanEditor = {
setTimeout(function () {
button.prop('disabled', false);
setTimeout(function () {
button.tooltip('show');
setTimeout(function () {
button.tooltip('hide');
}, this.REQUEST_TOOLTIP_TIME);
}, this.REQUEST_TOOLTIP_DELAY)
}.bind(this), this.REQUEST_FOR_COMMENTS_DELAY);
},
@ -623,15 +626,18 @@ var CodeOceanEditor = {
},
jumpToSourceLine: function (event) {
var file = $(event.target).data('file');
var line = $(event.target).data('line');
const file = $(event.target).data('file');
const line = $(event.target).data('line');
// set active file, only needed for codepilot, so skipped for now
var frame = $('div.frame[data-filename="' + file + '"]');
const frame = $('div.frame[data-filename="' + file + '"]');
this.showFrame(frame);
this.toggleButtonStates();
var editor = this.editor_for_file.get(file);
const file_id = frame.find('.editor').data('file-id');
this.setActiveFile(frame.data('filename'), file_id);
this.selectFileInJsTree($('#files'), file_id);
const editor = this.editor_for_file.get(file);
editor.gotoLine(line, 0);
event.preventDefault();
},
@ -829,7 +835,11 @@ var CodeOceanEditor = {
const percentile75 = data['working_time_75_percentile'];
const accumulatedWorkTimeUser = data['working_time_accumulated'];
const minTimeIntervention = 10 * 60 * 1000;
let minTimeIntervention = 10 * 60 * 1000;
if ($('#editor').data('exercise-id') === 909) {
// 30 minutes for our large Map exercise
minTimeIntervention = 30 * 60 * 1000;
}
let timeUntilIntervention;
if ((accumulatedWorkTimeUser - percentile75) > 0) {

View File

@ -115,7 +115,7 @@ CodeOceanEditorSubmissions = {
this.showSpinner(this);
this.ajax({
method: 'GET',
url: $('#start-over').data('url')
url: $('#start-over').data('url') || $('#start-over-active-file').data('url')
}).done(function(response) {
this.hideSpinner();
_.each(this.editors, function(editor) {
@ -196,9 +196,9 @@ CodeOceanEditorSubmissions = {
const button = $(event.target) || $('#submit');
this.createSubmission(button, null, function (response) {
if (response.redirect) {
this.unloadAutoSave();
this.editors = [];
Turbolinks.clearCache();
clearTimeout(this.autosaveTimer);
Turbolinks.visit(response.redirect);
} else if (response.status === 'container_depleted') {
this.showContainerDepletedMessage();

View File

@ -29,16 +29,17 @@ class CommunitySolutionsController < ApplicationController
if last_contribution.blank?
last_contribution = @community_solution.exercise
new_readme_file = {content: '', file_type: FileType.find_by(file_extension: '.txt'), hidden: false, read_only: false, name: 'ReadMe', role: 'regular_file', context: @community_solution}
@files << CodeOcean::File.create!(new_readme_file)
# If the first user did not save, the ReadMe file already exists
@files << CodeOcean::File.find_or_create_by!(new_readme_file)
end
all_visible_files = last_contribution.files.select(&:visible)
# Add the ReadMe file first
@files += all_visible_files.select {|f| CodeOcean::File.find_by(id: f.file_id).context_type == 'CommunitySolution' }
@files += all_visible_files.select {|f| CodeOcean::File.find_by(id: f.file_id)&.context_type == 'CommunitySolution' }
# Then, add all remaining files and sort them by name with extension
@files += (all_visible_files - @files).sort_by(&:name_with_extension)
@files += (all_visible_files - @files).sort_by(&:filepath)
# Own Submission as a reference
@own_files = @submission.collect_files.select(&:visible).sort_by(&:name_with_extension)
@own_files = @submission.collect_files.select(&:visible).sort_by(&:filepath)
# Remove the file_id from the second graph. Otherwise, the comparison and file-tree selection does not work as expected
@own_files.map do |file|
file.file_id = nil

View File

@ -5,7 +5,7 @@ module RedirectBehavior
def redirect_after_submit
Rails.logger.debug { "Redirecting user with score:s #{@submission.normalized_score}" }
if @submission.normalized_score.to_d == 1.0.to_d
if @submission.normalized_score.to_d == BigDecimal('1.0')
if redirect_to_community_solution?
redirect_to_community_solution
return

View File

@ -4,7 +4,7 @@ class ExecutionEnvironmentsController < ApplicationController
include CommonBehavior
before_action :set_docker_images, only: %i[create edit new update]
before_action :set_execution_environment, only: MEMBER_ACTIONS + %i[execute_command shell statistics]
before_action :set_execution_environment, only: MEMBER_ACTIONS + %i[execute_command shell statistics sync_to_runner_management]
before_action :set_testing_framework_adapters, only: %i[create edit new update]
def authorize!
@ -166,6 +166,20 @@ class ExecutionEnvironmentsController < ApplicationController
update_and_respond(object: @execution_environment, params: execution_environment_params)
end
def sync_to_runner_management
return unless Runner.management_active?
begin
Runner.strategy_class.sync_environment(@execution_environment)
rescue Runner::Error => e
Rails.logger.debug { "Runner error while synchronizing execution environment with id #{@execution_environment.id}: #{e.message}" }
Sentry.capture_exception(e)
redirect_to @execution_environment, alert: t('execution_environments.index.synchronize.failure', error: e.message)
else
redirect_to @execution_environment, notice: t('execution_environments.index.synchronize.success')
end
end
def sync_all_to_runner_management
authorize ExecutionEnvironment
@ -179,6 +193,7 @@ class ExecutionEnvironmentsController < ApplicationController
success << true
rescue Runner::Error => e
Rails.logger.debug { "Runner error while getting all execution environments: #{e.message}" }
Sentry.capture_exception(e)
environments_to_remove = []
success << false
end
@ -189,6 +204,7 @@ class ExecutionEnvironmentsController < ApplicationController
Runner.strategy_class.sync_environment(execution_environment)
rescue Runner::Error => e
Rails.logger.debug { "Runner error while synchronizing execution environment with id #{execution_environment.id}: #{e.message}" }
Sentry.capture_exception(e)
false
end
@ -198,6 +214,7 @@ class ExecutionEnvironmentsController < ApplicationController
Runner.strategy_class.remove_environment(execution_environment)
rescue Runner::Error => e
Rails.logger.debug { "Runner error while deleting execution environment with id #{execution_environment.id}: #{e.message}" }
Sentry.capture_exception(e)
false
end

View File

@ -59,7 +59,7 @@ raise: false
end
def collect_paths(files)
unique_paths = files.map(&:path).reject(&:blank?).uniq
unique_paths = files.map(&:path).compact_blank.uniq
subpaths = unique_paths.map do |path|
Array.new((path.split('/').length + 1)) do |n|
path.split('/').shift(n).join('/')
@ -321,7 +321,7 @@ raise: false
@search = Search.new
@search.exercise = @exercise
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:filepath)
@paths = collect_paths(@files)
@user_id = if current_user.respond_to? :external_id

View File

@ -15,8 +15,9 @@ class PingController < ApplicationController
private
def postgres_connected!
# any unhandled exception leads to a HTTP 500 response.
ApplicationRecord.establish_connection
ApplicationRecord.connection
ApplicationRecord.connected?
raise ActiveRecord::ConnectionNotEstablished unless ApplicationRecord.connected?
end
end

View File

@ -24,7 +24,7 @@ class ProxyExercisesController < ApplicationController
def create
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject(&:empty?))
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].compact_blank)
@proxy_exercise = ProxyExercise.new(myparams)
authorize!
@ -78,7 +78,7 @@ class ProxyExercisesController < ApplicationController
def update
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject(&:blank?))
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].compact_blank)
update_and_respond(object: @proxy_exercise, params: myparams)
end
end

View File

@ -23,7 +23,7 @@ class StudyGroupsController < ApplicationController
def update
myparams = study_group_params
myparams[:external_users] =
StudyGroupMembership.find(myparams[:study_group_membership_ids].reject(&:empty?)).map(&:user)
StudyGroupMembership.find(myparams[:study_group_membership_ids].compact_blank).map(&:user)
myparams.delete(:study_group_membership_ids)
update_and_respond(object: @study_group, params: myparams)
end

View File

@ -27,12 +27,7 @@ class SubmissionsController < ApplicationController
stringio = Zip::OutputStream.write_buffer do |zio|
@files.each do |file|
zio.put_next_entry(if file.path.to_s == ''
file.name_with_extension
else
File.join(file.path,
file.name_with_extension)
end)
zio.put_next_entry(file.filepath)
zio.write(file.content.presence || file.native_file.read)
end
@ -247,12 +242,9 @@ class SubmissionsController < ApplicationController
# parse remote request url
content += "#{request.base_url}/evaluate\n"
@submission.files.each do |file|
file_path = file.path.to_s == '' ? file.name_with_extension : File.join(file.path, file.name_with_extension)
content += "#{file_path}=#{file.file_id}\n"
end
File.open(path, 'w+') do |f|
f.write(content)
content += "#{file.filepath}=#{file.file_id}\n"
end
File.write(path, content)
path
end
@ -317,7 +309,7 @@ class SubmissionsController < ApplicationController
# @files contains all visible files for the user
# @file contains the specific file requested for run / test / render / ...
set_files
@file = @files.detect {|file| file.name_with_extension == sanitize_filename }
@file = @files.detect {|file| file.filepath == sanitize_filename }
head :not_found unless @file
end

View File

@ -39,7 +39,7 @@ class UserExerciseFeedbacksController < ApplicationController
authorize!
if validate_inputs(uef_params)
path =
if rfc && submission && submission.normalized_score.to_d == 1.0.to_d
if rfc && submission && submission.normalized_score.to_d == BigDecimal('1.0')
request_for_comment_path(rfc)
else
implement_exercise_path(@exercise)
@ -82,7 +82,7 @@ class UserExerciseFeedbacksController < ApplicationController
authorize!
if @exercise && validate_inputs(uef_params)
path =
if rfc && submission && submission.normalized_score.to_d == 1.0.to_d
if rfc && submission && submission.normalized_score.to_d == BigDecimal('1.0')
request_for_comment_path(rfc)
else
implement_exercise_path(@exercise)

View File

@ -11,12 +11,29 @@ module Admin
Runner.strategy_class.pool_size
rescue Runner::Error => e
Rails.logger.debug { "Runner error while fetching current pool size: #{e.message}" }
[]
{}
end
ExecutionEnvironment.order(:id).select(:id, :pool_size).map do |execution_environment|
execution_environment.attributes.merge(quantity: pool_size[execution_environment.id])
# Fetch the actual values (ID is stored as a symbol) or get an empty hash for merge
actual = pool_size[execution_environment.id.to_s.to_sym] || {}
template = {
id: execution_environment.id,
prewarmingPoolSize: execution_environment.pool_size,
idleRunners: 0,
usedRunners: 0,
}
# Existing values in the template get replaced with actual values
template.merge(actual)
end
end
def self.runner_management_release
Runner.strategy_class.release
rescue Runner::Error => e
e.inspect
end
end
end

View File

@ -41,7 +41,6 @@ module CodeOcean
validates :feedback_message, if: :teacher_defined_assessment?, presence: true
validates :feedback_message, absence: true, unless: :teacher_defined_assessment?
validates :file_type_id, presence: true
validates :hashed_content, if: :content_present?, presence: true
validates :hidden, boolean_presence: true
validates :name, presence: true
@ -99,7 +98,7 @@ module CodeOcean
private :incomplete_descendent?
def name_with_extension
name + (file_type.file_extension || '')
name.to_s + (file_type&.file_extension || '')
end
def set_ancestor_values

View File

@ -7,8 +7,5 @@ module Creation
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

View File

@ -36,6 +36,7 @@ class ExecutionEnvironment < ApplicationRecord
after_destroy :delete_runner_environment
after_save :working_docker_image?, if: :validate_docker_image?
after_update_commit :sync_runner_environment, unless: proc {|_| Rails.env.test? }
after_rollback :delete_runner_environment, on: :create
after_rollback :sync_runner_environment, on: %i[update destroy]
@ -79,7 +80,7 @@ class ExecutionEnvironment < ApplicationRecord
def validate_docker_image?
# We only validate the code execution with the provided image if there is at least one container to test with.
pool_size.positive? && docker_image.present? && !Rails.env.test?
pool_size.positive? && docker_image.present? && !Rails.env.test? && Runner.management_active?
end
def working_docker_image?
@ -92,9 +93,9 @@ class ExecutionEnvironment < ApplicationRecord
rescue Runner::Error => e
# In case of an Runner::Error, we retry multiple times before giving up.
# The time between each retry increases to allow the runner management to catch up.
if retries < 5 && !Rails.env.test?
if retries < 30 && !Rails.env.test?
retries += 1
sleep retries
sleep 1.second.to_i
retry
elsif errors.exclude?(:docker_image)
errors.add(:docker_image, "error: #{e}")
@ -104,7 +105,7 @@ class ExecutionEnvironment < ApplicationRecord
end
def delete_runner_environment
Runner.strategy_class.remove_environment(self)
Runner.strategy_class.remove_environment(self) if Runner.management_active?
rescue Runner::Error => e
unless errors.include?(:docker_image)
errors.add(:docker_image, "error: #{e}")
@ -114,7 +115,7 @@ class ExecutionEnvironment < ApplicationRecord
def sync_runner_environment
previous_saved_environment = self.class.find(id)
Runner.strategy_class.sync_environment(previous_saved_environment)
Runner.strategy_class.sync_environment(previous_saved_environment) if Runner.management_active?
rescue Runner::Error => e
unless errors.include?(:docker_image)
errors.add(:docker_image, "error: #{e}")

View File

@ -49,7 +49,7 @@ class Exercise < ApplicationRecord
MAX_GROUP_EXERCISE_FEEDBACKS = 20
def average_percentage
if average_score && (maximum_score.to_d != 0.0.to_d) && submissions.exists?(cause: 'submit')
if average_score && (maximum_score.to_d != BigDecimal('0.0')) && submissions.exists?(cause: 'submit')
(average_score / maximum_score * 100).round(2)
else
0
@ -580,7 +580,7 @@ cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
private :valid_submission_deadlines?
def needs_more_feedback?(submission)
if submission.normalized_score.to_d == 1.0.to_d
if submission.normalized_score.to_d == BigDecimal('1.0')
user_exercise_feedbacks.final.size <= MAX_GROUP_EXERCISE_FEEDBACKS
else
user_exercise_feedbacks.intermediate.size <= MAX_GROUP_EXERCISE_FEEDBACKS

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
class ExternalUser < User
validates :consumer_id, presence: true
validates :external_id, presence: true
def displayname

View File

@ -175,7 +175,7 @@ class ProxyExercise < ApplicationRecord
return 0.0
end
points_ratio = exercise.maximum_score(user) / max_score
if points_ratio.to_d == 0.0.to_d
if points_ratio.to_d == BigDecimal('0.0')
Rails.logger.debug { "scoring user #{user.id} for exercise #{exercise.id}: points_ratio=#{points_ratio} score: 0" }
return 0.0
elsif points_ratio > 1.0

View File

@ -6,7 +6,7 @@ class Runner < ApplicationRecord
before_validation :request_id
validates :execution_environment, :user, :runner_id, presence: true
validates :runner_id, presence: true
attr_accessor :strategy

View File

@ -45,7 +45,6 @@ class Submission < ApplicationRecord
scope :in_study_group_of, ->(user) { where(study_group_id: user.study_groups) unless user.admin? }
validates :cause, inclusion: {in: CAUSES}
validates :exercise_id, presence: true
# after_save :trigger_working_times_action_cable
@ -160,7 +159,7 @@ class Submission < ApplicationRecord
# @raise [Runner::Error] if the code could not be run due to a failure with the runner.
# See the specific type and message for more details.
def run(file, &block)
run_command = command_for execution_environment.run_command, file.name_with_extension
run_command = command_for execution_environment.run_command, file.filepath
durations = {}
prepared_runner do |runner, waiting_duration|
durations[:execution_duration] = runner.attach_to_execution(run_command, &block)
@ -185,7 +184,7 @@ class Submission < ApplicationRecord
end
def run_test_file(file, runner, waiting_duration)
test_command = command_for execution_environment.test_command, file.name_with_extension
test_command = command_for execution_environment.test_command, file.filepath
result = {file_role: file.role, waiting_for_container_time: waiting_duration}
output = runner.execute_command(test_command, raise_exception: false)
result.merge(output)
@ -222,7 +221,7 @@ class Submission < ApplicationRecord
end
def command_for(template, file)
filepath = collect_files.find {|f| f.name_with_extension == file }.filepath
filepath = collect_files.find {|f| f.filepath == file }.filepath
template % command_substitutions(filepath)
end
@ -255,7 +254,7 @@ class Submission < ApplicationRecord
waiting_for_container_time: output[:waiting_for_container_time]
)
filename = file.name_with_extension
filename = file.filepath
if file.teacher_defined_linter?
LinterCheckRun.create_from(testrun, assessment)
@ -291,8 +290,9 @@ class Submission < ApplicationRecord
score += output[:score] * output[:weight] unless output.nil?
end
end
update(score: score)
if normalized_score.to_d == 1.0.to_d
# Prevent floating point precision issues by converting to BigDecimal, e.g., for `0.28 * 25`
update(score: score.to_d)
if normalized_score.to_d == BigDecimal('1.0')
Thread.new do
RequestForComment.where(exercise_id: exercise_id, user_id: user_id, user_type: user_type).find_each do |rfc|
rfc.full_score_reached = true

View File

@ -4,8 +4,4 @@ class UserExerciseIntervention < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :intervention
belongs_to :exercise
validates :user, presence: true
validates :exercise, presence: true
validates :intervention, presence: true
end

View File

@ -5,10 +5,5 @@ class UserProxyExerciseExercise < ApplicationRecord
belongs_to :exercise
belongs_to :proxy_exercise
validates :user_id, presence: true
validates :user_type, presence: true
validates :exercise_id, presence: true
validates :proxy_exercise_id, presence: true
validates :user_id, uniqueness: {scope: %i[proxy_exercise_id user_type]}
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ExecutionEnvironmentPolicy < AdminOnlyPolicy
%i[execute_command? shell? statistics? show?].each do |action|
%i[execute_command? shell? statistics? show? sync_to_runner_management?].each do |action|
define_method(action) { admin? || author? }
end

View File

@ -40,9 +40,9 @@ module ProformaService
end
def task_files
@task_files ||= @task.all_files.reject {|file| file.id == 'ms-placeholder-file' }.map do |task_file|
@task_files ||= @task.all_files.reject {|file| file.id == 'ms-placeholder-file' }.to_h do |task_file|
[task_file.id, codeocean_file_from_task_file(task_file)]
end.to_h
end
end
def codeocean_file_from_task_file(file)

View File

@ -10,33 +10,38 @@ h1 = t('breadcrumbs.dashboard.show')
h2 Version
div.mb-4
= "CodeOcean Release:"
= application_name
=< t("admin.dashboard.show.release")
| :
pre = Sentry.configuration.release
- if Runner.management_active?
div.mb-4
= Runner.strategy_class.name.demodulize
=< "Release:"
pre = Runner.strategy_class.release
=< t("admin.dashboard.show.release")
| :
pre = Admin::DashboardHelper.runner_management_release
h2 Docker
- if Runner.management_active?
h3 = t('.current')
h3 = t('admin.dashboard.show.current')
.table-responsive
table.table
thead
tr
th = t('activerecord.models.execution_environment.one')
th = t('activerecord.attributes.execution_environment.pool_size')
th = t('.quantity')
th = t('admin.dashboard.show.idleRunners')
th = t('admin.dashboard.show.usedRunners')
tbody
- ExecutionEnvironment.order(:name).each do |execution_environment|
tr data-id=execution_environment.id
td.name = link_to_if(policy(execution_environment).show?, execution_environment, execution_environment)
td.pool-size
td.quantity = progress_bar(0)
h3 = t('.history')
td.prewarming-pool-size
td.idle-runners = progress_bar(0)
td.used-runners
h3 = t('admin.dashboard.show.history')
#graph
- else
p = t('.inactive')
p = t('admin.dashboard.show.inactive')

View File

@ -4,10 +4,14 @@
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:path, t('activerecord.attributes.file.path'))
= f.select(:path, @paths, {}, class: 'form-control')
| &nbsp;
a.toggle-input data={text_initial: t('shared.new'), text_toggled: t('shared.back')} href='#' = t('shared.new')
.original-input = f.select(:path, @paths, {}, class: 'form-control')
= f.text_field(:path, class: 'alternative-input form-control', disabled: true)
.form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, FileType.where(binary: false).order(:name), :id, :name, {selected: @exercise.execution_environment.file_type.try(:id)}, class: 'form-control')
- if FileTemplate.any?
.form-group
= f.label(:file_template_id, t('activerecord.attributes.file.file_template_id'))
= f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control')

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true
json.extract! @file, :id, :name_with_extension
json.extract! @file, :id, :filepath

View File

@ -1,6 +1,12 @@
h1
= @execution_environment
h1.d-inline-block = @execution_environment
.btn-group.float-right
= render('shared/edit_button', object: @execution_environment)
button.btn.btn-secondary.float-right.dropdown-toggle data-toggle='dropdown' type='button'
ul.dropdown-menu.dropdown-menu-right role='menu'
li = link_to(t('execution_environments.index.synchronize.button'), sync_to_runner_management_execution_environment_path(@execution_environment), method: :post, class: 'dropdown-item text-dark') if policy(@execution_environment).sync_to_runner_management?
li = link_to(t('execution_environments.index.shell'), shell_execution_environment_path(@execution_environment), class: 'dropdown-item text-dark') if policy(@execution_environment).shell?
li = link_to(t('shared.statistics'), statistics_execution_environment_path(@execution_environment), 'data-turbolinks' => "false", class: 'dropdown-item text-dark') if policy(@execution_environment).statistics?
li = link_to(t('shared.destroy'), @execution_environment, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item text-dark') if policy(@execution_environment).destroy?
= row(label: 'execution_environment.name', value: @execution_environment.name)
= row(label: 'execution_environment.user', value: link_to_if(policy(@execution_environment.author).show?, @execution_environment.author, @execution_environment.author))

View File

@ -1,4 +1,4 @@
div class=(defined?(own_solution) ? "own-frame" : "frame") data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type data-read-only=file.read_only
div class=(defined?(own_solution) ? "own-frame" : "frame") data-executable=file.file_type.executable? data-filename=file.filepath data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type data-read-only=file.read_only
- if file.file_type.binary?
.binary-file data-file-id=file.ancestor_id
- if file.file_type.renderable?

View File

@ -7,10 +7,7 @@ li.card.mt-2
a class=['file-heading', collapsed_class] data-toggle="collapse" href="#collapse#{f.index}" aria-expanded="#{aria_expanded}"
div.clearfix role="button"
i class="fa" aria-hidden="true"
- if f.object.name.present? && f.object.file_type.present?
span = f.object.name_with_extension
- else
span = f.object.name
span = f.object.filepath
.card-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{f.index}" role="tabpanel"
.card-body
- if policy(f.object).destroy? && id.present?

View File

@ -65,9 +65,9 @@ h1
td.align-middle
-this.testruns.includes(:file).order("files.name").each do |run|
- if run.passed
.unit-test-result.positive-result title=[run.file&.name_with_extension, run.output].join("\n").strip
.unit-test-result.positive-result title=[run.file&.filepath, run.output].join("\n").strip
- else
.unit-test-result.unknown-result title=[run.file&.name_with_extension, run.output].join("\n").strip
.unit-test-result.unknown-result title=[run.file&.filepath, run.output].join("\n").strip
td = @working_times_until[index] if index > 0 if policy(@exercise).detailed_statistics?
- elsif this.is_a? UserExerciseIntervention
td = this.created_at.strftime("%F %T")

View File

@ -49,7 +49,7 @@ ul.list-unstyled#files
a.file-heading.collapsed data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}"
div.clearfix role="button"
i class="fa" aria-hidden="true"
span = file.name_with_extension
span = file.filepath
.card-collapse.collapse class="collapse#{file.id}" role="tabpanel"
.card-body
- if policy(file).destroy?

View File

@ -254,7 +254,7 @@ javascript:
function deleteComment(commentId, editor, file_id, callback) {
var jqxhr = $.ajax({
type: 'DELETE',
url: Routes.comments_path(commentId)
url: Routes.comment_path(commentId)
});
jqxhr.done(function () {
setAnnotations(editor, file_id);
@ -266,7 +266,7 @@ javascript:
function updateComment(commentId, text, editor, file_id, callback) {
var jqxhr = $.ajax({
type: 'PATCH',
url: Routes.comments_path(commentId),
url: Routes.comment_path(commentId),
data: {
comment: {
text: text

View File

@ -7,4 +7,4 @@
- if file.teacher_defined_assessment?
= row(label: 'file.feedback_message', value: render_markdown(file.feedback_message), class: 'm-0')
= 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, file.file_type.programming_language))

View File

@ -7,5 +7,33 @@ Sentry.init do |config|
# Set tracesSampleRate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production
config.traces_sample_rate = 0.01
config.traces_sampler = lambda do |sampling_context|
# if this is the continuation of a trace, just use that decision (rate controlled by the caller)
unless sampling_context[:parent_sampled].nil?
next sampling_context[:parent_sampled]
end
# transaction_context is the transaction object in hash form
# keep in mind that sampling happens right after the transaction is initialized
# for example, at the beginning of the request
transaction_context = sampling_context[:transaction_context]
# transaction_context helps you sample transactions with more sophistication
# for example, you can provide different sample rates based on the operation or name
op = transaction_context[:op]
transaction_name = transaction_context[:name]
case op
when /request/
# for Rails applications, transaction_name would be the request's path (env["PATH_INFO"]) instead of "Controller#action"
case transaction_name
when '/', '/ping'
0.00 # ignore health check
else
0.05
end
else
0.0 # ignore all other transactions
end
end
end

View File

@ -193,7 +193,7 @@ de:
external_user:
one: Externer Nutzer
other: Externe Nutzer
file:
code_ocean/file:
one: Datei
other: Dateien
file_template:
@ -241,10 +241,12 @@ de:
admin:
dashboard:
show:
release: Release
current: Aktuelle Verfügbarkeit
history: Verfügbarkeitsverlauf
history: Nutzungsverlauf
inactive: Es ist kein Runner Management aktiv.
quantity: Verfügbare Container
idleRunners: Freie Runner
usedRunners: Reservierte Runner
application:
not_authorized: Sie Sind nicht berechtigt, diese Aktion auszuführen.
welcome:
@ -301,6 +303,10 @@ de:
not_synced_to_runner_management: Die Ausführungsumgebung wurde erstellt, aber aufgrund eines Fehlers nicht zum Runnermanagement synchronisiert.
index:
shell: Shell
synchronize:
button: Synchronisieren
success: Die Ausführungsumgebung wurde erfolgreich synchronisiert.
failure: "Beim Synchronisieren der Ausführungsumgebung ist folgender Fehler aufgetreten: %{error}"
synchronize_all:
button: Alle synchronisieren
success: Alle Ausführungsumgebungen wurden erfolgreich synchronisiert.

View File

@ -193,7 +193,7 @@ en:
external_user:
one: External User
other: External Users
file:
code_ocean/file:
one: File
other: Files
file_template:
@ -241,10 +241,12 @@ en:
admin:
dashboard:
show:
release: Release
current: Current Availability
history: Availability History
history: Usage History
inactive: No runner management is currently enabled.
quantity: Available Containers
idleRunners: Idle Runners
usedRunners: Reserved Runners
application:
not_authorized: You are not authorized to perform this action.
welcome:
@ -301,9 +303,13 @@ en:
not_synced_to_runner_management: The execution environment was created but not synced to the runner management due to an error.
index:
shell: Shell
synchronize:
button: Synchronize
success: The execution environment was synchronized successfully.
failure: "The execution environment could not be synchronised due to the following error: %{error}"
synchronize_all:
button: Synchronize all
success: All execution environemnts were synchronized successfully.
success: All execution environments were synchronized successfully.
failure: At least one execution environment could not be synchronised due to an error.
shell:
command: Command

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
FILENAME_REGEXP = /[\w.]+/.freeze unless Kernel.const_defined?(:FILENAME_REGEXP)
FILENAME_REGEXP = %r{[\w./]+}.freeze unless Kernel.const_defined?(:FILENAME_REGEXP)
Rails.application.routes.draw do
resources :community_solutions, only: %i[index edit update]
@ -66,6 +66,7 @@ Rails.application.routes.draw do
get :shell
post 'shell', as: :execute_command, action: :execute_command
get :statistics
post :sync_to_runner_management
end
post :sync_all_to_runner_management, on: :collection

View File

@ -9,7 +9,6 @@ class DropErrors < ActiveRecord::Migration[5.2]
scope :for_execution_environment, ->(execution_environment) { where(execution_environment_id: execution_environment.id) }
scope :grouped_by_message, -> { select('MAX(created_at) AS created_at, MAX(id) AS id, message, COUNT(id) AS count').group(:message).order('count DESC') }
validates :execution_environment_id, presence: true
validates :message, presence: true
def self.nested_resource?

View File

@ -8,7 +8,7 @@ module ActiveModel
def validate(record)
[attributes].flatten.each do |attribute|
value = record.send(:read_attribute_for_validation, attribute)
record.errors.add(attribute, nil, options) unless BOOLEAN_VALUES.include?(value)
record.errors.add(attribute, nil, **options) unless BOOLEAN_VALUES.include?(value)
end
end
end

View File

@ -12,7 +12,7 @@ class Assessor
def calculate_score(test_outcome)
score = 0.0
if test_outcome[:passed].to_d != 0.0.to_d && test_outcome[:count].to_d != 0.0.to_d
if test_outcome[:passed].to_d != BigDecimal('0.0') && test_outcome[:count].to_d != BigDecimal('0.0')
score = (test_outcome[:passed].to_f / test_outcome[:count])
# prevent negative scores
score = [0.0, score].max

View File

@ -9,7 +9,9 @@ module CodeOcean
def read(options = {})
path = Rails.root.join('config', "#{@filename}.yml#{options[:erb] ? '.erb' : ''}")
if ::File.exist?(path)
content = options[:erb] ? YAML.safe_load(ERB.new(::File.new(path, 'r').read).result, aliases: true, permitted_classes: [Range]) : YAML.load_file(path)
yaml_content = ::File.new(path, 'r').read || ''
yaml_content = ERB.new(yaml_content).result if options[:erb]
content = YAML.safe_load(yaml_content, aliases: true, permitted_classes: [Range, Symbol])
content[Rails.env].with_indifferent_access
else
raise Error.new("Configuration file not found: #{path}")

View File

@ -17,7 +17,7 @@ class CppCatch2Adapter < TestingFrameworkAdapter
count = output[:stdout].scan(COUNT_REGEXP).try(:last).try(:first).try(:to_i) || 0
failed = output[:stdout].scan(FAILURES_REGEXP).try(:last).try(:first).try(:to_i) || 0
error_matches = output[:stdout].scan(ASSERTION_ERROR_REGEXP) || []
{count: count, failed: failed, error_messages: error_matches.flatten.reject(&:blank?)}
{count: count, failed: failed, error_messages: error_matches.flatten.compact_blank}
end
end
end

View File

@ -315,7 +315,7 @@ container_execution_time: nil}
@tubesock&.send_data JSON.dump({'cmd' => 'timeout'})
if @socket
begin
@socket.send('#timeout')
@socket.send('#timeout') # rubocop:disable Performance/StringIdentifierArgument
# sleep one more second to ensure that the message reaches the submissions_controller.
sleep(1)
@socket.close
@ -434,9 +434,9 @@ container_execution_time: nil}
end
def self.mapped_ports(execution_environment)
execution_environment.exposed_ports.map do |port|
execution_environment.exposed_ports.to_h do |port|
["#{port}/tcp", [{'HostPort' => PortPool.available_port.to_s}]]
end.to_h
end
end
def self.pull(docker_image)

View File

@ -29,7 +29,7 @@ class FileTree
# Our tree needs a root node, but we won't display it.
@root = Tree::TreeNode.new('ROOT')
files.uniq(&:name_with_extension).each do |file|
files.uniq(&:filepath).each do |file|
parent = @root
(file.path || '').split('/').each do |segment|
node = parent.children.detect {|child| child.name == segment } || parent.add(Tree::TreeNode.new(segment))

View File

@ -16,7 +16,7 @@ class Junit5Adapter < TestingFrameworkAdapter
{count: count, passed: count}
else
error_matches = output[:stdout].scan(ASSERTION_ERROR_REGEXP) || []
{count: count, failed: failed, error_messages: error_matches.flatten.reject(&:blank?)}
{count: count, failed: failed, error_messages: error_matches.flatten.compact_blank}
end
end
end

View File

@ -17,7 +17,7 @@ class JunitAdapter < TestingFrameworkAdapter
count = output[:stdout].scan(COUNT_REGEXP).try(:last).try(:first).try(:to_i) || 0
failed = output[:stdout].scan(FAILURES_REGEXP).try(:last).try(:first).try(:to_i) || 0
error_matches = output[:stdout].scan(ASSERTION_ERROR_REGEXP) || []
{count: count, failed: failed, error_messages: error_matches.flatten.reject(&:blank?)}
{count: count, failed: failed, error_messages: error_matches.flatten.compact_blank}
end
end
end

View File

@ -43,8 +43,8 @@ class PyLintAdapter < TestingFrameworkAdapter
{
count: count,
failed: failed,
error_messages: concatenated_errors.flatten.reject(&:blank?),
detailed_linter_results: assertion_error_matches.flatten.reject(&:blank?),
error_messages: concatenated_errors.flatten.compact_blank,
detailed_linter_results: assertion_error_matches.flatten.compact_blank,
}
end

View File

@ -32,6 +32,6 @@ class PyUnitAdapter < TestingFrameworkAdapter
Sentry.capture_message({stderr: output[:stderr], regex: ASSERTION_ERROR_REGEXP}.to_json)
assertion_error_matches = []
end
{count: count, failed: failed + errors, error_messages: assertion_error_matches.flatten.reject(&:blank?)}
{count: count, failed: failed + errors, error_messages: assertion_error_matches.flatten.compact_blank}
end
end

View File

@ -14,6 +14,6 @@ class RScriptAdapter < TestingFrameworkAdapter
passed = captures.second
failed = count - passed
assertion_error_matches = output[:stdout].scan(ASSERTION_ERROR_REGEXP) || []
{count: count, failed: failed, error_messages: assertion_error_matches.flatten.reject(&:blank?)}
{count: count, failed: failed, error_messages: assertion_error_matches.flatten.compact_blank}
end
end

View File

@ -97,7 +97,7 @@ class Runner::Strategy::DockerContainerPool < Runner::Strategy
FileUtils.cp(file.native_file.path, local_file_path)
else
begin
File.open(local_file_path, 'w') {|f| f.write(file.content) }
File.write(local_file_path, file.content)
rescue IOError => e
raise Runner::Error::WorkspaceError.new("Could not create file #{file.filepath}: #{e.inspect}")
end
@ -166,7 +166,7 @@ class Runner::Strategy::DockerContainerPool < Runner::Strategy
url = "#{config[:url]}/docker_container_pool/quantities"
response = Faraday.get(url)
pool_size = JSON.parse(response.body)
pool_size.transform_keys(&:to_i)
pool_size.deep_symbolize_keys
rescue Faraday::Error => e
raise Runner::Error::FaradayError.new("Request to DockerContainerPool failed: #{e.inspect}")
rescue JSON::ParserError => e
@ -259,7 +259,7 @@ class Runner::Strategy::DockerContainerPool < Runner::Strategy
# TODO: Super dirty hack to redirect test output to stderr
# This is only required for Python and the unittest module but must not be used with PyLint
@stream = 'stderr'
when /\*\*\*\*\*\*\*\*\*\*\*\*\* Module/
when /\*\*\*\*\*\*\*\*\*\*\*\*\* Module/, / Your code has been rated at/
# Identification of PyLint output, change stream back to stdout and return event
@stream = 'stdout'
{'type' => @stream, 'data' => event_data}

View File

@ -152,11 +152,44 @@ class Runner::Strategy::Poseidon < Runner::Strategy
end
def self.release
nil
url = "#{config[:url]}/version"
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Getting release from #{url}" }
response = http_connection.get url
case response.status
when 200
JSON.parse(response.body)
when 404
'N/A'
else
handle_error response
end
rescue Faraday::Error => e
raise Runner::Error::FaradayError.new("Request to Poseidon failed: #{e.inspect}")
rescue JSON::ParserError => e
# Poseidon should not send invalid json
raise Runner::Error::UnexpectedResponse.new("Error parsing response from Poseidon: #{e.message}")
ensure
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Finished getting release information" }
end
def self.pool_size
{}
url = "#{config[:url]}/statistics/execution-environments"
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Getting statistics from #{url}" }
response = http_connection.get url
case response.status
when 200
response_body = parse response
response_body
else
handle_error response
end
rescue Faraday::Error => e
raise Runner::Error::FaradayError.new("Request to Poseidon failed: #{e.inspect}")
rescue JSON::ParserError => e
# Poseidon should not send invalid json
raise Runner::Error::UnexpectedResponse.new("Error parsing response from Poseidon: #{e.message}")
ensure
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Finished getting statistics" }
end
def self.websocket_header

View File

@ -6,9 +6,9 @@
"bootstrap": "^4.6.1",
"bootswatch": "^4.6.0",
"chosen-js": "^1.8.7",
"d3": "^7.1.1",
"d3": "^7.3.0",
"d3-tip": "^0.9.1",
"highlight.js": "^11.3.1",
"highlight.js": "^11.4.0",
"jquery": "^3.6.0",
"jquery-ui": "^1.13.0",
"jquery-ujs": "^1.2.3",
@ -17,7 +17,7 @@
"popper.js": "^1.16.1",
"rails-erb-loader": "^5.5.2",
"sortablejs": "^1.14.0",
"underscore": "^1.13.1",
"underscore": "^1.13.2",
"vis": "^4.21.0",
"webpack-merge": "^5.8.0"
},

View File

@ -2,9 +2,9 @@
######## VERSION INFORMATION ########
postgres_version=13
postgres_version=14
node_version=14
ruby_version=2.7.2
ruby_version=2.7.5
########## INSTALL SCRIPT ###########
@ -24,7 +24,7 @@ sudo apt -qq -y upgrade
# PostgreSQL
curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt -qq update && sudo apt -qq install -y postgresql-client postgresql
sudo apt -qq update && sudo apt -qq install -y postgresql-client-$postgres_version postgresql-$postgres_version
sudo sed -i "/# TYPE/q" /etc/postgresql/$postgres_version/main/pg_hba.conf
sudo tee -a /etc/postgresql/$postgres_version/main/pg_hba.conf <<EOF

View File

@ -8,11 +8,11 @@ end
describe FileParameters do
let(:controller) { Controller.new }
let(:hello_world) { FactoryBot.create(:hello_world) }
let(:hello_world) { create(:hello_world) }
describe '#reject_illegal_file_attributes!' do
def file_accepted?(file)
files = [[0, FactoryBot.attributes_for(:file, context: hello_world, file_id: file.id)]]
files = [[0, attributes_for(:file, context: hello_world, file_id: file.id)]]
filtered_files = controller.send(:reject_illegal_file_attributes, hello_world, files)
files.eql?(filtered_files)
end
@ -24,31 +24,31 @@ describe FileParameters do
end
it 'new file' do
submission = FactoryBot.create(:submission, exercise: hello_world, id: 1337)
new_file = FactoryBot.create(:file, context: submission)
submission = create(:submission, exercise: hello_world, id: 1337)
new_file = create(:file, context: submission)
expect(file_accepted?(new_file)).to be true
end
end
describe 'rejects' do
it 'file of different exercise' do
fibonacci = FactoryBot.create(:fibonacci, allow_file_creation: true)
other_exercises_file = FactoryBot.create(:file, context: fibonacci)
fibonacci = create(:fibonacci, allow_file_creation: true)
other_exercises_file = create(:file, context: fibonacci)
expect(file_accepted?(other_exercises_file)).to be false
end
it 'hidden file' do
hidden_file = FactoryBot.create(:file, context: hello_world, hidden: true)
hidden_file = create(:file, context: hello_world, hidden: true)
expect(file_accepted?(hidden_file)).to be false
end
it 'read only file' do
read_only_file = FactoryBot.create(:file, context: hello_world, read_only: true)
read_only_file = create(:file, context: hello_world, read_only: true)
expect(file_accepted?(read_only_file)).to be false
end
it 'non existent file' do
non_existent_file = FactoryBot.build(:file, context: hello_world, id: 42)
non_existent_file = build(:file, context: hello_world, id: 42)
expect(file_accepted?(non_existent_file)).to be false
end
end

View File

@ -13,7 +13,7 @@ describe Lti do
describe '#build_tool_provider' do
it 'instantiates a tool provider' do
expect(IMS::LTI::ToolProvider).to receive(:new)
controller.send(:build_tool_provider, consumer: FactoryBot.build(:consumer), parameters: {})
controller.send(:build_tool_provider, consumer: build(:consumer), parameters: {})
end
end
@ -101,12 +101,12 @@ describe Lti do
end
describe '#send_score' do
let(:consumer) { FactoryBot.create(:consumer) }
let(:consumer) { create(:consumer) }
let(:score) { 0.5 }
let(:submission) { FactoryBot.create(:submission) }
let(:submission) { create(:submission) }
before do
FactoryBot.create(:lti_parameter, consumers_id: consumer.id, external_users_id: submission.user_id, exercises_id: submission.exercise_id)
create(:lti_parameter, consumers_id: consumer.id, external_users_id: submission.user_id, exercises_id: submission.exercise_id)
end
context 'with an invalid score' do
@ -168,18 +168,18 @@ describe Lti do
let(:parameters) { ActionController::Parameters.new({}) }
it 'stores data in the session' do
controller.instance_variable_set(:@current_user, FactoryBot.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryBot.create(:fibonacci))
controller.instance_variable_set(:@current_user, create(:external_user))
controller.instance_variable_set(:@exercise, create(:fibonacci))
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
expect(controller.session).to receive(:[]=).with(:lti_parameters_id, anything)
controller.send(:store_lti_session_data, consumer: FactoryBot.build(:consumer), parameters: parameters)
controller.send(:store_lti_session_data, consumer: build(:consumer), parameters: parameters)
end
it 'creates an LtiParameter Object' do
before_count = LtiParameter.count
controller.instance_variable_set(:@current_user, FactoryBot.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryBot.create(:fibonacci))
controller.send(:store_lti_session_data, consumer: FactoryBot.build(:consumer), parameters: parameters)
controller.instance_variable_set(:@current_user, create(:external_user))
controller.instance_variable_set(:@exercise, create(:fibonacci))
controller.send(:store_lti_session_data, consumer: build(:consumer), parameters: parameters)
expect(LtiParameter.count).to eq(before_count + 1)
end
end

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe CodeharborLinksController do
let(:user) { FactoryBot.create(:teacher) }
let(:user) { create(:teacher) }
let(:codeocean_config) { instance_double(CodeOcean::Config) }
let(:codeharbor_config) { {codeharbor: {enabled: true, url: 'http://test.url'}} }
@ -23,7 +23,7 @@ describe CodeharborLinksController do
end
describe 'GET #edit' do
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let(:codeharbor_link) { create(:codeharbor_link, user: user) }
before { get :edit, params: {id: codeharbor_link.id} }
@ -57,7 +57,7 @@ describe CodeharborLinksController do
end
describe 'PUT #update' do
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:put_request) { patch :update, params: {id: codeharbor_link.id, codeharbor_link: params} }
let(:params) { {push_url: 'http://foo.bar/push', check_uuid_url: 'http://foo.bar/check', api_key: 'api_key'} }
@ -92,7 +92,7 @@ describe CodeharborLinksController do
end
describe 'DELETE #destroy' do
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:destroy_request) { delete :destroy, params: {id: codeharbor_link.id} }
it 'deletes codeharbor_link' do

View File

@ -3,14 +3,14 @@
require 'rails_helper'
describe ConsumersController do
let(:consumer) { FactoryBot.create(:consumer) }
let(:user) { FactoryBot.create(:admin) }
let(:consumer) { create(:consumer) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do
context 'with a valid consumer' do
let(:perform_request) { proc { post :create, params: {consumer: FactoryBot.attributes_for(:consumer)} } }
let(:perform_request) { proc { post :create, params: {consumer: attributes_for(:consumer)} } }
before { perform_request.call }
@ -38,7 +38,7 @@ describe ConsumersController do
expect_assigns(consumer: Consumer)
it 'destroys the consumer' do
consumer = FactoryBot.create(:consumer)
consumer = create(:consumer)
expect { delete :destroy, params: {id: consumer.id} }.to change(Consumer, :count).by(-1)
end
@ -55,7 +55,7 @@ describe ConsumersController do
describe 'GET #index' do
before do
FactoryBot.create_pair(:consumer)
create_pair(:consumer)
get :index
end
@ -82,7 +82,7 @@ describe ConsumersController do
describe 'PUT #update' do
context 'with a valid consumer' do
before { put :update, params: {consumer: FactoryBot.attributes_for(:consumer), id: consumer.id} }
before { put :update, params: {consumer: attributes_for(:consumer), id: consumer.id} }
expect_assigns(consumer: Consumer)
expect_redirect(:consumer)

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe ErrorTemplateAttributesController do
let!(:error_template_attribute) { FactoryBot.create(:error_template_attribute) }
let(:user) { FactoryBot.create(:admin) }
let!(:error_template_attribute) { create(:error_template_attribute) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
@ -35,7 +35,7 @@ describe ErrorTemplateAttributesController do
end
it 'updates error_template_attribute' do
patch :update, params: {id: error_template_attribute, error_template_attribute: FactoryBot.attributes_for(:error_template_attribute)}
patch :update, params: {id: error_template_attribute, error_template_attribute: attributes_for(:error_template_attribute)}
expect(response).to redirect_to(error_template_attribute_path(assigns(:error_template_attribute)))
end

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe ErrorTemplatesController do
let!(:error_template) { FactoryBot.create(:error_template) }
let(:user) { FactoryBot.create(:admin) }
let!(:error_template) { create(:error_template) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
@ -35,7 +35,7 @@ describe ErrorTemplatesController do
end
it 'updates error_template' do
patch :update, params: {id: error_template, error_template: FactoryBot.attributes_for(:error_template)}
patch :update, params: {id: error_template, error_template: attributes_for(:error_template)}
expect(response).to redirect_to(error_template_path(assigns(:error_template)))
end

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe EventsController do
let(:user) { FactoryBot.create(:admin) }
let(:exercise) { FactoryBot.create(:fibonacci) }
let(:user) { create(:admin) }
let(:exercise) { create(:fibonacci) }
before { allow(controller).to receive(:current_user).and_return(user) }

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe ExecutionEnvironmentsController do
let(:execution_environment) { FactoryBot.create(:ruby) }
let(:user) { FactoryBot.create(:admin) }
let(:execution_environment) { create(:ruby) }
let(:user) { create(:admin) }
before do
allow(controller).to receive(:current_user).and_return(user)
@ -13,7 +13,7 @@ describe ExecutionEnvironmentsController do
describe 'POST #create' do
context 'with a valid execution environment' do
let(:perform_request) { proc { post :create, params: {execution_environment: FactoryBot.build(:ruby, pool_size: 1).attributes} } }
let(:perform_request) { proc { post :create, params: {execution_environment: build(:ruby, pool_size: 1).attributes} } }
before do
allow(Rails.env).to receive(:test?).and_return(false, true)
@ -64,7 +64,7 @@ describe ExecutionEnvironmentsController do
expect_assigns(execution_environment: :execution_environment)
it 'destroys the execution environment' do
execution_environment = FactoryBot.create(:ruby)
execution_environment = create(:ruby)
expect { delete :destroy, params: {id: execution_environment.id} }.to change(ExecutionEnvironment, :count).by(-1)
end
@ -103,7 +103,7 @@ describe ExecutionEnvironmentsController do
describe 'GET #index' do
before do
FactoryBot.create_pair(:ruby)
create_pair(:ruby)
get :index
end
@ -186,7 +186,7 @@ describe ExecutionEnvironmentsController do
runner = instance_double 'runner'
allow(Runner).to receive(:for).and_return(runner)
allow(runner).to receive(:execute_command).and_return({})
put :update, params: {execution_environment: FactoryBot.attributes_for(:ruby, pool_size: 1), id: execution_environment.id}
put :update, params: {execution_environment: attributes_for(:ruby, pool_size: 1), id: execution_environment.id}
end
expect_assigns(docker_images: Array)
@ -216,8 +216,8 @@ describe ExecutionEnvironmentsController do
end
describe '#sync_all_to_runner_management' do
let(:execution_environments) { %i[ruby java python].map {|environment| FactoryBot.create(environment) } }
let(:outdated_execution_environments) { %i[node_js html].map {|environment| FactoryBot.build_stubbed(environment) } }
let(:execution_environments) { %i[ruby java python].map {|environment| create(environment) } }
let(:outdated_execution_environments) { %i[node_js html].map {|environment| build_stubbed(environment) } }
let(:codeocean_config) { instance_double(CodeOcean::Config) }
let(:runner_management_config) { {runner_management: {enabled: true, strategy: :poseidon}} }

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe ExercisesController do
let(:exercise) { FactoryBot.create(:dummy) }
let(:user) { FactoryBot.create(:admin) }
let(:exercise) { create(:dummy) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
@ -56,7 +56,7 @@ describe ExercisesController do
end
describe 'POST #create' do
let(:exercise_attributes) { FactoryBot.build(:dummy).attributes }
let(:exercise_attributes) { build(:dummy).attributes }
context 'with a valid exercise' do
let(:perform_request) { proc { post :create, params: {exercise: exercise_attributes} } }
@ -76,7 +76,7 @@ describe ExercisesController do
let(:perform_request) { proc { post :create, params: {exercise: exercise_attributes.merge(files_attributes: files_attributes)} } }
context 'when specifying the file content within the form' do
let(:files_attributes) { {'0' => FactoryBot.build(:file).attributes} }
let(:files_attributes) { {'0' => build(:file).attributes} }
it 'creates the file' do
expect { perform_request.call }.to change(CodeOcean::File, :count)
@ -84,11 +84,11 @@ describe ExercisesController do
end
context 'when uploading a file' do
let(:files_attributes) { {'0' => FactoryBot.build(:file, file_type: file_type).attributes.merge(content: uploaded_file)} }
let(:files_attributes) { {'0' => build(:file, file_type: file_type).attributes.merge(content: uploaded_file)} }
context 'when uploading a binary file' do
let(:file_path) { Rails.root.join('db/seeds/audio_video/devstories.mp4') }
let(:file_type) { FactoryBot.create(:dot_mp4) }
let(:file_type) { create(:dot_mp4) }
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'video/mp4', true) }
it 'creates the file' do
@ -103,7 +103,7 @@ describe ExercisesController do
context 'when uploading a non-binary file' do
let(:file_path) { Rails.root.join('db/seeds/fibonacci/exercise.rb') }
let(:file_type) { FactoryBot.create(:dot_rb) }
let(:file_type) { create(:dot_rb) }
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'text/x-ruby', false) }
it 'creates the file' do
@ -133,7 +133,7 @@ describe ExercisesController do
expect_assigns(exercise: :exercise)
it 'destroys the exercise' do
exercise = FactoryBot.create(:dummy)
exercise = create(:dummy)
expect { delete :destroy, params: {id: exercise.id} }.to change(Exercise, :count).by(-1)
end
@ -152,14 +152,14 @@ describe ExercisesController do
let(:perform_request) { proc { get :implement, params: {id: exercise.id} } }
context 'with an exercise with visible files' do
let(:exercise) { FactoryBot.create(:fibonacci) }
let(:exercise) { create(:fibonacci) }
before { perform_request.call }
expect_assigns(exercise: :exercise)
context 'with an existing submission' do
let!(:submission) { FactoryBot.create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: user.class.name) }
let!(:submission) { 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
perform_request.call
@ -190,7 +190,7 @@ describe ExercisesController do
let(:scope) { Pundit.policy_scope!(user, Exercise) }
before do
FactoryBot.create_pair(:dummy)
create_pair(:dummy)
get :index
end
@ -239,7 +239,7 @@ describe ExercisesController do
describe 'POST #submit' do
let(:output) { {} }
let(:perform_request) { post :submit, format: :json, params: {id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id}} }
let(:user) { FactoryBot.create(:external_user) }
let(:user) { create(:external_user) }
let(:scoring_response) do
[{
status: :ok,
@ -260,8 +260,8 @@ describe ExercisesController do
end
before do
FactoryBot.create(:lti_parameter, external_user: user, exercise: exercise)
submission = FactoryBot.build(:submission, exercise: exercise, user: user)
create(:lti_parameter, external_user: user, exercise: exercise)
submission = build(:submission, exercise: exercise, user: user)
allow(submission).to receive(:normalized_score).and_return(1)
allow(submission).to receive(:calculate_score).and_return(scoring_response)
allow(Submission).to receive(:create).and_return(submission)
@ -328,7 +328,7 @@ describe ExercisesController do
describe 'PUT #update' do
context 'with a valid exercise' do
let(:exercise_attributes) { FactoryBot.build(:dummy).attributes }
let(:exercise_attributes) { build(:dummy).attributes }
before { put :update, params: {exercise: exercise_attributes, id: exercise.id} }
@ -352,7 +352,7 @@ describe ExercisesController do
render_views
let(:post_request) { post :export_external_check, params: {id: exercise.id} }
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:external_check_hash) { {message: message, task_found: true, update_right: update_right, error: error} }
let(:message) { 'message' }
let(:update_right) { true }
@ -405,7 +405,7 @@ describe ExercisesController do
describe '#export_external_confirm' do
render_views
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:post_request) { post :export_external_confirm, params: {id: exercise.id, codeharbor_link: codeharbor_link.id} }
let(:error) { nil }
let(:zip) { 'zip' }
@ -440,8 +440,8 @@ describe ExercisesController do
end
describe '#import_uuid_check' do
let(:exercise) { FactoryBot.create(:dummy, uuid: SecureRandom.uuid) }
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let(:exercise) { create(:dummy, uuid: SecureRandom.uuid) }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:uuid) { exercise.reload.uuid }
let(:post_request) { post :import_uuid_check, params: {uuid: uuid} }
let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} }
@ -466,7 +466,7 @@ describe ExercisesController do
end
context 'when the user cannot update the exercise' do
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, api_key: 'anotherkey') }
let(:codeharbor_link) { create(:codeharbor_link, api_key: 'anotherkey') }
it 'renders correct response' do
post_request
@ -490,8 +490,8 @@ describe ExercisesController do
end
describe 'POST #import_exercise' do
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let!(:imported_exercise) { FactoryBot.create(:fibonacci) }
let(:codeharbor_link) { create(:codeharbor_link, user: user) }
let!(:imported_exercise) { create(:fibonacci) }
let(:post_request) { post :import_exercise, body: zip_file_content }
let(:zip_file_content) { 'zipped task xml' }
let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} }

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe RequestForCommentsController do
let(:user) { FactoryBot.create(:admin) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
@ -16,13 +16,13 @@ describe RequestForCommentsController do
end
it 'shows only rfc`s belonging to selected study group' do
my_study_group = FactoryBot.create(:study_group)
rfc_within_my_study_group = FactoryBot.create(:rfc, user: user)
my_study_group = create(:study_group)
rfc_within_my_study_group = create(:rfc, user: user)
user.update(study_groups: [my_study_group])
rfc_within_my_study_group.submission.update(study_group: my_study_group)
another_study_group = FactoryBot.create(:study_group)
rfc_other_study_group = FactoryBot.create(:rfc)
another_study_group = create(:study_group)
rfc_other_study_group = create(:rfc)
rfc_other_study_group.user.update(study_groups: [another_study_group])
rfc_other_study_group.submission.update(study_group: another_study_group)
@ -48,7 +48,7 @@ describe RequestForCommentsController do
describe 'GET #rfcs_for_exercise' do
before do
exercise = FactoryBot.create(:even_odd)
exercise = create(:even_odd)
get :rfcs_for_exercise, params: {exercise_id: exercise.id}
end

View File

@ -3,12 +3,12 @@
require 'rails_helper'
describe SessionsController do
let(:consumer) { FactoryBot.create(:consumer) }
let(:consumer) { create(:consumer) }
describe 'POST #create' do
let(:password) { FactoryBot.attributes_for(:teacher)[:password] }
let(:password) { attributes_for(:teacher)[:password] }
let(:user) { InternalUser.create(user_attributes.merge(password: password)) }
let(:user_attributes) { FactoryBot.build(:teacher).attributes }
let(:user_attributes) { build(:teacher).attributes }
context 'with valid credentials' do
before do
@ -29,8 +29,8 @@ describe SessionsController do
end
describe 'POST #create_through_lti' do
let(:exercise) { FactoryBot.create(:dummy) }
let(:exercise2) { FactoryBot.create(:dummy) }
let(:exercise) { create(:dummy) }
let(:exercise2) { create(:dummy) }
let(:nonce) { SecureRandom.hex }
context 'without OAuth parameters' do
@ -74,7 +74,7 @@ describe SessionsController do
context 'with valid launch parameters' do
let(:locale) { :de }
let(:perform_request) { post :create_through_lti, params: {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) { FactoryBot.create(:external_user, consumer_id: consumer.id) }
let(:user) { create(:external_user, consumer_id: consumer.id) }
before { allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true) }
@ -139,14 +139,14 @@ describe SessionsController do
end
it 'redirects to recommended exercise if requested token of proxy exercise' do
FactoryBot.create(:proxy_exercise, exercises: [exercise])
create(:proxy_exercise, exercises: [exercise])
post :create_through_lti, params: {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))
end
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
FactoryBot.create(:proxy_exercise, exercises: [exercise, exercise2])
create(:proxy_exercise, exercises: [exercise, exercise2])
exercise.expected_difficulty = 3
exercise.save
exercise2.expected_difficulty = 1
@ -202,7 +202,7 @@ describe SessionsController do
describe 'GET #destroy_through_lti' do
let(:perform_request) { proc { get :destroy_through_lti, params: {consumer_id: consumer.id, submission_id: submission.id} } }
let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:dummy)) }
let(:submission) { create(:submission, exercise: create(:dummy)) }
before do
# Todo replace session with lti_parameter
@ -238,7 +238,7 @@ describe SessionsController do
before do
allow(controller).to receive(:set_sentry_context).and_return(nil)
allow(controller).to receive(:current_user).and_return(FactoryBot.build(:teacher))
allow(controller).to receive(:current_user).and_return(build(:teacher))
get :new
end

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe StatisticsController do
let(:user) { FactoryBot.create(:admin) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe SubmissionsController do
let(:submission) { FactoryBot.create(:submission) }
let(:user) { FactoryBot.create(:admin) }
let(:submission) { create(:submission) }
let(:user) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(user) }
@ -14,8 +14,8 @@ describe SubmissionsController do
end
context 'with a valid submission' do
let(:exercise) { FactoryBot.create(:hello_world) }
let(:perform_request) { proc { post :create, format: :json, params: {submission: FactoryBot.attributes_for(:submission, exercise_id: exercise.id)} } }
let(:exercise) { create(:hello_world) }
let(:perform_request) { proc { post :create, format: :json, params: {submission: attributes_for(:submission, exercise_id: exercise.id)} } }
before { perform_request.call }
@ -46,7 +46,7 @@ describe SubmissionsController do
end
context 'with a valid binary filename' do
let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:sql_select)) }
let(:submission) { create(:submission, exercise: create(:sql_select)) }
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
@ -65,7 +65,7 @@ describe SubmissionsController do
end
context 'with a valid filename' do
let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:audio_video)) }
let(:submission) { create(:submission, exercise: create(:audio_video)) }
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
@ -99,7 +99,7 @@ describe SubmissionsController do
describe 'GET #index' do
before do
FactoryBot.create_pair(:submission)
create_pair(:submission)
get :index
end
@ -118,7 +118,7 @@ describe SubmissionsController do
end
context 'with a valid filename' do
let(:submission) { FactoryBot.create(:submission, exercise: FactoryBot.create(:audio_video)) }
let(:submission) { create(:submission, exercise: create(:audio_video)) }
before { get :render_file, params: {filename: file.name_with_extension, id: submission.id} }

View File

@ -17,6 +17,7 @@ describe 'seeds' do
ActiveRecord::Base.establish_connection(:test)
}
allow_any_instance_of(ExecutionEnvironment).to receive(:working_docker_image?).and_return true
allow_any_instance_of(ExecutionEnvironment).to receive(:sync_runner_environment).and_return true
end
describe 'execute db:seed', cleaning_strategy: :truncation do

View File

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

View File

@ -6,7 +6,7 @@ describe 'Authorization' do
before { allow(Runner.strategy_class).to receive(:available_images).and_return([]) }
context 'when being an admin' do
let(:user) { FactoryBot.create(:admin) }
let(:user) { create(:admin) }
before { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
@ -16,7 +16,7 @@ describe 'Authorization' do
end
context 'with being an external user' do
let(:user) { FactoryBot.create(:external_user) }
let(:user) { create(:external_user) }
before { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
@ -26,7 +26,7 @@ describe 'Authorization' do
end
context 'with being a teacher' do
let(:user) { FactoryBot.create(:teacher) }
let(:user) { create(:teacher) }
before { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe 'Editor', js: true do
let(:exercise) { FactoryBot.create(:audio_video, description: Forgery(:lorem_ipsum).sentence) }
let(:exercise) { create(:audio_video, description: Forgery(:lorem_ipsum).sentence) }
let(:scoring_response) do
[{
status: :ok,
@ -22,12 +22,12 @@ describe 'Editor', js: true do
weight: 2.0,
}]
end
let(:user) { FactoryBot.create(:teacher) }
let(:user) { create(:teacher) }
before do
visit(sign_in_path)
fill_in('email', with: user.email)
fill_in('password', with: FactoryBot.attributes_for(:teacher)[:password])
fill_in('password', with: attributes_for(:teacher)[:password])
click_button(I18n.t('sessions.new.link'))
allow_any_instance_of(LtiHelper).to receive(:lti_outcome_service?).and_return(true)
visit(implement_exercise_path(exercise))
@ -94,7 +94,7 @@ describe 'Editor', js: true do
end
it 'contains a button for submitting the exercise' do
submission = FactoryBot.build(:submission, user: user, exercise: exercise)
submission = build(:submission, user: user, exercise: exercise)
allow(submission).to receive(:calculate_score).and_return(scoring_response)
allow(Submission).to receive(:find).and_return(submission)
click_button(I18n.t('exercises.editor.score'))

View File

@ -26,7 +26,7 @@ describe Prometheus::Controller do
describe 'instance count' do
it 'initializes the metrics with the current database entries' do
FactoryBot.create_list(:proxy_exercise, 3)
create_list(:proxy_exercise, 3)
described_class.register_metrics
stub_metrics
described_class.initialize_instance_count
@ -35,25 +35,25 @@ describe Prometheus::Controller do
it 'gets notified when an object is created' do
allow(described_class).to receive(:create_notification)
proxy_exercise = FactoryBot.create(:proxy_exercise)
proxy_exercise = create(:proxy_exercise)
expect(described_class).to have_received(:create_notification).with(proxy_exercise).once
end
it 'gets notified when an object is destroyed' do
allow(described_class).to receive(:destroy_notification)
proxy_exercise = FactoryBot.create(:proxy_exercise).destroy
proxy_exercise = create(:proxy_exercise).destroy
expect(described_class).to have_received(:destroy_notification).with(proxy_exercise).once
end
it 'increments gauge when creating a new instance' do
FactoryBot.create(:proxy_exercise)
create(:proxy_exercise)
expect(described_class.instance_variable_get(:@instance_count)).to(
have_received(:increment).with(class: ProxyExercise.name).once
)
end
it 'decrements gauge when deleting an object' do
FactoryBot.create(:proxy_exercise).destroy
create(:proxy_exercise).destroy
expect(described_class.instance_variable_get(:@instance_count)).to(
have_received(:decrement).with(class: ProxyExercise.name).once
)
@ -63,7 +63,7 @@ describe Prometheus::Controller do
describe 'rfc count' do
context 'when initializing an rfc' do
it 'updates rfc count when creating an ongoing rfc' do
FactoryBot.create(:rfc)
create(:rfc)
expect(described_class.instance_variable_get(:@rfc_count)).to(
have_received(:increment).with(state: RequestForComment::ONGOING).once
)
@ -71,7 +71,7 @@ describe Prometheus::Controller do
end
context 'when changing the state of an rfc' do
let(:rfc) { FactoryBot.create(:rfc) }
let(:rfc) { create(:rfc) }
it 'updates rfc count when soft-solving an rfc' do
rfc.full_score_reached = true
@ -90,12 +90,12 @@ describe Prometheus::Controller do
context 'when commenting an rfc' do
it 'updates comment metric when commenting an rfc' do
FactoryBot.create(:rfc_with_comment)
create(:rfc_with_comment)
expect(described_class.instance_variable_get(:@rfc_commented_count)).to have_received(:increment).once
end
it 'does not update comment metric when commenting an rfc that already has a comment' do
rfc = FactoryBot.create(:rfc_with_comment)
rfc = create(:rfc_with_comment)
expect(described_class.instance_variable_get(:@rfc_commented_count)).to have_received(:increment).once
Comment.create(file: rfc.file, user: rfc.user, text: "comment a for rfc #{rfc.question}")

View File

@ -3,21 +3,21 @@
require 'rails_helper'
describe 'Request_for_Comments' do
let(:exercise) { FactoryBot.create(:audio_video, description: Forgery(:lorem_ipsum).sentence) }
let(:user) { FactoryBot.create(:teacher) }
let(:exercise) { create(:audio_video, description: Forgery(:lorem_ipsum).sentence) }
let(:user) { create(:teacher) }
before do
visit(sign_in_path)
fill_in('email', with: user.email)
fill_in('password', with: FactoryBot.attributes_for(:teacher)[:password])
fill_in('password', with: attributes_for(:teacher)[:password])
click_button(I18n.t('sessions.new.link'))
end
it 'does not contain rfcs for unpublished exercises' do
unpublished_rfc = FactoryBot.create(:rfc)
unpublished_rfc = create(:rfc)
unpublished_rfc.exercise.update(title: 'Unpublished Exercise')
unpublished_rfc.exercise.update(unpublished: true)
rfc = FactoryBot.create(:rfc)
rfc = create(:rfc)
rfc.exercise.update(title: 'Normal Exercise')
rfc.exercise.update(unpublished: false)

View File

@ -11,10 +11,10 @@ describe Admin::DashboardHelper do
describe '#docker_data' do
before do
FactoryBot.create(:ruby)
create(:ruby)
dcp = instance_double 'docker_container_pool'
allow(Runner).to receive(:strategy_class).and_return dcp
allow(dcp).to receive(:pool_size).and_return([])
allow(dcp).to receive(:pool_size).and_return({})
end
it 'contains an entry for every execution environment' do
@ -22,11 +22,15 @@ describe Admin::DashboardHelper do
end
it 'contains the pool size for every execution environment' do
expect(docker_data.first.symbolize_keys).to include(:pool_size)
expect(docker_data.first.symbolize_keys).to include(:prewarmingPoolSize)
end
it 'contains the number of available containers for every execution environment' do
expect(docker_data.first).to include(:quantity)
it 'contains the number of idle runners for every execution environment' do
expect(docker_data.first).to include(:idleRunners)
end
it 'contains the number of used runners for every execution environment' do
expect(docker_data.first).to include(:usedRunners)
end
end
end

View File

@ -4,7 +4,7 @@ require 'rails_helper'
describe ExerciseHelper do
describe '#embedding_parameters' do
let(:exercise) { FactoryBot.build(:dummy) }
let(:exercise) { build(:dummy) }
it 'contains the locale' do
expect(embedding_parameters(exercise)).to start_with("locale=#{I18n.locale}")

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe Assessor do
let(:assessor) { described_class.new(execution_environment: FactoryBot.build(:ruby)) }
let(:assessor) { described_class.new(execution_environment: build(:ruby)) }
describe '#assess' do
let(:assess) { assessor.assess(stdout: stdout) }
@ -55,7 +55,7 @@ describe Assessor do
context 'with an execution environment without a testing framework adapter' do
it 'raises an error' do
expect { described_class.new(execution_environment: FactoryBot.build(:ruby, testing_framework: nil)) }.to raise_error(Assessor::Error)
expect { described_class.new(execution_environment: build(:ruby, testing_framework: nil)) }.to raise_error(Assessor::Error)
end
end
end

View File

@ -7,14 +7,14 @@ WORKSPACE_PATH = Rails.root.join('tmp', 'files', Rails.env, 'code_ocean_test')
describe DockerClient do
let(:command) { 'whoami' }
let(:docker_client) { described_class.new(execution_environment: FactoryBot.build(:java), user: FactoryBot.build(:admin)) }
let(:execution_environment) { FactoryBot.build(:java) }
let(:docker_client) { described_class.new(execution_environment: build(:java), user: build(:admin)) }
let(:execution_environment) { build(:java) }
let(:image) { double }
let(:submission) { FactoryBot.create(:submission) }
let(:submission) { create(:submission) }
let(:workspace_path) { WORKSPACE_PATH }
before do
docker_image = Docker::Image.new(Docker::Connection.new('http://example.org', {}), 'id' => SecureRandom.hex, 'RepoTags' => [FactoryBot.attributes_for(:java)[:docker_image]])
docker_image = Docker::Image.new(Docker::Connection.new('http://example.org', {}), 'id' => SecureRandom.hex, 'RepoTags' => [attributes_for(:java)[:docker_image]])
allow(described_class).to receive(:find_image_by_tag).and_return(docker_image)
described_class.initialize_environment
allow(described_class).to receive(:container_creation_options).and_wrap_original do |original_method, *args, &block|
@ -177,7 +177,7 @@ describe DockerClient do
describe '#create_workspace_file' do
let(:container) { Docker::Container.send(:new, Docker::Connection.new('http://example.org', {}), 'id' => SecureRandom.hex) }
let(:file) { FactoryBot.build(:file, content: 'puts 42') }
let(:file) { build(:file, content: 'puts 42') }
let(:file_path) { File.join(workspace_path, file.name_with_extension) }
after { File.delete(file_path) }

View File

@ -10,7 +10,7 @@ describe FileTree do
context 'with a media file' do
context 'with an audio file' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_mp3)) }
let(:file) { build(:file, file_type: build(:dot_mp3)) }
it 'is an audio file icon' do
expect(file_icon).to include('fa-file-audio-o')
@ -18,7 +18,7 @@ describe FileTree do
end
context 'with an image file' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_jpg)) }
let(:file) { build(:file, file_type: build(:dot_jpg)) }
it 'is an image file icon' do
expect(file_icon).to include('fa-file-image-o')
@ -26,7 +26,7 @@ describe FileTree do
end
context 'with a video file' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_mp4)) }
let(:file) { build(:file, file_type: build(:dot_mp4)) }
it 'is a video file icon' do
expect(file_icon).to include('fa-file-video-o')
@ -36,7 +36,7 @@ describe FileTree do
context 'with other files' do
context 'with a read-only file' do
let(:file) { FactoryBot.build(:file, read_only: true) }
let(:file) { build(:file, read_only: true) }
it 'is a lock icon' do
expect(file_icon).to include('fa-lock')
@ -44,7 +44,7 @@ describe FileTree do
end
context 'with an executable file' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_py)) }
let(:file) { build(:file, file_type: build(:dot_py)) }
it 'is a code file icon' do
expect(file_icon).to include('fa-file-code-o')
@ -52,7 +52,7 @@ describe FileTree do
end
context 'with a renderable file' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_svg)) }
let(:file) { build(:file, file_type: build(:dot_svg)) }
it 'is a text file icon' do
expect(file_icon).to include('fa-file-text-o')
@ -60,7 +60,7 @@ describe FileTree do
end
context 'with all other files' do
let(:file) { FactoryBot.build(:file, file_type: FactoryBot.build(:dot_md)) }
let(:file) { build(:file, file_type: build(:dot_md)) }
it 'is a generic file icon' do
expect(file_icon).to include('fa-file-o')
@ -77,7 +77,7 @@ describe FileTree do
describe '#initialize' do
let(:file_tree) { described_class.new(files) }
let(:files) { FactoryBot.build_list(:file, 10, context: nil, path: 'foo/bar/baz') }
let(:files) { build_list(:file, 10, context: nil, path: 'foo/bar/baz') }
it 'creates a root node' do
# Instead of checking #initialize on the parent, we validate #set_as_root!
@ -95,7 +95,7 @@ describe FileTree do
end
describe '#map_to_js_tree' do
let(:file) { FactoryBot.build(:file) }
let(:file) { build(:file) }
let(:js_tree) { file_tree.send(:map_to_js_tree, node) }
let!(:leaf) { root.add(Tree::TreeNode.new('', file)) }
let(:root) { Tree::TreeNode.new('', file) }
@ -186,7 +186,7 @@ describe FileTree do
end
context 'with files' do
let(:files) { FactoryBot.build_list(:file, 10, context: nil, path: 'foo/bar/baz') }
let(:files) { build_list(:file, 10, context: nil, path: 'foo/bar/baz') }
let(:file_tree) { described_class.new(files) }
let(:js_tree) { file_tree.to_js_tree }

View File

@ -4,8 +4,8 @@ require 'rails_helper'
require 'pathname'
describe Runner::Strategy::DockerContainerPool do
let(:runner_id) { FactoryBot.attributes_for(:runner)[:runner_id] }
let(:execution_environment) { FactoryBot.create :ruby }
let(:runner_id) { attributes_for(:runner)[:runner_id] }
let(:execution_environment) { create :ruby }
let(:container_pool) { described_class.new(runner_id, execution_environment) }
let(:docker_container_pool_url) { 'http://localhost:1234' }
let(:config) { {url: docker_container_pool_url, unused_runner_expiration_time: 180} }
@ -112,22 +112,20 @@ describe Runner::Strategy::DockerContainerPool do
context 'when receiving a normal file' do
let(:file_content) { 'print("Hello World!")' }
let(:files) { [FactoryBot.build(:file, content: file_content)] }
let(:files) { [build(:file, content: file_content)] }
it 'writes the file to disk' do
file = instance_double(File)
allow(File).to receive(:open).and_yield(file)
expect(file).to receive(:write).with(file_content)
expect(File).to receive(:write).with(local_path.join(files.first.filepath), file_content)
container_pool.copy_files(files)
end
it 'creates the file inside the workspace' do
expect(File).to receive(:open).with(local_path.join(files.first.filepath), 'w')
expect(File).to receive(:write).with(local_path.join(files.first.filepath), files.first.content)
container_pool.copy_files(files)
end
it 'raises an error in case of an IOError' do
allow(File).to receive(:open).and_raise(IOError)
allow(File).to receive(:write).and_raise(IOError)
expect { container_pool.copy_files(files) }.to raise_error(Runner::Error::WorkspaceError, /#{files.first.filepath}/)
end
@ -137,10 +135,10 @@ describe Runner::Strategy::DockerContainerPool do
context 'when the file is inside a directory' do
let(:directory) { 'temp/dir' }
let(:files) { [FactoryBot.build(:file, path: directory)] }
let(:files) { [build(:file, path: directory)] }
before do
allow(File).to receive(:open)
allow(File).to receive(:write)
allow(FileUtils).to receive(:mkdir_p).with(local_path)
allow(FileUtils).to receive(:mkdir_p).with(local_path.join(directory))
end
@ -159,7 +157,7 @@ describe Runner::Strategy::DockerContainerPool do
end
context 'when receiving a binary file' do
let(:files) { [FactoryBot.build(:file, :image)] }
let(:files) { [build(:file, :image)] }
it 'copies the file inside the workspace' do
expect(FileUtils).to receive(:cp).with(files.first.native_file.path, local_path.join(files.first.filepath))
@ -168,11 +166,11 @@ describe Runner::Strategy::DockerContainerPool do
end
context 'when receiving multiple files' do
let(:files) { FactoryBot.build_list(:file, 3) }
let(:files) { build_list(:file, 3) }
it 'creates all files' do
files.each do |file|
expect(File).to receive(:open).with(local_path.join(file.filepath), 'w')
expect(File).to receive(:write).with(local_path.join(file.filepath), file.content)
end
container_pool.copy_files(files)
end

View File

@ -3,8 +3,8 @@
require 'rails_helper'
describe Runner::Strategy::Poseidon do
let(:runner_id) { FactoryBot.attributes_for(:runner)[:runner_id] }
let(:execution_environment) { FactoryBot.create :ruby }
let(:runner_id) { attributes_for(:runner)[:runner_id] }
let(:execution_environment) { create :ruby }
let(:poseidon) { described_class.new(runner_id, execution_environment) }
let(:error_message) { 'test error message' }
let(:response_body) { nil }
@ -128,7 +128,7 @@ describe Runner::Strategy::Poseidon do
describe '::sync_environment' do
let(:action) { -> { described_class.sync_environment(execution_environment) } }
let(:execution_environment) { FactoryBot.create(:ruby) }
let(:execution_environment) { create(:ruby) }
it 'makes the correct request to Poseidon' do
faraday_connection = instance_double 'Faraday::Connection'
@ -321,7 +321,7 @@ describe Runner::Strategy::Poseidon do
describe '#copy_files' do
let(:file_content) { 'print("Hello World!")' }
let(:file) { FactoryBot.build(:file, content: file_content) }
let(:file) { build(:file, content: file_content) }
let(:action) { -> { poseidon.copy_files([file]) } }
let(:encoded_file_content) { Base64.strict_encode64(file.content) }
let!(:copy_files_stub) do

View File

@ -3,7 +3,7 @@
require 'rails_helper'
describe UserMailer do
let(:user) { InternalUser.create(FactoryBot.attributes_for(:teacher)) }
let(:user) { InternalUser.create(attributes_for(:teacher)) }
describe '#activation_needed_email' do
let(:mail) { described_class.activation_needed_email(user) }

View File

@ -6,7 +6,7 @@ describe CodeOcean::File do
let(:file) { described_class.create.tap {|file| file.update(content: nil, hidden: nil, read_only: nil) } }
it 'validates the presence of a file type' do
expect(file.errors[:file_type_id]).to be_present
expect(file.errors[:file_type]).to be_present
end
it 'validates the presence of the hidden flag' do

View File

@ -11,7 +11,7 @@ describe CodeharborLink do
describe '#to_s' do
subject { codeharbor_link.to_s }
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link) }
let(:codeharbor_link) { create(:codeharbor_link) }
it { is_expected.to eql codeharbor_link.id.to_s }
end

View File

@ -14,7 +14,7 @@ describe Consumer do
end
it 'validates the uniqueness of the OAuth key' do
consumer.update(oauth_key: FactoryBot.create(:consumer).oauth_key)
consumer.update(oauth_key: create(:consumer).oauth_key)
expect(consumer.errors[:oauth_key]).to be_present
end

View File

@ -8,7 +8,7 @@ describe ExecutionEnvironment do
it 'validates that the Docker image works' do
allow(execution_environment).to receive(:validate_docker_image?).and_return(true)
allow(execution_environment).to receive(:working_docker_image?).and_return(true)
execution_environment.update(FactoryBot.build(:ruby).attributes)
execution_environment.update(build(:ruby).attributes)
expect(execution_environment).to have_received(:working_docker_image?)
end
@ -81,8 +81,7 @@ describe ExecutionEnvironment do
end
it 'validates the presence of a user' do
expect(execution_environment.errors[:user_id]).to be_present
expect(execution_environment.errors[:user_type]).to be_present
expect(execution_environment.errors[:user]).to be_present
end
it 'validates the format of the exposed ports' do
@ -95,7 +94,7 @@ describe ExecutionEnvironment do
describe '#valid_test_setup?' do
context 'with a test command and a testing framework' do
before { execution_environment.update(test_command: FactoryBot.attributes_for(:ruby)[:test_command], testing_framework: FactoryBot.attributes_for(:ruby)[:testing_framework]) }
before { execution_environment.update(test_command: attributes_for(:ruby)[:test_command], testing_framework: attributes_for(:ruby)[:testing_framework]) }
it 'is valid' do
expect(execution_environment.errors[:test_command]).to be_blank
@ -103,7 +102,7 @@ describe ExecutionEnvironment do
end
context 'with a test command but no testing framework' do
before { execution_environment.update(test_command: FactoryBot.attributes_for(:ruby)[:test_command], testing_framework: nil) }
before { execution_environment.update(test_command: attributes_for(:ruby)[:test_command], testing_framework: nil) }
it 'is invalid' do
expect(execution_environment.errors[:test_command]).to be_present
@ -111,7 +110,7 @@ describe ExecutionEnvironment do
end
context 'with no test command but a testing framework' do
before { execution_environment.update(test_command: nil, testing_framework: FactoryBot.attributes_for(:ruby)[:testing_framework]) }
before { execution_environment.update(test_command: nil, testing_framework: attributes_for(:ruby)[:testing_framework]) }
it 'is invalid' do
expect(execution_environment.errors[:test_command]).to be_present
@ -144,7 +143,7 @@ describe ExecutionEnvironment do
end
it 'is true otherwise' do
execution_environment.docker_image = FactoryBot.attributes_for(:ruby)[:docker_image]
execution_environment.docker_image = attributes_for(:ruby)[:docker_image]
execution_environment.pool_size = 1
allow(Rails.env).to receive(:test?).and_return(false)
expect(execution_environment.send(:validate_docker_image?)).to be true
@ -152,7 +151,7 @@ describe ExecutionEnvironment do
end
describe '#working_docker_image?' do
let(:execution_environment) { FactoryBot.create(:ruby) }
let(:execution_environment) { create(:ruby) }
let(:working_docker_image?) { execution_environment.send(:working_docker_image?) }
let(:runner) { instance_double 'runner' }

View File

@ -4,15 +4,15 @@ require 'rails_helper'
describe Exercise do
let(:exercise) { described_class.create.tap {|exercise| exercise.update(public: nil, token: nil) } }
let(:users) { FactoryBot.create_list(:external_user, 10) }
let(:users) { create_list(:external_user, 10) }
def create_submissions
FactoryBot.create_list(:submission, 10, cause: 'submit', exercise: exercise, score: Forgery(:basic).number, user: users.sample)
create_list(:submission, 10, cause: 'submit', exercise: exercise, score: Forgery(:basic).number, user: users.sample)
end
it 'validates the number of main files' do
exercise = FactoryBot.create(:dummy)
exercise.files += FactoryBot.create_pair(:file)
exercise = create(:dummy)
exercise.files += create_pair(:file)
expect(exercise).to receive(:valid_main_file?).and_call_original
exercise.save
expect(exercise.errors[:files]).to be_present
@ -37,36 +37,35 @@ describe Exercise do
end
it 'validates the presence of a user' do
expect(exercise.errors[:user_id]).to be_present
expect(exercise.errors[:user_type]).to be_present
expect(exercise.errors[:user]).to be_present
end
context 'when exercise is unpublished' do
subject { FactoryBot.build(:dummy, unpublished: true) }
subject { build(:dummy, unpublished: true) }
it { is_expected.not_to validate_presence_of(:execution_environment) }
end
context 'when exercise is not unpublished' do
subject { FactoryBot.build(:dummy, unpublished: false) }
subject { build(:dummy, unpublished: false) }
it { is_expected.to validate_presence_of(:execution_environment) }
end
context 'with uuid' do
subject { FactoryBot.build(:dummy, uuid: SecureRandom.uuid) }
subject { build(:dummy, uuid: SecureRandom.uuid) }
it { is_expected.to validate_uniqueness_of(:uuid).case_insensitive }
end
context 'without uuid' do
subject { FactoryBot.build(:dummy, uuid: nil) }
subject { build(:dummy, uuid: nil) }
it { is_expected.not_to validate_uniqueness_of(:uuid) }
end
describe '#average_percentage' do
let(:exercise) { FactoryBot.create(:fibonacci) }
let(:exercise) { create(:fibonacci) }
context 'without submissions' do
it 'returns nil' do
@ -85,7 +84,7 @@ describe Exercise do
end
describe '#average_score' do
let(:exercise) { FactoryBot.create(:fibonacci) }
let(:exercise) { create(:fibonacci) }
context 'without submissions' do
it 'returns nil' do
@ -104,7 +103,7 @@ describe Exercise do
end
describe '#duplicate' do
let(:exercise) { FactoryBot.create(:fibonacci) }
let(:exercise) { create(:fibonacci) }
after { exercise.duplicate }

View File

@ -6,7 +6,7 @@ describe ExternalUser do
let(:user) { described_class.create }
it 'validates the presence of a consumer' do
expect(user.errors[:consumer_id]).to be_present
expect(user.errors[:consumer]).to be_present
end
it 'validates the presence of an external ID' do
@ -15,7 +15,7 @@ describe ExternalUser do
describe '#admin?' do
it 'is false' do
expect(FactoryBot.build(:external_user).admin?).to be false
expect(build(:external_user).admin?).to be false
end
end
@ -33,7 +33,7 @@ describe ExternalUser do
describe '#teacher?' do
it 'is false' do
expect(FactoryBot.build(:external_user).teacher?).to be false
expect(build(:external_user).teacher?).to be false
end
end
end

View File

@ -52,7 +52,6 @@ describe FileType do
end
it 'validates the presence of a user' do
expect(file_type.errors[:user_id]).to be_present
expect(file_type.errors[:user_type]).to be_present
expect(file_type.errors[:user]).to be_present
end
end

View File

@ -11,12 +11,12 @@ describe InternalUser do
end
it 'validates the uniqueness of the email address' do
user.update(email: FactoryBot.create(:admin).email)
user.update(email: create(:admin).email)
expect(user.errors[:email]).to be_present
end
context 'when not activated' do
let(:user) { FactoryBot.create(:teacher) }
let(:user) { create(:teacher) }
before do
user.send(:setup_activation)
@ -35,7 +35,7 @@ describe InternalUser do
end
context 'with a pending password reset' do
let(:user) { FactoryBot.create(:teacher) }
let(:user) { create(:teacher) }
before { user.deliver_reset_password_instructions! }
@ -51,7 +51,7 @@ describe InternalUser do
end
context 'when complete' do
let(:user) { FactoryBot.create(:teacher, activation_state: 'active') }
let(:user) { create(:teacher, activation_state: 'active') }
it 'does not validate the confirmation of the password' do
user.update(password: password, password_confirmation: '')
@ -74,8 +74,8 @@ describe InternalUser do
describe '#admin?' do
it 'is only true for admins' do
expect(FactoryBot.build(:admin).admin?).to be true
expect(FactoryBot.build(:teacher).admin?).to be false
expect(build(:admin).admin?).to be true
expect(build(:teacher).admin?).to be false
end
end
@ -93,8 +93,8 @@ describe InternalUser do
describe '#teacher?' do
it 'is only true for teachers' do
expect(FactoryBot.build(:admin).teacher?).to be false
expect(FactoryBot.build(:teacher).teacher?).to be true
expect(build(:admin).teacher?).to be false
expect(build(:teacher).teacher?).to be true
end
end
end

View File

@ -3,10 +3,10 @@
require 'rails_helper'
describe RequestForComment do
let!(:rfc) { FactoryBot.create(:rfc) }
let!(:rfc) { create(:rfc) }
describe 'scope with_comments' do
let!(:rfc2) { FactoryBot.create(:rfc_with_comment) }
let!(:rfc2) { create(:rfc_with_comment) }
it 'includes all RfCs with comments' do
expect(described_class.with_comments).to include(rfc2)

View File

@ -3,12 +3,12 @@
require 'rails_helper'
describe Runner do
let(:runner_id) { FactoryBot.attributes_for(:runner)[:runner_id] }
let(:runner_id) { attributes_for(:runner)[:runner_id] }
let(:strategy_class) { described_class.strategy_class }
let(:strategy) { instance_double(strategy_class) }
describe 'attribute validation' do
let(:runner) { FactoryBot.create :runner }
let(:runner) { create :runner }
it 'validates the presence of the runner id' do
described_class.skip_callback(:validation, :before, :request_id)
@ -162,8 +162,8 @@ describe Runner do
end
describe 'creation' do
let(:user) { FactoryBot.create :external_user }
let(:execution_environment) { FactoryBot.create :ruby }
let(:user) { create :external_user }
let(:execution_environment) { create :ruby }
let(:create_action) { -> { described_class.create(user: user, execution_environment: execution_environment) } }
it 'requests a runner id from the runner management' do
@ -187,12 +187,12 @@ describe Runner do
it 'does not call the runner management again while a runner id is set' do
expect(strategy_class).to receive(:request_from_management).and_return(runner_id).once
runner = create_action.call
runner.update(user: FactoryBot.create(:external_user))
runner.update(user: create(:external_user))
end
end
describe '#request_new_id' do
let(:runner) { FactoryBot.create :runner }
let(:runner) { create :runner }
context 'when the environment is available in the runner management' do
it 'requests the runner management' do
@ -240,8 +240,8 @@ describe Runner do
end
describe '::for' do
let(:user) { FactoryBot.create :external_user }
let(:exercise) { FactoryBot.create :fibonacci }
let(:user) { create :external_user }
let(:exercise) { create :fibonacci }
context 'when the runner could not be saved' do
before { allow(strategy_class).to receive(:request_from_management).and_return(nil) }
@ -252,7 +252,7 @@ describe Runner do
end
context 'when a runner already exists' do
let!(:existing_runner) { FactoryBot.create(:runner, user: user, execution_environment: exercise.execution_environment) }
let!(:existing_runner) { create(:runner, user: user, execution_environment: exercise.execution_environment) }
it 'returns the existing runner' do
new_runner = described_class.for(user, exercise.execution_environment)

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