diff --git a/Gemfile b/Gemfile index bf089d6c..8cd13375 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,7 @@ gem 'faye-websocket' gem 'nokogiri' gem 'd3-rails' gem 'rest-client' +gem 'rubyzip' group :development do gem 'better_errors', platform: :ruby @@ -47,6 +48,7 @@ group :development do gem 'capistrano-rails' gem 'capistrano-rvm' gem 'capistrano-upload-config' + gem 'rack-mini-profiler' gem 'rubocop', require: false gem 'rubocop-rspec' end diff --git a/Gemfile.lock b/Gemfile.lock index a2739805..e440fd0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -195,6 +195,8 @@ GEM pundit (1.1.0) activesupport (>= 3.0.0) rack (1.5.5) + rack-mini-profiler (0.10.1) + rack (>= 1.2.0) rack-test (0.6.3) rack (>= 1.0) rails (4.1.14.1) @@ -324,6 +326,7 @@ GEM json (>= 1.8.0) unf (0.1.4) unf_ext + unf (0.1.4-java) unf_ext (0.0.7.1) unicode-display_width (0.3.1) web-console (2.3.0) @@ -383,6 +386,7 @@ DEPENDENCIES pry puma (~> 2.15.3) pundit + rack-mini-profiler rails (~> 4.1.13) rails-i18n (~> 4.0.0) rake @@ -393,6 +397,7 @@ DEPENDENCIES rubocop rubocop-rspec rubytree + rubyzip sass-rails (~> 4.0.3) sdoc (~> 0.4.0) selenium-webdriver diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index c00d7350..d173198f 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -19,6 +19,10 @@ $(function() { var SERVER_SEND_EVENT = 2; var editors = []; + var editor_for_file = new Map(); + var regex_for_language = new Map(); + var tracepositions_regex; + var active_file = undefined; var active_frame = undefined; var running = false; @@ -176,7 +180,10 @@ $(function() { var downloadCode = function(event) { event.preventDefault(); createSubmission(this, null,function(response) { - var url = response.download_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); + var url = response.download_url; + + // to download just a single file, use the following url + //var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); window.location = url; }); }; @@ -415,12 +422,18 @@ $(function() { editor.setTheme(THEME); editor.commands.bindKey("ctrl+alt+0", null); editors.push(editor); + editor_for_file.set($(element).parent().data('filename'), editor); var session = editor.getSession(); session.setMode($(element).data('mode')); session.setTabSize($(element).data('indent-size')); session.setUseSoftTabs(true); session.setUseWrapMode(true); + // set regex for parsing error traces based on the mode of the main file. + if( $(element).parent().data('role') == "main_file"){ + tracepositions_regex = regex_for_language.get($(element).data('mode')); + } + var file_id = $(element).data('id'); /* @@ -468,6 +481,12 @@ $(function() { $('#request-for-comments').on('click', requestComments); }; + + var initializeRegexes = function(){ + regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g); + regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g); + } + var initializeTooltips = function() { $('[data-tooltip]').tooltip(); }; @@ -602,8 +621,6 @@ $(function() { element.addClass('text-success').append(output.stdout); flowrOutputBuffer += output.stdout; QaApiOutputBuffer.stdout += output.stdout; - - //}else{ // element.addClass('text-success'); // element.data('content_buffer' , element.data('content_buffer') + output.stdout); @@ -892,7 +909,9 @@ $(function() { } var showWorkspaceTab = function(event) { - event.preventDefault(); + if(event){ + event.preventDefault(); + } showTab(0); }; @@ -1053,6 +1072,7 @@ $(function() { killWebsocketAndContainer(); handleQaApiOutput(); handleStderrOutputForFlowr(); + augmentStacktraceInOutput(); break; case 'timeout': // just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes. @@ -1064,6 +1084,41 @@ $(function() { } }; + + var jumpToSourceLine = function(event){ + var file = $(event.target).data('file'); + var line = $(event.target).data('line'); + + showWorkspaceTab(null); + // set active file ?!?! + + var frame = $('div.frame[data-filename="' + file + '"]'); + showFrame(frame); + + var editor = editor_for_file.get(file); + editor.gotoLine(line, 0); + + }; + + var augmentStacktraceInOutput = function() { + if(tracepositions_regex){ + var element = $('#output>pre'); + var text = element.text(); + element.on( "click", "a", jumpToSourceLine); + + var matches; + + while(matches = tracepositions_regex.exec(text)){ + var frame = $('div.frame[data-filename="' + matches[1] + '"]') + + if(frame.length > 0){ + element.html(text.replace(matches[0], "" + matches[0] + "")); + } + } + } + + }; + var renderWebsocketOutput = function(msg){ var element = findOrCreateRenderElement(0); element.append(msg.data); @@ -1177,25 +1232,25 @@ $(function() { var file_id = $('.editor').data('id') var question = $('#question').val(); - $.ajax({ - method: 'POST', - url: '/request_for_comments', - data: { - request_for_comment: { - exercise_id: exercise_id, - file_id: file_id, - question: question, - "requested_at(1i)": 2015, // these are the timestamp values that the request handler demands - "requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway - "requested_at(3i)":27, - "requested_at(4i)":17, - "requested_at(5i)":06 + var createRequestForComments = function(submission) { + $.ajax({ + method: 'POST', + url: '/request_for_comments', + data: { + request_for_comment: { + exercise_id: exercise_id, + file_id: file_id, + submission_id: submission.id, + question: question + } } - } - }).done(function() { - hideSpinner(); - $.flash.success({ text: $('#askForCommentsButton').data('message-success') }) - }).error(ajaxError); + }).done(function() { + hideSpinner(); + $.flash.success({ text: $('#askForCommentsButton').data('message-success') }); + }).error(ajaxError); + } + + createSubmission($('.requestCommentsButton'), null, createRequestForComments); $('#comment-modal').modal('hide'); var button = $('.requestCommentsButton'); @@ -1224,6 +1279,7 @@ $(function() { if ($('#editor').isPresent()) { if (isBrowserSupported()) { + initializeRegexes(); initializeCodePilot(); $('.score, #development-environment').show(); configureEditors(); diff --git a/app/assets/javascripts/exercises.js b/app/assets/javascripts/exercises.js index 1f861ef6..81da8cf8 100644 --- a/app/assets/javascripts/exercises.js +++ b/app/assets/javascripts/exercises.js @@ -12,6 +12,9 @@ $(function() { $('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id); $('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS); $('body, html').scrollTo('#add-file'); + // if we collapse the file forms by default, we need to click on the new element in order to open it. + // however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list. + //$('#files li:last>div:first>a>div').click(); }; var ajaxError = function() { @@ -148,6 +151,22 @@ $(function() { }); }; + var updateFileTemplates = function(fileType) { + var jqxhr = $.ajax({ + url: '/file_templates/by_file_type/' + fileType + '.json', + dataType: 'json' + }); + jqxhr.done(function(response) { + var noTemplateLabel = $('#noTemplateLabel').data('text'); + var options = ""; + for (var i = 0; i < response.length; i++) { + options += "" + } + $("#code_ocean_file_file_template_id").find('option').remove().end().append($(options)); + }); + jqxhr.fail(ajaxError); + } + if ($.isController('exercises')) { if ($('table').isPresent()) { enableBatchUpdate(); @@ -162,6 +181,10 @@ $(function() { inferFileAttributes(); observeFileRoleChanges(); overrideTextareaTabBehavior(); + } else if ($('#files.jstree').isPresent()) { + var fileTypeSelect = $('#code_ocean_file_file_type_id'); + fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())}); + updateFileTemplates(fileTypeSelect.val()); } toggleCodeHeight(); if (window.hljs) { diff --git a/app/assets/javascripts/file_templates.js.coffee b/app/assets/javascripts/file_templates.js.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/file_templates.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index f348c025..eb1b5300 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -49,7 +49,10 @@ div#chart_2 { } - +a.file-heading { + color: black !important; + text-decoration: none; +} .bar { fill: orange; diff --git a/app/assets/stylesheets/file_templates.css.scss b/app/assets/stylesheets/file_templates.css.scss new file mode 100644 index 00000000..bf8e27e8 --- /dev/null +++ b/app/assets/stylesheets/file_templates.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the FileTemplates controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/code_ocean/files_controller.rb b/app/controllers/code_ocean/files_controller.rb index 74c1932f..55d8b369 100644 --- a/app/controllers/code_ocean/files_controller.rb +++ b/app/controllers/code_ocean/files_controller.rb @@ -10,6 +10,11 @@ module CodeOcean def create @file = CodeOcean::File.new(file_params) + if @file.file_template_id + content = FileTemplate.find(@file.file_template_id).content + content.sub! '{{file_name}}', @file.name + @file.content = content + end authorize! create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) }) end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index fd6840ff..fe7f454c 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -46,7 +46,11 @@ class CommentsController < ApplicationController # POST /comments # POST /comments.json def create - @comment = Comment.new(comment_params) + @comment = Comment.new(comment_params_without_request_id) + + if comment_params[:request_id] + UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user) + end respond_to do |format| if @comment.save @@ -64,7 +68,7 @@ class CommentsController < ApplicationController # PATCH/PUT /comments/1.json def update respond_to do |format| - if @comment.update(comment_params) + if @comment.update(comment_params_without_request_id) format.html { head :no_content, notice: 'Comment was successfully updated.' } format.json { render :show, status: :ok, location: @comment } else @@ -101,10 +105,14 @@ class CommentsController < ApplicationController @comment = Comment.find(params[:id]) end + def comment_params_without_request_id + comment_params.except :request_id + end + # Never trust parameters from the scary internet, only allow the white list through. def comment_params #params.require(:comment).permit(:user_id, :file_id, :row, :column, :text) # fuer production mode, damit böse menschen keine falsche user_id uebergeben: - params.require(:comment).permit(:file_id, :row, :column, :text).merge(user_id: current_user.id, user_type: current_user.class.name) + params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name) end end diff --git a/app/controllers/concerns/file_parameters.rb b/app/controllers/concerns/file_parameters.rb index e61e719e..295b66c3 100644 --- a/app/controllers/concerns/file_parameters.rb +++ b/app/controllers/concerns/file_parameters.rb @@ -1,6 +1,6 @@ module FileParameters def file_attributes - %w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight) + %w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight file_template_id) end private :file_attributes end diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 0304abb4..45fd04d9 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -9,7 +9,6 @@ class ExercisesController < ApplicationController before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload] before_action :set_external_user, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] - before_action :set_teams, only: [:create, :edit, :new, :update] skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml] skip_after_action :verify_authorized, only: [:import_proforma_xml] @@ -119,7 +118,7 @@ class ExercisesController < ApplicationController private :user_by_code_harbor_token def exercise_params - params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) + params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) end private :exercise_params @@ -195,11 +194,6 @@ class ExercisesController < ApplicationController end private :set_file_types - def set_teams - @teams = Team.all.order(:name) - end - private :set_teams - def show end diff --git a/app/controllers/file_templates_controller.rb b/app/controllers/file_templates_controller.rb new file mode 100644 index 00000000..a6039500 --- /dev/null +++ b/app/controllers/file_templates_controller.rb @@ -0,0 +1,94 @@ +class FileTemplatesController < ApplicationController + before_action :set_file_template, only: [:show, :edit, :update, :destroy] + + def authorize! + authorize(@file_template || @file_templates) + end + private :authorize! + + def by_file_type + @file_templates = FileTemplate.where(:file_type_id => params[:file_type_id]) + authorize! + respond_to do |format| + format.json { render :show, status: :ok, json: @file_templates.to_json } + end + end + + # GET /file_templates + # GET /file_templates.json + def index + @file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page]) + authorize! + end + + # GET /file_templates/1 + # GET /file_templates/1.json + def show + authorize! + end + + # GET /file_templates/new + def new + @file_template = FileTemplate.new + authorize! + end + + # GET /file_templates/1/edit + def edit + authorize! + end + + # POST /file_templates + # POST /file_templates.json + def create + @file_template = FileTemplate.new(file_template_params) + authorize! + + respond_to do |format| + if @file_template.save + format.html { redirect_to @file_template, notice: 'File template was successfully created.' } + format.json { render :show, status: :created, location: @file_template } + else + format.html { render :new } + format.json { render json: @file_template.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /file_templates/1 + # PATCH/PUT /file_templates/1.json + def update + authorize! + respond_to do |format| + if @file_template.update(file_template_params) + format.html { redirect_to @file_template, notice: 'File template was successfully updated.' } + format.json { render :show, status: :ok, location: @file_template } + else + format.html { render :edit } + format.json { render json: @file_template.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /file_templates/1 + # DELETE /file_templates/1.json + def destroy + authorize! + @file_template.destroy + respond_to do |format| + format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_file_template + @file_template = FileTemplate.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def file_template_params + params[:file_template].permit(:name, :file_type_id, :content) + end +end diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index 555dad09..37d8bef9 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -1,5 +1,5 @@ class RequestForCommentsController < ApplicationController - before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy] + before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved] skip_after_action :verify_authorized @@ -20,6 +20,18 @@ class RequestForCommentsController < ApplicationController render 'index' end + def mark_as_solved + authorize! + @request_for_comment.solved = true + respond_to do |format| + if @request_for_comment.save + format.json { render :show, status: :ok, location: @request_for_comment } + else + format.json { render json: @request_for_comment.errors, status: :unprocessable_entity } + end + end + end + # GET /request_for_comments/1 # GET /request_for_comments/1.json def show @@ -70,6 +82,6 @@ class RequestForCommentsController < ApplicationController # Never trust parameters from the scary internet, only allow the white list through. def request_for_comment_params - params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at).merge(user_id: current_user.id, user_type: current_user.class.name) + 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 end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 335e4b0c..1b6f9421 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -6,9 +6,9 @@ class SubmissionsController < ApplicationController include SubmissionScoring include Tubesock::Hijack - before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test] + before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :show, :statistics, :stop, :test] before_action :set_docker_client, only: [:run, :test] - before_action :set_files, only: [:download_file, :render_file, :show] + before_action :set_files, only: [:download, :download_file, :render_file, :show] before_action :set_file, only: [:download_file, :render_file] before_action :set_mime_type, only: [:download_file, :render_file] skip_before_action :verify_authenticity_token, only: [:download_file, :render_file] @@ -53,6 +53,20 @@ class SubmissionsController < ApplicationController end end + def download + # files = @submission.files.map{ } + # zipline( files, 'submission.zip') + # send_data(@file.content, filename: @file.name_with_extension) + require 'zip' + stringio = Zip::OutputStream.write_buffer do |zio| + @files.each do |file| + zio.put_next_entry(file.name_with_extension) + zio.write(file.content) + end + end + send_data(stringio.string, filename: @submission.exercise.title.tr(" ", "_") + ".zip") + end + def download_file if @file.native_file? send_file(@file.native_file.path) diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb deleted file mode 100644 index 1ca8f1fe..00000000 --- a/app/controllers/teams_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -class TeamsController < ApplicationController - include CommonBehavior - - before_action :set_team, only: MEMBER_ACTIONS - - def authorize! - authorize(@team || @teams) - end - private :authorize! - - def create - @team = Team.new(team_params) - authorize! - create_and_respond(object: @team) - end - - def destroy - destroy_and_respond(object: @team) - end - - def edit - end - - def index - @teams = Team.all.includes(:internal_users).order(:name).paginate(page: params[:page]) - authorize! - end - - def new - @team = Team.new - authorize! - end - - def set_team - @team = Team.find(params[:id]) - authorize! - end - private :set_team - - def show - end - - def team_params - params[:team].permit(:name, internal_user_ids: []) - end - private :team_params - - def update - update_and_respond(object: @team, params: team_params) - end -end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 4b3c71f4..e1773d48 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -11,4 +11,13 @@ class UserMailer < ActionMailer::Base @reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token) mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email) end + + def got_new_comment(comment, request_for_comment, commenting_user) + # todo: check whether we can take the last known locale of the receiver? + @receiver_displayname = request_for_comment.user.displayname + @commenting_user_displayname = commenting_user.displayname + @comment_text = comment.text + @rfc_link = request_for_comment_url(request_for_comment) + mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver + end end diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb index 58440f1b..22c6e877 100644 --- a/app/models/code_ocean/file.rb +++ b/app/models/code_ocean/file.rb @@ -35,6 +35,7 @@ module CodeOcean has_many :files has_many :testruns + has_many :comments alias_method :descendants, :files mount_uploader :native_file, FileUploader diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 4a1e0486..5b7ff9b2 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -11,7 +11,6 @@ class Exercise < ActiveRecord::Base belongs_to :execution_environment has_many :submissions - belongs_to :team has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions diff --git a/app/models/external_user.rb b/app/models/external_user.rb index 8038d2dc..538f4997 100644 --- a/app/models/external_user.rb +++ b/app/models/external_user.rb @@ -7,7 +7,9 @@ class ExternalUser < ActiveRecord::Base def displayname result = name if(consumer.name == 'openHPI') - result = Xikolo::UserClient.get(external_id.to_s)[:display_name] + result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do + Xikolo::UserClient.get(external_id.to_s)[:display_name] + end end result end diff --git a/app/models/file_template.rb b/app/models/file_template.rb new file mode 100644 index 00000000..ef068e13 --- /dev/null +++ b/app/models/file_template.rb @@ -0,0 +1,10 @@ +class FileTemplate < ActiveRecord::Base + + belongs_to :file_type + + + def to_s + name + end + +end diff --git a/app/models/file_type.rb b/app/models/file_type.rb index 53bf18dc..d3b519d5 100644 --- a/app/models/file_type.rb +++ b/app/models/file_type.rb @@ -12,6 +12,7 @@ class FileType < ActiveRecord::Base has_many :execution_environments has_many :files + has_many :file_templates validates :binary, boolean_presence: true validates :editor_mode, presence: true, unless: :binary? diff --git a/app/models/internal_user.rb b/app/models/internal_user.rb index e5cebde9..8f1bf04b 100644 --- a/app/models/internal_user.rb +++ b/app/models/internal_user.rb @@ -3,8 +3,6 @@ class InternalUser < ActiveRecord::Base authenticates_with_sorcery! - has_and_belongs_to_many :teams - validates :email, presence: true, uniqueness: true validates :password, confirmation: true, if: :password_void?, on: :update, presence: true validates :role, inclusion: {in: ROLES} diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb index aca99938..63d932fc 100644 --- a/app/models/request_for_comment.rb +++ b/app/models/request_for_comment.rb @@ -1,7 +1,8 @@ class RequestForComment < ActiveRecord::Base + include Creation + belongs_to :submission belongs_to :exercise belongs_to :file, class_name: 'CodeOcean::File' - belongs_to :user, polymorphic: true before_create :set_requested_timestamp @@ -13,10 +14,8 @@ class RequestForComment < ActiveRecord::Base self.requested_at = Time.now end - def submission - Submission.find(file.context_id) - end - + # not used right now, finds the last submission for the respective user and exercise. + # might be helpful to check whether the exercise has been solved in the meantime. def last_submission Submission.find_by_sql(" select * from submissions where exercise_id = #{exercise_id} AND @@ -25,12 +24,27 @@ class RequestForComment < ActiveRecord::Base limit 1").first end + # not used any longer, since we directly saved the submission_id now. + # Was used before that to determine the submission belonging to the request_for_comment. + def last_submission_before_creation + Submission.find_by_sql(" select * from submissions + where exercise_id = #{exercise_id} AND + user_id = #{user_id} AND + '#{created_at.localtime}' > created_at + order by created_at desc + limit 1").first + end + + def comments_count + submission.files.map { |file| file.comments.size}.sum + end + def to_s "RFC-" + self.id.to_s end private def self.row_number_user_sql - select("id, user_id, exercise_id, file_id, question, requested_at, created_at, updated_at, user_type, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql + select("id, user_id, exercise_id, file_id, question, created_at, updated_at, user_type, solved, submission_id, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql end end diff --git a/app/models/submission.rb b/app/models/submission.rb index 323f1d58..5a95587f 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -2,7 +2,7 @@ class Submission < ActiveRecord::Base include Context include Creation - CAUSES = %w(assess download file render run save submit test autosave) + CAUSES = %w(assess download file render run save submit test autosave requestComments) FILENAME_URL_PLACEHOLDER = '{filename}' belongs_to :exercise @@ -28,13 +28,17 @@ class Submission < ActiveRecord::Base ancestors.merge(descendants).values end - [:download, :render, :run, :test].each do |action| + [:download_file, :render, :run, :test].each do |action| filename = FILENAME_URL_PLACEHOLDER.gsub(/\W/, '') define_method("#{action}_url") do Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self, filename).sub(filename, FILENAME_URL_PLACEHOLDER) end end + def download_url + Rails.application.routes.url_helpers.send(:download_submission_path, self) + end + def main_file collect_files.detect(&:main_file?) end diff --git a/app/models/team.rb b/app/models/team.rb deleted file mode 100644 index a0dcb8d6..00000000 --- a/app/models/team.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Team < ActiveRecord::Base - has_and_belongs_to_many :internal_users - alias_method :members, :internal_users - - validates :name, presence: true - - def to_s - name - end -end diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 29a1c570..55f7d16b 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -13,24 +13,19 @@ class ExercisePolicy < AdminOrAuthorPolicy end [:clone?, :destroy?, :edit?, :statistics?, :update?].each do |action| - define_method(action) { admin? || author? || team_member? } + define_method(action) { admin? || author?} end [:implement?, :submit?, :reload?].each do |action| define_method(action) { everyone } end - def team_member? - @record.team.try(:members, []).include?(@user) if @record.team - end - private :team_member? - class Scope < Scope def resolve if @user.admin? @scope.all elsif @user.internal_user? - @scope.where('user_id = ? OR public = TRUE OR (team_id IS NOT NULL AND team_id IN (SELECT t.id FROM teams t JOIN internal_users_teams iut ON t.id = iut.team_id WHERE iut.internal_user_id = ?))', @user.id, @user.id) + @scope.where('user_id = ? OR public = TRUE', @user.id) else @scope.none end diff --git a/app/policies/file_template_policy.rb b/app/policies/file_template_policy.rb new file mode 100644 index 00000000..92ced442 --- /dev/null +++ b/app/policies/file_template_policy.rb @@ -0,0 +1,11 @@ +class FileTemplatePolicy < AdminOnlyPolicy + + def show? + everyone + end + + def by_file_type? + everyone + end + +end diff --git a/app/policies/request_for_comment_policy.rb b/app/policies/request_for_comment_policy.rb index cf252338..f592e3bd 100644 --- a/app/policies/request_for_comment_policy.rb +++ b/app/policies/request_for_comment_policy.rb @@ -1,5 +1,8 @@ class RequestForCommentPolicy < ApplicationPolicy - + def author? + @user == @record.author + end + private :author? def create? everyone @@ -13,6 +16,10 @@ class RequestForCommentPolicy < ApplicationPolicy define_method(action) { admin? } end + def mark_as_solved? + admin? || author? + end + def edit? admin? end diff --git a/app/policies/submission_policy.rb b/app/policies/submission_policy.rb index 18d39f4c..861f5695 100644 --- a/app/policies/submission_policy.rb +++ b/app/policies/submission_policy.rb @@ -8,7 +8,7 @@ class SubmissionPolicy < ApplicationPolicy everyone end - [:download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action| + [:download?, :download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/policies/team_policy.rb b/app/policies/team_policy.rb deleted file mode 100644 index ff05c0c3..00000000 --- a/app/policies/team_policy.rb +++ /dev/null @@ -1,14 +0,0 @@ -class TeamPolicy < ApplicationPolicy - [:create?, :index?, :new?].each do |action| - define_method(action) { admin? } - end - - [:destroy?, :edit?, :show?, :update?].each do |action| - define_method(action) { admin? || member? } - end - - def member? - @record.members.include?(@user) - end - private :member? -end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 4ab39e30..8aa289d3 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -8,7 +8,7 @@ - if current_user.admin? li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path) li.divider - - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) } + - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate, InternalUser, Submission].sort_by { |model| model.model_name.human(count: 2) } - models.each do |model| - if policy(model).index? li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) diff --git a/app/views/code_ocean/files/_form.html.slim b/app/views/code_ocean/files/_form.html.slim index 07dd3355..46c5b2c2 100644 --- a/app/views/code_ocean/files/_form.html.slim +++ b/app/views/code_ocean/files/_form.html.slim @@ -8,5 +8,9 @@ .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') + .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') = f.hidden_field(:context_id) + .hidden#noTemplateLabel data-text=t('file_template.no_template_label') .actions = render('shared/submit_button', f: f, object: CodeOcean::File.new) diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index eacc62a9..01640fa8 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -14,6 +14,6 @@ .editor-content.hidden data-file-id=file.ancestor_id = file.content .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-id=file.id - button.btn.btn-primary.requestCommentsButton type='button' - i.fa.fa-comment-o + button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" + i.fa.fa-comment = t('exercises.editor.requestComments') \ No newline at end of file diff --git a/app/views/exercises/_file_form.html.slim b/app/views/exercises/_file_form.html.slim index c737f068..7bb4cd27 100644 --- a/app/views/exercises/_file_form.html.slim +++ b/app/views/exercises/_file_form.html.slim @@ -1,10 +1,10 @@ - id = f.object.id li.panel.panel-default .panel-heading role="tab" id="heading" - div.clearfix role="button" - span = f.object.name - a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{id}" collapse - .panel-collapse.collapse.in id="collapse#{id}" role="tabpanel" + a.file-heading data-toggle="collapse" data-parent="#files" href="#collapse#{id}" + div.clearfix role="button" + span = f.object.name + .panel-collapse.collapse-in id="collapse#{id}" role="tabpanel" .panel-body .clearfix = link_to(t('shared.destroy'), '#', class:'btn btn-warning btn-sm discard-file pull-right') .form-group diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index f0f69f7a..968540af 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -17,9 +17,6 @@ = f.label(:instructions) = f.hidden_field(:instructions) .form-control.markdown - /.form-group - = f.label(:team_id) - = f.collection_select(:team_id, @teams, :id, :name, {include_blank: true}, class: 'form-control') .checkbox label = f.check_box(:public) diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim index 56bd614b..3c755be6 100644 --- a/app/views/exercises/external_users/statistics.html.slim +++ b/app/views/exercises/external_users/statistics.html.slim @@ -50,9 +50,9 @@ h1 = "#{@exercise} (external user #{@external_user})" td -submission.testruns.each do |run| - if run.passed - .unit-test-result.positive-result + .unit-test-result.positive-result title=run.output - else - .unit-test-result.negative-result + .unit-test-result.negative-result title=run.output td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 -working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0)) p = t('.addendum') diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index fc28272a..5c554da8 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -12,7 +12,6 @@ h1 = row(label: 'exercise.description', value: render_markdown(@exercise.description)) = row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) -= row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) = row(label: 'exercise.public', value: @exercise.public?) = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) @@ -23,13 +22,15 @@ h1 h2 = t('activerecord.attributes.exercise.files') ul.list-unstyled.panel-group#files - - @exercise.files.each do |file| + - @exercise.files.order('name').each do |file| li.panel.panel-default .panel-heading role="tab" id="heading" - div.clearfix role="button" - span.panel-title = file.name_with_extension - a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{file.id}" collapse - .panel-collapse.collapse.in id="collapse#{file.id}" role="tabpanel" + a.file-heading data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}" + div.clearfix role="button" + span = file.name_with_extension + // probably set an icon here that shows that the rows can be collapsed + //span.pull-right.collapse.in class="collapse#{file.id}" ☼ + .panel-collapse.collapse class="collapse#{file.id}" role="tabpanel" .panel-body - if policy(file).destroy? .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm pull-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/file_templates/_form.html.slim b/app/views/file_templates/_form.html.slim new file mode 100644 index 00000000..1a5c34bc --- /dev/null +++ b/app/views/file_templates/_form.html.slim @@ -0,0 +1,12 @@ += form_for(@file_template) do |f| + = render('shared/form_errors', object: @file_template) + .form-group + = f.label(:name) + = f.text_field(:name, class: 'form-control', required: true) + .form-group + = f.label(:file_type_id) + = f.collection_select(:file_type_id, FileType.all.order(:name), :id, :name, {}, class: 'form-control') + .form-group + = f.label(:content) + = f.text_area(:content, class: 'form-control') + .actions = render('shared/submit_button', f: f, object: @file_template) diff --git a/app/views/file_templates/edit.html.slim b/app/views/file_templates/edit.html.slim new file mode 100644 index 00000000..c198271f --- /dev/null +++ b/app/views/file_templates/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @file_template + += render('form') diff --git a/app/views/file_templates/index.html.slim b/app/views/file_templates/index.html.slim new file mode 100644 index 00000000..3022ea53 --- /dev/null +++ b/app/views/file_templates/index.html.slim @@ -0,0 +1,20 @@ +h1 = FileTemplate.model_name.human(count: 2) + +.table-responsive + table.table + thead + tr + th = t('activerecord.attributes.file_template.name') + th = t('activerecord.attributes.file_template.file_type') + th colspan=3 = t('shared.actions') + tbody + - @file_templates.each do |file_template| + tr + td = file_template.name + td = link_to(file_template.file_type, file_type_path(file_template.file_type)) + td = link_to(t('shared.show'), file_template) + td = link_to(t('shared.edit'), edit_file_template_path(file_template)) + td = link_to(t('shared.destroy'), file_template, data: {confirm: t('shared.confirm_destroy')}, method: :delete) + += render('shared/pagination', collection: @file_templates) +p = render('shared/new_button', model: FileTemplate) diff --git a/app/views/file_templates/new.html.slim b/app/views/file_templates/new.html.slim new file mode 100644 index 00000000..bf434860 --- /dev/null +++ b/app/views/file_templates/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: FileTemplate.model_name.human) + += render('form') diff --git a/app/views/file_templates/show.html.slim b/app/views/file_templates/show.html.slim new file mode 100644 index 00000000..19f0d28f --- /dev/null +++ b/app/views/file_templates/show.html.slim @@ -0,0 +1,7 @@ +h1 + = @file_template + = render('shared/edit_button', object: @file_template) + += row(label: 'file_template.name', value: @file_template.name) += row(label: 'file_template.file_type', value: link_to(@file_template.file_type, file_type_path(@file_template.file_type))) += row(label: 'file_template.content', value: @file_template.content) diff --git a/app/views/request_for_comments/index.html.slim b/app/views/request_for_comments/index.html.slim index 6d4e059c..b7ada0a2 100644 --- a/app/views/request_for_comments/index.html.slim +++ b/app/views/request_for_comments/index.html.slim @@ -4,19 +4,29 @@ h1 = RequestForComment.model_name.human(count: 2) table.table.sortable thead tr + th + i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right" th = t('activerecord.attributes.request_for_comments.exercise') th = t('activerecord.attributes.request_for_comments.question') + th + i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center" th = t('activerecord.attributes.request_for_comments.username') th = t('activerecord.attributes.request_for_comments.requested_at') tbody - @request_for_comments.each do |request_for_comment| tr data-id=request_for_comment.id + - if request_for_comment.solved? + td + span class="fa fa-check" aria-hidden="true" + - else + td = '' td = link_to(request_for_comment.exercise.title, request_for_comment) - if request_for_comment.has_attribute?(:question) && request_for_comment.question td = truncate(request_for_comment.question, length: 200) - else td = '-' + td = request_for_comment.comments_count td = request_for_comment.user.displayname - td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.requested_at)) + td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at)) = render('shared/pagination', collection: @request_for_comments) \ No newline at end of file diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index e4dc7f06..4089d878 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -1,46 +1,76 @@
-

<%= Exercise.find(@request_for_comment.exercise_id) %>

+

<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>

<% user = @request_for_comment.user - submission_id = ActiveRecord::Base.connection.execute("select id from submissions - where exercise_id = - #{@request_for_comment.exercise_id} AND - user_id = #{@request_for_comment.user_id} AND - '#{@request_for_comment.created_at}' > created_at - order by created_at desc - limit 1").first['id'].to_i - submission = Submission.find(submission_id) + submission = @request_for_comment.submission %> - <%= user.displayname %> | <%= @request_for_comment.requested_at %> + <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>

+
+ <%= t('activerecord.attributes.exercise.description') %>: "<%= render_markdown(@request_for_comment.exercise.description) %>" +
+
<% if @request_for_comment.question and not @request_for_comment.question == '' %> - <%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>" + <%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>" <% else %> - <%= t('request_for_comments.no_question') %> + <%= t('activerecord.attributes.request_for_comments.question')%>: <%= t('request_for_comments.no_question') %> <% end %>
+ <% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %> + + <% elsif (@request_for_comment.solved?) %> + + <% else %> + + <% end %>
<% submission.files.each do |file| %> <%= (file.path or "") + "/" + file.name + file.file_type.file_extension %> -
<%= file.content %> +
<%= file.content %>
<% end %> <%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>