diff --git a/Gemfile b/Gemfile index 99870094..540b11b2 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ gem 'tubesock' gem 'faye-websocket' gem 'nokogiri' gem 'd3-rails' +gem 'rest-client’ group :development do gem 'better_errors', platform: :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 579c45e9..a2739805 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,8 @@ GEM docker-api (1.25.0) excon (>= 0.38.0) json + domain_name (0.5.25) + unf (>= 0.0.5, < 1.0.0) erubis (2.7.0) eventmachine (1.0.9.1) eventmachine (1.0.9.1-java) @@ -127,6 +129,8 @@ GEM forgery (0.6.0) highline (1.7.8) hike (1.2.3) + http-cookie (1.0.2) + domain_name (~> 0.5) i18n (0.7.0) ims-lti (1.1.10) builder @@ -157,6 +161,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (3.0.2) + netrc (0.10.3) newrelic_rpm (3.14.3.313) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) @@ -220,6 +225,10 @@ GEM polyamorous (~> 1.2) rdoc (4.2.2) json (~> 1.4) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rspec (3.4.0) rspec-core (~> 3.4.0) rspec-expectations (~> 3.4.0) @@ -313,6 +322,9 @@ GEM uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.1) unicode-display_width (0.3.1) web-console (2.3.0) activemodel (>= 4.0) @@ -375,6 +387,7 @@ DEPENDENCIES rails-i18n (~> 4.0.0) rake ransack + rest-client rspec-autotest rspec-rails rubocop diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index 968951a8..c69551bb 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -13,6 +13,7 @@ $(function() { var THEME = 'ace/theme/textmate'; var REMEMBER_TAB = false; var AUTOSAVE_INTERVAL = 15 * 1000; + var REQUEST_FOR_COMMENTS_DELAY = 3 * 60 * 1000; var NONE = 0; var WEBSOCKET = 1; var SERVER_SEND_EVENT = 2; @@ -110,18 +111,6 @@ $(function() { var createSubmission = function(initiator, filter, callback) { showSpinner(initiator); - - var annotations_arr = []; - - $('.editor').each(function(index, element) { - var editor = ace.edit(element); - var cleaned_annotations = editor.getSession().getAnnotations(); - for(var i = cleaned_annotations.length-1; i>=0; --i){ - cleaned_annotations[i].text = cleaned_annotations[i].text.replace(cleaned_annotations[i].username + ": ", ""); - } - annotations_arr = annotations_arr.concat(editor.getSession().getAnnotations()); - }); - var jqxhr = ajax({ data: { submission: { @@ -129,7 +118,7 @@ $(function() { exercise_id: $('#editor').data('exercise-id'), files_attributes: (filter || _.identity)(collectFiles()) }, - annotations_arr: annotations_arr + annotations_arr: [] }, dataType: 'json', method: 'POST', @@ -169,7 +158,6 @@ $(function() { } } } - setAnnotations(editors[i], $(editors[i].container).data('id')); } // toggle button states (it might be the case that the request for comments button has to be enabled toggleButtonStates(); @@ -256,8 +244,6 @@ $(function() { } }; - - var getPanelClass = function(result) { if (result.stderr && !result.score) { return 'panel-danger'; @@ -425,7 +411,6 @@ $(function() { session.setUseWrapMode(true); var file_id = $(element).data('id'); - //setAnnotations(editor, file_id); /* * Register event handlers @@ -434,17 +419,6 @@ $(function() { // editor itself editor.on("paste", handlePasteEvent); editor.on("copy", handleCopyEvent); - editor.on("guttermousedown", handleSidebarClick); - - /* // alternative: - editor.on("guttermousedown", function(e) { - handleSidebarClick(e); - }); - */ - - //session - session.on('annotationRemoval', handleAnnotationRemoval); - session.on('annotationChange', handleAnnotationChange); // listener for autosave session.on("change", function (deltaObject) { @@ -453,158 +427,6 @@ $(function() { }); }; - var hasCommentsInRow = function (editor, row){ - return editor.getSession().getAnnotations().some(function(element) { - return element.row === row; - }) - }; - - var getCommentsForRow = function (editor, row){ - return editor.getSession().getAnnotations().filter(function(element) { - return element.row === row; - }) - }; - - var setAnnotations = function (editor, file_id){ - var session = editor.getSession(); - var url = "/comments"; - - var jqrequest = $.ajax({ - dataType: 'json', - method: 'GET', - url: url, - data: { - file_id: file_id - } - }); - - jqrequest.done(function(response){ - setAnnotationsCallback(response, session); - }); - jqrequest.fail(ajaxError); - }; - - var setAnnotationsCallback = function (response, session) { - var annotations = response; - - // add classname and the username in front of each comment - $.each(annotations, function(index, comment){ - comment.className = "code-ocean_comment"; - comment.text = comment.username + ": " + comment.text; - }); - - session.setAnnotations(annotations); - } - - var deleteComment = function (file_id, row, editor) { - var jqxhr = $.ajax({ - type: 'DELETE', - url: "/comments", - data: { - row: row, - file_id: file_id } - }); - jqxhr.done(function (response) { - setAnnotations(editor, file_id); - }); - jqxhr.fail(ajaxError); - } - - var createComment = function (file_id, row, editor, commenttext){ - var jqxhr = $.ajax({ - data: { - comment: { - file_id: file_id, - row: row, - column: 0, - text: commenttext - } - }, - dataType: 'json', - method: 'POST', - url: "/comments" - }); - jqxhr.done(function(response){ - setAnnotations(editor, file_id); - }); - jqxhr.fail(ajaxError); - }; - - var handleAnnotationRemoval = function(removedAnnotations) { - removedAnnotations.forEach(function(annotation) { - $.ajax({ - method: 'DELETE', - url: '/comment_by_id', - data: { - id: annotation.id, - } - }) - }) - }; - - var handleAnnotationChange = function(changedAnnotations) { - changedAnnotations.forEach(function(annotation) { - $.ajax({ - method: 'PUT', - url: '/comments', - data: { - id: annotation.id, - user_id: $('#editor').data('user-id'), - comment: { - row: annotation.row, - text: annotation.text - } - } - }) - }) - }; - - // Code for clicks on gutter / sidepanel - var handleSidebarClick = function(e) { - var target = e.domEvent.target; - var editor = e.editor; - - if (target.className.indexOf("ace_gutter-cell") == -1) return; - if (!editor.isFocused()) return; - if (e.clientX > 25 + target.getBoundingClientRect().left) return; - - var row = e.getDocumentPosition().row; - e.stop(); - - var commentModal = $('#comment-modal'); - - if (hasCommentsInRow(editor, row)) { - var rowComments = getCommentsForRow(editor, row); - var comments = _.pluck(rowComments, 'text').join('\n'); - commentModal.find('#other-comments').text(comments); - } else { - commentModal.find('#other-comments').text('none'); - } - - commentModal.find('#addCommentButton').off('click'); - commentModal.find('#removeAllButton').off('click'); - - commentModal.find('#addCommentButton').on('click', function(e){ - var commenttext = commentModal.find('textarea').val(); - // attention: use id of data attribute here, not file-id (file-id is the original file) - var file_id = $(editor.container).data('id'); - - if (commenttext !== "") { - createComment(file_id, row, editor, commenttext); - commentModal.modal('hide'); - } - }); - - commentModal.find('#removeAllButton').on('click', function(e){ - // attention: use id of data attribute here, not file-id (file-id is the original file) - var file_id = $(editor.container).data('id'); - deleteComment(file_id,row, editor); - commentModal.modal('hide'); - }); - - commentModal.modal('show'); - }; - var initializeEventHandlers = function() { $(document).on('click', '#results a', showOutput); $(document).on('keypress', handleKeyPress); @@ -612,6 +434,7 @@ $(function() { initializeFileTreeButtons(); initializeWorkflowButtons(); initializeWorkspaceButtons(); + initializeRequestForComments() }; var initializeFileTree = function() { @@ -654,6 +477,20 @@ $(function() { $('#start-over').on('click', confirmReset); }; + var initializeRequestForComments = function () { + var button = $('.requestCommentsButton'); + button.hide(); + button.on('click', function() { + $('#comment-modal').modal('show'); + }); + + $('#askForCommentsButton').on('click', requestComments); + + setTimeout(function() { + button.fadeIn(); + }, REQUEST_FOR_COMMENTS_DELAY); + }; + var isActiveFileBinary = function() { return 'binary' in active_frame.data(); }; @@ -667,7 +504,7 @@ $(function() { filename: filename, id: fileId }; - } + }; var isActiveFileRenderable = function() { return 'renderable' in active_frame.data(); @@ -737,10 +574,14 @@ $(function() { // output_mode_is_streaming = false; //} if (!colorize) { - var stream = _.sortBy([output.stderr || '', output.stdout || ''], function(stream) { - return stream.length; - })[1]; - element.append(stream); + if(output.stdout != ''){ + element.append(output.stdout) + } + + if(output.stderr != ''){ + element.append('There was an error: StdErr: ' + output.stderr); + } + } else if (output.stderr) { element.addClass('text-warning').append(output.stderr); } else if (output.stdout) { @@ -796,10 +637,16 @@ $(function() { var payload = { user: { - resource_uuid: $('#editor').data('user-id') + type: 'User', + uuid: $('#editor').data('user-id') + }, + verb: { + type: eventName + }, + resource: { + type: 'page', + uuid: document.location.href }, - verb: eventName, - resource: {}, timestamp: new Date().toISOString(), with_result: {}, in_context: contextData @@ -1119,7 +966,6 @@ $(function() { $('#run').toggle(isActiveFileRunnable() && !running); $('#stop').toggle(isActiveFileStoppable()); $('#test').toggle(isActiveFileTestable()); - $('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary()); }; var initWebsocketConnection = function(url) { @@ -1302,10 +1148,11 @@ $(function() { } }; - var requestComments = function(e) { + var requestComments = function() { var user_id = $('#editor').data('user-id') var exercise_id = $('#editor').data('exercise-id') var file_id = $('.editor').data('id') + var question = $('#question').val(); $.ajax({ method: 'POST', @@ -1314,6 +1161,7 @@ $(function() { 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, @@ -1322,13 +1170,13 @@ $(function() { } } }).done(function() { - hideSpinner() - $.flash.success({ text: 'Request for comments sent!' }) - }) + hideSpinner(); + $.flash.success({ text: $('#askForCommentsButton').data('message-success') }) + }).error(ajaxError); - showSpinner($('#request-for-comments')) - // hide button until next submission is created - $('#request-for-comments').toggle(false); + $('#comment-modal').modal('hide'); + var button = $('.requestCommentsButton'); + button.fadeOut(); } var initializeCodePilot = function() { diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index efcf1d25..e41153e2 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -88,3 +88,10 @@ button i.fa-spin { color: #777; font-size: 0.8em; } + +.requestCommentsButton { + position: relative; + margin-top: -50px; + margin-right: 25px; + float: right; +} diff --git a/app/controllers/code_ocean/files_controller.rb b/app/controllers/code_ocean/files_controller.rb index 3c255ffa..74c1932f 100644 --- a/app/controllers/code_ocean/files_controller.rb +++ b/app/controllers/code_ocean/files_controller.rb @@ -14,6 +14,21 @@ module CodeOcean create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) }) end + def create_and_respond(options = {}) + @object = options[:object] + respond_to do |format| + if @object.save + yield if block_given? + path = options[:path].try(:call) || @object + respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human), path: path, status: :created) + else + filename = (@object.path || '') + '/' + (@object.name || '') + (@object.file_type.file_extension || '') + format.html { redirect_to(options[:path]); flash[:danger] = t('files.error.filename', name: filename) } + format.json { render(json: @object.errors, status: :unprocessable_entity) } + end + end + end + def destroy @file = CodeOcean::File.find(params[:id]) authorize! diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 15bf99fb..fd6840ff 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -12,41 +12,16 @@ class CommentsController < ApplicationController # GET /comments # GET /comments.json def index - #@comments = Comment.all - #if admin, show all comments. - #check whether user is the author of the passed file_id, if so, show all comments. otherwise, only show comments of the file-author and own comments file = CodeOcean::File.find(params[:file_id]) #there might be no submission yet, so dont use find submission = Submission.find_by(id: file.context_id) if submission - is_admin = false - user_id = current_user.id - - # if we have an internal user, check whether he is an admin - if not current_user.respond_to? :external_id - is_admin = current_user.role == 'admin' - end - - if(is_admin || user_id == submission.user_id) - # fetch all comments for this file - @comments = Comment.where(file_id: params[:file_id]) - else - # fetch comments of the current user - #@comments = Comment.where(file_id: params[:file_id], user_id: user_id) - # fetch comments of file-author and the current user - @comments = Comment.where(file_id: params[:file_id], user_id: [user_id, submission.user_id]) - end - - #add names to comments - # if the user is internal, set the name - + @comments = Comment.where(file_id: params[:file_id]) @comments.map{|comment| - comment.username = comment.user.name - # alternative: # if the user is external, fetch the displayname from xikolo - # Xikolo::UserClient.get(comment.user_id.to_s)[:display_name] + comment.username = comment.user.displayname } else - @comments = Comment.all.limit(0) #we need an empty relation here + @comments = [] end authorize! end @@ -111,14 +86,13 @@ class CommentsController < ApplicationController end def destroy - @comments = Comment.where(file_id: params[:file_id], row: params[:row]) - @comments.delete_all + @comments = Comment.where(file_id: params[:file_id], row: params[:row], user: current_user) + @comments.each { |comment| authorize comment; comment.destroy } respond_to do |format| #format.html { redirect_to comments_url, notice: 'Comments were successfully destroyed.' } format.html { head :no_content, notice: 'Comments were successfully destroyed.' } format.json { head :no_content } end - authorize! end private diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 69d1f46f..0304abb4 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -119,7 +119,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, :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, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) end private :exercise_params diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index 1394f0c3..555dad09 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -70,6 +70,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, :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).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 8f26d986..1fec0b1b 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -124,7 +124,7 @@ class SubmissionsController < ApplicationController tubesock.onmessage do |data| Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) - # Check wether the client send a JSON command and kill container + # Check whether the client send a JSON command and kill container # if the command is 'exit', send it to docker otherwise. begin parsed = JSON.parse(data) diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb index 48ec97d0..58440f1b 100644 --- a/app/models/code_ocean/file.rb +++ b/app/models/code_ocean/file.rb @@ -2,6 +2,19 @@ require File.expand_path('../../../uploaders/file_uploader', __FILE__) require File.expand_path('../../../../lib/active_model/validations/boolean_presence_validator', __FILE__) module CodeOcean + + class FileNameValidator < ActiveModel::Validator + def validate(record) + existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id, + context_id: record.context_id, context_type: record.context_type).to_a + unless existing_files.empty? + if (not record.context.is_a?(Exercise)) || (record.context.new_record?) + record.errors[:base] << 'Duplicate' + end + end + end + end + class File < ActiveRecord::Base include DefaultValues @@ -44,6 +57,8 @@ module CodeOcean validates :weight, if: :teacher_defined_test?, numericality: true, presence: true validates :weight, absence: true, unless: :teacher_defined_test? + validates_with FileNameValidator, fields: [:name, :path, :file_type_id] + ROLES.each do |role| define_method("#{role}?") { self.role == role } end diff --git a/app/models/external_user.rb b/app/models/external_user.rb index c1d9ad28..8038d2dc 100644 --- a/app/models/external_user.rb +++ b/app/models/external_user.rb @@ -3,4 +3,13 @@ class ExternalUser < ActiveRecord::Base validates :consumer_id, presence: true validates :external_id, presence: true + + def displayname + result = name + if(consumer.name == 'openHPI') + result = Xikolo::UserClient.get(external_id.to_s)[:display_name] + end + result + end + end diff --git a/app/models/internal_user.rb b/app/models/internal_user.rb index 54b8df4d..e5cebde9 100644 --- a/app/models/internal_user.rb +++ b/app/models/internal_user.rb @@ -21,4 +21,9 @@ class InternalUser < ActiveRecord::Base def teacher? role == 'teacher' end + + def displayname + name + end + end diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb index e685805f..aca99938 100644 --- a/app/models/request_for_comment.rb +++ b/app/models/request_for_comment.rb @@ -25,8 +25,12 @@ class RequestForComment < ActiveRecord::Base limit 1").first 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, 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, 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 end end diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb index 091ed5e2..ef7a0922 100644 --- a/app/policies/comment_policy.rb +++ b/app/policies/comment_policy.rb @@ -8,7 +8,11 @@ class CommentPolicy < ApplicationPolicy everyone end - [:new?, :show?, :destroy?].each do |action| + def show? + everyone + end + + [:new?, :destroy?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/views/application/_session.html.slim b/app/views/application/_session.html.slim index 52858f8d..cbbf3b12 100644 --- a/app/views/application/_session.html.slim +++ b/app/views/application/_session.html.slim @@ -1,19 +1,17 @@ - if current_user - - if current_user.internal_user? - li.dropdown - a.dropdown-toggle data-toggle='dropdown' href='#' - i.fa.fa-user - = current_user - span.caret - ul.dropdown-menu role='menu' + li.dropdown + a.dropdown-toggle data-toggle='dropdown' href='#' + i.fa.fa-user + = current_user + span.caret + ul.dropdown-menu role='menu' + - if current_user.internal_user? li = link_to(t('consumers.show.link'), current_user.consumer) if current_user.consumer li = link_to(t('internal_users.show.link'), current_user) + li = link_to(t('request_for_comments.index.all'), request_for_comments_path) + li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path) + - if current_user.internal_user? li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete) - - else - li - p.navbar-text - i.fa.fa-user - = current_user - else li = link_to(sign_in_path) do i.fa.fa-sign-in diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index cc5dfe7e..42b12e42 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -38,4 +38,4 @@ = t('exercises.editor.test') = render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) -= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') \ No newline at end of file += render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') \ No newline at end of file diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 9ed5626d..c23158b9 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -2,9 +2,9 @@ hr -= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) -= render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) -= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) -= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-bullhorn', id: 'request-for-comments', label: t('exercises.editor.requestComments')) +- if @exercise.allow_file_creation? + = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) + = render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) + = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) -= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) += render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index dc077e02..eacc62a9 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -13,3 +13,7 @@ - else .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 + = t('exercises.editor.requestComments') \ No newline at end of file diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index 1995c8d8..f0f69f7a 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -28,6 +28,10 @@ label = f.check_box(:hide_file_tree) = t('activerecord.attributes.exercise.hide_file_tree') + .checkbox + label + = f.check_box(:allow_file_creation) + = t('activerecord.attributes.exercise.allow_file_creation') h2 = t('activerecord.attributes.exercise.files') ul#files.list-unstyled.panel-group = f.fields_for :files do |files_form| diff --git a/app/views/exercises/_request_comment_dialogcontent.html.slim b/app/views/exercises/_request_comment_dialogcontent.html.slim new file mode 100644 index 00000000..4fc34dcd --- /dev/null +++ b/app/views/exercises/_request_comment_dialogcontent.html.slim @@ -0,0 +1,4 @@ +h5 = t('exercises.implement.comment.question') +textarea.form-control#question(style='resize:none;') +p = '' +button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request') \ No newline at end of file diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 47714943..97847f1f 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -28,7 +28,7 @@ h1 = Exercise.model_name.human(count: 2) tr data-id=exercise.id td = exercise.title td = link_to_if(policy(exercise.author).show?, exercise.author, exercise.author) - td = link_to_if(policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) + td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = exercise.files.teacher_defined_tests.count td = exercise.maximum_score td.public data-value=exercise.public? = symbol_for(exercise.public?) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 2181acf2..fc28272a 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -16,6 +16,7 @@ h1 = 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?) += row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.embedding_parameters') do = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 80eebe41..8bc539a7 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -22,7 +22,7 @@ html lang='en' span.icon-bar span.icon-bar span.icon-bar - a.navbar-brand href=root_path + .navbar-brand i.fa.fa-code = application_name #navbar-collapse.collapse.navbar-collapse diff --git a/app/views/request_for_comments/index.html.slim b/app/views/request_for_comments/index.html.slim index 89d8c239..6d4e059c 100644 --- a/app/views/request_for_comments/index.html.slim +++ b/app/views/request_for_comments/index.html.slim @@ -1,19 +1,22 @@ h1 = RequestForComment.model_name.human(count: 2) .table-responsive - table.table + table.table.sortable thead tr th = t('activerecord.attributes.request_for_comments.exercise') - th = t('activerecord.attributes.request_for_comments.execution_environment') + th = t('activerecord.attributes.request_for_comments.question') 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 td = link_to(request_for_comment.exercise.title, request_for_comment) - td = request_for_comment.exercise.execution_environment - td = request_for_comment.user.name - td = request_for_comment.requested_at + - 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.user.displayname + td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.requested_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 a47eba32..ab53ebd0 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -14,6 +14,13 @@ %> <%= user %> | <%= @request_for_comment.requested_at %>
+