Add events for pair programming study
This commit is contained in:

committed by
Sebastian Serth

parent
d1d5b0df6f
commit
79422225a8
@ -382,6 +382,20 @@ var CodeOceanEditor = {
|
|||||||
$(document).on('click', '#results a', this.showOutput.bind(this));
|
$(document).on('click', '#results a', this.showOutput.bind(this));
|
||||||
$(document).on('keydown', this.handleKeyPress.bind(this));
|
$(document).on('keydown', this.handleKeyPress.bind(this));
|
||||||
$(document).on('theme:change:ace', this.handleAceThemeChangeEvent.bind(this));
|
$(document).on('theme:change:ace', this.handleAceThemeChangeEvent.bind(this));
|
||||||
|
$('#start_chat').on('click', function(event) {
|
||||||
|
this.createEventHandler('pp_start_chat', null)(event)
|
||||||
|
// Allow to open the new tab even in Safari.
|
||||||
|
// See: https://stackoverflow.com/a/70463940
|
||||||
|
setTimeout(() => {
|
||||||
|
var pop_up_window = window.open($('#start_chat').data('url'), '_blank');
|
||||||
|
if (pop_up_window) {
|
||||||
|
pop_up_window.onerror = function (message) {
|
||||||
|
$.flash.danger({text: message});
|
||||||
|
this.sendError(message, null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.bind(this));
|
||||||
this.initializeFileTreeButtons();
|
this.initializeFileTreeButtons();
|
||||||
this.initializeWorkspaceButtons();
|
this.initializeWorkspaceButtons();
|
||||||
this.initializeRequestForComments()
|
this.initializeRequestForComments()
|
||||||
|
@ -24,6 +24,7 @@ module ApplicationCable
|
|||||||
def find_verified_user
|
def find_verified_user
|
||||||
# Finding the current_user is similar to the code used in application_controller.rb#current_user
|
# Finding the current_user is similar to the code used in application_controller.rb#current_user
|
||||||
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&.store_current_study_group_id(session[:study_group_id])
|
||||||
current_user || reject_unauthorized_connection
|
current_user || reject_unauthorized_connection
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,7 +22,10 @@ class SynchronizedEditorChannel < ApplicationCable::Channel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_changes(message)
|
def send_changes(message)
|
||||||
ActionCable.server.broadcast(specific_channel, message['delta_with_user_id'])
|
change = message['delta_with_user_id'].deep_symbolize_keys
|
||||||
|
|
||||||
|
Event::SynchronizedEditor.create_for_editor_change(change, current_user, programming_group)
|
||||||
|
ActionCable.server.broadcast(specific_channel, change)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_hello
|
def send_hello
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EventsController < ApplicationController
|
class EventsController < ApplicationController
|
||||||
def authorize!
|
before_action :require_user!
|
||||||
authorize(@event || @events)
|
|
||||||
end
|
|
||||||
private :authorize!
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@event = Event.new(event_params)
|
@event = Event.new(event_params)
|
||||||
@ -20,12 +17,21 @@ class EventsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorize!
|
||||||
|
authorize(@event || @events)
|
||||||
|
end
|
||||||
|
|
||||||
def event_params
|
def event_params
|
||||||
# The file ID processed here is the context of the exercise (template),
|
# The file ID processed here is the context of the exercise (template),
|
||||||
# not in the context of the submission!
|
# not in the context of the submission!
|
||||||
params[:event]
|
params[:event]
|
||||||
&.permit(:category, :data, :exercise_id, :file_id)
|
&.permit(:category, :data, :exercise_id, :file_id)
|
||||||
&.merge(user: current_user)
|
&.merge(user: current_user, programming_group:, study_group_id: current_user.current_study_group_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def programming_group
|
||||||
|
current_contributor if current_contributor.programming_group?
|
||||||
end
|
end
|
||||||
private :event_params
|
|
||||||
end
|
end
|
||||||
|
@ -315,6 +315,8 @@ class ExercisesController < ApplicationController
|
|||||||
# we are just acting on behalf of a single user who has already worked on this exercise as part of a programming group **in the context of the current study group**
|
# we are just acting on behalf of a single user who has already worked on this exercise as part of a programming group **in the context of the current study group**
|
||||||
session[:pg_id] = pg.id
|
session[:pg_id] = pg.id
|
||||||
@current_contributor = pg
|
@current_contributor = pg
|
||||||
|
elsif PairProgramming23Study.participate?(current_user, @exercise) && current_user.submissions.where(study_group_id: current_user.current_study_group_id, exercise: @exercise).any?
|
||||||
|
Event.create(category: 'pp_work_alone', user: current_user, exercise: @exercise, data: nil, file_id: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
user_solved_exercise = @exercise.solved_by?(current_contributor)
|
user_solved_exercise = @exercise.solved_by?(current_contributor)
|
||||||
|
@ -7,6 +7,7 @@ class ProgrammingGroupsController < ApplicationController
|
|||||||
before_action :set_exercise_and_authorize
|
before_action :set_exercise_and_authorize
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
Event.create(category: 'page_visit', user: current_user, exercise: @exercise, data: 'programming_groups_new', file_id: nil)
|
||||||
if current_user.submissions.where(exercise: @exercise, study_group_id: current_user.current_study_group_id).any?
|
if current_user.submissions.where(exercise: @exercise, study_group_id: current_user.current_study_group_id).any?
|
||||||
# A learner has worked on this exercise **alone** in the context of the **current study group**, so we redirect them to their progress.
|
# A learner has worked on this exercise **alone** in the context of the **current study group**, so we redirect them to their progress.
|
||||||
redirect_to_exercise
|
redirect_to_exercise
|
||||||
@ -36,6 +37,10 @@ class ProgrammingGroupsController < ApplicationController
|
|||||||
@programming_group.add(current_user)
|
@programming_group.add(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless @programming_group.valid?
|
||||||
|
Event.create(category: 'pp_invalid_partners', user: current_user, exercise: @exercise, data: programming_group_params[:programming_partner_ids], file_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
create_and_respond(object: @programming_group, path: proc { implement_exercise_path(@exercise) }) do
|
create_and_respond(object: @programming_group, path: proc { implement_exercise_path(@exercise) }) do
|
||||||
session[:pg_id] = @programming_group.id
|
session[:pg_id] = @programming_group.id
|
||||||
nil
|
nil
|
||||||
|
@ -32,6 +32,7 @@ module CodeOcean
|
|||||||
has_many :files, class_name: 'CodeOcean::File'
|
has_many :files, class_name: 'CodeOcean::File'
|
||||||
has_many :testruns
|
has_many :testruns
|
||||||
has_many :comments
|
has_many :comments
|
||||||
|
has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor'
|
||||||
alias descendants files
|
alias descendants files
|
||||||
|
|
||||||
mount_uploader :native_file, FileUploader
|
mount_uploader :native_file, FileUploader
|
||||||
|
@ -3,8 +3,19 @@
|
|||||||
class Event < ApplicationRecord
|
class Event < ApplicationRecord
|
||||||
include Creation
|
include Creation
|
||||||
belongs_to :exercise
|
belongs_to :exercise
|
||||||
belongs_to :file, class_name: 'CodeOcean::File'
|
belongs_to :file, class_name: 'CodeOcean::File', optional: true
|
||||||
|
belongs_to :study_group, optional: true
|
||||||
|
belongs_to :programming_group, optional: true
|
||||||
|
|
||||||
validates :category, presence: true
|
validates :category, presence: true
|
||||||
validates :data, presence: true
|
|
||||||
|
# We temporary allow an event to be stored without data.
|
||||||
|
# This is useful if the category (together with the user and exercise) is already enough.
|
||||||
|
validates :data, presence: true, if: -> { %w[pp_start_chat pp_invalid_partners pp_work_alone].exclude?(category) }
|
||||||
|
|
||||||
|
before_validation :data_presence
|
||||||
|
|
||||||
|
def data_presence
|
||||||
|
self.data = data.presence
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
92
app/models/event/synchronized_editor.rb
Normal file
92
app/models/event/synchronized_editor.rb
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Event::SynchronizedEditor < ApplicationRecord
|
||||||
|
self.table_name = 'events_synchronized_editor'
|
||||||
|
|
||||||
|
include Creation
|
||||||
|
|
||||||
|
belongs_to :programming_group
|
||||||
|
belongs_to :study_group
|
||||||
|
belongs_to :file, class_name: 'CodeOcean::File'
|
||||||
|
|
||||||
|
enum command: {
|
||||||
|
editor_change: 0,
|
||||||
|
connection_change: 1,
|
||||||
|
hello: 2,
|
||||||
|
### TODO: Kira's commands
|
||||||
|
}, _prefix: true
|
||||||
|
|
||||||
|
enum status: {
|
||||||
|
connected: 0,
|
||||||
|
disconnected: 1,
|
||||||
|
### TODO: connected, disconnected ...
|
||||||
|
}, _prefix: true
|
||||||
|
|
||||||
|
enum action: {
|
||||||
|
insertText: 0,
|
||||||
|
insertLines: 1,
|
||||||
|
removeText: 2,
|
||||||
|
removeLines: 3,
|
||||||
|
changeFold: 4,
|
||||||
|
removeFold: 5,
|
||||||
|
### TODO: AceEditor Actions: insertText, insertLines, removeText, removesLines, ...
|
||||||
|
}, _prefix: true
|
||||||
|
|
||||||
|
validates :status, presence: true, if: -> { command_connection_change? }
|
||||||
|
validates :action, presence: true, if: -> { command_editor_change? }
|
||||||
|
validates :range_start_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? }
|
||||||
|
validates :range_start_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? }
|
||||||
|
validates :range_end_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? }
|
||||||
|
validates :range_end_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? }
|
||||||
|
validates :nl, inclusion: {in: %W[\n \r\n]}, if: -> { action_removeLines? }
|
||||||
|
|
||||||
|
validate :either_lines_or_text
|
||||||
|
|
||||||
|
def self.create_for_editor_change(event, user, programming_group)
|
||||||
|
event_copy = event.deep_dup
|
||||||
|
file = event_copy.delete(:active_file)
|
||||||
|
delta = event_copy.delete(:delta)[:data]
|
||||||
|
range = delta.delete(:range)
|
||||||
|
|
||||||
|
create!(
|
||||||
|
user:,
|
||||||
|
programming_group:,
|
||||||
|
study_group_id: user.current_study_group_id,
|
||||||
|
command: event_copy.delete(:command),
|
||||||
|
action: delta.delete(:action),
|
||||||
|
file_id: file[:id],
|
||||||
|
range_start_row: range[:start][:row],
|
||||||
|
range_start_column: range[:start][:column],
|
||||||
|
range_end_row: range[:end][:row],
|
||||||
|
range_end_column: range[:end][:column],
|
||||||
|
text: delta.delete(:text),
|
||||||
|
nl: delta.delete(:nl),
|
||||||
|
lines: delta.delete(:lines),
|
||||||
|
data: data_attribute(event_copy, delta)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.data_attribute(event, delta)
|
||||||
|
event[:delta] = {data: delta} if delta.present?
|
||||||
|
event.presence if event.present? # TODO: As of now, we are storing the `current_user_id` most of the times. Intended?
|
||||||
|
end
|
||||||
|
private_class_method :data_attribute
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def strip_strings
|
||||||
|
# trim whitespace from beginning and end of string attributes
|
||||||
|
# except the `text` and `nl` of Event::SynchronizedEditor
|
||||||
|
attribute_names.without('text', 'nl').each do |name|
|
||||||
|
if send(name.to_sym).respond_to?(:strip)
|
||||||
|
send("#{name}=".to_sym, send(name).strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def either_lines_or_text
|
||||||
|
if [lines, text].count(&:present?) > 1
|
||||||
|
errors.add(:text, "can't be present if lines is also present")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -9,6 +9,8 @@ class ProgrammingGroup < ApplicationRecord
|
|||||||
has_many :internal_users, through: :programming_group_memberships, source_type: 'InternalUser', source: :user
|
has_many :internal_users, through: :programming_group_memberships, source_type: 'InternalUser', source: :user
|
||||||
has_many :testruns, through: :submissions
|
has_many :testruns, through: :submissions
|
||||||
has_many :runners, as: :contributor, dependent: :destroy
|
has_many :runners, as: :contributor, dependent: :destroy
|
||||||
|
has_many :events
|
||||||
|
has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor'
|
||||||
belongs_to :exercise
|
belongs_to :exercise
|
||||||
|
|
||||||
validate :min_group_size
|
validate :min_group_size
|
||||||
|
@ -9,6 +9,8 @@ class StudyGroup < ApplicationRecord
|
|||||||
has_many :subscriptions, dependent: :nullify
|
has_many :subscriptions, dependent: :nullify
|
||||||
has_many :authentication_tokens, dependent: :nullify
|
has_many :authentication_tokens, dependent: :nullify
|
||||||
has_many :lti_parameters, dependent: :delete_all
|
has_many :lti_parameters, dependent: :delete_all
|
||||||
|
has_many :events
|
||||||
|
has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor'
|
||||||
belongs_to :consumer
|
belongs_to :consumer
|
||||||
|
|
||||||
def users
|
def users
|
||||||
|
@ -9,8 +9,9 @@
|
|||||||
div.col-3.float-end
|
div.col-3.float-end
|
||||||
span.badge.rounded-pill.bg-primary.float-end.mt-2.score
|
span.badge.rounded-pill.bg-primary.float-end.mt-2.score
|
||||||
- if current_contributor.programming_group?
|
- if current_contributor.programming_group?
|
||||||
a.btn.btn-sm.btn-primary.me-3.mt-1(href='https://jitsi.fem.tu-ilmenau.de/openHPI_ProgrammingGroup#{current_contributor.id}', target='_blank')
|
button.btn.btn-sm.btn-primary.me-3.mt-1#start_chat data= {url: "https://jitsi.fem.tu-ilmenau.de/openHPI_ProgrammingGroup#{current_contributor.id}"}
|
||||||
i.fa-solid.fa-video
|
i.fa-solid.fa-video
|
||||||
|
|
||||||
span = t('exercises.editor.start_video')
|
span = t('exercises.editor.start_video')
|
||||||
|
|
||||||
div.small.text-body-tertiary.mt-1
|
div.small.text-body-tertiary.mt-1
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddContributorAndStudyGroupToEvents < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_reference :events, :programming_group, index: true, null: true, foreign_key: true
|
||||||
|
add_reference :events, :study_group, index: true, null: true, foreign_key: true
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateEventsSynchronizedEditor < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :events_synchronized_editor, id: :uuid do |t|
|
||||||
|
t.references :programming_group, index: true, null: false, foreign_key: true
|
||||||
|
t.references :study_group, index: true, null: false, foreign_key: true
|
||||||
|
t.references :user, index: true, null: false, polymorphic: true
|
||||||
|
t.integer :command, limit: 1, null: false, default: 0, comment: 'Used as enum in Rails'
|
||||||
|
t.integer :status, limit: 1, null: true, comment: 'Used as enum in Rails'
|
||||||
|
|
||||||
|
# The following attributes are only stored for delta objects
|
||||||
|
t.references :file, index: true, null: true, foreign_key: true
|
||||||
|
t.integer :action, limit: 1, null: true, comment: 'Used as enum in Rails'
|
||||||
|
t.integer :range_start_row, null: true
|
||||||
|
t.integer :range_start_column, null: true
|
||||||
|
t.integer :range_end_row, null: true
|
||||||
|
t.integer :range_end_column, null: true
|
||||||
|
t.text :text, null: true
|
||||||
|
t.text :lines, null: true, array: true
|
||||||
|
t.string :nl, limit: 2, null: true, comment: 'Identifies the line break type (i.e., \r\n or \n)'
|
||||||
|
t.jsonb :data, null: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
db/schema.rb
36
db/schema.rb
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_21_063101) do
|
ActiveRecord::Schema[7.0].define(version: 2023_09_04_180123) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
@ -143,6 +143,35 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_21_063101) do
|
|||||||
t.integer "file_id"
|
t.integer "file_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.bigint "programming_group_id"
|
||||||
|
t.bigint "study_group_id"
|
||||||
|
t.index ["programming_group_id"], name: "index_events_on_programming_group_id"
|
||||||
|
t.index ["study_group_id"], name: "index_events_on_study_group_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "events_synchronized_editor", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
t.bigint "programming_group_id", null: false
|
||||||
|
t.bigint "study_group_id", null: false
|
||||||
|
t.string "user_type", null: false
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.integer "command", limit: 2, default: 0, null: false, comment: "Used as enum in Rails"
|
||||||
|
t.integer "status", limit: 2, comment: "Used as enum in Rails"
|
||||||
|
t.bigint "file_id"
|
||||||
|
t.integer "action", limit: 2, comment: "Used as enum in Rails"
|
||||||
|
t.integer "range_start_row"
|
||||||
|
t.integer "range_start_column"
|
||||||
|
t.integer "range_end_row"
|
||||||
|
t.integer "range_end_column"
|
||||||
|
t.text "text"
|
||||||
|
t.text "lines", array: true
|
||||||
|
t.string "nl", limit: 2, comment: "Identifies the line break type (i.e., \\r\\n or \\n)"
|
||||||
|
t.jsonb "data"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["file_id"], name: "index_events_synchronized_editor_on_file_id"
|
||||||
|
t.index ["programming_group_id"], name: "index_events_synchronized_editor_on_programming_group_id"
|
||||||
|
t.index ["study_group_id"], name: "index_events_synchronized_editor_on_study_group_id"
|
||||||
|
t.index ["user_type", "user_id"], name: "index_events_synchronized_editor_on_user"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "execution_environments", id: :serial, force: :cascade do |t|
|
create_table "execution_environments", id: :serial, force: :cascade do |t|
|
||||||
@ -603,6 +632,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_21_063101) do
|
|||||||
add_foreign_key "community_solution_contributions", "study_groups"
|
add_foreign_key "community_solution_contributions", "study_groups"
|
||||||
add_foreign_key "community_solution_locks", "community_solutions"
|
add_foreign_key "community_solution_locks", "community_solutions"
|
||||||
add_foreign_key "community_solutions", "exercises"
|
add_foreign_key "community_solutions", "exercises"
|
||||||
|
add_foreign_key "events", "programming_groups"
|
||||||
|
add_foreign_key "events", "study_groups"
|
||||||
|
add_foreign_key "events_synchronized_editor", "files"
|
||||||
|
add_foreign_key "events_synchronized_editor", "programming_groups"
|
||||||
|
add_foreign_key "events_synchronized_editor", "study_groups"
|
||||||
add_foreign_key "exercise_tips", "exercise_tips", column: "parent_exercise_tip_id"
|
add_foreign_key "exercise_tips", "exercise_tips", column: "parent_exercise_tip_id"
|
||||||
add_foreign_key "exercise_tips", "exercises"
|
add_foreign_key "exercise_tips", "exercises"
|
||||||
add_foreign_key "exercise_tips", "tips"
|
add_foreign_key "exercise_tips", "tips"
|
||||||
|
Reference in New Issue
Block a user