diff --git a/.travis.yml b/.travis.yml index 6d533916..90fd5471 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,14 +12,11 @@ addons: before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start -# Config to run docker tests - doesn't work so far -# - sudo apt-get update -# - sudo apt-get upgrade lxc-docker -# - echo 'DOCKER_OPTS="-H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --iptables=false"' | sudo tee /etc/default/docker > /dev/null -# - export DOCKER_HOST=tcp://192.168.23.75:2375 -# - sudo service docker restart -# - sleep 5 -# - docker pull openhpi/docker_ruby + - echo 'DOCKER_OPTS="-H tcp://127.0.0.1:2376 -H unix:///var/run/docker.sock --iptables=false"' | sudo tee /etc/default/docker > /dev/null + - sudo service docker restart + - sleep 5 + - docker pull openhpi/co_execenv_python + - docker pull openhpi/co_execenv_java before_script: - cp .rspec.travis .rspec @@ -27,6 +24,7 @@ before_script: - cp config/code_ocean.yml.travis config/code_ocean.yml - cp config/database.yml.travis config/database.yml - cp config/secrets.yml.travis config/secrets.yml + - cp config/docker.yml.erb.travis config/docker.yml.erb - psql --command='CREATE DATABASE travis_ci_test;' --username=postgres - bundle exec rake db:schema:load RAILS_ENV=test @@ -35,8 +33,4 @@ language: ruby rvm: - 2.3.6 -script: bundle exec rspec --color --format documentation --require spec_helper --require rails_helper --tag ~docker && bundle exec codeclimate-test-reporter -# one of the solutions I've found -# - sudo docker run --rm=true -v `pwd`:/ansible-apache:rw weldpua2008/docker-ansible:${OS_TYPE}${OS_VERSION}_v${ANSIBLE_VERSION} /bin/bash -c "/ansible-apache/tests/test-in-docker-image.sh ${OS_TYPE} ${OS_VERSION} ${ANSIBLE_VERSION}" - - +script: bundle exec rspec --color --format documentation --require spec_helper --require rails_helper && bundle exec codeclimate-test-reporter diff --git a/Gemfile b/Gemfile index e71bead4..8482ca69 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'carrierwave' gem 'coffee-rails' gem 'concurrent-ruby' gem 'concurrent-ruby-ext', platform: :ruby +gem 'activerecord-deprecated_finders', require: 'active_record/deprecated_finders' gem 'docker-api', require: 'docker' gem 'factory_bot_rails' gem 'forgery' diff --git a/Gemfile.lock b/Gemfile.lock index 8b3c2dbc..43737a82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,6 +31,7 @@ GEM activemodel (= 4.2.10) activesupport (= 4.2.10) arel (~> 6.0) + activerecord-deprecated_finders (1.0.4) activerecord-jdbc-adapter (50.0) activerecord (>= 2.2) activerecord-jdbcpostgresql-adapter (50.0) @@ -388,6 +389,7 @@ PLATFORMS ruby DEPENDENCIES + activerecord-deprecated_finders activerecord-jdbcpostgresql-adapter autotest-rails bcrypt diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index c22ba185..a38157b4 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -42,17 +42,14 @@ button i.fa-spin { background-color: #008CBA; margin-top: 0; width: 100%; + display: flex; button { font-size: 80%; } - .button-two-only, .btn-group-two-only { - width: 50%; - } - button, .btn-group { - width: 50%; + flex-grow: 1; } .btn-group { @@ -195,4 +192,4 @@ button i.fa-spin { .enforce-bottom-margin { margin-bottom: 5px !important; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/forms.css.scss b/app/assets/stylesheets/forms.css.scss index 201b5841..69c1722c 100644 --- a/app/assets/stylesheets/forms.css.scss +++ b/app/assets/stylesheets/forms.css.scss @@ -3,7 +3,8 @@ } .chosen-container { - width: 250px !important; + min-width: 250px !important; + width: 100% !important; } .code-field { diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index 2f8219a9..29c5a6ea 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -14,46 +14,40 @@ class RequestForCommentsController < ApplicationController def index @search = RequestForComment .last_per_user(2) - .joins('join "submissions" s on s.id = request_for_comments.submission_id - left outer join "files" f on f.context_id = s.id - left outer join "comments" on comments.file_id = f.id') - .group('request_for_comments.id, request_for_comments.user_id, request_for_comments.exercise_id, - request_for_comments.file_id, request_for_comments.question, request_for_comments.created_at, - request_for_comments.updated_at, request_for_comments.user_type, request_for_comments.solved, - request_for_comments.full_score_reached, request_for_comments.submission_id, request_for_comments.row_number') # ugly, but rails wants it this way - .select('request_for_comments.*, max(comments.updated_at) as last_comment') + .with_last_activity .search(params[:q]) - @request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page], total_entries: @search.result.length) + @request_for_comments = @search.result + .order('created_at DESC') + .paginate(page: params[:page], total_entries: @search.result.length) authorize! end + # GET /my_request_for_comments def get_my_comment_requests @search = RequestForComment + .with_last_activity .where(user_id: current_user.id) - .joins('join "submissions" s on s.id = request_for_comments.submission_id - left outer join "files" f on f.context_id = s.id - left outer join "comments" on comments.file_id = f.id') - .group('request_for_comments.id') - .select('request_for_comments.*, max(comments.updated_at) as last_comment') .search(params[:q]) - @request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page]) + @request_for_comments = @search.result + .order('created_at DESC') + .paginate(page: params[:page]) render 'index' end + # GET /my_rfc_activity def get_rfcs_with_my_comments @search = RequestForComment + .with_last_activity .joins(:comments) # we don't need to outer join here, because we know the user has commented on these .where(comments: {user_id: current_user.id}) - .joins('join "submissions" s on s.id = request_for_comments.submission_id - left outer join "files" f on f.context_id = s.id - left outer join "comments" as c on c.file_id = f.id') - .group('request_for_comments.id') - .select('request_for_comments.*, max(c.updated_at) as last_comment') .search(params[:q]) - @request_for_comments = @search.result.order('last_comment DESC').paginate(page: params[:page]) + @request_for_comments = @search.result + .order('last_comment DESC') + .paginate(page: params[:page]) render 'index' end + # GET /request_for_comments/1/mark_as_solved def mark_as_solved authorize! @request_for_comment.solved = true @@ -66,6 +60,7 @@ class RequestForCommentsController < ApplicationController end end + # POST /request_for_comments/1/set_thank_you_note def set_thank_you_note authorize! @request_for_comment.thank_you_note = params[:note] @@ -82,10 +77,6 @@ class RequestForCommentsController < ApplicationController end end - def submit - - end - # GET /request_for_comments/1 # GET /request_for_comments/1.json def show @@ -146,10 +137,6 @@ class RequestForCommentsController < ApplicationController authorize! end - def comment_params - params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name) - end - private # Use callbacks to share common setup or constraints between actions. def set_request_for_comment @@ -162,4 +149,8 @@ class RequestForCommentsController < ApplicationController params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name) end + def comment_params + params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name) + end + end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 3e24af1c..410c30cd 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -75,6 +75,11 @@ class SubmissionsController < ApplicationController zio.write(file.content) end + # zip exercise description + zio.put_next_entry(t('activerecord.models.exercise.one') + '.txt') + zio.write(@submission.exercise.title + "\r\n======================\r\n") + zio.write(@submission.exercise.description) + # zip .co file zio.put_next_entry(".co") zio.write(File.read id_file) @@ -167,7 +172,7 @@ class SubmissionsController < ApplicationController # if the command is 'client_kill', send it to docker otherwise. begin parsed = JSON.parse(data) - if parsed['cmd'] == 'client_kill' + if parsed.class == Hash && parsed['cmd'] == 'client_kill' Rails.logger.debug("Client exited container.") @docker_client.kill_container(result[:container]) else diff --git a/app/models/execution_environment.rb b/app/models/execution_environment.rb index 74177173..9cd23a48 100644 --- a/app/models/execution_environment.rb +++ b/app/models/execution_environment.rb @@ -47,7 +47,7 @@ class ExecutionEnvironment < ActiveRecord::Base private :validate_docker_image? def working_docker_image? - DockerClient.pull(docker_image) unless DockerClient.image_tags.include?(docker_image) + DockerClient.pull(docker_image) unless DockerClient.find_image_by_tag(docker_image).blank? output = DockerClient.new(execution_environment: self).execute_arbitrary_command(VALIDATION_COMMAND) errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present? rescue DockerClient::Error => error diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb index e46f3048..2d91cc8f 100644 --- a/app/models/request_for_comment.rb +++ b/app/models/request_for_comment.rb @@ -11,7 +11,13 @@ class RequestForComment < ActiveRecord::Base scope :not_stale, -> { where("user_id%10 <2 OR user_id%10 >= 4").where(exercise.exercise_collections.none{|ec| ec.id = 3} } ########### todo def self.last_per_user(n = 5) - from("(#{row_number_user_sql}) as request_for_comments").where("row_number <= ?", n) + from("(#{row_number_user_sql}) as request_for_comments") + .where("row_number <= ?", n) + .group('request_for_comments.id, request_for_comments.user_id, request_for_comments.exercise_id, + request_for_comments.file_id, request_for_comments.question, request_for_comments.created_at, + request_for_comments.updated_at, request_for_comments.user_type, request_for_comments.solved, + request_for_comments.full_score_reached, request_for_comments.submission_id, request_for_comments.row_number') + # ugly, but necessary end # not used right now, finds the last submission for the respective user and exercise. @@ -47,6 +53,14 @@ class RequestForComment < ActiveRecord::Base commenters.uniq {|user| user.id} end + def self.with_last_activity + self.joins('join "submissions" s on s.id = request_for_comments.submission_id + left outer join "files" f on f.context_id = s.id + left outer join "comments" c on c.file_id = f.id') + .group('request_for_comments.id') + .select('request_for_comments.*, max(c.updated_at) as last_comment') + end + def to_s "RFC-" + self.id.to_s end diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim index 135adcf4..a1f7191c 100644 --- a/app/views/application/_flash.html.slim +++ b/app/views/application/_flash.html.slim @@ -1,5 +1,6 @@ -#flash.fixed_error_messages data-message-failure=t('shared.message_failure') - - %w[alert danger info notice success warning].each do |severity| - div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" - p id="flash-#{severity}" = flash[severity] - span.fa.fa-times \ No newline at end of file +#flash-container + #flash.container.fixed_error_messages data-message-failure=t('shared.message_failure') + - %w[alert danger info notice success warning].each do |severity| + div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" + p id="flash-#{severity}" = flash[severity] + span.fa.fa-times diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 8bc539a7..bd78a321 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -32,8 +32,8 @@ html lang='en' li = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}) = render('session') .container data-controller=controller_name - = render('breadcrumbs') = render('flash') + = render('breadcrumbs') - if (controller_name == "exercises" && action_name == "implement") .container-fluid = yield diff --git a/app/views/sessions/destroy_through_lti.html.slim b/app/views/sessions/destroy_through_lti.html.slim index 3e55e0fc..d33ced8d 100644 --- a/app/views/sessions/destroy_through_lti.html.slim +++ b/app/views/sessions/destroy_through_lti.html.slim @@ -10,7 +10,7 @@ h2 = t('shared.statistics') = row(label: '.score') do p == t('shared.out_of', maximum_value: @submission.exercise.maximum_score, value: @submission.score) p = progress_bar(@submission.percentage) -= row(label: '.final_submissions', value: @submission.exercise.submissions.final.distinct.count(:user_id, :user_type) - 1) +/= row(label: '.final_submissions', value: @submission.exercise.submissions.final.distinct.count(:user_id, :user_type) - 1) /= row(label: '.average_score') do / p == t('shared.out_of', maximum_value: @submission.exercise.maximum_score, value: @submission.exercise.average_score.round(2)) / p = progress_bar(@submission.exercise.average_percentage) diff --git a/config/docker.yml.erb.example b/config/docker.yml.erb.example index 9f55d126..eb1af61e 100644 --- a/config/docker.yml.erb.example +++ b/config/docker.yml.erb.example @@ -34,7 +34,21 @@ production: ws_host: ws://localhost:4243 #url to connect rails server to docker host ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) +staging: + <<: *default + host: unix:///var/run/docker.sock + pool: + active: true + refill: + async: false + batch_size: 8 + interval: 15 + timeout: 60 + workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> + ws_host: ws://localhost:4243 #url to connect rails server to docker host + ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) + test: <<: *default - host: tcp://192.168.59.104:2376 + host: tcp://127.0.0.1:2376 workspace_root: <%= File.join('/', 'shared', Rails.env) %> diff --git a/config/docker.yml.erb b/config/docker.yml.erb.travis similarity index 94% rename from config/docker.yml.erb rename to config/docker.yml.erb.travis index c1871de7..eb1af61e 100644 --- a/config/docker.yml.erb +++ b/config/docker.yml.erb.travis @@ -32,7 +32,7 @@ production: timeout: 60 workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> ws_host: ws://localhost:4243 #url to connect rails server to docker host - ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) + ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) staging: <<: *default @@ -50,5 +50,5 @@ staging: test: <<: *default - host: tcp://192.168.59.104:2376 + host: tcp://127.0.0.1:2376 workspace_root: <%= File.join('/', 'shared', Rails.env) %> diff --git a/db/migrate/20160630154310_add_submission_to_request_for_comments.rb b/db/migrate/20160630154310_add_submission_to_request_for_comments.rb index d7d13e67..a532bcf9 100644 --- a/db/migrate/20160630154310_add_submission_to_request_for_comments.rb +++ b/db/migrate/20160630154310_add_submission_to_request_for_comments.rb @@ -3,3 +3,31 @@ class AddSubmissionToRequestForComments < ActiveRecord::Migration add_reference :request_for_comments, :submission end end + +=begin +We issued the following on the database to add the submission_ids for existing entries + +UPDATE request_for_comments +SET submission_id = sub.submission_id_external +FROM +(SELECT s.id AS submission_id_external, + rfc.id AS rfc_id, + s.created_at AS submission_created_at, + rfc.created_at AS rfc_created_at +FROM submissions s, + request_for_comments rfc +WHERE s.user_id = rfc.user_id +AND s.exercise_id = rfc.exercise_id +AND rfc.created_at + interval '2 hours' > s.created_at +AND s.created_at = + (SELECT MAX(created_at) + FROM submissions + WHERE exercise_id = s.exercise_id + AND user_id = s.user_id + AND rfc.created_at + interval '2 hours' > created_at + GROUP BY s.exercise_id, + s.user_id)) as sub +WHERE id = sub.rfc_id +AND submission_id IS NULL; + +=end diff --git a/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb b/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb index ed1d27a9..c29919fd 100644 --- a/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb +++ b/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb @@ -1,8 +1,8 @@ class AddReachedFullScoreToRequestForComment < ActiveRecord::Migration def up add_column :request_for_comments, :full_score_reached, :boolean, default: false - RequestForComment.all.each { |rfc| - if (rfc.submission.present? && rfc.submission.exercise.has_user_solved(rfc.user)) + RequestForComment.find_each { |rfc| + if rfc.submission.present? and rfc.submission.exercise.has_user_solved(rfc.user) rfc.full_score_reached = true rfc.save end diff --git a/db/migrate/20180222145909_fix_timestamps_on_feedback.rb b/db/migrate/20180222145909_fix_timestamps_on_feedback.rb index 95941c5a..f07372c7 100644 --- a/db/migrate/20180222145909_fix_timestamps_on_feedback.rb +++ b/db/migrate/20180222145909_fix_timestamps_on_feedback.rb @@ -1,6 +1,10 @@ class FixTimestampsOnFeedback < ActiveRecord::Migration - def change + def up change_column_default(:user_exercise_feedbacks, :created_at, nil) change_column_default(:user_exercise_feedbacks, :updated_at, nil) end + + def down + + end end diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index df52993a..0abb6dc9 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -1,3 +1,8 @@ +#flash-container { + position: relative; + top: -21px; +} + .flash { display: none; @@ -10,14 +15,10 @@ } .fixed_error_messages { - position: fixed; + position: absolute; z-index: 1000; - top: 20px; - left: 0; + padding: inherit; width: 100%; - padding-left: 10%; - padding-right: 10%; - padding-top: 0; } diff --git a/public/uploads/files/4/chai.ogg b/public/uploads/files/4/chai.ogg deleted file mode 100644 index 86190d0c..00000000 Binary files a/public/uploads/files/4/chai.ogg and /dev/null differ diff --git a/public/uploads/files/5/devstories.mp4 b/public/uploads/files/5/devstories.mp4 deleted file mode 100644 index 9e7d328d..00000000 Binary files a/public/uploads/files/5/devstories.mp4 and /dev/null differ diff --git a/public/uploads/files/6/devstories.webm b/public/uploads/files/6/devstories.webm deleted file mode 100644 index 84fff0ed..00000000 Binary files a/public/uploads/files/6/devstories.webm and /dev/null differ diff --git a/public/uploads/files/7/poster.png b/public/uploads/files/7/poster.png deleted file mode 100644 index d950cef4..00000000 Binary files a/public/uploads/files/7/poster.png and /dev/null differ diff --git a/spec/concerns/lti_spec.rb b/spec/concerns/lti_spec.rb index a7b1dfcc..4fb8dc5f 100644 --- a/spec/concerns/lti_spec.rb +++ b/spec/concerns/lti_spec.rb @@ -98,7 +98,7 @@ describe Lti do let(:consumer) { FactoryBot.create(:consumer) } let(:score) { 0.5 } let(:submission) { FactoryBot.create(:submission) } - let!(:lti_parameter) { FactoryBot.create(:lti_parameter)} + let!(:lti_parameter) { FactoryBot.create(:lti_parameter, consumers_id: consumer.id, external_users_id: submission.user_id, exercises_id: submission.exercise_id)} context 'with an invalid score' do it 'raises an exception' do @@ -114,7 +114,6 @@ describe Lti do context 'when grading is not supported' do it 'returns a corresponding status' do - skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked') expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false) expect(controller.send(:send_score, submission.exercise_id, score, submission.user_id)[:status]).to eq('unsupported') end @@ -133,12 +132,10 @@ describe Lti do end it 'sends the score' do - skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked') controller.send(:send_score, submission.exercise_id, score, submission.user_id) end it 'returns code, message, and status' do - skip('ralf: this does not work, since send_score pulls data from the database, which then returns an empty array. On this is called .first, which returns nil and lets the test fail. Before Toms changes, this was taken from the session, which could be mocked') result = controller.send(:send_score, submission.exercise_id, score, submission.user_id) expect(result[:code]).to eq(response.response_code) expect(result[:message]).to eq(response.body) diff --git a/spec/factories/user_exercise_feedback.rb b/spec/factories/user_exercise_feedback.rb index 42c5d803..3c43158f 100644 --- a/spec/factories/user_exercise_feedback.rb +++ b/spec/factories/user_exercise_feedback.rb @@ -2,6 +2,7 @@ FactoryBot.define do factory :user_exercise_feedback, class: UserExerciseFeedback do created_by_external_user feedback_text 'Most suitable exercise ever' + association :exercise, factory: :math end end diff --git a/spec/lib/docker_client_spec.rb b/spec/lib/docker_client_spec.rb index 5901c130..5d1118bd 100644 --- a/spec/lib/docker_client_spec.rb +++ b/spec/lib/docker_client_spec.rb @@ -2,12 +2,22 @@ require 'rails_helper' require 'seeds_helper' describe DockerClient, docker: true do + WORKSPACE_PATH = '/tmp/code_ocean_test' + 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(:image) { double } let(:submission) { FactoryBot.create(:submission) } - let(:workspace_path) { '/tmp' } + let(:workspace_path) { WORKSPACE_PATH } + + before(:all) do + FileUtils.mkdir_p(WORKSPACE_PATH) + end + + after(:all) do + FileUtils.rm_rf(WORKSPACE_PATH) + end describe '.check_availability!' do context 'when a socket error occurs' do @@ -129,7 +139,7 @@ describe DockerClient, docker: true do after(:each) { docker_client.send(:create_workspace_files, container, submission) } it 'creates submission-specific directories' do - expect(Dir).to receive(:mkdir).at_least(:once) + expect(Dir).to receive(:mkdir).at_least(:once).and_call_original end it 'copies binary files' do diff --git a/test/factories/error_template_attributes.rb b/test/factories/error_template_attributes.rb index 84e65757..2e867ec5 100644 --- a/test/factories/error_template_attributes.rb +++ b/test/factories/error_template_attributes.rb @@ -1,6 +1,5 @@ FactoryBot.define do factory :error_template_attribute do - error_template nil key "MyString" regex "MyString" end