diff --git a/app/assets/javascripts/osem-datatables.js b/app/assets/javascripts/osem-datatables.js index f8816f77..4d7a8d9f 100644 --- a/app/assets/javascripts/osem-datatables.js +++ b/app/assets/javascripts/osem-datatables.js @@ -1,9 +1,11 @@ $(function () { - $('.datatable').DataTable({ - // ajax: ..., - stateSave: true, - autoWidth: false, - pagingType: 'full_numbers', + $.extend(true, $.fn.dataTable.defaults, { + "stateSave": true, + "autoWidth": false, + "pagingType": "full_numbers", "lengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]], }); + + $('.datatable:not([data-source])').DataTable(); + }); diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 484f0229..4d7925dd 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -20,7 +20,12 @@ module Admin end def index - @users = User.all + respond_to do |format| + format.html + format.json do + render json: UserDatatable.new(view_context) + end + end end # This action allow admins to manually toggle confirmation state of another user diff --git a/app/datatables/user_datatable.rb b/app/datatables/user_datatable.rb new file mode 100644 index 00000000..2271931d --- /dev/null +++ b/app/datatables/user_datatable.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class UserDatatable < AjaxDatatablesRails::Base + def_delegator :@view, :show_roles + def_delegator :@view, :admin_user_path + def_delegator :@view, :edit_admin_user_path + + def view_columns + # Declare strings in this format: ModelName.column_name + # or in aliased_join_table.column_name format + @view_columns ||= { + id: { source: 'User.id', cond: :eq }, + confirmed_at: { source: 'User.confirmed_at', searchable: false }, + email: { source: 'User.email' }, + name: { source: 'User.name' }, + attended: { source: 'attended_count', searchable: false }, + roles: { source: 'Role.name' }, + view_url: { source: 'User.id', searchable: false, orderable: false }, + edit_url: { source: 'User.id', searchable: false, orderable: false } + } + end + + private + + def data + records.map do |record| + { + id: record.id, + confirmed_at: record.confirmed_at, + email: record.email, + name: record.name, + attended: record.attended_count, + roles: record.roles.any? ? show_roles(record.get_roles) : 'None', + view_url: admin_user_path(record), + edit_url: edit_admin_user_path(record), + DT_RowId: record.id + } + end + end + + # rubocop:disable Naming/AccessorMethodName + def get_raw_records + User.left_outer_joins(:registrations, :roles).distinct + .select('users.*, COUNT(CASE WHEN registrations.attended = "t" THEN 1 END) AS attended_count').group('users.id') + end + # rubocop:enable Naming/AccessorMethodName + + def records_total_count + fetch_records.unscope(:group).count(:all) + end + + def records_filtered_count + filter_records(fetch_records).unscope(:group).count(:all) + end + + # ==== These methods represent the basic operations to perform on records + # and feel free to override them + + # def filter_records(records) + # end + + # def sort_records(records) + # end + + # def paginate_records(records) + # end + + # ==== Insert 'presenter'-like methods below if necessary +end diff --git a/app/models/user.rb b/app/models/user.rb index 11dcdc40..9e54eeb9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -220,6 +220,10 @@ class User < ApplicationRecord end end + def attended_count + attributes['attended_count'] || registrations.where(attended: true).count + end + def confirmed? !confirmed_at.nil? end diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 69bc6c31..e2b47504 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -10,52 +10,67 @@ = link_to 'Add User', new_admin_user_path, :class => 'button btn btn-default btn-info' .row .col-md-12.table-responsive - %table.datatable + %table.datatable#users{ data: { source: admin_users_path(format: :json) } } %thead - %th ID - %th Confirmed? - %th Email - %th Name - %th Attended Conferences - %th Roles - %th View - %th Edit + %th{ width: '0' } ID + %th{ width: '0' } Confirmed? + %th{ width: '0' } Email + %th{ width: '50%' } Name + %th{ width: '0' } Conferences Attended + %th{ width: '50%' } Roles + %th{ width: '0' } Actions %tbody - - @users.each do |user| - %tr - %td - = user.id - %td{ 'data-order' => "#{user.confirmed?}" } - - if can? :toggle_confirmation, user - = check_box_tag user.id, user.id, user.confirmed?, - method: :patch, - url: "/admin/users/#{user.id}/toggle_confirmation?user[to_confirm]=", - class: 'switch-checkbox', - readonly: false, - data: { size: 'small', on_color: 'success', off_color: 'warning', on_text: 'Yes', off_text: 'No' } - - else - = check_box_tag user.id, user.id, user.confirmed?, - method: :patch, - url: "/admin/users/#{user.id}/toggle_confirmation?user[to_confirm]=", - class: 'switch-checkbox', - readonly: true, - data: { size: 'small', on_color: 'success', off_color: 'warning', on_text: 'Yes', off_text: 'No' } - %td - = user.email - %td - = user.name - %td - = user.registrations.where(attended: true).count - %td - - unless user.get_roles.blank? - = show_roles(user.get_roles.first(2)) - - if user.get_roles.count > 2 - = '...' - - else - None - %td - - if can? :show, user - = link_to 'View', admin_user_path(user), class: 'btn btn-success' - %td - - if can? :update, user - = link_to 'Edit', edit_admin_user_path(user), class: 'btn btn-primary' + +:javascript + $(function () { + $('#users.datatable').DataTable({ + "processing": true, + "serverSide": true, + "ajax": $('#users.datatable').data('source'), + "drawCallback": function(settings) { + checkboxSwitch("[class='switch-checkbox']"); + }, + "columns": [ + { "data": "id" }, + { + "data": "confirmed_at", + "render": function (data, type, row, meta) { + return '' + }, + "searchable": false + }, + { "data": "email" }, + { + "data": "name", + "className": "truncate", + "render": function (data, type, row, meta) { + return ''+data+'' + } + }, + { "data": "attended" }, + { + "data": "roles", + "className": "truncate" , + "render": function (data, type, row, meta) { + return ''+data+'' + } + }, + { + "data": null, + "className": "actions", + "sortable": false, + "render": function (data, type, row, meta) { + return '
'+ + 'View'+ + 'Edit'+ + '
'; + } + } + ] + }); + }); diff --git a/spec/datatables/user_datatable_spec.rb b/spec/datatables/user_datatable_spec.rb new file mode 100644 index 00000000..1663a249 --- /dev/null +++ b/spec/datatables/user_datatable_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserDatatable do + subject! do + described_class.new(view) + end + + let(:data_cols) do + [:id, :confirmed_at, :email, :name, :attended, :roles, :view_url, :edit_url, :DT_RowId] + end + let(:view) do + view = double( + 'view', + params: { + 'draw' => '1', + 'columns' => { + '0' => { + 'data' => 'id', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '1' => { + 'data' => 'confirmed_at', + 'name' => '', + 'searchable' => 'false', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '2' => { + 'data' => 'email', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '3' => { + 'data' => 'name', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '4' => { + 'data' => 'attended', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '5' => { + 'data' => 'roles', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => { 'value' => '', 'regex' => 'false' } + }, + '6' => { + 'data' => '', + 'name' => '', + 'searchable' => 'true', + 'orderable' => 'false', + 'search' => { 'value' => '', 'regex' => 'false' } + } + }, + 'order' => { '0' => { 'column' => '0', 'dir' => 'asc' } }, + 'start' => '0', + 'length' => '10', + 'search' => { 'value' => '', 'regex' => 'false' }, + '_' => '1532637360488' + }.with_indifferent_access + ) + allow(view).to receive(:admin_user_path) do |arg| + "/admin/users/#{arg.to_param}" + end + allow(view).to receive(:edit_admin_user_path) do |arg| + "/admin/users/#{arg.to_param}/edit" + end + return view + end + + before do + allow(AjaxDatatablesRails).to receive(:old_rails?).and_return(true) + end + + describe 'implements AjaxDatatablesRails::Base' do + it { is_expected.to respond_to(:view_columns) } + end + + context 'outputs' do + let(:user) { User.first } + let(:output) { subject.as_json } + + it 'recordsTotal' do + expect(output[:recordsTotal]).to eq(1) + end + + it 'recordsFiltered' do + expect(output[:recordsFiltered]).to eq(1) + end + + it 'data length' do + expect(output[:data].length).to eq(1) + end + + it 'has expected data columns' do + expect(output[:data].first.keys).to eq(data_cols) + end + + context 'data columns:' do + let(:user_data) { output[:data].first } + + it 'id' do + expect(user_data[:id].to_i).to eq(user.id) + end + + it 'name' do + expect(user_data[:name]).to eq(user.name) + end + + it 'email' do + expect(user_data[:email]).to eq(user.email) + end + + it 'confirmed_at' do + + expect(Date.parse(user_data[:confirmed_at])).to eq(user.confirmed_at.to_date) + end + + it 'attended' do + expect(user_data[:attended].to_i).to eq(user.attended_count) + end + + it 'roles' do + expect(user_data[:roles]).to eq('None') + end + end + end +end