516 lines
20 KiB
Ruby
516 lines
20 KiB
Ruby
#
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# @API Conferences
|
|
#
|
|
# API for accessing information on conferences.
|
|
#
|
|
# @model ConferenceRecording
|
|
# {
|
|
# "id": "ConferenceRecording",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "duration_minutes": {
|
|
# "example": 0,
|
|
# "type": "integer"
|
|
# },
|
|
# "title": {
|
|
# "example": "course2: Test conference 3 [170]_0",
|
|
# "type": "string"
|
|
# },
|
|
# "updated_at": {
|
|
# "example": "2013-12-12T16:09:33.903-07:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "created_at": {
|
|
# "example": "2013-12-12T16:09:09.960-07:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "playback_url": {
|
|
# "example": "http://example.com/recording_url",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model Conference
|
|
# {
|
|
# "id": "Conference",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The id of the conference",
|
|
# "example": 170,
|
|
# "type": "integer"
|
|
# },
|
|
# "conference_type": {
|
|
# "description": "The type of conference",
|
|
# "example": "AdobeConnect",
|
|
# "type": "string"
|
|
# },
|
|
# "conference_key": {
|
|
# "description": "The 3rd party's ID for the conference",
|
|
# "example": "abcdjoelisgreatxyz",
|
|
# "type": "string"
|
|
# },
|
|
# "description": {
|
|
# "description": "The description for the conference",
|
|
# "example": "Conference Description",
|
|
# "type": "string"
|
|
# },
|
|
# "duration": {
|
|
# "description": "The expected duration the conference is supposed to last",
|
|
# "example": 60,
|
|
# "type": "integer"
|
|
# },
|
|
# "ended_at": {
|
|
# "description": "The date that the conference ended at, null if it hasn't ended",
|
|
# "example": "2013-12-13T17:23:26Z",
|
|
# "type": "datetime"
|
|
# },
|
|
# "started_at": {
|
|
# "description": "The date the conference started at, null if it hasn't started",
|
|
# "example": "2013-12-12T23:02:17Z",
|
|
# "type": "datetime"
|
|
# },
|
|
# "title": {
|
|
# "description": "The title of the conference",
|
|
# "example": "Test conference",
|
|
# "type": "string"
|
|
# },
|
|
# "users": {
|
|
# "description": "Array of user ids that are participants in the conference",
|
|
# "example": [1, 7, 8, 9, 10],
|
|
# "type": "array",
|
|
# "items": { "type": "integer"}
|
|
# },
|
|
# "has_advanced_settings": {
|
|
# "description": "True if the conference type has advanced settings.",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "long_running": {
|
|
# "description": "If true the conference is long running and has no expected end time",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "user_settings": {
|
|
# "description": "A collection of settings specific to the conference type",
|
|
# "example": {"record": true},
|
|
# "type": "object"
|
|
# },
|
|
# "recordings": {
|
|
# "description": "A List of recordings for the conference",
|
|
# "type": "array",
|
|
# "items": { "$ref": "ConferenceRecording" }
|
|
# },
|
|
# "url": {
|
|
# "description": "URL for the conference, may be null if the conference type doesn't set it",
|
|
# "type": "string"
|
|
# },
|
|
# "join_url": {
|
|
# "description": "URL to join the conference, may be null if the conference type doesn't set it",
|
|
# "type": "string"
|
|
# },
|
|
# "context_type": {
|
|
# "description": "The type of this conference's context, typically 'Course' or 'Group'.",
|
|
# "type": "string"
|
|
# },
|
|
# "context_id": {
|
|
# "description": "The ID of this conference's context.",
|
|
# "type": "integer"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class ConferencesController < ApplicationController
|
|
include Api::V1::Conferences
|
|
|
|
before_action :require_context, except: :for_user
|
|
skip_before_action :load_user, :only => [:recording_ready]
|
|
|
|
add_crumb(proc{ t '#crumbs.conferences', "Conferences"}) do |c|
|
|
if c.context.present?
|
|
c.send(:named_context_url, c.context, :context_conferences_url)
|
|
end
|
|
end
|
|
|
|
before_action { |c| c.active_tab = "conferences" }
|
|
before_action :require_config
|
|
before_action :reject_student_view_student
|
|
before_action :get_conference, :except => [:index, :create, :for_user]
|
|
|
|
# @API List conferences
|
|
# Retrieve the paginated list of conferences for this context
|
|
#
|
|
# This API returns a JSON object containing the list of conferences,
|
|
# the key for the list of conferences is "conferences"
|
|
#
|
|
# @example_request
|
|
# curl 'https://<canvas>/api/v1/courses/<course_id>/conferences' \
|
|
# -H "Authorization: Bearer <token>"
|
|
#
|
|
# curl 'https://<canvas>/api/v1/groups/<group_id>/conferences' \
|
|
# -H "Authorization: Bearer <token>"
|
|
#
|
|
# @returns [Conference]
|
|
def index
|
|
return unless authorized_action(@context, @current_user, :read)
|
|
return unless tab_enabled?(@context.class::TAB_CONFERENCES)
|
|
return unless @current_user
|
|
log_api_asset_access([ "conferences", @context ], "conferences", "other")
|
|
conferences = @context.grants_right?(@current_user, :manage_content) ?
|
|
@context.web_conferences.active :
|
|
@current_user.web_conferences.active.shard(@context.shard).where(context_type: @context.class.to_s, context_id: @context.id)
|
|
conferences = conferences.with_config.order("created_at DESC, id DESC")
|
|
api_request? ? api_index(conferences, polymorphic_url([:api_v1, @context, :conferences])) : web_index(conferences)
|
|
end
|
|
|
|
module UserConferencesBookmarker
|
|
def self.bookmark_for(conference)
|
|
# We're sorting in descending order, so we need to "flip" our sort values
|
|
# to make sure a conference is ordered properly vis-a-vis its neighbors
|
|
[Time.zone.now - conference.created_at, -conference.id]
|
|
end
|
|
|
|
def self.validate(bookmark)
|
|
return false unless bookmark.is_a?(Array) && bookmark.length == 2
|
|
bookmark.first.is_a?(ActiveSupport::TimeWithZone) && bookmark.second.is_a?(Integer)
|
|
end
|
|
|
|
def self.restrict_scope(scope, pager)
|
|
if pager.current_bookmark
|
|
creation_date, id = pager.current_bookmark
|
|
comparison = pager.include_bookmark ? "<=" : "<"
|
|
|
|
scope = scope.where("ROW(created_at, id) #{comparison} ROW(?, ?)", creation_date, id)
|
|
end
|
|
scope.order("created_at DESC, id DESC")
|
|
end
|
|
end
|
|
|
|
# @API List conferences for the current user
|
|
# Retrieve the paginated list of conferences for all courses and groups
|
|
# the current user belongs to
|
|
#
|
|
# This API returns a JSON object containing the list of conferences.
|
|
# The key for the list of conferences is "conferences".
|
|
#
|
|
# @argument state [String]
|
|
# If set to "live", returns only conferences that are live (i.e., have
|
|
# started and not finished yet). If omitted, returns all conferences for
|
|
# this user's groups and courses.
|
|
#
|
|
# @example_request
|
|
# curl 'https://<canvas>/api/v1/conferences' \
|
|
# -H "Authorization: Bearer <token>"
|
|
#
|
|
# @returns [Conference]
|
|
def for_user
|
|
return render_unauthorized_action unless @current_user
|
|
|
|
log_api_asset_access(["conferences"], "conferences", "other")
|
|
|
|
courses_collection = ShardedBookmarkedCollection.build(UserConferencesBookmarker, @current_user.enrollments) do |enrollments_scope|
|
|
conference_scope = WebConference.active.where(context_type: "Course", context_id: enrollments_scope.active.select(:course_id)).
|
|
where("EXISTS (?)", WebConferenceParticipant.where("web_conference_id = web_conferences.id AND user_id = ?", @current_user.id))
|
|
conference_scope = conference_scope.live if params[:state] == "live"
|
|
conference_scope.order("created_at DESC, id DESC")
|
|
end
|
|
|
|
groups_collection = ShardedBookmarkedCollection.build(UserConferencesBookmarker, @current_user.groups) do |groups_scope|
|
|
conference_scope = WebConference.active.where(context_type: "Group", context_id: groups_scope.active.select(:id)).
|
|
where("EXISTS (?)", WebConferenceParticipant.where("web_conference_id = web_conferences.id AND user_id = ?", @current_user.id))
|
|
conference_scope = conference_scope.live if params[:state] == "live"
|
|
conference_scope.order("created_at DESC, id DESC")
|
|
end
|
|
|
|
# ShardedBookmarkedCollection.build will return an ActiveRecord relation as
|
|
# a shortcut if it finds results on fewer than two shards. We still need to
|
|
# merge these two result sets, so re-wrap results in a bookmarked
|
|
# collection if needed.
|
|
courses_collection = BookmarkedCollection.wrap(UserConferencesBookmarker, courses_collection) if courses_collection.is_a?(ActiveRecord::Relation)
|
|
groups_collection = BookmarkedCollection.wrap(UserConferencesBookmarker, groups_collection) if groups_collection.is_a?(ActiveRecord::Relation)
|
|
|
|
merged_collection = BookmarkedCollection.merge(
|
|
['courses', courses_collection],
|
|
['groups', groups_collection]
|
|
)
|
|
|
|
results_page = Api.paginate(merged_collection, self, api_v1_conferences_url)
|
|
render json: api_conferences_json(results_page, @current_user, session)
|
|
end
|
|
|
|
def api_index(conferences, route)
|
|
web_conferences = Api.paginate(conferences, self, route)
|
|
preload_recordings(web_conferences)
|
|
render json: api_conferences_json(web_conferences, @current_user, session)
|
|
end
|
|
protected :api_index
|
|
|
|
def web_index(conferences)
|
|
conferences = conferences.to_a
|
|
preload_recordings(conferences)
|
|
@new_conferences, @concluded_conferences = conferences.partition { |conference|
|
|
conference.ended_at.nil?
|
|
}
|
|
log_asset_access([ "conferences", @context ], "conferences", "other")
|
|
|
|
Shackles.activate(:slave) do
|
|
@render_alternatives = WebConference.conference_types.all? { |ct| ct[:replace_with_alternatives] }
|
|
case @context
|
|
when Course
|
|
@users = User.where(:id => @context.current_enrollments.not_fake.active_by_date.where.not(:user_id => @current_user).select(:user_id)).
|
|
order(User.sortable_name_order_by_clause).to_a
|
|
@render_alternatives ||= @context.settings[:show_conference_alternatives].present?
|
|
when Group
|
|
@users = @context.participating_users_in_context.where("users.id<>?", @current_user).order(User.sortable_name_order_by_clause).to_a.uniq
|
|
@render_alternatives ||= @context.context.settings[:show_conference_alternatives].present?
|
|
else
|
|
@users = @context.users.where("users.id<>?", @current_user).order(User.sortable_name_order_by_clause).to_a.uniq
|
|
end
|
|
end
|
|
|
|
# exposing the initial data as json embedded on page.
|
|
js_env(
|
|
current_conferences: ui_conferences_json(@new_conferences, @context, @current_user, session),
|
|
concluded_conferences: ui_conferences_json(@concluded_conferences, @context, @current_user, session),
|
|
default_conference: default_conference_json(@context, @current_user, session),
|
|
conference_type_details: conference_types_json(WebConference.conference_types),
|
|
users: @users.map { |u| {:id => u.id, :name => u.last_name_first} },
|
|
can_create_conferences: @context.grants_right?(@current_user, session, :create_conferences),
|
|
render_alternatives: @render_alternatives
|
|
)
|
|
set_tutorial_js_env
|
|
flash[:error] = t('Some conferences on this page are hidden because of errors while retrieving their status') if @errors
|
|
end
|
|
protected :web_index
|
|
|
|
def show
|
|
if authorized_action(@conference, @current_user, :read)
|
|
if params[:external_url]
|
|
urls = @conference.external_url_for(params[:external_url], @current_user, params[:url_id])
|
|
if request.xhr?
|
|
return render :json => urls
|
|
elsif urls.size == 1
|
|
return redirect_to(urls.first[:url])
|
|
end
|
|
end
|
|
return redirect_to course_conferences_url(@context, :anchor => "conference_#{@conference.id}")
|
|
end
|
|
end
|
|
|
|
def create
|
|
if authorized_action(@context.web_conferences.temp_record, @current_user, :create)
|
|
@conference = @context.web_conferences.build(conference_params)
|
|
@conference.settings[:default_return_url] = named_context_url(@context, :context_url, :include_host => true)
|
|
@conference.user = @current_user
|
|
members = get_new_members
|
|
respond_to do |format|
|
|
if @conference.save
|
|
@conference.add_initiator(@current_user)
|
|
members.uniq.each do |u|
|
|
@conference.add_invitee(u)
|
|
end
|
|
@conference.save
|
|
format.html { redirect_to named_context_url(@context, :context_conference_url, @conference.id) }
|
|
format.json { render :json => WebConference.find(@conference.id).as_json(:permissions => {:user => @current_user, :session => session},
|
|
:url => named_context_url(@context, :context_conference_url, @conference)) }
|
|
else
|
|
format.html { render :index }
|
|
format.json { render :json => @conference.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def update
|
|
if authorized_action(@conference, @current_user, :update)
|
|
@conference.user ||= @current_user
|
|
members = get_new_members
|
|
respond_to do |format|
|
|
params[:web_conference].try(:delete, :long_running)
|
|
params[:web_conference].try(:delete, :conference_type)
|
|
if @conference.update(conference_params)
|
|
# TODO: ability to dis-invite people
|
|
members.uniq.each do |u|
|
|
@conference.add_invitee(u)
|
|
end
|
|
@conference.save
|
|
format.html { redirect_to named_context_url(@context, :context_conference_url, @conference.id) }
|
|
format.json { render :json => @conference.as_json(:permissions => {:user => @current_user, :session => session},
|
|
:url => named_context_url(@context, :context_conference_url, @conference)) }
|
|
else
|
|
format.html { render :edit }
|
|
format.json { render :json => @conference.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def join
|
|
if authorized_action(@conference, @current_user, :join)
|
|
unless @conference.valid_config?
|
|
flash[:error] = t(:type_disabled_error, "This type of conference is no longer enabled for this Canvas site")
|
|
redirect_to named_context_url(@context, :context_conferences_url)
|
|
return
|
|
end
|
|
if @conference.grants_right?(@current_user, session, :initiate) || @conference.grants_right?(@current_user, session, :resume) || @conference.active?(true)
|
|
@conference.add_attendee(@current_user)
|
|
@conference.restart if @conference.ended_at && @conference.grants_right?(@current_user, session, :initiate)
|
|
log_asset_access(@conference, "conferences", "conferences", 'participate')
|
|
if url = @conference.craft_url(@current_user, session, named_context_url(@context, :context_url, :include_host => true))
|
|
redirect_to url
|
|
else
|
|
flash[:error] = t(:general_error, "There was an error joining the conference")
|
|
redirect_to named_context_url(@context, :context_url)
|
|
end
|
|
else
|
|
flash[:notice] = t(:inactive_error, "That conference is not currently active")
|
|
redirect_to named_context_url(@context, :context_url)
|
|
end
|
|
end
|
|
rescue StandardError => e
|
|
flash[:error] = t(:general_error_with_message, "There was an error joining the conference. Message: '%{message}'", :message => e.message)
|
|
redirect_to named_context_url(@context, :context_conferences_url)
|
|
end
|
|
|
|
def recording_ready
|
|
secret = @conference.config[:secret_dec]
|
|
begin
|
|
signed_params = Canvas::Security.decode_jwt(params[:signed_parameters], [secret])
|
|
if signed_params[:meeting_id] == @conference.conference_key
|
|
@conference.recording_ready!
|
|
render json: [], status: :accepted
|
|
else
|
|
render json: signed_id_invalid_json, status: :unprocessable_entity
|
|
end
|
|
rescue Canvas::Security::InvalidToken
|
|
render json: invalid_jwt_token_json, status: :unauthorized
|
|
end
|
|
end
|
|
|
|
def close
|
|
if authorized_action(@conference, @current_user, :close)
|
|
unless @conference.active?
|
|
return render :json => { :message => 'conference is not active', :status => :bad_request }
|
|
end
|
|
|
|
if @conference.close
|
|
render :json => @conference.as_json(:permissions => {:user => @current_user, :session => session},
|
|
:url => named_context_url(@context, :context_conference_url, @conference))
|
|
else
|
|
render :json => @conference.errors
|
|
end
|
|
end
|
|
end
|
|
|
|
def settings
|
|
if authorized_action(@conference, @current_user, :update)
|
|
if @conference.has_advanced_settings?
|
|
redirect_to @conference.admin_settings_url(@current_user)
|
|
else
|
|
flash[:error] = t(:no_settings_error, "The conference does not have an advanced settings page")
|
|
redirect_to named_context_url(@context, :context_conference_url, @conference.id)
|
|
end
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
if authorized_action(@conference, @current_user, :delete)
|
|
@conference.transaction do
|
|
@conference.web_conference_participants.scope.delete_all
|
|
@conference.destroy
|
|
end
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_conferences_url) }
|
|
format.json { render :json => @conference }
|
|
end
|
|
end
|
|
end
|
|
|
|
def recording
|
|
if authorized_action(@conference, @current_user, :read)
|
|
@response = @conference.recording(params[:recording_id]) || {}
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_conferences_url) }
|
|
format.json { render :json => @response }
|
|
end
|
|
end
|
|
end
|
|
|
|
def delete_recording
|
|
if authorized_action(@conference, @current_user, :delete)
|
|
@response = @conference.delete_recording(params[:recording_id])
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_conferences_url) }
|
|
format.json { render :json => @response, :status => :ok }
|
|
end
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def require_config
|
|
unless WebConference.config
|
|
flash[:error] = t('#conferences.disabled_error', "Web conferencing has not been enabled for this Canvas site")
|
|
redirect_to named_context_url(@context, :context_url)
|
|
end
|
|
end
|
|
|
|
def get_new_members
|
|
members = [@current_user]
|
|
if params[:observers] && params[:observers][:remove] == '1'
|
|
ids = @context.user_ids - @context.observers.pluck(:id)
|
|
elsif params[:user] && params[:user][:all] != '1'
|
|
ids = []
|
|
params[:user].each do |id, val|
|
|
ids << id.to_i if val == '1'
|
|
end
|
|
else
|
|
ids = @context.user_ids
|
|
end
|
|
|
|
if @context.is_a? Course
|
|
members += @context.participating_users(ids).to_a
|
|
else
|
|
members += @context.participating_users_in_context(ids).to_a
|
|
end
|
|
|
|
members - @conference.invitees
|
|
end
|
|
|
|
private
|
|
def get_conference
|
|
@conference = @context.web_conferences.find(params[:conference_id] || params[:id])
|
|
end
|
|
|
|
def conference_params
|
|
params.require(:web_conference).
|
|
permit(:title, :duration, :description, :conference_type, :user_settings => strong_anything)
|
|
end
|
|
|
|
def preload_recordings(conferences)
|
|
conferences.group_by(&:class).each do |klass, klass_conferences|
|
|
if klass.respond_to?(:preload_recordings) # should only be BigBlueButton for now
|
|
klass.preload_recordings(klass_conferences)
|
|
end
|
|
end
|
|
end
|
|
end
|