diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb index 8552bdd7456eae2827eeb90c026ba8f408e4003f..b510c1b896102d8c8699562beca39ae64bd4a04b 100644 --- a/app/controllers/api/v1/rooms_controller.rb +++ b/app/controllers/api/v1/rooms_controller.rb @@ -19,9 +19,9 @@ module Api module V1 class RoomsController < ApiController - skip_before_action :ensure_authenticated, only: %i[public_show] + skip_before_action :ensure_authenticated, only: %i[public_show public_recordings] - before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show] + before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show public_recordings] before_action only: %i[create index] do ensure_authorized('CreateRoom') @@ -136,6 +136,16 @@ module Api render_data data: room_recordings, meta: pagy_metadata(pagy), status: :ok end + # GET /api/v1/rooms/:friendly_id/public_recordings.json + # Returns all of a specific room's PUBLIC recordings + def public_recordings + sort_config = config_sorting(allowed_columns: %w[name length]) + + pagy, recordings = pagy(@room.public_recordings.order(sort_config, recorded_at: :desc).public_search(params[:search])) + + render_data data: recordings, meta: pagy_metadata(pagy), serializer: PublicRecordingSerializer, status: :ok + end + # GET /api/v1/rooms/:friendly_id/recordings_processing.json # Returns the total number of processing recordings for a specific room def recordings_processing diff --git a/app/models/recording.rb b/app/models/recording.rb index 41bde789850e95a614d9f27838b3471365210261..94578602c2917649fb86149759e0e8fe4b9ca70e 100644 --- a/app/models/recording.rb +++ b/app/models/recording.rb @@ -46,4 +46,13 @@ class Recording < ApplicationRecord all.includes(:formats) end + + def self.public_search(input) + if input + return joins(:formats).where('recordings.name ILIKE :input OR formats.recording_type ILIKE :input', + input: "%#{input}%").includes(:formats) + end + + all.includes(:formats) + end end diff --git a/app/models/room.rb b/app/models/room.rb index edc9d58a76c9c0c8147baca9081d86a4e5ad7f04..649c5c445c54a7721eeae9cecafed02d4e5422a6 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -75,6 +75,10 @@ class Room < ApplicationRecord end end + def public_recordings + recordings.where(visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]]) + end + private def set_friendly_id diff --git a/app/serializers/public_recording_serializer.rb b/app/serializers/public_recording_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..a687d449dee5839dc88999e307d6ab6544a7cd2a --- /dev/null +++ b/app/serializers/public_recording_serializer.rb @@ -0,0 +1,23 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see <http://www.gnu.org/licenses/>. + +# frozen_string_literal: true + +class PublicRecordingSerializer < ApplicationSerializer + attributes :id, :record_id, :name, :length, :recorded_at + + has_many :formats +end diff --git a/config/routes.rb b/config/routes.rb index 33711f3b417896d9ecd5c3c1c99b958306a2431c..31b3881f023a54198f2f3b2469885b8bf54b3a85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,6 +45,7 @@ Rails.application.routes.draw do resources :rooms, param: :friendly_id do member do get '/recordings', to: 'rooms#recordings' + get '/public_recordings', to: 'rooms#public_recordings' get '/recordings_processing', to: 'rooms#recordings_processing' get '/public', to: 'rooms#public_show' delete :purge_presentation diff --git a/spec/controllers/rooms_controller_spec.rb b/spec/controllers/rooms_controller_spec.rb index d86c9a3fdf5297b54113401f142c81d6949e9e47..7418355236e6bbbd829aef5aca87533d587f921e 100644 --- a/spec/controllers/rooms_controller_spec.rb +++ b/spec/controllers/rooms_controller_spec.rb @@ -327,4 +327,218 @@ RSpec.describe Api::V1::RoomsController, type: :controller do expect(recording_ids).to match_array(recordings.pluck(:id)) end end + + describe '#public_recordings' do + let(:room) { create(:room) } + + before { sign_out_user } + + context 'Filtration' do + let!(:public_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public]) } + let!(:public_protected_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected]) } + + before do + create(:recording, room:, visibility: Recording::VISIBILITIES[:unpublished]) + create(:recording, room:, visibility: Recording::VISIBILITIES[:published]) + create(:recording, room:, visibility: Recording::VISIBILITIES[:protected]) + end + + it 'returns :ok with a list of the room public recordings only' do + expected_response = JSON.parse( + [PublicRecordingSerializer.new(public_recording), PublicRecordingSerializer.new(public_protected_recording)].to_json + ) + + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to match_array(expected_response) + end + end + + context 'Pagination' do + # The order of creation and the matching of :recorded_at value impacts a page recordings list. + # Thus fixing those values ensures the determinism of these examples. + let!(:first_page_recordings) do + create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664), visibility: Recording::VISIBILITIES[:public]) + end + let!(:second_page_recordings) do + create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664), + visibility: Recording::VISIBILITIES[:public_protected]) + end + + def expect_response_to_have(page:, pages:, recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id')) + expect(JSON.parse(response.body)['meta']['pages']).to eq(pages) + expect(JSON.parse(response.body)['meta']['page']).to eq(page) + end + + context 'First page' do + it 'returns :ok with a list of the first page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 1 } + + expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings + end + end + + context 'Second page' do + it 'returns :ok with a list of the first second page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 2 } + + expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings + end + end + + context 'No page' do + it 'returns :ok with a list of the first page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings + end + end + + context 'Overflowing page' do + it 'returns :ok with a list of the last page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 3 } + + expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings + end + end + end + + context 'Sorting' do + # The order of creation and the choice of :recorded_at, :name and :length is not RANDOM and was carefully crafted. + # It was meant to have a scenario with high entropy. + # We decrease inter-records correlation for those values (especially respective to :created_at). + let!(:third_recorded_named_c_length_one) do + create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_800), length: 1, visibility: Recording::VISIBILITIES[:public]) + end + let!(:second_recorded_named_a_length_three) do + create(:recording, room:, name: 'A', recorded_at: Time.zone.at(1_686_943_700), length: 3, visibility: Recording::VISIBILITIES[:public]) + end + let!(:first_recorded_named_b_length_two) do + create(:recording, room:, name: 'B', recorded_at: Time.zone.at(1_686_943_600), length: 2, visibility: Recording::VISIBILITIES[:public]) + end + let!(:last_recorded_named_c_length_one) do + create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_900), length: 1, visibility: Recording::VISIBILITIES[:public]) + end + + def expect_response_to_have_ordered(recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to eq(recordings.pluck('id')) + end + + describe 'Sort by name' do + context 'ASC' do + it 'returns :ok with the list of the room public recordings sorted by name' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'ASC' } } + + expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two, + last_recorded_named_c_length_one, third_recorded_named_c_length_one] + end + end + + context 'DESC' do + it 'returns :ok with the list of the room public recordings sorted by name' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'DESC' } } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + first_recorded_named_b_length_two, second_recorded_named_a_length_three] + end + end + end + + describe 'Sort by length' do + context 'ASC' do + it 'returns :ok with the list of the room public recordings sorted by length' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'ASC' } } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + first_recorded_named_b_length_two, second_recorded_named_a_length_three] + end + end + + context 'DESC' do + it 'returns :ok with the list of the room public recordings sorted by length' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'DESC' } } + + expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two, + last_recorded_named_c_length_one, third_recorded_named_c_length_one] + end + end + end + + describe 'No sort by' do + it 'returns :ok with the list of the room public recordings sorted by recorded_at DESC' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + second_recorded_named_a_length_three, first_recorded_named_b_length_two] + end + end + end + + context 'Search' do + let!(:applied_math) do + create(:recording, room:, name: 'Applied mathematics', visibility: Recording::VISIBILITIES[:public]) + end + let!(:advanced_math) do + create(:recording, room:, name: 'Advanced MaTHematics', visibility: Recording::VISIBILITIES[:public_protected]) + end + let!(:thermodynamics) do + create(:recording, room:, name: 'Thermodynamics', visibility: Recording::VISIBILITIES[:public_protected]) + end + + def expect_response_to_match(recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id')) + end + + describe 'Matched by name' do + it 'returns :ok with the list of the room public recordings having matching name case insensitive' do + get :public_recordings, params: { friendly_id: room.friendly_id, search: 'math' } + + expect_response_to_match recordings: [applied_math, advanced_math] + end + end + + describe 'Matched by format type' do + before do + create(:format, recording: applied_math, recording_type: 'podcast') + end + + it 'returns :ok with the list of the room public recordings having matching visibility case insensitive' do + get :public_recordings, params: { friendly_id: room.friendly_id, search: 'podcast' } + + expect_response_to_match recordings: [applied_math] + end + end + + describe 'No match' do + it 'returns :ok with an empty list' do + get :public_recordings, + params: { friendly_id: room.friendly_id, search: [Recording::VISIBILITIES[:public_protected], Recording::VISIBILITIES[:public]].sample } + + expect_response_to_match recordings: [] + end + end + + describe 'No Search' do + it 'returns :ok with the list of the room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_match recordings: [applied_math, advanced_math, thermodynamics] + end + end + end + + context 'Inexistent room' do + it 'returns :not_found' do + get :public_recordings, params: { friendly_id: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end end diff --git a/spec/helpers.rb b/spec/helpers.rb index 05f6b8f67404f0de027896ac085dbe46aa81fa80..6e679ec4a064a36dbd46e8219304f757b5c317a9 100644 --- a/spec/helpers.rb +++ b/spec/helpers.rb @@ -20,4 +20,8 @@ module Helpers def sign_in_user(user) session[:session_token] = user.session_token end + + def sign_out_user + session[:session_token] = nil + end end diff --git a/spec/models/recording_spec.rb b/spec/models/recording_spec.rb index 952362f3b465690a5ff8c85f1b1aec0fc6fe3403..81a586f62fdf8dc0bae01d3cee144b21ab0c507d 100644 --- a/spec/models/recording_spec.rb +++ b/spec/models/recording_spec.rb @@ -77,4 +77,43 @@ RSpec.describe Recording, type: :model do expect(described_class.all.search('').pluck(:id)).to match_array(described_class.all.pluck(:id)) end end + + describe '#public_search' do + let(:recording1) { create(:recording, name: 'Greenlight 101', visibility: Recording::VISIBILITIES[:public]) } + let(:recording2) { create(:recording, name: 'Greenlight 201', visibility: Recording::VISIBILITIES[:public]) } + let(:recording3) { create(:recording, name: 'Bluelight 301', visibility: Recording::VISIBILITIES[:public]) } + + before do + create_list(:recording, 5) + create(:format, recording: recording3, recording_type: 'podcast') + end + + context 'Matching name' do + it 'returns the searched recordings' do + expect(described_class.public_search('greenlight')).to match_array([recording1, recording2]) + end + end + + context 'Matching format type' do + it 'returns the searched recordings' do + expect(described_class.public_search('podcast')).to match_array([recording3]) + end + end + + context 'Matching visibility' do + it 'returns an empty list' do + expect(described_class.public_search('public')).to be_empty + end + end + + context 'No match' do + it 'returns an empty list' do + expect(described_class.public_search('404')).to be_empty + end + end + + it 'returns all recordings if input is empty' do + expect(described_class.all.search('')).to match_array(described_class.all) + end + end end diff --git a/spec/models/room_spec.rb b/spec/models/room_spec.rb index 721c682184022c4e2150f57b4c9ad6df06f34c25..fca0f4f397c4389b5fd1e038516f2fbe408a235a 100644 --- a/spec/models/room_spec.rb +++ b/spec/models/room_spec.rb @@ -164,5 +164,24 @@ RSpec.describe Room, type: :model do expect(room.get_setting(name: '404')).to be_nil end end + + describe '#public_recordings' do + let(:public_recordings) do + [ + create(:recording, room:, visibility: Recording::VISIBILITIES[:public]), + create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected]) + ] + end + + before do + [Recording::VISIBILITIES[:unpublished], Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:protected]].each do |visibility| + create(:recording, room:, visibility:) + end + end + + it 'retuns filters out the room public recordings' do + expect(room.public_recordings).to match_array(public_recordings) + end + end end end