Add waiting room to create programming groups (#1919)
Co-authored-by: Sebastian Serth <Sebastian.Serth@hpi.de>
This commit is contained in:
@ -3,10 +3,9 @@ $(document).on('turbolinks:load', function () {
|
||||
if ($.isController('programming_groups') && window.location.pathname.includes('programming_groups/new')) {
|
||||
const matching_page = $('#matching');
|
||||
const exercise_id = matching_page.data('exercise-id');
|
||||
const specific_channel = { channel: "PgMatchingChannel", exercise_id: exercise_id};
|
||||
|
||||
App.pg_matching = App.cable.subscriptions.create({
|
||||
channel: "PgMatchingChannel", exercise_id: exercise_id
|
||||
}, {
|
||||
App.pg_matching = App.cable.subscriptions.create(specific_channel, {
|
||||
connected() {
|
||||
// Called when the subscription is ready for use on the server
|
||||
},
|
||||
@ -23,8 +22,17 @@ $(document).on('turbolinks:load', function () {
|
||||
window.location.reload();
|
||||
}
|
||||
break;
|
||||
case 'joined_pg':
|
||||
if (ProgrammingGroups.contains_own_user(data.users)) {
|
||||
window.location.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
waiting_for_match() {
|
||||
this.perform('waiting_for_match');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -20,6 +20,10 @@ var ProgrammingGroups = {
|
||||
is_other_session: function (other_session_id) {
|
||||
return this.session_id !== other_session_id;
|
||||
},
|
||||
|
||||
contains_own_user: function (users) {
|
||||
return users.find(e => ProgrammingGroups.is_other_user(e) === false) !== undefined
|
||||
}
|
||||
};
|
||||
|
||||
$(document).on('turbolinks:load', function () {
|
||||
@ -35,4 +39,12 @@ $(document).on('turbolinks:load', function () {
|
||||
new bootstrap.Modal(modal).show();
|
||||
}
|
||||
}
|
||||
|
||||
const join_pair_button = $('.join_programming_pair');
|
||||
if (join_pair_button.isPresent()) {
|
||||
join_pair_button.on('click', function() {
|
||||
App.pg_matching?.waiting_for_match();
|
||||
CodeOceanEditor.showSpinner(join_pair_button);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -8,6 +8,8 @@ class PgMatchingChannel < ApplicationCable::Channel
|
||||
|
||||
def unsubscribed
|
||||
# Any cleanup needed when channel is unsubscribed
|
||||
@current_waiting_user.status_disconnected! if @current_waiting_user&.reload&.status_waiting?
|
||||
|
||||
stop_all_streams
|
||||
end
|
||||
|
||||
@ -15,8 +17,29 @@ class PgMatchingChannel < ApplicationCable::Channel
|
||||
"pg_matching_channel_exercise_#{@exercise.id}"
|
||||
end
|
||||
|
||||
def waiting_for_match
|
||||
@current_waiting_user = PairProgrammingWaitingUser.find_or_initialize_by(user: current_user, exercise: @exercise)
|
||||
@current_waiting_user.status_waiting!
|
||||
|
||||
match_waiting_users
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def match_waiting_users
|
||||
# Check if there is another waiting user for this exercise
|
||||
waiting_user = PairProgrammingWaitingUser.where(exercise: @exercise, status: :waiting).where.not(user: current_user).order(created_at: :asc).first
|
||||
return if waiting_user.blank?
|
||||
|
||||
# If there is another waiting user, create a programming group with both users
|
||||
match = [waiting_user, @current_waiting_user]
|
||||
# Create the programming group. Note that an unhandled exception will be raised if the programming group
|
||||
# is not valid (i.e., if one of the users already joined a programming group for this exercise).
|
||||
pg = ProgrammingGroup.create!(exercise: @exercise, users: match.map(&:user))
|
||||
match.each {|wu| wu.update(status: :created_pg, programming_group: pg) }
|
||||
ActionCable.server.broadcast(specific_channel, {action: 'joined_pg', users: pg.users.map(&:to_page_context)})
|
||||
end
|
||||
|
||||
def set_and_authorize_exercise
|
||||
@exercise = Exercise.find(params[:exercise_id])
|
||||
reject unless ExercisePolicy.new(current_user, @exercise).implement?
|
||||
|
@ -299,7 +299,7 @@ class ExercisesController < ApplicationController
|
||||
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
def implement # rubocop:disable Metrics/CyclomaticComplexity
|
||||
if session[:pg_id] && current_contributor.exercise != @exercise
|
||||
# we are acting on behalf of a programming group
|
||||
if current_user.admin?
|
||||
@ -315,8 +315,9 @@ 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**
|
||||
session[:pg_id] = pg.id
|
||||
@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?
|
||||
elsif PairProgramming23Study.participate?(current_user, @exercise) && current_user.submissions.where(study_group_id: current_user.current_study_group_id, exercise: @exercise).none?
|
||||
Event.create(category: 'pp_work_alone', user: current_user, exercise: @exercise, data: nil, file_id: nil)
|
||||
current_user.pair_programming_waiting_users&.find_by(exercise: @exercise)&.update(status: :worked_alone)
|
||||
end
|
||||
|
||||
user_solved_exercise = @exercise.solved_by?(current_contributor)
|
||||
|
@ -53,6 +53,9 @@ class ProgrammingGroupsController < ApplicationController
|
||||
ActionCable.server.broadcast("pg_matching_channel_exercise_#{@exercise.id}", message)
|
||||
end
|
||||
|
||||
# Check if the user was waiting for a programming group match and update the status
|
||||
current_user.pair_programming_waiting_users&.find_by(exercise: @exercise)&.update(status: :created_pg, programming_group: @programming_group)
|
||||
|
||||
# Just set the programming group id in the session for the creator of the group, so that the user can be redirected.
|
||||
session[:pg_id] = @programming_group.id
|
||||
|
||||
|
@ -32,6 +32,7 @@ class Exercise < ApplicationRecord
|
||||
has_many :external_users, source: :contributor, source_type: 'ExternalUser', through: :submissions
|
||||
has_many :internal_users, source: :contributor, source_type: 'InternalUser', through: :submissions
|
||||
has_many :programming_groups
|
||||
has_many :pair_programming_waiting_users
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
|
||||
|
25
app/models/pair_programming_waiting_user.rb
Normal file
25
app/models/pair_programming_waiting_user.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PairProgrammingWaitingUser < ApplicationRecord
|
||||
include Creation
|
||||
|
||||
belongs_to :exercise
|
||||
belongs_to :programming_group, optional: true
|
||||
|
||||
enum status: {
|
||||
waiting: 0,
|
||||
joined_pg: 1,
|
||||
disconnected: 2,
|
||||
worked_alone: 3,
|
||||
created_pg: 4,
|
||||
}, _prefix: true
|
||||
|
||||
validates :user_id, uniqueness: {scope: %i[exercise_id user_type]}
|
||||
validates :programming_group_id, presence: true, if: -> { status_joined_pg? || status_created_pg? }
|
||||
|
||||
after_save :capture_event
|
||||
|
||||
def capture_event
|
||||
Event.create(category: 'pp_matching', user:, exercise:, data: status.to_s)
|
||||
end
|
||||
end
|
@ -12,6 +12,7 @@ class ProgrammingGroup < ApplicationRecord
|
||||
has_many :events
|
||||
has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor'
|
||||
has_many :pair_programming_exercise_feedbacks
|
||||
has_many :pair_programming_waiting_users
|
||||
belongs_to :exercise
|
||||
|
||||
validate :min_group_size
|
||||
|
@ -25,6 +25,7 @@ class User < ApplicationRecord
|
||||
has_many :events
|
||||
has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor'
|
||||
has_many :pair_programming_exercise_feedbacks
|
||||
has_many :pair_programming_waiting_users
|
||||
has_one :codeharbor_link, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
@ -1,8 +1,16 @@
|
||||
= form_for(@programming_group, url: exercise_programming_groups_path) do |f|
|
||||
= render('shared/form_errors', object: @programming_group)
|
||||
.mb-3
|
||||
= f.label(:programming_partner_ids, class: 'form-label')
|
||||
= f.text_field(:programming_partner_ids, class: 'form-control', required: true, value: (@programming_group.programming_partner_ids - [current_user.id_with_type]).join(', '))
|
||||
/.help-block.form-text = t('.hints.programming_partner_ids')
|
||||
.actions.mb-0
|
||||
= render('shared/submit_button', f: f, object: @programming_group)
|
||||
.form-group
|
||||
.row
|
||||
.col-md-6
|
||||
= f.label(:programming_partner_ids, class: 'form-label')
|
||||
.row
|
||||
.col-md-6
|
||||
.input-group.mb-3
|
||||
= f.text_field(:programming_partner_ids, class: 'form-control', required: true, value: (@programming_group.programming_partner_ids - [current_user.id_with_type]).join(', '))
|
||||
= render('shared/submit_button', f: f, object: @programming_group)
|
||||
/.help-block.form-text = t('.hints.programming_partner_ids')
|
||||
.col-md-6
|
||||
.join_programming_pair.button.btn.btn-primary.d-none.d-md-block
|
||||
i.fa-solid.fa-circle-notch.fa-spin.d-none
|
||||
= t('programming_groups.new.find_programming_partner')
|
||||
|
@ -1,30 +1,38 @@
|
||||
h1 = t('programming_groups.new.create_programming_pair')
|
||||
#matching.row data-exercise-id=@exercise.id
|
||||
.col-md-6
|
||||
p
|
||||
h1.d-inline-block = t('programming_groups.new.create_programming_pair')
|
||||
.btn.btn-success.float-end data-bs-toggle= 'modal' data-bs-target= '#modal-info-pair-programming'
|
||||
i.fa-solid.fa-circle-info
|
||||
= t('programming_groups.new.pair_programming_info')
|
||||
#matching data-exercise-id=@exercise.id.to_s
|
||||
.row
|
||||
.col-12.mt-2.mb-4
|
||||
p = t('programming_groups.new.info_work_together', exercise_title: @exercise.title)
|
||||
.text-body-tertiary == t('exercises.implement.pair_programming_feedback', url: "https://etherpad.xopic.de/p/openHPI_PairProgrammingFeedback?userName=#{CGI.escape(current_user.displayname)}")
|
||||
|
||||
.row
|
||||
.col-md-6
|
||||
h5 = t('programming_groups.new.work_with_a_friend')
|
||||
p = t('programming_groups.new.enter_partner_id', exercise_title: @exercise.title)
|
||||
|
||||
|
||||
=> t('programming_groups.new.own_user_id')
|
||||
b
|
||||
= current_user.id_with_type
|
||||
b = current_user.id_with_type
|
||||
.d-md-none
|
||||
= render('form')
|
||||
|
||||
button.btn.btn-success data-bs-toggle= 'modal' data-bs-target= '#modal-info-pair-programming'
|
||||
i.fa-solid.fa-circle-info
|
||||
= t('programming_groups.new.pair_programming_info')
|
||||
.col-md-6
|
||||
h5 = t('programming_groups.new.find_partner_title')
|
||||
p = t('programming_groups.new.find_partner_description')
|
||||
|
||||
p.mt-4
|
||||
= t('programming_groups.new.enter_partner_id', exercise_title: @exercise.title)
|
||||
= render('form')
|
||||
|
||||
div.mt-4
|
||||
== t('programming_groups.new.work_alone', path: implement_exercise_path(@exercise))
|
||||
|
||||
.col-md-6
|
||||
h5 = t('programming_groups.new.find_partner_title')
|
||||
p
|
||||
= t('programming_groups.new.find_partner_description')
|
||||
- if !browser.safari?
|
||||
iframe name="embed_readwrite" src="https://etherpad.xopic.de/p/find_programming_group_for_exercise_#{@exercise.id}?userName=#{CGI.escape(current_user.displayname)}&showControls=false&showChat=false&showLineNumbers=true&useMonospaceFont=false" width="100%" height="300" style='border: 1px solid black;'
|
||||
- else
|
||||
== t('programming_groups.new.safari_not_supported', url: "https://etherpad.xopic.de/p/find_programming_group_for_exercise_#{@exercise.id}?userName=#{CGI.escape(current_user.displayname)}&showControls=false&showChat=false&showLineNumbers=true&useMonospaceFont=false")
|
||||
.join_programming_pair.button.btn.btn-primary.d-md-none.mb-3
|
||||
i.fa-solid.fa-circle-notch.fa-spin.d-none
|
||||
= t('programming_groups.new.find_programming_partner')
|
||||
.row
|
||||
.col-12.d-none.d-md-block
|
||||
= render('form')
|
||||
|
||||
.row
|
||||
.col-12
|
||||
h5 = t('programming_groups.new.work_alone')
|
||||
== t('programming_groups.new.work_alone_description', path: implement_exercise_path(@exercise))
|
||||
|
||||
= render('shared/modal', classes: 'modal-lg', id: 'modal-info-pair-programming', template: 'programming_groups/_info_pair_programming', title: t('programming_groups.new.pair_programming_info'))
|
||||
|
Reference in New Issue
Block a user