diff --git a/app/controllers/report_controller.rb b/app/controllers/report_controller.rb --- a/app/controllers/report_controller.rb +++ b/app/controllers/report_controller.rb @@ -4,7 +4,9 @@ before_action :check_valid_login - before_action :admin_authorization, only: [:login_stat,:submission, :submission_query, :stuck, :cheat_report, :cheat_scruntinize, :show_max_score, :current_score] + before_action :admin_authorization, only: [:login_stat,:submission, :submission_query, + :login, :login_detail_query, :login_summary_query, + :stuck, :cheat_report, :cheat_scruntinize, :show_max_score, :current_score] before_action(only: [:problem_hof]) { |c| return false unless check_valid_login @@ -104,7 +106,53 @@ end - def login_stat + def login + end + + def login_summary_query + @users = Array.new + + date_and_time = '%Y-%m-%d %H:%M' + begin + md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/) + @since_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i) + rescue + @since_time = DateTime.new(1000,1,1) + end + begin + md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/) + @until_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i) + rescue + @until_time = DateTime.new(3000,1,1) + end + + record = User + .left_outer_joins(:logins).group('users.id') + .where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time) + case params[:users] + when 'enabled' + record = record.where(enabled: true) + when 'group' + record = record.joins(:groups).where(groups: {id: params[:groups]}) if params[:groups] + end + + record = record.pluck("users.id,users.login,users.full_name,count(logins.created_at),min(logins.created_at),max(logins.created_at)") + record.each do |user| + x = Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", + user[0],@since_time,@until_time) + .pluck(:ip_address).uniq + @users << { id: user[0], + login: user[1], + full_name: user[2], + count: user[3], + min: user[4], + max: user[5], + ip: x + } + end + end + + def login_detail_query @logins = Array.new date_and_time = '%Y-%m-%d %H:%M' @@ -120,25 +168,13 @@ rescue @until_time = DateTime.new(3000,1,1) end - - User.all.each do |user| - @logins << { id: user.id, - login: user.login, - full_name: user.full_name, - count: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", - user.id,@since_time,@until_time) - .count(:id), - min: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", - user.id,@since_time,@until_time) - .minimum(:created_at), - max: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", - user.id,@since_time,@until_time) - .maximum(:created_at), - ip: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", - user.id,@since_time,@until_time) - .select(:ip_address).uniq - } + @logins = Login.includes(:user).where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time) + case params[:users] + when 'enabled' + @logins = @logins.where(users: {enabled: true}) + when 'group' + @logins = @logins.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups] end end @@ -149,15 +185,18 @@ @submissions = Submission .includes(:problem).includes(:user).includes(:language) - if params[:problem] - @submission = @submission.where(problem_id: params[:problem]) + case params[:users] + when 'enabled' + @submissions = @submissions.where(users: {enabled: true}) + when 'group' + @submissions = @submissions.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups] end - case params[:users] + case params[:problems] when 'enabled' - @submissions = @submissions.where('user.enabled': true) - when 'group' - @submissions = @submissions.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups] + @submissions = @submissions.where(problems: {available: true}) + when 'selected' + @submissions = @submissions.where(problem_id: params[:problem_id]) end #set default @@ -172,6 +211,9 @@ ) end + def login + end + def problem_hof # gen problem list @user = User.find(session[:user_id]) diff --git a/app/controllers/user_admin_controller.rb b/app/controllers/user_admin_controller.rb --- a/app/controllers/user_admin_controller.rb +++ b/app/controllers/user_admin_controller.rb @@ -8,13 +8,6 @@ def index @user_count = User.count - if params[:page] == 'all' - @users = User.all - @paginated = false - else - @users = User.paginate :page => params[:page] - @paginated = true - end @users = User.all @hidden_columns = ['hashed_password', 'salt', 'created_at', 'updated_at'] @contests = Contest.enabled diff --git a/app/models/user.rb b/app/models/user.rb --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,8 @@ :class_name => "Message", :foreign_key => "receiver_id" + has_many :logins + has_one :contest_stat, :class_name => "UserContestStat", :dependent => :destroy belongs_to :site diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -65,6 +65,7 @@ = add_menu( 'Current Score', 'report', 'current_score') = add_menu( 'Score Report', 'report', 'max_score') = add_menu( 'Submission Report', 'report', 'submission') + = add_menu( 'Login Report', 'report', 'login') - if (ungraded = Submission.where('graded_at is null').where('submitted_at < ?', 1.minutes.ago).count) > 0 =link_to "#{ungraded} backlogs!", grader_list_path, diff --git a/app/views/report/login.html.haml b/app/views/report/login.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/login.html.haml @@ -0,0 +1,130 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + +%h1 Logins detail + +.row + .col-md-4 + .alert.alert-info + %ul + %li You have to click refresh when changing the filter above + %li Detail tab shows each logins separately + %li Summary tab shows logins summary of each user + .col-md-4 + = render partial: 'shared/date_filter' + .col-md-4 + = render partial: 'shared/user_select' + +.row.form-group + .col-sm-12 + %ul.nav.nav-tabs + %li.active + %a{href: '#detail', data: {toggle: :tab}} Detail + %li + %a{href: '#summary', data: {toggle: :tab}} Summary +.row + .col-sm-12 + .tab-content + .tab-pane.active#detail + %table#detail-table.table.table-hover.table-condense.datatable{style: 'width: 100%'} + .tab-pane#summary + %table#summary-table.table.table-hover.table-condense.datatable{style: 'width: 100%'} + + + +:javascript + $(function() { + detail_table = $('#detail-table').DataTable({ + dom: "<'row'<'col-sm-3'B><'col-sm-3'l><'col-sm-6'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>", + autoWidth: true, + buttons: [ + { + text: 'Refresh', + action: (e,dt,node,config) => { + detail_table.clear().draw() + detail_table.ajax.reload( () => { detail_table.columns.adjust().draw() } ) + summary_table.clear().draw() + summary_table.ajax.reload( () => { summary_table.columns.adjust().draw() } ) + } + }, + 'copy', + { + extend: 'excel', + title: 'Login detail', + } + ], + columns: [ + {title: 'User', data: 'login_text'}, + {title: 'Time', data: 'created_at'}, + {title: 'IP', data: 'ip_address'}, + ], + ajax: { + url: '#{login_detail_query_report_path}', + type: 'POST', + data: (d) => { + d.since_datetime = $('#since_datetime').val() + d.until_datetime = $('#until_datetime').val() + d.users = $("input[name='users']:checked").val() + d.groups = $("#group_id").select2('val') + }, + dataType: 'json', + beforeSend: (request) => { + request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')); + }, + }, //end ajax + pageLength: 25, + processing: true, + }); + + summary_table = $('#summary-table').DataTable({ + dom: "<'row'<'col-sm-3'B><'col-sm-3'l><'col-sm-6'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>", + autoWidth: true, + buttons: [ + { + text: 'Refresh', + action: (e,dt,node,config) => { + summary_table.clear().draw() + summary_table.ajax.reload( () => { summary_table.columns.adjust().draw() } ) + detail_table.clear().draw() + detail_table.ajax.reload( () => { detail_table.columns.adjust().draw() } ) + } + }, + 'copy', + { + extend: 'excel', + title: 'Login summary', + } + ], + columns: [ + {title: 'User', data: 'login_text'}, + {title: 'Login Count', data: 'count'}, + {title: 'Earliest', data: 'earliest'}, + {title: 'Latest', data: 'latest'}, + {title: 'IP', data: 'ip_address'}, + ], + ajax: { + url: '#{login_summary_query_report_path}', + type: 'POST', + data: (d) => { + d.since_datetime = $('#since_datetime').val() + d.until_datetime = $('#until_datetime').val() + d.users = $("input[name='users']:checked").val() + d.groups = $("#group_id").select2('val') + }, + dataType: 'json', + beforeSend: (request) => { + request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')); + }, + }, //end ajax + pageLength: 25, + processing: true, + }); + + $('.input-group.date').datetimepicker({ + format: 'YYYY-MM-DD HH:mm', + showTodayButton: true, + locale: 'en', + widgetPositioning: {horizontal: 'auto', vertical: 'bottom'}, + defaultDate: moment() + }); + }); diff --git a/app/views/report/login_detail_query.json.jbuilder b/app/views/report/login_detail_query.json.jbuilder new file mode 100644 --- /dev/null +++ b/app/views/report/login_detail_query.json.jbuilder @@ -0,0 +1,10 @@ +json.draw params['draw']&.to_i +json.recordsTotal @recordsTotal +json.recordsFiltered @recordsFiltered +json.data do + json.array! @logins do |login| + json.login_text login.user ? "(#{login.user.login}) #{login.user.full_name}" : '-- deletec user --' + json.created_at login.created_at.strftime('%Y-%m-%d %H:%M') + json.ip_address login.ip_address + end +end diff --git a/app/views/report/login_stat.html.haml b/app/views/report/login_stat.html.haml deleted file mode 100644 --- a/app/views/report/login_stat.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- content_for :header do - = stylesheet_link_tag 'tablesorter-theme.cafe' - = javascript_include_tag 'local_jquery' - -%script{:type=>"text/javascript"} - $(function () { - $('#since_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); - $('#until_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); - $('#my_table').tablesorter({widthFixed: true, widgets: ['zebra']}); - }); - -%h1 Login status - -=render partial: 'report_menu' -=render partial: 'date_range', locals: {param_text: 'Login date range:', title: 'Query login stat in the range' } - -%table.tablesorter-cafe#my_table - %thead - %tr - %th login - %th full name - %th login count - %th earliest - %th latest - %th IP - %tbody - - @logins.each do |l| - %tr{class: cycle('info-even','info-odd')} - %td= link_to l[:login], controller: 'users', action: 'profile', id: l[:id] - %td= l[:full_name] - %td= l[:count] - %td= l[:min] ? l[:min].in_time_zone.strftime('%Y-%m-%d %H:%M') : '' - %td= l[:max] ? "#{l[:max].in_time_zone.strftime('%Y-%m-%d %H:%M.%S')} (#{time_ago_in_words(l[:max].in_time_zone)} ago)" : '' - %td - - l[:ip].each do |ip| - #{ip.ip_address}
diff --git a/app/views/report/login_summary_query.json.jbuilder b/app/views/report/login_summary_query.json.jbuilder new file mode 100644 --- /dev/null +++ b/app/views/report/login_summary_query.json.jbuilder @@ -0,0 +1,12 @@ +json.draw params['draw']&.to_i +json.recordsTotal @recordsTotal +json.recordsFiltered @recordsFiltered +json.data do + json.array! @users do |user| + json.login_text "(#{user[:login]}) #{user[:full_name]}" + json.count user[:count] + json.earliest user[:min].strftime('%Y-%m-%d %H:%M') + json.latest user[:max].strftime('%Y-%m-%d %H:%M') + json.ip_address user[:ip].join('
') + end +end diff --git a/app/views/report/submission.html.haml b/app/views/report/submission.html.haml --- a/app/views/report/submission.html.haml +++ b/app/views/report/submission.html.haml @@ -36,7 +36,11 @@ submission_table.ajax.reload( () => { submission_table.columns.adjust().draw() } ) } }, - 'copy', 'excel' + 'copy', + { + extend: 'excel', + title: 'Submission detail' + } ], columns: [ {title: 'Sub ID', data: 'id'}, @@ -55,8 +59,9 @@ d.since_datetime = $('#since_datetime').val() d.until_datetime = $('#until_datetime').val() d.users = $("input[name='users']:checked").val() - d.problems = $("#problem_id").select2('val') d.groups = $("#group_id").select2('val') + d.problems = $("input[name='problems']:checked").val() + d.problem_id = $("#problem_id").select2('val') }, dataType: 'json', beforeSend: (request) => { diff --git a/app/views/shared/_problem_select.html.haml b/app/views/shared/_problem_select.html.haml --- a/app/views/shared/_problem_select.html.haml +++ b/app/views/shared/_problem_select.html.haml @@ -2,9 +2,18 @@ .panel-heading Problems .panel-body - %p - Select problem(s) to be included in the report - = label_tag :problem_id, "Problems" + .radio + %label + = radio_button_tag 'problems', 'all', (params[:users] == "all") + All problems + .radio + %label + = radio_button_tag 'problems', 'enabled', (params[:users] == "enabled"), checked: true + Only problems with available = "yes" + .radio + %label + = radio_button_tag 'problems', 'selected', (params[:users] == "group") + Only these selected problems = select_tag 'problem_id[]', options_for_select(Problem.all.collect {|p| ["[#{p.name}] #{p.full_name}", p.id]},params[:problem_id]), { id: :problem_id, class: 'select2 form-control', multiple: "true" } diff --git a/app/views/shared/_user_select.html.haml b/app/views/shared/_user_select.html.haml --- a/app/views/shared/_user_select.html.haml +++ b/app/views/shared/_user_select.html.haml @@ -8,7 +8,7 @@ All users .radio %label - = radio_button_tag 'users', 'enabled', (params[:users] == "enabled") + = radio_button_tag 'users', 'enabled', (params[:users] == "enabled"), checked: true Only enabled users .radio %label diff --git a/config/routes.rb b/config/routes.rb --- a/config/routes.rb +++ b/config/routes.rb @@ -154,6 +154,11 @@ post 'cheat_scruntinize' get 'submission' post 'submission_query' + get 'login_stat' + post 'login_stat' + get 'login' + post 'login_summary_query' + post 'login_detail_query' end #get 'report/current_score', to: 'report#current_score', as: 'report_current_score' #get 'report/problem_hof(/:id)', to: 'report#problem_hof', as: 'report_problem_hof' diff --git a/db/migrate/20200405112919_add_index_to_login.rb b/db/migrate/20200405112919_add_index_to_login.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20200405112919_add_index_to_login.rb @@ -0,0 +1,5 @@ +class AddIndexToLogin < ActiveRecord::Migration[5.2] + def change + add_index :logins, :user_id + end +end diff --git a/db/schema.rb b/db/schema.rb --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_04_04_142959) do +ActiveRecord::Schema.define(version: 2020_04_05_112919) do create_table "announcements", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "author" @@ -115,6 +115,7 @@ t.string "ip_address" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_logins_on_user_id" end create_table "messages", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|