commit
df3adb6542
@ -0,0 +1,81 @@
|
||||
$(document).on('turbolinks:load', function() {
|
||||
if ($('body.admins-user-schools-statistics-index-page').length > 0) {
|
||||
var $form = $('.user-schools-statistic-list-form');
|
||||
|
||||
// ************** 学校选择 *************
|
||||
var matcherFunc = function(params, data){
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
if (typeof data.text === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.name && data.name.indexOf(params.term) > -1) {
|
||||
var modifiedData = $.extend({}, data, true);
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
// Return `null` if the term should not be displayed
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var defineSchoolSelect = function (schools) {
|
||||
$form.find('.school-select').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: '选择学校/单位',
|
||||
minimumInputLength: 1,
|
||||
data: schools,
|
||||
templateResult: function (item) {
|
||||
if(!item.id || item.id === '') return item.text;
|
||||
return item.name;
|
||||
},
|
||||
templateSelection: function(item){
|
||||
if (item.id) {
|
||||
$form.find('#school_id').val(item.id);
|
||||
}
|
||||
return item.name || item.text;
|
||||
},
|
||||
matcher: matcherFunc
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 初始化学校选择器
|
||||
$.ajax({
|
||||
url: '/api/schools/for_option.json',
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
defineSchoolSelect(data.schools);
|
||||
}
|
||||
});
|
||||
|
||||
// 清空
|
||||
$form.on('click', '.clear-btn', function(){
|
||||
$form.find('select[name="date"]').val('');
|
||||
$form.find('select[name="province"]').val('');
|
||||
$form.find('.school-select').val('').trigger('change');
|
||||
$form.find('input[type="submit"]').trigger('click');
|
||||
})
|
||||
|
||||
|
||||
// 导出
|
||||
$('.export-action').on('click', function(){
|
||||
var form = $(".user-schools-statistic-list-form")
|
||||
var exportLink = $(this);
|
||||
var date = form.find("select[name='date']").val();
|
||||
var schoolId = form.find('input[name="school_id"]').val();
|
||||
var province = form.find('input[name="province"]').val();
|
||||
console.log(province)
|
||||
if(province == "" || province == null){
|
||||
alert("只能按省份导出");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = exportLink.data("url").split('?')[0] + "?date=" + date + "&school_id=" + schoolId + "&province=" + province;
|
||||
window.open(url);
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
class Admins::UserSchoolsStatisticsController < Admins::BaseController
|
||||
|
||||
def export
|
||||
params[:per_page] = 500
|
||||
_count, @schools = Admins::UserSchoolsStatisticQuery.call(params)
|
||||
|
||||
filename = ['用户运营统计', Time.zone.now.strftime('%Y%m%d%H%M%S')].join('-') << '.xlsx'
|
||||
render xlsx: 'export', filename: filename
|
||||
end
|
||||
|
||||
def index
|
||||
default_sort('cnt', 'desc')
|
||||
total_count, schools = Admins::UserSchoolsStatisticQuery.call(params)
|
||||
|
||||
@schools = paginate schools, total_count: total_count
|
||||
|
||||
end
|
||||
end
|
@ -0,0 +1,26 @@
|
||||
class CreateStudentAttendanceRecordJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(attendance_id, group_ids)
|
||||
attendance = CourseAttendance.find_by(id: attendance_id)
|
||||
course = attendance.course
|
||||
return if attendance.blank? || course.blank?
|
||||
|
||||
if group_ids.include?(0)
|
||||
students = course.students
|
||||
else
|
||||
students = course.students.where(course_group_id: group_ids)
|
||||
end
|
||||
|
||||
attrs = %i[course_attendance_id user_id course_member_id course_id course_group_id created_at updated_at]
|
||||
|
||||
same_attrs = {course_attendance_id: attendance.id, course_id: course.id}
|
||||
|
||||
CourseMemberAttendance.bulk_insert(*attrs) do |worker|
|
||||
|
||||
students.each do |student|
|
||||
worker.add same_attrs.merge(user_id: student.user_id, course_member_id: student.id, course_group_id: student.course_group_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,28 @@
|
||||
class StudentJoinAttendanceRecordJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(member_id)
|
||||
member = CourseMember.find_by(id: member_id)
|
||||
course = member&.course
|
||||
return if member.blank? || course.blank?
|
||||
|
||||
current_date = Date.current
|
||||
current_end_time = Time.current.strftime("%H:%M:%S")
|
||||
group_ids = member.course_group_id == 0 ? [0] : [member.course_group_id, 0]
|
||||
|
||||
current_attendance_ids = course.course_attendances.joins(:course_attendance_groups).where(course_group_id: group_ids).
|
||||
where("(attendance_date = '#{current_date}' and start_time <= '#{current_end_time}' and end_time > '#{current_end_time}') or (attendance_date > '#{current_date}')").pluck(:id)
|
||||
|
||||
|
||||
attrs = %i[course_attendance_id user_id course_member_id course_id course_group_id created_at updated_at]
|
||||
|
||||
same_attrs = {course_member_id: member_id, course_id: course.id, user_id: member.user_id, course_group_id: member.course_group_id}
|
||||
|
||||
CourseMemberAttendance.bulk_insert(*attrs) do |worker|
|
||||
|
||||
current_attendance_ids.each do |attendance_id|
|
||||
worker.add same_attrs.merge(course_attendance_id: attendance_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,114 @@
|
||||
class Admins::UserSchoolsStatisticQuery < ApplicationQuery
|
||||
include CustomSortable
|
||||
|
||||
attr_reader :params
|
||||
|
||||
sort_columns :cnt,
|
||||
default_by: :cnt, default_direction: :desc
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
schools = School
|
||||
if params[:province].present?
|
||||
schools = schools.where("province like ?", "%#{params[:province]}%")
|
||||
end
|
||||
|
||||
if params[:school_id].present?
|
||||
schools = schools.where(id: params[:school_id])
|
||||
end
|
||||
|
||||
total = schools.count
|
||||
# 根据排序字段进行查询
|
||||
schools = query_by_sort_column(schools.group(:id), params[:sort_by])
|
||||
#schools = custom_sort(schools, params[:sort_by], params[:sort_direction])
|
||||
schools = schools.limit(page_size).offset(offset).to_a
|
||||
# 查询并组装其它数据
|
||||
schools = package_other_data(schools)
|
||||
[total, schools]
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def package_other_data(schools)
|
||||
ids = schools.map(&:id)
|
||||
user_e = UserExtension.where(school_id: schools.map(&:id))
|
||||
#study_myshixun = Myshixun.joins("join user_extensions ue on ue.user_id = myshixuns.user_id").where(ue: {school_id: ids})
|
||||
#finish_myshixun = Myshixun.joins("join user_extensions ue on ue.user_id = myshixuns.user_id")
|
||||
# .where(ue: {school_id: ids}, myshixuns: {status: 1})
|
||||
study_challenge = Game.joins("join user_extensions ue on ue.user_id = games.user_id")
|
||||
.where(ue: {school_id: ids},).where( games:{status: [0, 1, 2]})
|
||||
finish_challenge = Game.joins("join user_extensions ue on ue.user_id = games.user_id")
|
||||
.where(ue: {school_id: ids}).where(games: {status: 2})
|
||||
reg_teacher = user_e.where(identity: 'teacher')
|
||||
reg_student = user_e.where.not(identity: 'teacher')
|
||||
|
||||
if time_range.present?
|
||||
#study_myshixun = study_myshixun.where(updated_at: time_range)
|
||||
#finish_myshixun = finish_myshixun.where(updated_at: time_range)
|
||||
study_challenge = study_challenge.where(updated_at: time_range)
|
||||
finish_challenge = finish_challenge.where(updated_at: time_range)
|
||||
reg_teacher = reg_teacher.where(created_at: time_range)
|
||||
reg_student = reg_student.where(created_at: time_range)
|
||||
user_e = user_e.joins(:user).where(users: {last_login_on: time_range})
|
||||
end
|
||||
|
||||
#study_myshixun_map = study_myshixun.reorder(nil).group(:school_id).count
|
||||
#finish_myshixun_map = finish_myshixun.reorder(nil).group(:school_id).count
|
||||
study_challenge_map = study_challenge.reorder(nil).group(:school_id).count
|
||||
finish_challenge_map = finish_challenge.reorder(nil).group(:school_id).count
|
||||
evaluate_count_map = study_challenge.reorder(nil).group(:school_id).sum(:evaluate_count)
|
||||
reg_teacher_map = reg_teacher.reorder(nil).group(:school_id).count
|
||||
reg_student_map = reg_student.reorder(nil).group(:school_id).count
|
||||
user_e_map = user_e.reorder(nil).group(:school_id).count
|
||||
|
||||
schools.each do |school|
|
||||
school._extra_data = {
|
||||
#study_shixun_count: study_myshixun_map.fetch(schools.id, 0),
|
||||
#finish_shixun_count: finish_myshixun_map.fetch(schools.id, 0),
|
||||
study_challenge_count: study_challenge_map.fetch(school.id, 0),
|
||||
finish_challenge_count: finish_challenge_map.fetch(school.id, 0),
|
||||
evaluate_count: evaluate_count_map.fetch(school.id, 0),
|
||||
reg_teacher_count: reg_teacher_map.fetch(school.id, 0),
|
||||
reg_student_count: reg_student_map.fetch(school.id, 0),
|
||||
user_active_count: user_e_map.fetch(school.id, 0)
|
||||
}
|
||||
end
|
||||
schools
|
||||
end
|
||||
|
||||
def query_by_sort_column(schools, sort_by_column)
|
||||
#base_query_column = 'schools.*'
|
||||
|
||||
case sort_by_column.to_s
|
||||
when 'cnt' then
|
||||
schools.left_joins(:user_extensions).select("schools.*, count(*) cnt").order("cnt desc")
|
||||
else
|
||||
schools
|
||||
end
|
||||
end
|
||||
|
||||
def time_range
|
||||
@_time_range ||= begin
|
||||
case params[:date]
|
||||
when 'dayly' then 1.days.ago..Time.now
|
||||
when 'weekly' then 1.weeks.ago..Time.now
|
||||
when 'monthly' then 1.months.ago..Time.now
|
||||
when 'quarterly' then 3.months.ago..Time.now
|
||||
when 'yearly' then 1.years.ago..Time.now
|
||||
else ''
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def page_size
|
||||
params[:per_page].to_i.zero? ? 20 : params[:per_page].to_i
|
||||
end
|
||||
|
||||
def offset
|
||||
(params[:page].to_i.zero? ? 0 : params[:page].to_i - 1) * page_size
|
||||
end
|
||||
end
|
@ -0,0 +1,18 @@
|
||||
wb = xlsx_package.workbook
|
||||
wb.add_worksheet(name: '用户运营统计') do |sheet|
|
||||
sheet.add_row %w(单位名称 省份 注册老师数量 注册学生数量 活跃用户 学习关卡数 完成关卡数 评测次数)
|
||||
|
||||
@schools.each do |school|
|
||||
data = [
|
||||
school.name,
|
||||
school.province,
|
||||
school.display_extra_data(:reg_teacher_count),
|
||||
school.display_extra_data(:reg_student_count),
|
||||
school.display_extra_data(:user_active_count),
|
||||
school.display_extra_data(:study_challenge_count),
|
||||
school.display_extra_data(:finish_challenge_count),
|
||||
school.display_extra_data(:evaluate_count),
|
||||
]
|
||||
sheet.add_row(data)
|
||||
end
|
||||
end
|
@ -0,0 +1,30 @@
|
||||
<% define_admin_breadcrumbs do %>
|
||||
<% add_admin_breadcrumb('用户运营情况') %>
|
||||
<% end %>
|
||||
|
||||
<div class="box search-form-container user-schools-statistic-list-form">
|
||||
<%= form_tag(admins_user_schools_statistics_path, method: :get, class: 'form-inline search-form flex-1', remote: true) do %>
|
||||
<div class="form-group col-12 col-md-auto">
|
||||
<label for="status">时间范围:</label>
|
||||
<% data_arrs = [['不限', ''],['最近一天', 'dayly'], ['最近一周', 'weekly'], ['最近一个月', 'monthly'],
|
||||
['最近三个月', 'quarterly'], ['最近一年', 'yearly']] %>
|
||||
<%= select_tag(:date, options_for_select(data_arrs, params[:date]), class: 'form-control') %>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12 col-md-3">
|
||||
<label for="school_name">所属单位:</label>
|
||||
<%= hidden_field_tag(:school_id, params[:school_id]) %>
|
||||
<%= select_tag :school_name, options_for_select([''], params[:school_id]), class: 'form-control school-select flex-1' %>
|
||||
</div>
|
||||
|
||||
<%= text_field_tag(:province, params[:province], class: 'form-control col-sm-2 ml-3', placeholder: '所属省份') %>
|
||||
|
||||
<%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %>
|
||||
<input type="reset" class="btn btn-secondary clear-btn" value="清空"/>
|
||||
<% end %>
|
||||
<%= javascript_void_link '导出', class: 'btn btn-outline-primary export-action', 'data-url': export_admins_user_schools_statistics_path(format: :xlsx) %>
|
||||
</div>
|
||||
|
||||
<div class="box admin-list-container user-schools-statistic-list-container">
|
||||
<%= render partial: 'admins/user_schools_statistics/shared/list', locals: { schools: @schools } %>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
$('.user-schools-statistic-list-container').html("<%= j( render partial: 'admins/user_schools_statistics/shared/list', locals: { schools: @schools } ) %>");
|
@ -0,0 +1,38 @@
|
||||
<table class="table table-hover text-center user-statistic-list-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th width="6%">序号</th>
|
||||
<th width="22%" class="text-left">单位名称</th>
|
||||
<th width="8%" class="text-left">省份</th>
|
||||
<th width="10%">注册老师数量<%#= sort_tag('学习关卡数', name: 'study_challenge_count', path: admins_user_statistics_path) %></th>
|
||||
<th width="10%">注册学生数量<%#= sort_tag('完成关卡数', name: 'finish_challenge_count', path: admins_user_statistics_path) %></th>
|
||||
<th width="10%">活跃用户<%#= sort_tag('活跃用户', name: 'user_active_count', path: admins_user_schools_statistics_path) %></th>
|
||||
<th width="10%">学习关卡数<%#= sort_tag('学习关卡数', name: 'finish_shixun_count', path: admins_user_schools_statistics_path) %></th>
|
||||
<th width="10%">完成关卡数</th>
|
||||
<th width="14%">评测次数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if schools.present? %>
|
||||
<% schools.each_with_index do |school, index| %>
|
||||
<tr class="user-statistic-item-<%= school.id %>">
|
||||
<td><%= list_index_no((params[:page] || 1).to_i, index) %></td>
|
||||
<td class="text-left">
|
||||
<%= school.name %>
|
||||
</td>
|
||||
<td class="text-left"><%= school.province %></td>
|
||||
<td><%= school.display_extra_data(:reg_teacher_count) %></td>
|
||||
<td><%= school.display_extra_data(:reg_student_count) %></td>
|
||||
<td><%= school.display_extra_data(:user_active_count) %></td>
|
||||
<td><%= school.display_extra_data(:study_challenge_count) %></td>
|
||||
<td><%= school.display_extra_data(:finish_challenge_count) %></td>
|
||||
<td><%= school.display_extra_data(:evaluate_count) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render 'admins/shared/no_data_for_table' %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= render partial: 'admins/shared/paginate', locals: { objects: schools } %>
|
@ -0,0 +1,9 @@
|
||||
json.history_attendances @history_attendances do |attendance|
|
||||
json.(attendance, :id, :name)
|
||||
json.created_at attendance.created_at.strftime("%Y/%m/%d %H:%M")
|
||||
json.normal_count attendance.normal_count
|
||||
json.leave_count attendance.leave_count
|
||||
json.absence_count attendance.absence_count
|
||||
json.edit_auth @user_course_identity < Course::PROFESSOR || attendance.user_id == User.current.id
|
||||
end
|
||||
json.all_history_count @all_history_count
|
@ -0,0 +1,24 @@
|
||||
json.current_attendance @current_attendance do |attendance|
|
||||
json.(attendance, :id, :normal_count, :all_count)
|
||||
json.attendance_date attendance.attendance_date.strftime("%Y/%m/%d")
|
||||
json.start_time attendance.start_time.strftime("%H:%M")
|
||||
json.end_time attendance.end_time.strftime("%H:%M")
|
||||
end
|
||||
|
||||
all_normal_rate = []
|
||||
all_absence_rate = []
|
||||
json.history_attendances @history_attendances.each_with_index.to_a do |attendance, index|
|
||||
normal_count = history_member_count(@all_member_attendances, "NORMAL", attendance.id)
|
||||
absence_count = history_member_count(@all_member_attendances, "ABSENCE", attendance.id)
|
||||
all_count = @all_member_attendances.select{|member_attendance| member_attendance.course_attendance_id == attendance.id}.size
|
||||
|
||||
json.index index + 1
|
||||
json.normal_rate cal_rate(normal_count, all_count)
|
||||
all_normal_rate << cal_rate(normal_count, all_count)
|
||||
json.absence_rate cal_rate(absence_count, all_count)
|
||||
all_absence_rate << cal_rate(absence_count, all_count)
|
||||
end
|
||||
|
||||
json.all_history_count @all_history_count
|
||||
json.avg_normal_rate @all_history_count == 0 ? 0 : all_normal_rate.sum / @all_history_count
|
||||
json.avg_absence_rate @all_history_count == 0 ? 0 : all_absence_rate.sum / @all_history_count
|
@ -0,0 +1,15 @@
|
||||
json.normal_count @normal_count
|
||||
json.leave_count @leave_count
|
||||
json.absence_count @absence_count
|
||||
json.all_count @all_count
|
||||
json.code @attendance.attendance_code
|
||||
json.mode @attendance.mode
|
||||
json.edit_auth @user_course_identity < Course::PROFESSOR || @attendance.user_id == User.current.id
|
||||
json.attendance_date @attendance.attendance_date.strftime("%Y/%m/%d")
|
||||
json.start_time @attendance.start_time.strftime("%H:%M")
|
||||
json.end_time @attendance.end_time.strftime("%H:%M")
|
||||
|
||||
json.course_groups @groups do |group|
|
||||
json.(group, :id, :name, :course_members_count)
|
||||
json.attendance_count group_attendance_count(@all_attendances, group) if @_is_current_attendance
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
json.member_attendances @member_attendances.each do |member|
|
||||
json.(member, :user_id, :attendance_status)
|
||||
json.user_name member.user&.real_name
|
||||
json.student_id member.user&.student_id
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class ModidyHiddenDefaultForDiscusses < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
change_column :discusses, :hidden, :boolean, :default => false
|
||||
end
|
||||
end
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CreateStudentAttendanceRecordJob, type: :job do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe StudentJoinAttendanceRecordJob, type: :job do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
Loading…
Reference in new issue