Add paging to Recordings Table (GRN2-26) (#512)

* Add translations for the validation messages

* Add translations for next/prev button

* Add paging to recordings

* sync

* Fix line endings
This commit is contained in:
shawn-higgins1 2019-05-14 09:01:41 -04:00 committed by Jesus Federico
parent d8f6c3f872
commit 23abdb52ee
18 changed files with 725 additions and 129 deletions

View File

@ -41,32 +41,6 @@ $(document).on('turbolinks:load', function(){
location.reload()
});
});
// Submit search if the user hits enter
$("#search-input").keypress(function(key) {
var keyPressed = key.which
if (keyPressed == 13) {
searchPage()
}
})
// Add listeners for sort
$("th[data-order]").click(function(data){
var header_elem = $(data.target)
if(header_elem.data('order') === 'asc'){ // asc
header_elem.data('order', 'desc');
}
else if(header_elem.data('order') === 'desc'){ // desc
header_elem.data('order', 'none');
}
else{ // none
header_elem.data('order', 'asc');
}
var search = $("#search-input").val()
window.location.replace(window.location.pathname + "?page=1&search=" + search + "&column=" + header_elem.data("header") + "&direction="+ header_elem.data('order'))
})
}
// Only run on the admins edit user page.
@ -76,8 +50,8 @@ $(document).on('turbolinks:load', function(){
if (!url.endsWith("/")) {
url += "/"
}
url += "admins?setting=" + data.target.id
window.location.href = url
})
}
@ -88,15 +62,3 @@ function changeBrandingImage(path) {
var url = $("#branding-url").val()
$.post(path, {url: url})
}
// Searches the user table for the given string
function searchPage() {
var search = $("#search-input").val()
window.location.replace(window.location.pathname + "?page=1&search=" + search)
}
// Clears the search bar
function clearSearch() {
window.location.replace(window.location.pathname + "?page=1")
}

View File

@ -18,36 +18,81 @@ $(document).on('turbolinks:load', function(){
var controller = $("body").data('controller');
var action = $("body").data('action');
if(controller == "rooms" && action == "show" || controller == "rooms" && action == "update"){
var search_input = $('#search_bar');
if ((controller == "admins" && action == "index") ||
(controller == "rooms" && action == "show") ||
(controller == "rooms" && action == "update") ||
(controller == "rooms" && action == "join") ||
(controller == "users" && action == "recordings")) {
// Submit search if the user hits enter
$("#search-input").keypress(function(key) {
var keyPressed = key.which
if (keyPressed == 13) {
searchPage()
}
})
search_input.bind("keyup", function(){
// Add listeners for sort
$("th[data-order]").click(function(data){
var header_elem = $(data.target)
var controller = $("body").data('controller');
var action = $("body").data('action');
// Retrieve the current search query
var search_query = search_input.find(".form-control").val();
if(header_elem.data('order') === 'asc'){ // asc
header_elem.data('order', 'desc');
}
else if(header_elem.data('order') === 'desc'){ // desc
header_elem.data('order', 'none');
}
else{ // none
header_elem.data('order', 'asc');
}
//Search for recordings and display them based on name match
var recordings_found = 0;
var search = $("#search-input").val();
var recordings = $('#recording-table').find('tr');
recordings.each(function(){
if($(this).find('text').text().toLowerCase().includes(search_query.toLowerCase())){
recordings_found = recordings_found + 1;
$(this).show();
}
else{
$(this).hide();
}
});
// Show "No recordings match your search" if no recordings found
if(recordings_found === 0){
$('#no_recordings_found').show();
if(controller === "rooms" && action === "show"){
window.location.replace(window.location.pathname + "?page=1&search=" + search +
"&column=" + header_elem.data("header") + "&direction="+ header_elem.data('order') +
"#recordings-table");
}
else{
$('#no_recordings_found').hide();
window.location.replace(window.location.pathname + "?page=1&search=" + search +
"&column=" + header_elem.data("header") + "&direction="+ header_elem.data('order'));
}
});
})
if(controller === "rooms" && action === "show"){
$(".page-item > a").each(function(){
if(!$(this).attr('href').endsWith("#")){
$(this).attr('href', $(this).attr('href') + "#recordings-table")
}
})
}
}
});
})
// Searches the user table for the given string
function searchPage() {
var search = $("#search-input").val();
var controller = $("body").data('controller');
var action = $("body").data('action');
if(controller === "rooms" && action === "show"){
window.location.replace(window.location.pathname + "?page=1&search=" + search + "#recordings-table");
} else{
window.location.replace(window.location.pathname + "?page=1&search=" + search);
}
}
// Clears the search bar
function clearSearch() {
var controller = $("body").data('controller');
var action = $("body").data('action');
if(controller === "rooms" && action === "show"){
window.location.replace(window.location.pathname + "?page=1" + "#recordings-table");
} else{
window.location.replace(window.location.pathname + "?page=1");
}
}

View File

@ -18,6 +18,7 @@
class RoomsController < ApplicationController
include RecordingsHelper
include Pagy::Backend
before_action :validate_accepted_terms, unless: -> { !Rails.configuration.terms }
before_action :validate_verified_email, except: [:show, :join],
@ -52,9 +53,11 @@ class RoomsController < ApplicationController
# GET /:room_uid
def show
if current_user && @room.owned_by?(current_user)
recs = @room.recordings
@search, @order_column, @order_direction, recs =
@room.recordings(params.permit(:search, :column, :direction), true)
@pagy, @recordings = pagy_array(recs)
@recordings = recs
@is_running = @room.running?
else
# Get users name
@ -66,6 +69,11 @@ class RoomsController < ApplicationController
""
end
@search, @order_column, @order_direction, pub_recs =
@room.public_recordings(params.permit(:search, :column, :direction), true)
@pagy, @public_recordings = pagy_array(pub_recs)
render :join
end
end
@ -119,6 +127,13 @@ class RoomsController < ApplicationController
redirect_to @room.join_path(join_name, opts)
end
else
search_params = params[@room.invite_path] || params
@search, @order_column, @order_direction, pub_recs =
@room.public_recordings(search_params.permit(:search, :column, :direction), true)
@pagy, @public_recordings = pagy_array(pub_recs)
# They need to wait until the meeting begins.
render :wait
end

View File

@ -18,6 +18,7 @@
class UsersController < ApplicationController
include RecordingsHelper
include Pagy::Backend
include Emailer
before_action :find_user, only: [:edit, :update, :destroy]
@ -141,7 +142,9 @@ class UsersController < ApplicationController
# GET /u/:user_uid/recordings
def recordings
if current_user && current_user.uid == params[:user_uid]
@recordings = current_user.all_recordings
@search, @order_column, @order_direction, recs =
current_user.all_recordings(params.permit(:search, :column, :direction), true)
@pagy, @recordings = pagy_array(recs)
else
redirect_to root_path
end

View File

@ -17,6 +17,8 @@
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
module RecordingsHelper
include Pagy::Frontend
# Helper for converting BigBlueButton dates into the desired format.
def recording_date(date)
date.strftime("%B #{date.day.ordinalize}, %Y.")

View File

@ -19,8 +19,14 @@
module APIConcern
extend ActiveSupport::Concern
# Format recordings to match their current use in the app
def format_recordings(api_res)
# Format, filter, and sort recordings to match their current use in the app
def format_recordings(api_res, search_params, ret_search_params)
search = search_params[:search] || ""
order_col = search_params[:column] && search_params[:direction] != "none" ? search_params[:column] : "end_time"
order_dir = search_params[:column] && search_params[:direction] != "none" ? search_params[:direction] : "asc"
search = search.downcase
api_res[:recordings].each do |r|
next if r.key?(:error)
# Format playbacks in a more pleasant way.
@ -34,6 +40,57 @@ module APIConcern
r.delete(:playback)
end
api_res[:recordings].sort_by { |rec| rec[:endTime] }.reverse
recs = filter_recordings(api_res, search)
recs = sort_recordings(recs, order_col, order_dir)
if ret_search_params
[search, order_col, order_dir, recs]
else
recs
end
end
def filter_recordings(api_res, search)
api_res[:recordings].select do |r|
(!r[:metadata].nil? && ((!r[:metadata][:name].nil? &&
r[:metadata][:name].downcase.include?(search)) ||
(r[:metadata][:"gl-listed"] == "true" && search == "public") ||
(r[:metadata][:"gl-listed"] == "false" && search == "unlisted"))) ||
((r[:metadata].nil? || r[:metadata][:name].nil?) &&
r[:name].downcase.include?(search)) ||
r[:participants].include?(search) ||
!r[:playbacks].select { |p| p[:type].downcase.include?(search) }.empty?
end
end
def sort_recordings(recs, order_col, order_dir)
recs = case order_col
when "end_time"
recs.sort_by { |r| r[:endTime] }
when "name"
recs.sort_by do |r|
if !r[:metadata].nil? && !r[:metadata][:name].nil?
r[:metadata][:name].downcase
else
r[:name].downcase
end
end
when "length"
recs.sort_by { |r| r[:playbacks].reject { |p| p[:type] == "statistics" }.first[:length] }
when "users"
recs.sort_by { |r| r[:participants] }
when "visibility"
recs.sort_by { |r| r[:metadata][:"gl-listed"] }
when "formats"
recs.sort_by { |r| r[:playbacks].first[:type].downcase }
else
recs.sort_by { |r| r[:endTime] }
end
if order_dir == 'asc'
recs
else
recs.reverse
end
end
end

View File

@ -121,15 +121,16 @@ class Room < ApplicationRecord
end
# Fetches all recordings for a room.
def recordings
def recordings(search_params = {}, ret_search_params = false)
res = bbb.get_recordings(meetingID: bbb_id)
format_recordings(res)
format_recordings(res, search_params, ret_search_params)
end
# Fetches a rooms public recordings.
def public_recordings
recordings.select { |r| r[:metadata][:"gl-listed"] == "true" }
def public_recordings(search_params = {}, ret_search_params = false)
search, order_col, order_dir, recs = recordings(search_params, ret_search_params)
[search, order_col, order_dir, recs.select { |r| r[:metadata][:"gl-listed"] == "true" }]
end
def update_recording(record_id, meta)

View File

@ -121,7 +121,7 @@ class User < ApplicationRecord
order("#{column} #{direction}")
end
def all_recordings
def all_recordings(search_params = {}, ret_search_params = false)
pag_num = Rails.configuration.pagination_number
pag_loops = rooms.length / pag_num - 1
@ -142,7 +142,7 @@ class User < ApplicationRecord
full_res = bbb.get_recordings(meetingID: last_pag_room.pluck(:bbb_id))
res[:recordings].push(*full_res[:recordings])
format_recordings(res)
format_recordings(res, search_params, ret_search_params)
end
# Activates an account and initialize a users main room

View File

@ -16,6 +16,9 @@
<%= render 'shared/room_event' do %>
<%= form_for room_path(@room), method: :post do |f| %>
<div class="input-group join-input">
<%= f.hidden_field(:search, :value => params[:search])%>
<%= f.hidden_field(:column, :value => params[:column])%>
<%= f.hidden_field(:direction, :value => params[:direction])%>
<%= f.text_field :join_name,
required: true,
class: "form-control join-form",

View File

@ -86,6 +86,6 @@
</div>
</div>
<%= render "shared/sessions", recordings: @recordings, only_public: false, user_recordings: false, title: t("room.recordings")%>
<%= render "shared/sessions", recordings: @recordings, pagy: @pagy, only_public: false, user_recordings: false, title: t("room.recordings")%>
<%= render "shared/modals/create_room_modal" %>

View File

@ -40,4 +40,4 @@
</div>
</div>
<%= render "shared/sessions", recordings: @room.public_recordings, only_public: true, user_recordings: false, title: t("room.recordings") %>
<%= render "shared/sessions", recordings: @public_recordings, pagy: @pagy, only_public: true, user_recordings: false, title: t("room.recordings") %>

View File

@ -21,21 +21,54 @@
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-hover table-outline table-vcenter text-nowrap card-table">
<table id="recordings-table" class="table table-hover table-outline table-vcenter text-nowrap card-table">
<thead>
<tr>
<th data-header="<%= t("recording.table.name") %>" data-order="none"><%= t("recording.table.name") %></th>
<th data-header="name" data-order="<%= @order_column == "name" ? @order_direction : "none" %>">
<%= t("recording.table.name") %>
<% if @order_column == "name" && @order_direction == "desc" %>
<% elsif @order_column == "name" && @order_direction == "asc" %>
<% end %>
</th>
<% if recording_thumbnails? %>
<th><%= t("recording.table.thumbnails") %></th>
<th>
<%= t("recording.table.thumbnails") %>
</th>
<% end %>
<th class="text-left" data-header="<%= t("recording.table.length") %>" data-order="none">
<th class="text-left" data-header="length" data-order="<%= @order_column == "length" ? @order_direction : "none" %>">
<%= t("recording.table.length") %>
<% if @order_column == "length" && @order_direction == "desc" %>
<% elsif @order_column == "length" && @order_direction == "asc" %>
<% end %>
</th>
<th class="text-left" data-header="<%= t("recording.table.users") %>" data-order="none">
<th class="text-left" data-header="users" data-order="<%= @order_column == "users" ? @order_direction : "none" %>">
<%= t("recording.table.users") %>
<% if @order_column == "users" && @order_direction == "desc" %>
<% elsif @order_column == "users" && @order_direction == "asc" %>
<% end %>
</th>
<th class="text-left" data-header="visibility" data-order="<%= @order_column == "visibility" ? @order_direction : "none" %>">
<%= t("recording.table.visibility") %>
<% if @order_column == "visibility" && @order_direction == "desc" %>
<% elsif @order_column == "visibility" && @order_direction == "asc" %>
<% end %>
</th>
<th data-header="formats" data-order="<%= @order_column == "formats" ? @order_direction : "none" %>">
<%= t("recording.table.formats") %>
<% if @order_column == "formats" && @order_direction == "desc" %>
<% elsif @order_column == "formats" && @order_direction == "asc" %>
<% end %>
</th>
<th class="text-left"><%= t("recording.table.visibility") %></th>
<th><%= t("recording.table.formats") %></th>
<% unless only_public %>
<th class="text-center"><i class="icon-settings"></i></th>
<% end %>
@ -68,6 +101,11 @@
<% end %>
</tbody>
</table>
<% if !recordings.empty?%>
<div class="float-right mr-4 mt-4">
<%== pagy_bootstrap_nav(pagy) %>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -18,14 +18,24 @@
<p class="subtitle"><%= subtitle %></p>
</div>
<% if search %>
<div id="search_bar" class="col-4">
<div class="input-icon">
<input type="text" class="form-control btn-pill" placeholder="Search...">
<span class="input-icon-addon">
<i class="fas fa-search"></i>
</span>
<div class="col-4">
<div id="search-bar">
<div class="input-group">
<input id="search-input" type="text" class="form-control" placeholder="<%= t("settings.search") %>..." value="<%= @search %>">
<% unless @search.blank? %>
<span id="clear-search" class="text-primary" onclick="clearSearch()">
<i class="fas fa-times"></i>
</span>
<% end %>
<span class="input-group-append">
<button class="btn btn-primary" type="button" onclick="searchPage()">
<i class="fas fa-search"></i>
</button>
</span>
</div>
</div>
</div>
<% end %>
</div>
<hr class="mt-0">
<hr class="mt-0">

View File

@ -17,38 +17,9 @@
<div class="card-body p-6">
<div class="card-title text-primary">
<div class="form-group">
<div class="row">
<% if setting_id == "users" %>
<div class="col-7 mt-2">
<h4><%= setting_title %></h4>
</div>
<div class="col-5 float-right">
<div id="search-bar">
<div class="input-group">
<input id="search-input" type="text" class="form-control" placeholder="<%= t("settings.search") %>..." value="<%= @search %>">
<% unless @search.blank? %>
<span id="clear-search" class="text-primary" onclick="clearSearch()">
<i class="fas fa-times"></i>
</span>
<% end %>
<span class="input-group-append">
<button class="btn btn-primary" type="button" onclick="searchPage()">
<i class="fas fa-search"></i>
</button>
</span>
</div>
</div>
</div>
<% else %>
<div class="col-12 mt-2">
<h4 class="text-primary"><%= setting_title %></h4>
</div>
<% end %>
</div>
<%= render "shared/components/subtitle", subtitle: setting_title, search: setting_id == "users" %>
</div>
</div>
<hr>
<% unless (defined?(admin_view)).nil? %>
<%= render "shared/admin_settings/#{setting_id}" %>

View File

@ -18,4 +18,4 @@
# without losing all css
%>
<%= render "shared/sessions", recordings: @recordings, only_public: false, user_recordings: true, title: t("recording.all_recordings") %>
<%= render "shared/sessions", recordings: @recordings, pagy: @pagy, only_public: false, user_recordings: true, title: t("recording.all_recordings") %>

View File

@ -13,7 +13,7 @@
# Array extra: Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding
# See https://ddnexus.github.io/pagy/extras/array
# require 'pagy/extras/array'
require 'pagy/extras/array'
# Countless extra: Paginate without any count, saving one query per rendering
# See https://ddnexus.github.io/pagy/extras/countless

View File

@ -139,18 +139,414 @@ describe Room, type: :model do
{
name: "Example",
playback: {
format: "presentation",
},
},
],
format:
{
type: "presentation"
}
}
}
]
)
expect(@room.recordings).to contain_exactly(
name: "Example",
playbacks: %w(presentation),
playbacks:
[
{
type: "presentation"
}
]
)
end
context '#filtering' do
before do
allow_any_instance_of(BigBlueButton::BigBlueButtonApi).to receive(:get_recordings).and_return(
recordings: [
{
name: "Example",
participants: "3",
playback: {
format:
{
type: "presentation"
}
},
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "5",
playback: {
format:
{
type: "other"
}
},
metadata: {
"gl-listed": "false",
}
},
{
name: "test",
participants: "1",
playback: {
format:
{
type: "presentation"
}
},
metadata: {
"gl-listed": "true",
}
},
{
name: "Exam",
participants: "1",
playback: {
format:
{
type: "other"
}
},
metadata: {
"gl-listed": "false",
name: "z",
}
}
]
)
end
it "should filter recordings on name" do
expect(@room.recordings(search: "Exam")).to contain_exactly(
{
name: "aExamaaa",
participants: "5",
playbacks:
[
{
type: "other"
}
],
metadata: {
"gl-listed": "false",
}
},
name: "Example",
participants: "3",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
}
)
end
it "should filter recordings on participants" do
expect(@room.recordings(search: "5")).to contain_exactly(
name: "aExamaaa",
participants: "5",
playbacks:
[
{
type: "other"
}
],
metadata: {
"gl-listed": "false",
}
)
end
it "should filter recordings on format" do
expect(@room.recordings(search: "presentation")).to contain_exactly(
{
name: "test",
participants: "1",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
}
},
name: "Example",
participants: "3",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
}
)
end
it "should filter recordings on visibility" do
expect(@room.recordings(search: "public")).to contain_exactly(
{
name: "test",
participants: "1",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
},
},
name: "Example",
participants: "3",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
}
)
end
it "should filter recordings on metadata name by default" do
expect(@room.recordings(search: "z")).to contain_exactly(
name: "Exam",
participants: "1",
playbacks:
[
{
type: "other"
}
],
metadata: {
"gl-listed": "false",
name: "z",
}
)
end
end
context '#sorting' do
before do
allow_any_instance_of(BigBlueButton::BigBlueButtonApi).to receive(:get_recordings).and_return(
recordings: [
{
name: "Example",
participants: "3",
playback: {
format: {
type: "presentation",
length: "4"
}
},
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "1",
playback: {
format: {
type: "other",
length: "3"
}
},
metadata: {
name: "Z",
"gl-listed": "false"
}
}
]
)
end
it "should sort recordings on name" do
expect(@room.recordings(column: "name", direction: "asc")).to eq(
[
{
name: "Example",
participants: "3",
playbacks: [
{
type: "presentation",
length: "4"
}
],
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "1",
playbacks: [
{
type: "other",
length: "3"
}
],
metadata: {
name: "Z",
"gl-listed": "false"
}
}
]
)
end
it "should sort recordings on participants" do
expect(@room.recordings(column: "users", direction: "desc")).to eq(
[
{
name: "Example",
participants: "3",
playbacks: [
{
type: "presentation",
length: "4"
}
],
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "1",
playbacks: [
{
type: "other",
length: "3"
}
],
metadata: {
name: "Z",
"gl-listed": "false"
}
}
]
)
end
it "should sort recordings on visibility" do
expect(@room.recordings(column: "visibility", direction: "desc")).to eq(
[
{
name: "Example",
participants: "3",
playbacks: [
{
type: "presentation",
length: "4"
}
],
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "1",
playbacks: [
{
type: "other",
length: "3"
}
],
metadata: {
name: "Z",
"gl-listed": "false"
}
}
]
)
end
it "should sort recordings on length" do
expect(@room.recordings(column: "length", direction: "asc")).to eq(
[
{
name: "aExamaaa",
participants: "1",
playbacks: [
{
type: "other",
length: "3"
}
],
metadata: {
name: "Z",
"gl-listed": "false"
}
},
{
name: "Example",
participants: "3",
playbacks: [
{
type: "presentation",
length: "4"
}
],
metadata: {
"gl-listed": "true",
}
}
]
)
end
it "should sort recordings on format" do
expect(@room.recordings(column: "formats", direction: "desc")).to eq(
[
{
name: "Example",
participants: "3",
playbacks: [
{
type: "presentation",
length: "4"
}
],
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "1",
playbacks: [
{
type: "other",
length: "3"
}
],
metadata: {
name: "Z",
"gl-listed": "false"
}
}
]
)
end
end
it "deletes the recording" do
allow_any_instance_of(BigBlueButton::BigBlueButtonApi).to receive(:delete_recordings).and_return(
returncode: true, deleted: true

View File

@ -173,4 +173,97 @@ describe User, type: :model do
.to raise_exception(ActiveRecord::RecordInvalid, "Validation failed: Email can't be blank")
end
end
context '#recordings' do
it "gets all filtered and sorted recordings for the user" do
allow_any_instance_of(BigBlueButton::BigBlueButtonApi).to receive(:get_recordings).and_return(
recordings: [
{
name: "Example",
participants: "3",
playback: {
format:
{
type: "presentation"
}
},
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "5",
playback: {
format:
{
type: "other"
}
},
metadata: {
"gl-listed": "false",
}
},
{
name: "test",
participants: "1",
playback: {
format:
{
type: "presentation"
}
},
metadata: {
"gl-listed": "true",
}
},
{
name: "Exam",
participants: "1",
playback: {
format:
{
type: "other"
}
},
metadata: {
"gl-listed": "false",
name: "z",
}
}
]
)
expect(@user.all_recordings(search: "Exam", column: "name", direction: "desc")).to eq(
[
{
name: "Example",
participants: "3",
playbacks:
[
{
type: "presentation"
}
],
metadata: {
"gl-listed": "true",
}
},
{
name: "aExamaaa",
participants: "5",
playbacks:
[
{
type: "other"
}
],
metadata: {
"gl-listed": "false",
}
}
]
)
end
end
end