Add CommunitySolution

* Also slightly refactor some JS files
This commit is contained in:
Sebastian Serth
2021-11-21 19:18:52 +01:00
parent d559cfb323
commit da4e10b990
21 changed files with 509 additions and 31 deletions

View 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));
}
});

View File

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

View File

@ -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));
}
})
},

View File

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

View 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

View File

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

View File

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

View 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

View 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

View 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

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class CommunitySolutionPolicy < AdminOnlyPolicy
def edit?
everyone
end
def update?
everyone
end
end

View 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

View 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'

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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