Merge branch 'master' into score-websocket

Conflicts:
	app/assets/javascripts/editor.js.erb
This commit is contained in:
Ralf Teusner
2016-07-28 15:32:22 +02:00
69 changed files with 600 additions and 426 deletions

View File

@ -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], "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>"));
}
}
}
};
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();

View File

@ -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 = "<option value>" + noTemplateLabel + "</option>";
for (var i = 0; i < response.length; i++) {
options += "<option value='" + response[i].id + "'>" + response[i].name + "</option>"
}
$("#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) {

View File

@ -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/

View File

@ -49,7 +49,10 @@ div#chart_2 {
}
a.file-heading {
color: black !important;
text-decoration: none;
}
.bar {
fill: orange;

View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -35,6 +35,7 @@ module CodeOcean
has_many :files
has_many :testruns
has_many :comments
alias_method :descendants, :files
mount_uploader :native_file, FileUploader

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,10 @@
class FileTemplate < ActiveRecord::Base
belongs_to :file_type
def to_s
name
end
end

View File

@ -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?

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
class FileTemplatePolicy < AdminOnlyPolicy
def show?
everyone
end
def by_file_type?
everyone
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"))

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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}" &#9788
.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)

View File

@ -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)

View File

@ -0,0 +1,3 @@
h1 = @file_template
= render('form')

View File

@ -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)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: FileTemplate.model_name.human)
= render('form')

View File

@ -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)

View File

@ -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)

View File

@ -1,46 +1,76 @@
<div class="list-group">
<h4 class="list-group-item-heading"><%= Exercise.find(@request_for_comment.exercise_id) %></h4>
<h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
<p class="list-group-item-text">
<%
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 %>
</p>
<h5>
<u><%= t('activerecord.attributes.exercise.description') %>:</u> "<%= render_markdown(@request_for_comment.exercise.description) %>"
</h5>
<h5>
<% if @request_for_comment.question and not @request_for_comment.question == '' %>
<%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>"
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> "<%= @request_for_comment.question %>"
<% else %>
<%= t('request_for_comments.no_question') %>
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
<% end %>
</h5>
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
<button class="btn btn-default" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
<% elsif (@request_for_comment.solved?) %>
<button type="button" class="btn btn-success"><%= t('request_for_comments.solved') %></button>
<% else %>
<% end %>
</div>
<!--
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
-->
<% submission.files.each do |file| %>
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
<div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>'><%= file.content %>
<div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>' data-mode='<%=file.file_type.editor_mode%>'><%= file.content %>
</div>
<% end %>
<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>
<script type="text/javascript">
var solvedButton = $('#mark-as-solved-button');
solvedButton.on('click', function(event){
var jqrequest = $.ajax({
dataType: 'json',
method: 'GET',
url: location + '/mark_as_solved'
});
jqrequest.done(function(response){
if(response.solved){
solvedButton.hide();
}
});
});
// set file paths for ace
var ACE_FILES_PATH = '/assets/ace/';
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
var commentitor = $('.editor');
var userid = commentitor.data('user-id');
commentitor.each(function (index, editor) {
var currentEditor = ace.edit(editor);
currentEditor.setReadOnly(true);
// set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode'));
setAnnotations(currentEditor, $(editor).data('file-id'));
currentEditor.on("guttermousedown", handleSidebarClick);
@ -101,7 +131,8 @@ do not put a carriage return in the line below. it will be present in the presen
file_id: file_id,
row: row,
column: 0,
text: commenttext
text: commenttext,
request_id: $('h4#exercise_caption').data('rfc-id')
}
},
dataType: 'json',

View File

@ -1 +1 @@
json.extract! @request_for_comment, :id, :user_id, :exercise_id, :file_id, :requested_at, :created_at, :updated_at, :user_type
json.extract! @request_for_comment, :id, :user_id, :exercise_id, :file_id, :requested_at, :created_at, :updated_at, :user_type, :solved

View File

@ -1 +1 @@
json.extract! @submission, :download_url, :id, :score_url, :render_url, :run_url, :stop_url, :test_url, :files
json.extract! @submission, :download_url, :download_file_url, :id, :score_url, :render_url, :run_url, :stop_url, :test_url, :files

View File

@ -1,9 +0,0 @@
= form_for(@team) do |f|
= render('shared/form_errors', object: @team)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:internal_user_ids)
= f.collection_select(:internal_user_ids, InternalUser.all.order(:name), :id, :name, {}, {class: 'form-control', multiple: true})
.actions = render('shared/submit_button', f: f, object: @team)

View File

@ -1,3 +0,0 @@
h1 = @hint
= render('form')

View File

@ -1,20 +0,0 @@
h1 = Team.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.team.name')
th = t('activerecord.attributes.team.internal_user_ids')
th colspan=3 = t('shared.actions')
tbody
- @teams.each do |team|
tr
td = team.name
td = team.members.count
td = link_to(t('shared.show'), team_path(team.id))
td = link_to(t('shared.edit'), edit_team_path(team.id))
td = link_to(t('shared.destroy'), team_path(team.id), data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @teams)
p = render('shared/new_button', model: Team, path: new_team_path)

View File

@ -1,3 +0,0 @@
h1 = t('shared.new_model', model: Team.model_name.human)
= render('form')

View File

@ -1,9 +0,0 @@
h1
= @team
= render('shared/edit_button', object: @team, path: edit_team_path(@team.id))
= row(label: 'team.name', value: @team.name)
= row(label: 'team.internal_user_ids') do
ul.list-unstyled
- @team.members.order(:name).each do |internal_user|
li = link_to(internal_user, internal_user)

View File

@ -0,0 +1 @@
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text)