Add waiting room to create programming groups (#1919)

Co-authored-by: Sebastian Serth <Sebastian.Serth@hpi.de>
This commit is contained in:
Kira Grammel
2023-09-21 15:07:10 +02:00
committed by GitHub
parent 1dfc306e76
commit 9f837412c7
15 changed files with 174 additions and 48 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)') }

View 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

View File

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

View File

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

View File

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

View File

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