Add CommunitySolution
* Also slightly refactor some JS files
This commit is contained in:
27
app/assets/javascripts/community_solution.js
Normal file
27
app/assets/javascripts/community_solution.js
Normal file
@ -0,0 +1,27 @@
|
||||
$(document).on('turbolinks:load', function() {
|
||||
|
||||
if ($.isController('community_solutions') && $('#community-solution-editor').isPresent()) {
|
||||
CodeOceanEditor.sendEvents = false;
|
||||
CodeOceanEditor.editors = [];
|
||||
CodeOceanEditor.initializeDescriptionToggle();
|
||||
CodeOceanEditor.configureEditors();
|
||||
CodeOceanEditor.initializeEditors();
|
||||
CodeOceanEditor.initializeEditors(true);
|
||||
CodeOceanEditor.initializeFileTree();
|
||||
CodeOceanEditor.initializeFileTree(true);
|
||||
CodeOceanEditor.showFirstFile();
|
||||
CodeOceanEditor.showFirstFile(true);
|
||||
CodeOceanEditor.resizeAceEditors();
|
||||
CodeOceanEditor.resizeAceEditors(true);
|
||||
|
||||
$.extend(
|
||||
CodeOceanEditor,
|
||||
CodeOceanEditorAJAX,
|
||||
CodeOceanEditorSubmissions
|
||||
)
|
||||
|
||||
$('#submit').one('click', CodeOceanEditorSubmissions.submitCode.bind(CodeOceanEditor));
|
||||
$('#accept').one('click', CodeOceanEditorSubmissions.submitCode.bind(CodeOceanEditor));
|
||||
}
|
||||
|
||||
});
|
@ -15,7 +15,7 @@ var CodeOceanEditor = {
|
||||
ENTER_KEY_CODE: 13,
|
||||
|
||||
//Request-For-Comments-Configuration
|
||||
REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000,
|
||||
REQUEST_FOR_COMMENTS_DELAY: 0,
|
||||
REQUEST_TOOLTIP_TIME: 5000,
|
||||
|
||||
editors: [],
|
||||
@ -140,20 +140,37 @@ var CodeOceanEditor = {
|
||||
}
|
||||
},
|
||||
|
||||
showFirstFile: function () {
|
||||
var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
|
||||
var file_id = frame.find('.editor').data('file-id');
|
||||
showFirstFile: function (own_solution = false) {
|
||||
let frame;
|
||||
let filetree;
|
||||
let editorSelector;
|
||||
if (own_solution) {
|
||||
frame = $('.own-frame[data-role="main_file"]').isPresent() ? $('.own-frame[data-role="main_file"]') : $('.own-frame').first();
|
||||
filetree = $('#own-files');
|
||||
editorSelector = '.own-editor';
|
||||
} else {
|
||||
frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
|
||||
filetree = $('#files');
|
||||
editorSelector = '.editor';
|
||||
}
|
||||
|
||||
var file_id = frame.find(editorSelector).data('file-id');
|
||||
this.setActiveFile(frame.data('filename'), file_id);
|
||||
var filetree = $('#files');
|
||||
this.selectFileInJsTree(filetree, file_id);
|
||||
this.showFrame(frame);
|
||||
this.toggleButtonStates();
|
||||
},
|
||||
|
||||
showFrame: function (frame) {
|
||||
if (frame.hasClass('own-frame')) {
|
||||
$('.own-frame').hide();
|
||||
} else {
|
||||
$('.frame').hide();
|
||||
}
|
||||
|
||||
this.active_frame = frame;
|
||||
$('.frame').hide();
|
||||
frame.show();
|
||||
this.resizeParentOfAceEditor(frame.find('.ace_editor.ace-tm'));
|
||||
},
|
||||
|
||||
getProgressBarClass: function (percentage) {
|
||||
@ -203,8 +220,15 @@ var CodeOceanEditor = {
|
||||
},
|
||||
|
||||
|
||||
resizeAceEditors: function () {
|
||||
$('.editor').each(function (index, element) {
|
||||
resizeAceEditors: function (own_solution = false) {
|
||||
let editorSelector;
|
||||
if (own_solution) {
|
||||
editorSelector = $('.own-editor')
|
||||
} else {
|
||||
editorSelector = $('.editor')
|
||||
}
|
||||
|
||||
editorSelector.each(function (index, element) {
|
||||
this.resizeParentOfAceEditor(element);
|
||||
}.bind(this));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
@ -212,13 +236,21 @@ var CodeOceanEditor = {
|
||||
|
||||
resizeParentOfAceEditor: function (element) {
|
||||
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
|
||||
var windowHeight = window.innerHeight - $(element).offset().top - $('#statusbar').height() - 5;
|
||||
var windowHeight = window.innerHeight - $(element).offset().top - ($('#statusbar').height() || 0) - 5;
|
||||
$(element).parent().height(windowHeight);
|
||||
},
|
||||
|
||||
initializeEditors: function () {
|
||||
this.editors = [];
|
||||
$('.editor').each(function (index, element) {
|
||||
initializeEditors: function (own_solution = false) {
|
||||
// Initialize the editors array if not present already. This is mainly required for community solutions
|
||||
this.editors = this.editors || [];
|
||||
let editorSelector;
|
||||
if (own_solution) {
|
||||
editorSelector = $('.own-editor')
|
||||
} else {
|
||||
editorSelector = $('.editor')
|
||||
}
|
||||
|
||||
editorSelector.each(function (index, element) {
|
||||
|
||||
// Resize frame on load
|
||||
this.resizeParentOfAceEditor(element);
|
||||
@ -279,12 +311,10 @@ var CodeOceanEditor = {
|
||||
session.setUseWrapMode(true);
|
||||
|
||||
// set regex for parsing error traces based on the mode of the main file.
|
||||
if ($(element).parent().data('role') == "main_file") {
|
||||
if ($(element).parent().data('role') === "main_file") {
|
||||
this.tracepositions_regex = this.regex_for_language.get($(element).data('mode'));
|
||||
}
|
||||
|
||||
var file_id = $(element).data('id');
|
||||
|
||||
/*
|
||||
* Register event handlers
|
||||
*/
|
||||
@ -326,9 +356,15 @@ var CodeOceanEditor = {
|
||||
});
|
||||
},
|
||||
|
||||
initializeFileTree: function () {
|
||||
$('#files').jstree($('#files').data('entries'));
|
||||
$('#files').on('click', 'li.jstree-leaf > a', function (event) {
|
||||
initializeFileTree: function (own_solution = false) {
|
||||
let filesInstance;
|
||||
if (own_solution) {
|
||||
filesInstance = $('#own-files');
|
||||
} else {
|
||||
filesInstance = $('#files');
|
||||
}
|
||||
filesInstance.jstree(filesInstance.data('entries'));
|
||||
filesInstance.on('click', 'li.jstree-leaf > a', function (event) {
|
||||
this.setActiveFile(
|
||||
$(event.target).parent().text(),
|
||||
parseInt($(event.target).parent().attr('id'))
|
||||
@ -793,7 +829,7 @@ var CodeOceanEditor = {
|
||||
const percentile75 = data['working_time_75_percentile'];
|
||||
const accumulatedWorkTimeUser = data['working_time_accumulated'];
|
||||
|
||||
const minTimeIntervention = 10 * 1000;
|
||||
const minTimeIntervention = 10 * 60 * 1000;
|
||||
|
||||
let timeUntilIntervention;
|
||||
if ((accumulatedWorkTimeUser - percentile75) > 0) {
|
||||
@ -879,6 +915,7 @@ var CodeOceanEditor = {
|
||||
|
||||
|
||||
initializeEverything: function () {
|
||||
CodeOceanEditor.editors = [];
|
||||
this.initializeRegexes();
|
||||
this.initializeCodePilot();
|
||||
$('.score, #development-environment').show();
|
||||
|
@ -9,8 +9,9 @@ CodeOceanEditorSubmissions = {
|
||||
* Submission-Creation
|
||||
*/
|
||||
createSubmission: function (initiator, filter, callback) {
|
||||
const editor = $('#editor');
|
||||
this.showSpinner(initiator);
|
||||
var url = $(initiator).data('url') || $('#editor').data('submissions-url');
|
||||
var url = $(initiator).data('url') || editor.data('submissions-url');
|
||||
|
||||
if (url === undefined) {
|
||||
const data = {
|
||||
@ -24,12 +25,12 @@ CodeOceanEditorSubmissions = {
|
||||
data: {
|
||||
submission: {
|
||||
cause: $(initiator).data('cause') || $(initiator).prop('id'),
|
||||
exercise_id: $('#editor').data('exercise-id'),
|
||||
exercise_id: editor.data('exercise-id') || $(initiator).data('exercise-id'),
|
||||
files_attributes: (filter || _.identity)(this.collectFiles())
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
method: $(initiator).data('http-method') || 'POST',
|
||||
url: url + '.json'
|
||||
});
|
||||
jqxhr.always(this.hideSpinner.bind(this));
|
||||
@ -191,20 +192,22 @@ CodeOceanEditorSubmissions = {
|
||||
}
|
||||
},
|
||||
|
||||
submitCode: function() {
|
||||
this.createSubmission($('#submit'), null, function (response) {
|
||||
submitCode: function(event) {
|
||||
const button = $(event.target) || $('#submit');
|
||||
this.createSubmission(button, null, function (response) {
|
||||
if (response.redirect) {
|
||||
this.editors = [];
|
||||
Turbolinks.clearCache();
|
||||
clearTimeout(this.autosaveTimer);
|
||||
Turbolinks.visit(response.redirect);
|
||||
} else if (response.status === 'container_depleted') {
|
||||
this.showContainerDepletedMessage();
|
||||
$('#submit').one('click', this.submitCode.bind(this));
|
||||
button.one('click', this.submitCode.bind(this));
|
||||
} else if (response.message) {
|
||||
$.flash.danger({
|
||||
text: response.message
|
||||
});
|
||||
$('#submit').one('click', this.submitCode.bind(this));
|
||||
button.one('click', this.submitCode.bind(this));
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -7,6 +7,14 @@ button i.fa-spin {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.own-editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.ace_scroller .ace_content {
|
||||
background: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
/* this class is used for the edit view of an exercise. It needs the height set, as it does not automatically resize */
|
||||
.edit-frame {
|
||||
height: 400px;
|
||||
@ -26,6 +34,15 @@ button i.fa-spin {
|
||||
}
|
||||
}
|
||||
|
||||
.own-frame {
|
||||
display: none;
|
||||
min-height: 300px;
|
||||
|
||||
audio, img, video {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.score {
|
||||
display: none;
|
||||
vertical-align: bottom;
|
||||
@ -64,6 +81,10 @@ button i.fa-spin {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#own-files {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#outputInformation {
|
||||
#output {
|
||||
max-height: 500px;
|
||||
|
103
app/controllers/community_solutions_controller.rb
Normal file
103
app/controllers/community_solutions_controller.rb
Normal file
@ -0,0 +1,103 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommunitySolutionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
include RedirectBehavior
|
||||
include SubmissionParameters
|
||||
|
||||
before_action :set_community_solution, only: %i[edit update]
|
||||
before_action :set_community_solution_lock, only: %i[edit]
|
||||
before_action :set_exercise_and_submission, only: %i[edit update]
|
||||
|
||||
# GET /community_solutions
|
||||
def index
|
||||
@community_solutions = CommunitySolution.all
|
||||
authorize!
|
||||
end
|
||||
|
||||
# GET /community_solutions/1/edit
|
||||
def edit
|
||||
authorize!
|
||||
|
||||
# Be safe. Only allow access to this page if user has valid lock
|
||||
redirect_after_submit unless @community_solution_lock.present? && @community_solution_lock.active? && @community_solution_lock.user == current_user && @community_solution_lock.community_solution == @community_solution
|
||||
# We don't want to perform any of the following steps if we rendered already (e.g. due to a redirect)
|
||||
return if performed?
|
||||
|
||||
last_contribution = CommunitySolutionContribution.where(community_solution: @community_solution, timely_contribution: true, autosave: false, proposed_changes: true).order(created_at: :asc).last
|
||||
@files = []
|
||||
if last_contribution.blank?
|
||||
last_contribution = @community_solution.exercise
|
||||
new_readme_file = {content: '', file_type: FileType.find_by(file_extension: '.txt'), hidden: false, read_only: false, name: 'ReadMe', role: 'regular_file', context: @community_solution}
|
||||
@files << CodeOcean::File.create!(new_readme_file)
|
||||
end
|
||||
all_visible_files = last_contribution.files.select(&:visible)
|
||||
# Add the ReadMe file first
|
||||
@files += all_visible_files.select {|f| CodeOcean::File.find_by(id: f.file_id).context_type == 'CommunitySolution' }
|
||||
# Then, add all remaining files and sort them by name with extension
|
||||
@files += (all_visible_files - @files).sort_by(&:name_with_extension)
|
||||
|
||||
# Own Submission as a reference
|
||||
@own_files = @submission.collect_files.select(&:visible).sort_by(&:name_with_extension)
|
||||
# Remove the file_id from the second graph. Otherwise, the comparison and file-tree selection does not work as expected
|
||||
@own_files.map do |file|
|
||||
file.file_id = nil
|
||||
file.read_only = true
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /community_solutions/1
|
||||
def update
|
||||
authorize!
|
||||
contribution_params = submission_params
|
||||
cause = contribution_params.delete(:cause)
|
||||
contribution_params[:proposed_changes] = cause == 'change-community-solution'
|
||||
contribution_params[:autosave] = cause == 'autosave-community-solution'
|
||||
contribution_params.delete(:exercise_id)
|
||||
contribution_params[:community_solution] = @community_solution
|
||||
|
||||
# Acquire lock here! This is expensive but required for synchronization
|
||||
@community_solution_lock = ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.connection.execute("LOCK #{CommunitySolutionLock.table_name} IN ACCESS EXCLUSIVE MODE")
|
||||
|
||||
lock = CommunitySolutionLock.where(user: current_user, community_solution: @community_solution).order(locked_until: :asc).last
|
||||
|
||||
if lock.active?
|
||||
contribution_params[:timely_contribution] = true
|
||||
# Update lock: Either expand the time (autosave) or return it (change / accept)
|
||||
new_lock_time = contribution_params[:autosave] ? 5.minutes.from_now : Time.zone.now
|
||||
lock.update!(locked_until: new_lock_time)
|
||||
else
|
||||
contribution_params[:timely_contribution] = false
|
||||
end
|
||||
# This is returned
|
||||
lock
|
||||
end
|
||||
|
||||
contribution_params[:community_solution_lock] = @community_solution_lock
|
||||
contribution_params[:working_time] = @community_solution_lock.working_time
|
||||
CommunitySolutionContribution.create(contribution_params)
|
||||
|
||||
redirect_after_submit
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize!
|
||||
authorize(@community_solution)
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_community_solution
|
||||
@community_solution = CommunitySolution.find(params[:id])
|
||||
end
|
||||
|
||||
def set_community_solution_lock
|
||||
@community_solution_lock = CommunitySolutionLock.find(params[:lock_id])
|
||||
end
|
||||
|
||||
def set_exercise_and_submission
|
||||
@exercise = @community_solution.exercise
|
||||
@submission = current_user.submissions.final.where(exercise_id: @community_solution.exercise.id).order('created_at DESC').first
|
||||
end
|
||||
end
|
@ -6,7 +6,7 @@ module FileParameters
|
||||
params.reject do |_, file_attributes|
|
||||
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
|
||||
# avoid that public files from other contexts can be created
|
||||
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id)
|
||||
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id) || (file.context_type == 'CommunitySolution' && controller_name != 'community_solutions')
|
||||
end
|
||||
else
|
||||
[]
|
||||
|
@ -6,6 +6,11 @@ module RedirectBehavior
|
||||
def redirect_after_submit
|
||||
Rails.logger.debug { "Redirecting user with score:s #{@submission.normalized_score}" }
|
||||
if @submission.normalized_score.to_d == 1.0.to_d
|
||||
if redirect_to_community_solution?
|
||||
redirect_to_community_solution
|
||||
return
|
||||
end
|
||||
|
||||
# if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external,
|
||||
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
|
||||
# redirect 10 percent pseudorandomly to the feedback page
|
||||
@ -63,6 +68,40 @@ module RedirectBehavior
|
||||
|
||||
private
|
||||
|
||||
def redirect_to_community_solution
|
||||
url = edit_community_solution_path(@community_solution, lock_id: @community_solution_lock.id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(url) }
|
||||
format.json { render(json: {redirect: url}) }
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_community_solution?
|
||||
return false unless Java21Study.allow_redirect_to_community_solution?(current_user, @exercise)
|
||||
|
||||
@community_solution = CommunitySolution.find_by(exercise: @exercise)
|
||||
return false if @community_solution.blank?
|
||||
|
||||
last_contribution = CommunitySolutionContribution.where(community_solution: @community_solution).order(created_at: :asc).last
|
||||
|
||||
# Only redirect if last contribution is from another user.
|
||||
eligible = last_contribution.blank? || last_contribution.user != current_user
|
||||
return false unless eligible
|
||||
|
||||
# Acquire lock here! This is expensive but required for synchronization
|
||||
@community_solution_lock = ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.connection.execute("LOCK #{CommunitySolutionLock.table_name} IN ACCESS EXCLUSIVE MODE")
|
||||
|
||||
# This is returned
|
||||
CommunitySolutionLock.find_or_create_by(community_solution: @community_solution, locked_until: Time.zone.now...) do |lock|
|
||||
lock.user = current_user
|
||||
lock.locked_until = 5.minutes.from_now
|
||||
end
|
||||
end
|
||||
|
||||
@community_solution_lock.user == current_user
|
||||
end
|
||||
|
||||
def redirect_to_user_feedback
|
||||
uef = UserExerciseFeedback.find_by(exercise: @exercise, user: current_user)
|
||||
url = if uef
|
||||
|
13
app/models/community_solution.rb
Normal file
13
app/models/community_solution.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommunitySolution < ApplicationRecord
|
||||
belongs_to :exercise
|
||||
has_many :community_solution_locks
|
||||
has_many :community_solution_contributions
|
||||
has_and_belongs_to_many :users, polymorphic: true, through: :community_solution_contributions
|
||||
has_many :files, class_name: 'CodeOcean::File', through: :community_solution_contributions
|
||||
|
||||
def to_s
|
||||
"Gemeinschaftslösung für #{exercise}"
|
||||
end
|
||||
end
|
14
app/models/community_solution_contribution.rb
Normal file
14
app/models/community_solution_contribution.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommunitySolutionContribution < ApplicationRecord
|
||||
include Creation
|
||||
include Context
|
||||
|
||||
belongs_to :community_solution
|
||||
belongs_to :community_solution_lock
|
||||
|
||||
validates :proposed_changes, boolean_presence: true
|
||||
validates :timely_contribution, boolean_presence: true
|
||||
validates :autosave, boolean_presence: true
|
||||
validates :working_time, presence: true
|
||||
end
|
18
app/models/community_solution_lock.rb
Normal file
18
app/models/community_solution_lock.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommunitySolutionLock < ApplicationRecord
|
||||
include Creation
|
||||
|
||||
belongs_to :community_solution
|
||||
has_many :community_solution_contributions
|
||||
|
||||
validates :locked_until, presence: true
|
||||
|
||||
def active?
|
||||
Time.zone.now <= locked_until
|
||||
end
|
||||
|
||||
def working_time
|
||||
ActiveSupport::Duration.build(locked_until - created_at)
|
||||
end
|
||||
end
|
11
app/policies/community_solution_policy.rb
Normal file
11
app/policies/community_solution_policy.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommunitySolutionPolicy < AdminOnlyPolicy
|
||||
def edit?
|
||||
everyone
|
||||
end
|
||||
|
||||
def update?
|
||||
everyone
|
||||
end
|
||||
end
|
66
app/views/community_solutions/_form.html.slim
Normal file
66
app/views/community_solutions/_form.html.slim
Normal file
@ -0,0 +1,66 @@
|
||||
.exercise.clearfix
|
||||
div
|
||||
span.badge.badge-pill.badge-primary.float-right.score
|
||||
|
||||
h1 id="exercise-headline"
|
||||
i id="description-symbol" class=(@embed_options[:collapse_exercise_description] ? 'fa fa-chevron-right' : 'fa fa-chevron-down')
|
||||
=> @community_solution.model_name.human(count: 1)
|
||||
= @community_solution.exercise.title
|
||||
|
||||
#description-card.lead class=(@embed_options[:collapse_exercise_description] ? 'description-card-collapsed' : 'description-card')
|
||||
.card.border-success.mb-3
|
||||
.card-header
|
||||
i.fa.fa-info-circle.text-success
|
||||
strong.text-success
|
||||
=> t('community_solutions.help_us_out')
|
||||
= t('community_solutions.explanation')
|
||||
br
|
||||
i.fa.fa-flask.text-success
|
||||
strong.text-success
|
||||
=> t('community_solutions.research_status')
|
||||
== t('community_solutions.research_explanation')
|
||||
hr
|
||||
= render_markdown(@community_solution.exercise.description)
|
||||
|
||||
a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide')
|
||||
- if @embed_options[:collapse_exercise_description]
|
||||
= t('shared.show')
|
||||
- else
|
||||
= t('shared.hide')
|
||||
.row.mt-4
|
||||
.col-xl-6
|
||||
h4
|
||||
= t('community_solutions.current_community_solution')
|
||||
#community-solution-editor.row
|
||||
.pr-0 class=(@community_solution.exercise.hide_file_tree ? 'd-none col-sm-3' : 'col-sm-3')
|
||||
.card.border-secondary
|
||||
.card-header.d-flex.justify-content-between.align-items-center.px-0.py-2
|
||||
.px-2 = I18n.t('exercises.editor_file_tree.file_root')
|
||||
.card-body.pt-0.pr-0.pl-1.pb-1
|
||||
#files data-entries=FileTree.new(@files).to_js_tree
|
||||
div class=(@community_solution.exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9')
|
||||
div.editor-col.col.p-0 id='frames'
|
||||
- @files.each do |file|
|
||||
= render('exercises/editor_frame', exercise: @community_solution.exercise, file: file)
|
||||
|
||||
.col-xl-6.container-fluid
|
||||
div.bg-dark.h-100.float-left.row style="width: 1px"
|
||||
div
|
||||
h4
|
||||
= t('community_solutions.your_submission')
|
||||
#own-solution-editor.row
|
||||
.pr-0 class=(@community_solution.exercise.hide_file_tree ? 'd-none col-sm-3' : 'col-sm-3')
|
||||
.card.border-secondary
|
||||
.card-header.d-flex.justify-content-between.align-items-center.px-0.py-2
|
||||
.px-2 = I18n.t('exercises.editor_file_tree.file_root')
|
||||
.card-body.pt-0.pr-0.pl-1.pb-1
|
||||
#own-files data-entries=FileTree.new(@own_files).to_js_tree
|
||||
div class=(@community_solution.exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9')
|
||||
div.editor-col.col.p-0 id='own-frames'
|
||||
- @own_files.each do |file|
|
||||
= render('exercises/editor_frame', exercise: @community_solution.exercise, file: file, own_solution: true)
|
||||
#statusbar.visible.mt-2 style="height: 5em"
|
||||
p.text-center
|
||||
= render('exercises/editor_button', classes: 'btn-lg btn-success ml-5 mr-3', data: {'data-url': community_solution_path(@community_solution), 'data-http-method': 'PUT', 'data-cause': 'change-community-solution', 'data-exercise-id': @community_solution.exercise.id}, icon: 'fa fa-send', id: 'submit', label: t('community_solutions.change_community_solution'))
|
||||
= render('exercises/editor_button', classes: 'btn-lg btn-secondary ml-5', data: {'data-url': community_solution_path(@community_solution), 'data-http-method': 'PUT', 'data-cause': 'accept-community-solution', 'data-exercise-id': @community_solution.exercise.id}, icon: 'fa fa-check', id: 'accept', label: t('community_solutions.accept_community_solution'))
|
||||
button style="display:none" id="autosave" data-url=community_solution_path(@community_solution) data-http-method='PUT' data-cause='autosave-community-solution' data-exercise-id=@community_solution.exercise.id
|
6
app/views/community_solutions/edit.html.slim
Normal file
6
app/views/community_solutions/edit.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
- content_for :head do
|
||||
// Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
|
||||
Otherwise, code might not be highlighted correctly (race condition)
|
||||
meta name='turbolinks-visit-control' content='reload'
|
||||
|
||||
== render 'form'
|
19
app/views/community_solutions/index.html.slim
Normal file
19
app/views/community_solutions/index.html.slim
Normal file
@ -0,0 +1,19 @@
|
||||
h1 Listing community_solutions
|
||||
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th
|
||||
th
|
||||
|
||||
tbody
|
||||
- @community_solutions.each do |community_solution|
|
||||
tr
|
||||
td = link_to 'Show', community_solution
|
||||
td = link_to 'Edit', edit_community_solution_path(community_solution)
|
||||
td = link_to 'Destroy', community_solution, data: { confirm: 'Are you sure?' }, method: :delete
|
||||
|
||||
br
|
||||
|
||||
= link_to 'New Community solution', new_community_solution_path
|
@ -1,4 +1,4 @@
|
||||
.frame data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type data-read-only=file.read_only
|
||||
div class=(defined?(own_solution) ? "own-frame" : "frame") data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type data-read-only=file.read_only
|
||||
- if file.file_type.binary?
|
||||
.binary-file data-file-id=file.ancestor_id
|
||||
- if file.file_type.renderable?
|
||||
@ -12,4 +12,4 @@
|
||||
= link_to(file.native_file.file.filename, file.native_file.url)
|
||||
- else
|
||||
.editor-content.d-none 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-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id
|
||||
div class=(defined?(own_solution) ? "own-editor" : "editor") data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id
|
@ -55,7 +55,7 @@ html lang="#{I18n.locale || I18n.default_locale}"
|
||||
= render('flash')
|
||||
- if current_user.try(:admin?) or current_user.try(:teacher?) && !@embed_options[:hide_navbar]
|
||||
= yield(:breadcrumbs)
|
||||
- if (controller_name == "exercises" && action_name == "implement")
|
||||
- if (controller_name == "exercises" && action_name == "implement") || (controller_name == 'community_solutions' && action_name == 'edit')
|
||||
.container-fluid
|
||||
= yield
|
||||
- else
|
||||
|
@ -163,6 +163,9 @@ de:
|
||||
exercise_collection_item:
|
||||
exercise: "Aufgabe"
|
||||
models:
|
||||
community_solution:
|
||||
one: Gemeinschaftslösung
|
||||
other: Gemeinschaftslösungen
|
||||
codeharbor_link:
|
||||
one: CodeHarbor-Link
|
||||
other: CodeHarbor-Links
|
||||
@ -265,6 +268,15 @@ de:
|
||||
rfc_activity_history: Kommentaranfragenhistorie
|
||||
rails_admin:
|
||||
show: "Rails Admin"
|
||||
community_solutions:
|
||||
help_us_out: Helfen Sie mit!
|
||||
explanation: In diesem Kurs möchten wir gerne mit Ihnen und allen anderen Lernenden eine Gemeinschaftslösung für diese Aufgabe erarbeiten, die zum Ende des Kurses allen Teilnehmenden zugänglich gemacht werden soll. Unten finden Sie sowohl den aktuellen Stand der Gemeinschaftslösung als auch Ihre Abgabe. Bitte sehen Sie sich die Gemeinschaftslösung an und überarbeiten Sie diese bei Bedarf. Ihre eigene Lösung wird nicht verändert.
|
||||
research_status: 'Neue Beta-Funktion aus der Forschung:'
|
||||
research_explanation: Die hier angebotene Mitwirkungsmöglichkeit an einer Gemeinschaftslösung ist Bestandteil unserer Forschung; daher würden wir uns sehr über Ihre aktive Beteiligung freuen.
|
||||
current_community_solution: Aktuelle Gemeinschaftslösung
|
||||
your_submission: Ihre Abgabe (schreibgeschützt, als Referenz)
|
||||
change_community_solution: Änderungen an Gemeinschaftslösung speichern
|
||||
accept_community_solution: Gemeinschaftslösung ohne Änderung verlassen
|
||||
consumers:
|
||||
show:
|
||||
link: Konsument
|
||||
|
@ -163,6 +163,9 @@ en:
|
||||
exercise_collection_item:
|
||||
exercise: "Exercise"
|
||||
models:
|
||||
community_solution:
|
||||
one: Community Solution
|
||||
other: Community Solutions
|
||||
codeharbor_link:
|
||||
one: CodeHarbor Link
|
||||
other: CodeHarbor Links
|
||||
@ -265,6 +268,15 @@ en:
|
||||
rfc_activity_history: RfC Activity History
|
||||
rails_admin:
|
||||
show: "Rails Admin"
|
||||
community_solutions:
|
||||
help_us_out: Help us out!
|
||||
explanation: In this course, we would like to work together with you and all other learners to create a community solution for this exercise, which will be made available to all participants at the end of the course. For this we need your active support. Below you will find both the current status of the community solution and your submission. Please review the community solution and revise it as needed. Your own solution will not be changed.
|
||||
research_status: 'New beta feature based on our research:'
|
||||
research_explanation: The opportunity to participate in a community solution is part of our research; therefore, we would greatly appreciate your active participation.
|
||||
current_community_solution: Current community solution
|
||||
your_submission: Your submission (read-only, for reference)
|
||||
change_community_solution: Save Changes to Community Solution
|
||||
accept_community_solution: Quit Community Solution without Changes
|
||||
consumers:
|
||||
show:
|
||||
link: Consumer
|
||||
|
@ -3,6 +3,7 @@
|
||||
FILENAME_REGEXP = /[\w.]+/.freeze unless Kernel.const_defined?(:FILENAME_REGEXP)
|
||||
|
||||
Rails.application.routes.draw do
|
||||
resources :community_solutions, only: %i[index edit update]
|
||||
resources :error_template_attributes
|
||||
resources :error_templates do
|
||||
member do
|
||||
|
36
db/migrate/20211118185051_create_community_solution.rb
Normal file
36
db/migrate/20211118185051_create_community_solution.rb
Normal file
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateCommunitySolution < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :community_solutions do |t|
|
||||
t.belongs_to :exercise, foreign_key: true, null: false, index: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :community_solution_locks do |t|
|
||||
t.belongs_to :community_solution, foreign_key: true, null: false, index: false
|
||||
t.references :user, polymorphic: true, null: false
|
||||
t.timestamp :locked_until, null: true
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index %i[community_solution_id locked_until], unique: true, name: 'index_community_solution_locks_until'
|
||||
end
|
||||
|
||||
create_table :community_solution_contributions do |t|
|
||||
t.belongs_to :community_solution, foreign_key: true, null: false, index: false
|
||||
t.belongs_to :study_group, foreign_key: true, null: true, index: false
|
||||
t.references :user, polymorphic: true, null: false
|
||||
t.belongs_to :community_solution_lock, foreign_key: true, null: false, index: {name: 'index_community_solution_contributions_lock'}
|
||||
t.boolean :proposed_changes, null: false
|
||||
t.boolean :timely_contribution, null: false
|
||||
t.boolean :autosave, null: false
|
||||
t.interval :working_time, null: false
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index %i[community_solution_id timely_contribution autosave proposed_changes], name: 'index_community_solution_valid_contributions'
|
||||
end
|
||||
end
|
||||
end
|
42
db/schema.rb
42
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_11_14_145024) do
|
||||
ActiveRecord::Schema.define(version: 2021_11_18_185051) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_trgm"
|
||||
@ -53,6 +53,41 @@ ActiveRecord::Schema.define(version: 2021_11_14_145024) do
|
||||
t.index ["user_id"], name: "index_comments_on_user_id"
|
||||
end
|
||||
|
||||
create_table "community_solution_contributions", force: :cascade do |t|
|
||||
t.bigint "community_solution_id", null: false
|
||||
t.bigint "study_group_id"
|
||||
t.string "user_type", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "community_solution_lock_id", null: false
|
||||
t.boolean "proposed_changes", null: false
|
||||
t.boolean "timely_contribution", null: false
|
||||
t.boolean "autosave", null: false
|
||||
t.interval "working_time", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["community_solution_id", "timely_contribution", "autosave", "proposed_changes"], name: "index_community_solution_valid_contributions"
|
||||
t.index ["community_solution_lock_id"], name: "index_community_solution_contributions_lock"
|
||||
t.index ["user_type", "user_id"], name: "index_community_solution_contributions_on_user"
|
||||
end
|
||||
|
||||
create_table "community_solution_locks", force: :cascade do |t|
|
||||
t.bigint "community_solution_id", null: false
|
||||
t.string "user_type", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "locked_until"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["community_solution_id", "locked_until"], name: "index_community_solution_locks_until", unique: true
|
||||
t.index ["user_type", "user_id"], name: "index_community_solution_locks_on_user"
|
||||
end
|
||||
|
||||
create_table "community_solutions", force: :cascade do |t|
|
||||
t.bigint "exercise_id", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["exercise_id"], name: "index_community_solutions_on_exercise_id"
|
||||
end
|
||||
|
||||
create_table "consumers", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at"
|
||||
@ -491,6 +526,11 @@ ActiveRecord::Schema.define(version: 2021_11_14_145024) do
|
||||
t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user"
|
||||
end
|
||||
|
||||
add_foreign_key "community_solution_contributions", "community_solution_locks"
|
||||
add_foreign_key "community_solution_contributions", "community_solutions"
|
||||
add_foreign_key "community_solution_contributions", "study_groups"
|
||||
add_foreign_key "community_solution_locks", "community_solutions"
|
||||
add_foreign_key "community_solutions", "exercises"
|
||||
add_foreign_key "exercise_tips", "exercise_tips", column: "parent_exercise_tip_id"
|
||||
add_foreign_key "exercise_tips", "exercises"
|
||||
add_foreign_key "exercise_tips", "tips"
|
||||
|
Reference in New Issue
Block a user