Apply changes from team members in own editor
This commit is contained in:

committed by
Sebastian Serth

parent
69ba7270dd
commit
89afb599e4
@ -0,0 +1,38 @@
|
|||||||
|
$(document).on('turbolinks:load', function () {
|
||||||
|
|
||||||
|
if (window.location.pathname.includes('/implement')) {
|
||||||
|
const editor = $('#editor');
|
||||||
|
const exercise_id = editor.data('exercise-id');
|
||||||
|
const current_user_id = editor.data('user-id');
|
||||||
|
const current_contributor_id = editor.data('contributor-id');
|
||||||
|
|
||||||
|
if ($.isController('exercises') && current_user_id !== current_contributor_id) {
|
||||||
|
|
||||||
|
App.synchronized_editor = App.cable.subscriptions.create({
|
||||||
|
channel: "SynchronizedEditorChannel", exercise_id: exercise_id
|
||||||
|
}, {
|
||||||
|
|
||||||
|
|
||||||
|
connected() {
|
||||||
|
// Called when the subscription is ready for use on the server
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected() {
|
||||||
|
// Called when the subscription has been terminated by the server
|
||||||
|
},
|
||||||
|
|
||||||
|
received(data) {
|
||||||
|
// Called when there's incoming data on the websocket for this channel
|
||||||
|
if (current_user_id !== data['current_user_id']) {
|
||||||
|
CodeOceanEditor.applyChanges(data['delta']['data']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
send_changes(delta) {
|
||||||
|
const delta_with_user_id = {current_user_id: current_user_id, delta: delta}
|
||||||
|
this.perform('send_changes', {delta_with_user_id: delta_with_user_id});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -29,6 +29,7 @@ var CodeOceanEditor = {
|
|||||||
running: false,
|
running: false,
|
||||||
|
|
||||||
lastCopyText: null,
|
lastCopyText: null,
|
||||||
|
lastDeltaObject: null,
|
||||||
|
|
||||||
<% self.class.include Rails.application.routes.url_helpers %>
|
<% self.class.include Rails.application.routes.url_helpers %>
|
||||||
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
|
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
|
||||||
@ -334,6 +335,12 @@ var CodeOceanEditor = {
|
|||||||
|
|
||||||
// listener for autosave
|
// listener for autosave
|
||||||
session.on("change", function (deltaObject, session) {
|
session.on("change", function (deltaObject, session) {
|
||||||
|
if (this.compareDeltaObjects(deltaObject, CodeOceanEditor.lastDeltaObject)) {
|
||||||
|
CodeOceanEditor.lastDeltaObject = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
App.synchronized_editor?.send_changes(deltaObject);
|
||||||
|
|
||||||
// TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace.
|
// TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace.
|
||||||
this.handleUTF16Surrogates(deltaObject, session);
|
this.handleUTF16Surrogates(deltaObject, session);
|
||||||
this.resetSaveTimer();
|
this.resetSaveTimer();
|
||||||
@ -1015,6 +1022,27 @@ var CodeOceanEditor = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyChanges: function (delta) {
|
||||||
|
this.lastDeltaObject = delta;
|
||||||
|
this.editors[0].session.doc.applyDeltas([delta]);
|
||||||
|
},
|
||||||
|
|
||||||
|
compareDeltaObjects: function (delta, last_delta) {
|
||||||
|
if (delta === null || last_delta === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta_data = delta.data
|
||||||
|
// We need this manual comparison because the range uses two different classes
|
||||||
|
// and there is no function to compare these two objects.
|
||||||
|
return delta_data.action === last_delta.action &&
|
||||||
|
delta_data.range.start.row === last_delta.range.start.row &&
|
||||||
|
delta_data.range.start.column === last_delta.range.start.column &&
|
||||||
|
delta_data.range.end.row === last_delta.range.end.row &&
|
||||||
|
delta_data.range.end.column === last_delta.range.end.column &&
|
||||||
|
delta_data.text === last_delta.text;
|
||||||
|
},
|
||||||
|
|
||||||
initializeEverything: function () {
|
initializeEverything: function () {
|
||||||
CodeOceanEditor.editors = [];
|
CodeOceanEditor.editors = [];
|
||||||
this.initializeRegexes();
|
this.initializeRegexes();
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
module ApplicationCable
|
module ApplicationCable
|
||||||
class Connection < ActionCable::Connection::Base
|
class Connection < ActionCable::Connection::Base
|
||||||
identified_by :current_user
|
identified_by :current_user, :current_contributor
|
||||||
|
|
||||||
def connect
|
def connect
|
||||||
|
# The order is important here, because a valid user is required to find a valid contributor.
|
||||||
self.current_user = find_verified_user
|
self.current_user = find_verified_user
|
||||||
|
self.current_contributor = find_verified_contributor
|
||||||
end
|
end
|
||||||
|
|
||||||
def disconnect
|
def disconnect
|
||||||
@ -24,5 +26,14 @@ module ApplicationCable
|
|||||||
current_user = ExternalUser.find_by(id: session[:external_user_id]) || InternalUser.find_by(id: session[:user_id])
|
current_user = ExternalUser.find_by(id: session[:external_user_id]) || InternalUser.find_by(id: session[:user_id])
|
||||||
current_user || reject_unauthorized_connection
|
current_user || reject_unauthorized_connection
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_verified_contributor
|
||||||
|
# Finding the current_contributor is similar to the code used in application_controller.rb#current_contributor
|
||||||
|
if session[:pg_id]
|
||||||
|
current_user.programming_groups.find(session[:pg_id])
|
||||||
|
else
|
||||||
|
current_user
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
25
app/channels/synchronized_editor_channel.rb
Normal file
25
app/channels/synchronized_editor_channel.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SynchronizedEditorChannel < ApplicationCable::Channel
|
||||||
|
def subscribed
|
||||||
|
stream_from specific_channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribed
|
||||||
|
# Any cleanup needed when channel is unsubscribed
|
||||||
|
stop_all_streams
|
||||||
|
end
|
||||||
|
|
||||||
|
def specific_channel
|
||||||
|
reject unless ProgrammingGroupPolicy.new(current_user, programming_group).stream_sync_editor?
|
||||||
|
"synchronized_editor_channel_group_#{programming_group.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def programming_group
|
||||||
|
current_contributor if current_contributor.programming_group?
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_changes(message)
|
||||||
|
ActionCable.server.broadcast(specific_channel, message['delta_with_user_id'])
|
||||||
|
end
|
||||||
|
end
|
@ -50,14 +50,15 @@ class ApplicationPolicy
|
|||||||
private :teacher_in_study_group?
|
private :teacher_in_study_group?
|
||||||
|
|
||||||
def author_in_programming_group?
|
def author_in_programming_group?
|
||||||
|
# !! Order is important !!
|
||||||
if @record.respond_to? :contributor # e.g. submission
|
if @record.respond_to? :contributor # e.g. submission
|
||||||
possible_programming_group = @record.contributor
|
possible_programming_group = @record.contributor
|
||||||
|
|
||||||
elsif @record.respond_to? :context # e.g. file
|
elsif @record.respond_to? :context # e.g. file
|
||||||
possible_programming_group = @record.context.contributor
|
possible_programming_group = @record.context.contributor
|
||||||
|
|
||||||
elsif @record.respond_to? :submission # e.g. request_for_comment
|
elsif @record.respond_to? :submission # e.g. request_for_comment
|
||||||
possible_programming_group = @record.submission.contributor
|
possible_programming_group = @record.submission.contributor
|
||||||
|
elsif @record.respond_to? :users # e.g. programming_group
|
||||||
|
possible_programming_group = @record
|
||||||
else
|
else
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
@ -8,4 +8,8 @@ class ProgrammingGroupPolicy < ApplicationPolicy
|
|||||||
def create?
|
def create?
|
||||||
everyone
|
everyone
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stream_sync_editor?
|
||||||
|
admin? || author_in_programming_group?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
- show_tips_interventions = @show_tips_interventions || "false"
|
- show_tips_interventions = @show_tips_interventions || "false"
|
||||||
- hide_rfc_button = @hide_rfc_button || false
|
- hide_rfc_button = @hide_rfc_button || false
|
||||||
|
|
||||||
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-message-out-of-memory=t('exercises.editor.out_of_memory', memory_limit: @exercise.execution_environment.memory_limit) data-submissions-url=submissions_path data-user-id=current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-tips-interventions=show_tips_interventions
|
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-message-out-of-memory=t('exercises.editor.out_of_memory', memory_limit: @exercise.execution_environment.memory_limit) data-submissions-url=submissions_path data-user-id=current_user.id data-contributor-id=current_contributor.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-tips-interventions=show_tips_interventions
|
||||||
- unless @embed_options[:hide_sidebar]
|
- unless @embed_options[:hide_sidebar]
|
||||||
- additional_classes = 'sidebar-col'
|
- additional_classes = 'sidebar-col'
|
||||||
- if @tips.blank?
|
- if @tips.blank?
|
||||||
|
@ -15,5 +15,21 @@ describe ProgrammingGroupPolicy do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
permissions(:stream_sync_editor?) do
|
||||||
|
it 'grants access to admins' do
|
||||||
|
expect(policy).to permit(create(:admin), programming_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'grants access to members of the programming group' do
|
||||||
|
programming_group.users do |user|
|
||||||
|
expect(policy).to permit(user, programming_group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not grant access to someone who is not a member of the programming group' do
|
||||||
|
expect(policy).not_to permit(create(:external_user), programming_group)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user