Merge branch 'dev_aliyun' into develop

video_transcode
daiao 5 years ago
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);
});
}
});

@ -6,6 +6,7 @@ class Admins::DashboardsController < Admins::BaseController
@new_user_count = User.where(created_on: current_month).count @new_user_count = User.where(created_on: current_month).count
unless Rails.env.development?
shixun_tomcat = edu_setting('cloud_bridge') shixun_tomcat = edu_setting('cloud_bridge')
uri = "#{shixun_tomcat}/bridge/monitor/getPodsInfo" uri = "#{shixun_tomcat}/bridge/monitor/getPodsInfo"
@ -14,6 +15,7 @@ class Admins::DashboardsController < Admins::BaseController
@pod_num = res['sum'] || 0 @pod_num = res['sum'] || 0
end end
end end
end
def month_active_user def month_active_user
count = UserExtension.where(created_at: current_month).group(:identity).count count = UserExtension.where(created_at: current_month).group(:identity).count

@ -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

@ -321,7 +321,7 @@ class ApplicationController < ActionController::Base
end end
if !User.current.logged? && Rails.env.development? if !User.current.logged? && Rails.env.development?
User.current = User.find 3117 User.current = User.find 1
end end

@ -46,7 +46,7 @@ class DiscussesController < ApplicationController
end end
sql = "select d.id from discusses d join shixuns s on d.dis_id = s.id where s.status = 2 and s.hidden = false and d.root_id is null sql = "select d.id from discusses d join shixuns s on d.dis_id = s.id where s.status = 2 and s.hidden = false and d.root_id is null
and d.hidden = false #{sql1} #{sql2} order by d.created_at desc" and d.hidden = false and d.dis_type = 'Shixun' #{sql1} #{sql2} order by d.created_at desc"
memo_ids = Discuss.find_by_sql(sql).pluck(:id) memo_ids = Discuss.find_by_sql(sql).pluck(:id)
@memo_count = memo_ids.size @memo_count = memo_ids.size
@ -81,8 +81,7 @@ class DiscussesController < ApplicationController
begin begin
@discuss = Discuss.create!(:dis_id => params[:container_id], :dis_type => params[:container_type], @discuss = Discuss.create!(:dis_id => params[:container_id], :dis_type => params[:container_type],
:content => params[:content].gsub("&nbsp\;", "").strip, :user_id => current_user.id, :content => params[:content].gsub("&nbsp\;", "").strip, :user_id => current_user.id,
:praise_count => 0, :position => params[:position], :challenge_id => params[:challenge_id], :praise_count => 0, :position => params[:position], :challenge_id => params[:challenge_id])
:hidden => !current_user.admin?) # 管理员回复的能够显示
rescue Exception => e rescue Exception => e
uid_logger_error("create discuss failed : #{e.message}") uid_logger_error("create discuss failed : #{e.message}")
raise Educoder::TipException.new("评论异常,原因:#{e.message}") raise Educoder::TipException.new("评论异常,原因:#{e.message}")

@ -418,8 +418,8 @@ class ShixunsController < ApplicationController
logger.info("#########service_update_params: #{service_update_params}") logger.info("#########service_update_params: #{service_update_params}")
begin begin
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@shixun.update_attributes(shixun_params) @shixun.update_attributes!(shixun_params)
@shixun.shixun_info.update_attributes(shixun_info_params) @shixun.shixun_info.update_attributes!(shixun_info_params)
# 镜像变动 # 镜像变动
@shixun.shixun_mirror_repositories.where.not(mirror_repository_id: old_mirror_ids).destroy_all @shixun.shixun_mirror_repositories.where.not(mirror_repository_id: old_mirror_ids).destroy_all
@shixun.shixun_mirror_repositories.create!(new_mirror_id) if new_mirror_id.present? @shixun.shixun_mirror_repositories.create!(new_mirror_id) if new_mirror_id.present?

@ -1,31 +1,48 @@
class Weapps::AttendancesController < ApplicationController class Weapps::AttendancesController < ApplicationController
before_action :require_login before_action :require_login
before_action :find_course, only: [:create, :index, :student_attendances] before_action :find_course, only: [:create, :index, :student_attendances, :history_attendances]
before_action :find_attendance, except: [:create, :index, :student_attendances] before_action :find_attendance, except: [:create, :index, :student_attendances, :history_attendances]
before_action :user_course_identity before_action :user_course_identity
before_action :teacher_allowed, only: [:create] before_action :teacher_allowed, only: [:create]
before_action :edit_auth, only: [:update, :destroy, :end]
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
attendance = @course.course_attendances.create!(create_params.merge(user_id: current_user.id)) attendance = @course.course_attendances.create!(create_params.merge(user_id: current_user.id))
unless params[:group_ids].blank? unless params[:group_ids].blank?
group_ids = @course.charge_group_ids(current_user) & params[:group_ids] group_ids = @course.charge_group_ids(current_user) & params[:group_ids].map(&:to_i)
group_ids.each do |group_id| group_ids.each do |group_id|
@course.course_attendance_groups.create!(course_group_id: group_id, course_attendance: attendance) @course.course_attendance_groups.create!(course_group_id: group_id, course_attendance: attendance)
end end
CreateStudentAttendanceRecordJob.perform_later(attendance.id, group_ids)
else else
@course.course_attendance_groups.create!(course_group_id: 0, course_attendance: attendance) @course.course_attendance_groups.create!(course_group_id: 0, course_attendance: attendance)
CreateStudentAttendanceRecordJob.perform_later(attendance.id, [0])
end end
render_ok({attendance_id: attendance.id}) render_ok({attendance_id: attendance.id})
end end
end end
def index def index
tip_exception(403) if @user_course_identity >= Course::STUDENT
current_date = Date.current
current_end_time = Time.current.strftime("%H:%M:%S")
@current_attendance = @course.course_attendances.where("attendance_date = '#{current_date}' and end_time > '#{current_end_time}'")
.order("attendance_date asc, start_time asc")
all_attendances = @course.course_attendances.where("attendance_date < '#{current_date}' or (attendance_date = '#{current_date}' and end_time < '#{current_end_time}')")
@all_member_attendances = CourseMemberAttendance.where(course_attendance_id: all_attendances)
if params[:group_id].present?
all_attendances = all_attendances.joins(:course_attendance_groups).where(course_attendance_groups: {course_group_id: [params[:group_id], 0]})
@all_member_attendances = @all_member_attendances.joins(:course_member).where(course_members: {course_group_id: params[:group_id]})
end
@history_attendances = all_attendances.order("id asc")
@all_history_count = @history_attendances.size
end end
def student_attendances def student_attendances
tip_exception(403, "") if @user_course_identity != Course::STUDENT # tip_exception("学生身份的签到列表") if @user_course_identity != Course::STUDENT
member = @course.students.find_by(user_id: current_user.id) member = @course.students.find_by(user_id: current_user.id)
current_date = Date.current current_date = Date.current
current_end_time = Time.current.strftime("%H:%M:%S") current_end_time = Time.current.strftime("%H:%M:%S")
@ -38,19 +55,19 @@ class Weapps::AttendancesController < ApplicationController
@history_attendances = @course.course_attendances.where(id: all_attendance_ids.uniq). @history_attendances = @course.course_attendances.where(id: all_attendance_ids.uniq).
where("attendance_date < '#{current_date}' or (attendance_date = '#{current_date}' and end_time < '#{current_end_time}')").order("id desc") where("attendance_date < '#{current_date}' or (attendance_date = '#{current_date}' and end_time < '#{current_end_time}')").order("id desc")
@current_attendance = @course.course_attendances.where(id: all_attendance_ids.uniq). @current_attendance = @course.course_attendances.where(id: all_attendance_ids.uniq).
where("attendance_date = '#{current_date}' and start_time <= '#{current_end_time}' and end_time > '#{current_end_time}'").take where("attendance_date = '#{current_date}' and start_time <= '#{current_end_time}' and end_time > '#{current_end_time}'")
@history_count = @history_attendances.size @history_count = @history_attendances.size
student_attendance_ids = @history_attendances.pluck(:id) student_attendance_ids = @history_attendances.pluck(:id)
student_attendance_ids += @current_attendance.present? ? [@current_attendance.id] : [] student_attendance_ids += @current_attendance.present? ? @current_attendance.pluck(:id) : []
if student_attendance_ids.uniq.blank? if student_attendance_ids.uniq.blank?
@normal_count = 0 @normal_count = 0
@leave_count = 0 @leave_count = 0
@absence_count = 0 @absence_count = 0
else else
@normal_count = @course.course_member_attendances.where(course_attendance_id: student_attendance_ids, attendance_status: 1).size @normal_count = @course.course_member_attendances.where(course_attendance_id: student_attendance_ids, attendance_status: "NORMAL").size
@leave_count = @course.course_member_attendances.where(course_attendance_id: student_attendance_ids, attendance_status: 2).size @leave_count = @course.course_member_attendances.where(course_attendance_id: student_attendance_ids, attendance_status: "LEAVE").size
@absence_count = student_attendance_ids.uniq.size - @normal_count - @leave_count @absence_count = student_attendance_ids.uniq.size - @normal_count - @leave_count
end end
@ -59,17 +76,49 @@ class Weapps::AttendancesController < ApplicationController
end end
def show def show
@normal_count = @attendance.normal_count
@leave_count = @attendance.leave_count
@absence_count = @attendance.absence_count
@all_count = @attendance.course_member_attendances.size
@_is_current_attendance = @attendance.current_attendance?
if @attendance.course_attendance_groups.first&.course_group_id.to_i == 0
@groups = @course.course_groups
else
@groups = @course.course_groups.where(id: @attendance.course_attendance_groups.pluck(:course_group_id))
end
@groups = @groups.includes(:course_members) if @_is_current_attendance
@all_attendances = @attendance.course_member_attendances
end end
def update def update
tip_exception(403, "") unless @user_course_identity < Course::PROFESSOR || @attendance.user_id == current_user.id
@attendance.update!(name: params[:name]) @attendance.update!(name: params[:name])
render_ok render_ok
end end
def destroy def destroy
@attendance.destroy!
render_ok
end
def history_attendances
current_date = Date.current
current_end_time = Time.current.strftime("%H:%M:%S")
@history_attendances = @course.course_attendances.where("attendance_date < '#{current_date}' or
(attendance_date = '#{current_date}' and end_time < '#{current_end_time}')").order("id desc")
@all_history_count = @history_attendances.size
@history_attendances = paginate @history_attendances.includes(:course_member_attendances)
end
def end
a_end_time = "#{@attendance.attendance_date} #{@attendance.end_time}".to_time
tip_exception("该签到已截止") unless @attendance.current_attendance?
@attendance.update!(end_time: Time.current)
render_ok
end end
private private
@ -81,4 +130,8 @@ class Weapps::AttendancesController < ApplicationController
@attendance = CourseAttendance.find params[:id] @attendance = CourseAttendance.find params[:id]
@course = @attendance.course @course = @attendance.course
end end
def edit_auth
tip_exception(403, "") unless @user_course_identity < Course::PROFESSOR || @attendance.user_id == current_user.id
end
end end

@ -1,9 +1,21 @@
class Weapps::CourseMemberAttendancesController < ApplicationController class Weapps::CourseMemberAttendancesController < ApplicationController
before_action :require_login before_action :require_login
before_action :find_course, :user_course_identity, only: [:update_status]
def index
attendance = CourseAttendance.find params[:attendance_id]
@member_attendances = attendance.course_member_attendances
if params[:group_ids].present?
@member_attendances = @member_attendances.joins(:course_member).where(course_members: {course_group_id: params[:group_ids]})
end
@member_attendances = @member_attendances.where(attendance_status: params[:attendance_status]) if params[:attendance_status].present?
@member_attendances = @member_attendances.joins(user: :user_extension).order("attendance_status=1 desc, course_member_attendances.updated_at desc, user_extensions.student_id asc")
@member_attendances = paginate @member_attendances.preload(user: :user_extension)
end
def create def create
tip_exception("签到码不能为空") if params[:code].blank? tip_exception("签到码不能为空") if params[:code].blank?
tip_exception("attendance_mode参数不对") if [1, 2].include?(params[:attendance_mode]) tip_exception("attendance_mode参数不对") unless ["NUMBER", "QRCODE"].include?(params[:attendance_mode])
attendance = CourseAttendance.find_by(attendance_code: params[:code]) attendance = CourseAttendance.find_by(attendance_code: params[:code])
tip_exception("签到码输入有误") if attendance.blank? || attendance.course.blank? tip_exception("签到码输入有误") if attendance.blank? || attendance.course.blank?
@ -11,19 +23,38 @@ class Weapps::CourseMemberAttendancesController < ApplicationController
member = attendance.course.students.find_by(user_id: current_user.id) member = attendance.course.students.find_by(user_id: current_user.id)
tip_exception("签到码输入有误") if member.blank? tip_exception("签到码输入有误") if member.blank?
start_time = "#{attendance.attendance_date} #{attendance.start_time}".to_time tip_exception("不在签到时间内") unless attendance.current_attendance?
end_time = "#{attendance.attendance_date} #{attendance.end_time}".to_time
Rails.logger.info("##############{start_time} #{end_time}")
tip_exception("不在签到时间内") unless start_time < Time.current && Time.current < end_time
current_attendance = attendance.course_member_attendances.find_by(user_id: current_user.id) tip_exception("只支持数字签到") if attendance.mode != "ALL" && attendance.mode == "NUMBER" && params[:attendance_mode] == "QRCODE"
tip_exception("请勿重复签到") if current_attendance.present? && current_attendance.attendance_status == 1 tip_exception("只支持二维码签到") if attendance.mode != "ALL" && attendance.mode == "QRCODE" && params[:attendance_mode] == "NUMBER"
tip_exception("您当前是请假状态,无法签到") if current_attendance.present? && current_attendance.attendance_status == 2
tip_exception("您当前是旷课状态,无法签到") if current_attendance.present? && current_attendance.attendance_status == 0
unless current_attendance.present? current_attendance = attendance.course_member_attendances.find_by(user_id: current_user.id)
if current_attendance.present?
tip_exception("请勿重复签到") if current_attendance.attendance_status == "NORMAL"
tip_exception("您当前是请假状态,无法签到") if current_attendance.attendance_status == "LEAVE"
tip_exception("您当前是旷课状态,无法签到") if current_attendance.attendance_status == "ABSENCE" && current_attendance.attendance_mode == "TEACHER"
current_attendance.update!(attendance_status: "NORMAL", attendance_mode: params[:attendance_mode])
else
attendance.course_member_attendances.create!(course_member_id: member.id, user_id: current_user.id, course_id: attendance.course_id, attendance.course_member_attendances.create!(course_member_id: member.id, user_id: current_user.id, course_id: attendance.course_id,
course_group_id: member.course_group_id, attendance_status: 1, attendance_mode: params[:attendance_mode] || 2) course_group_id: member.course_group_id, attendance_status: "NORMAL", attendance_mode: params[:attendance_mode])
end
render_ok
end
def update_status
tip_exception("user_id不能为空") if params[:user_id].blank?
tip_exception(403, "无权限调整签到状态") if @user_course_identity > Course::ASSISTANT_PROFESSOR
tip_exception("attendance_status参数不对") unless ["NORMAL", "LEAVE", "ABSENCE"].include?(params[:attendance_status])
attendance = @course.course_attendances.find_by!(id: params[:attendance_id])
current_attendance = attendance.course_member_attendances.find_by(user_id: params[:user_id])
if current_attendance.present?
current_attendance.update!(attendance_status: params[:attendance_status], attendance_mode: "TEACHER")
else
member = attendance.course.students.find_by(user_id: params[:user_id])
tip_exception( "该用户非课堂学生") if member.blank?
attendance.course_member_attendances.create!(course_member_id: member.id, user_id: params[:user_id], course_id: attendance.course_id,
course_group_id: member.course_group_id, attendance_status: params[:attendance_status], attendance_mode: "TEACHER")
end end
render_ok render_ok
end end

@ -8,18 +8,32 @@ class Weapps::CoursesController < Weapps::BaseController
def course_activities def course_activities
@course = current_course @course = current_course
homework_commons = @course.homework_commons.where(homework_type: ["practice", "normal"]).homework_published
member = @course.course_members.find_by(user_id: current_user.id, is_active: 1) member = @course.course_members.find_by(user_id: current_user.id, is_active: 1)
# 签到数据
attendances = @course.course_attendances
current_date = Date.current
current_end_time = Time.current.strftime("%H:%M:%S")
if @user_course_identity == Course::STUDENT
attendances = attendances.joins(:course_attendance_groups).where(course_attendance_groups: {course_group_id: [member.try(:course_group_id).to_i, 0]})
.where("attendance_date < '#{current_date}' or (attendance_date = '#{current_date}' and start_time < '#{current_end_time}')")
end
attendance_ids = attendances.blank? ? "(-1)" : "(" + attendances.pluck(:id).join(",") + ")"
homework_commons = @course.homework_commons.where(homework_type: ["practice", "normal"]).homework_published
if (@user_course_identity == Course::STUDENT && member.try(:course_group_id).to_i == 0) || @user_course_identity > Course::STUDENT if (@user_course_identity == Course::STUDENT && member.try(:course_group_id).to_i == 0) || @user_course_identity > Course::STUDENT
homework_commons = homework_commons.unified_setting homework_commons = homework_commons.unified_setting
elsif @user_course_identity == Course::STUDENT elsif @user_course_identity == Course::STUDENT
not_homework_ids = @course.homework_group_settings.none_published.where("course_group_id = #{member.try(:course_group_id)}").pluck(:homework_common_id) not_homework_ids = @course.homework_group_settings.none_published.where("course_group_id = #{member.try(:course_group_id)}")
.pluck(:homework_common_id)
homework_commons = homework_commons.where.not(id: not_homework_ids) homework_commons = homework_commons.where.not(id: not_homework_ids)
end end
homework_ids = homework_commons.blank? ? "(-1)" : "(" + homework_commons.pluck(:id).join(",") + ")" homework_ids = homework_commons.blank? ? "(-1)" : "(" + homework_commons.pluck(:id).join(",") + ")"
activities = @course.course_activities.where("course_act_type in ('Course', 'CourseMessage') or activities = @course.course_activities.where("course_act_type in ('Course', 'CourseMessage') or
(course_act_type = 'HomeworkCommon' and course_act_id in #{homework_ids})").order("id desc") (course_act_type = 'HomeworkCommon' and course_act_id in #{homework_ids}) or
(course_act_type = 'CourseAttendance' and course_act_id in #{attendance_ids})").order("id desc")
@activities_count = activities.size @activities_count = activities.size
@activities = paginate activities.includes(:course_act, user: :user_extension) @activities = paginate activities.includes(:course_act, user: :user_extension)
end end

@ -2,6 +2,19 @@ module Weapps::AttendancesHelper
def student_attendance_status attendance, user def student_attendance_status attendance, user
st_attendance = attendance.course_member_attendances.find_by(user_id: user.id) st_attendance = attendance.course_member_attendances.find_by(user_id: user.id)
st_attendance.present? ? st_attendance.attendance_status : 0 st_attendance.present? ? st_attendance.attendance_status : "ABSENCE"
end
def group_attendance_count attendances, group
course_member_ids = group.course_members.pluck(:id)
attendances.select{|attendance| course_member_ids.include?(attendance.course_member_id) && attendance.attendance_status == "NORMAL"}.size
end
def history_member_count member_attendances, status, attendance_id
member_attendances.select{|member_attendance| member_attendance.attendance_status == status && member_attendance.course_attendance_id == attendance_id}.size
end
def cal_rate base, sum
sum == 0 ? 0 : (base.to_f / sum)
end end
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

@ -2,10 +2,11 @@ class CourseActivity < ApplicationRecord
belongs_to :course_act, polymorphic: true belongs_to :course_act, polymorphic: true
belongs_to :course belongs_to :course
belongs_to :user belongs_to :user
belongs_to :exercise belongs_to :exercise, optional: true
belongs_to :poll belongs_to :poll, optional: true
belongs_to :course_message belongs_to :course_message, optional: true
belongs_to :homework_common belongs_to :homework_common, optional: true
belongs_to :course_attendance, optional: true
# after_create :add_course_lead # after_create :add_course_lead

@ -1,18 +1,54 @@
class CourseAttendance < ApplicationRecord class CourseAttendance < ApplicationRecord
# status: 0: 未开启1已开启2已截止
# mode: 0 两种签到1 二维码签到2 数字签到 # mode: 0 两种签到1 二维码签到2 数字签到
enum mode: { ALL: 0, QRCODE: 1, NUMBER: 2 }
belongs_to :course belongs_to :course
belongs_to :user belongs_to :user
has_many :course_attendance_groups, dependent: :destroy has_many :course_attendance_groups, dependent: :destroy
has_many :course_member_attendances, dependent: :destroy has_many :course_member_attendances, dependent: :destroy
has_one :course_act, class_name: 'CourseActivity', as: :course_act, dependent: :destroy
validates :name, presence: true validates :name, presence: true
validates :mode, presence: true validates :mode, presence: true
validates :attendance_date, presence: true validates :attendance_date, presence: true
validates :start_time, presence: true validates :start_time, presence: true
validates :end_time, presence: true validates :end_time, presence: true
after_create :generate_attendance_code after_create :generate_attendance_code, :act_as_course_activity
# 正常签到人数
def normal_count
course_member_attendances.select{|member_attendance| member_attendance.attendance_status == "NORMAL"}.size
end
# 请假人数
def leave_count
course_member_attendances.select{|member_attendance| member_attendance.attendance_status == "LEAVE"}.size
end
# 旷课人数
def absence_count
course_member_attendances.select{|member_attendance| member_attendance.attendance_status == "ABSENCE"}.size
end
# 总人数
def all_count
course_member_attendances.size
end
def current_attendance?
a_start_time = "#{attendance_date} #{start_time}".to_time
a_end_time = "#{attendance_date} #{end_time}".to_time
a_start_time < Time.current && Time.current < a_end_time
end
#课程动态公共表记录
def act_as_course_activity
CourseActivity.create(user_id: user_id, course_id: course_id, course_act: self)
end
# 延迟生成邀请码 # 延迟生成邀请码
def attendance_code def attendance_code

@ -23,6 +23,12 @@ class CourseMember < ApplicationRecord
# after_destroy :delete_works # after_destroy :delete_works
# after_create :work_operation # after_create :work_operation
after_create :create_attendance_record
def create_attendance_record
StudentJoinAttendanceRecordJob.perform_later(id)
end
def delete_works def delete_works
if self.role == "STUDENT" if self.role == "STUDENT"
course = self.course course = self.course

@ -1,6 +1,8 @@
class CourseMemberAttendance < ApplicationRecord class CourseMemberAttendance < ApplicationRecord
# attendance_mode 1 二维码签到2 数字签到3 老师签到 # attendance_mode 0 初始数据1 二维码签到2 数字签到3 老师签到
enum attendance_mode: { DEFAULT: 0, QRCODE: 1, NUMBER: 2, TEACHER: 3}
# attendance_status 1 正常签到2 请假0 旷课 # attendance_status 1 正常签到2 请假0 旷课
enum attendance_status: { NORMAL: 1, LEAVE: 2, ABSENCE: 0 }
belongs_to :course_member belongs_to :course_member
belongs_to :user belongs_to :user
belongs_to :course belongs_to :course

@ -5,7 +5,7 @@ class ShixunInfo < ApplicationRecord
# validates_presence_of :evaluate_script, message: "实训脚本不能为空" # validates_presence_of :evaluate_script, message: "实训脚本不能为空"
after_commit :create_diff_record after_commit :create_diff_record
validates :description, length: { maximum: 5000, too_long: "不能超过5000个字符" } validates :description, length: { maximum: 10000, too_long: "不能超过10000个字符" }
private private

@ -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

@ -9,6 +9,10 @@ class ApplicationService
content.gsub(regex, '') content.gsub(regex, '')
end end
def convert_https content
content.gsub("http:", "https:")
end
private private
def strip(str) def strip(str)

@ -12,7 +12,7 @@ class Videos::DispatchCallbackService < ApplicationService
# TODO:: 拆分事件分发 # TODO:: 拆分事件分发
case params['EventType'] case params['EventType']
when 'FileUploadComplete' then # 视频上传完成 when 'FileUploadComplete' then # 视频上传完成
video.file_url = params['FileUrl'] video.file_url = convert_https(params['FileUrl'])
video.filesize = params['Size'] video.filesize = params['Size']
video.upload_success video.upload_success
video.save! video.save!

@ -62,6 +62,7 @@
<%= sidebar_item_group('#user-submenu', '用户', icon: 'user') do %> <%= sidebar_item_group('#user-submenu', '用户', icon: 'user') do %>
<li><%= sidebar_item(admins_users_path, '用户列表', icon: 'user', controller: 'admins-users') %></li> <li><%= sidebar_item(admins_users_path, '用户列表', icon: 'user', controller: 'admins-users') %></li>
<li><%= sidebar_item(admins_user_statistics_path, '用户实训情况', icon: 'area-chart', controller: 'admins-user_statistics') %></li> <li><%= sidebar_item(admins_user_statistics_path, '用户实训情况', icon: 'area-chart', controller: 'admins-user_statistics') %></li>
<li><%= sidebar_item(admins_user_schools_statistics_path, '用户运营统计', icon: 'user-md', controller: 'admins-user_schools_statistics') %></li>
<% end %> <% end %>
</li> </li>

@ -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

@ -1,5 +1,5 @@
json.current_attendance do json.current_attendance @current_attendance do |attendance|
json.partial! 'student_attendance', locals: {attendance: @current_attendance} if @current_attendance.present? json.partial! 'student_attendance', locals: {attendance: attendance}
end end
json.history_attendances @history_attendances do |attendance| json.history_attendances @history_attendances do |attendance|

@ -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

@ -870,6 +870,7 @@ Rails.application.routes.draw do
collection do collection do
get :school_list get :school_list
get :for_option get :for_option
get :for_province_option
get :search get :search
end end
@ -1061,11 +1062,15 @@ Rails.application.routes.draw do
resources :attendances, only: [:index, :update, :create, :show, :destroy], shallow: true do resources :attendances, only: [:index, :update, :create, :show, :destroy], shallow: true do
collection do collection do
get :student_attendances get :student_attendances
get :history_attendances
end end
post :end, on: :member
end end
end end
resources :course_member_attendances, only: [:create] resources :course_member_attendances, only: [:create, :index] do
post :update_status, on: :collection
end
resources :homework_commons do resources :homework_commons do
post :update_settings, on: :member post :update_settings, on: :member
@ -1159,6 +1164,9 @@ Rails.application.routes.draw do
resources :user_statistics, only: [:index] do resources :user_statistics, only: [:index] do
get :export, on: :collection get :export, on: :collection
end end
resources :user_schools_statistics, only: [:index] do
get :export, on: :collection
end
resources :library_applies, only: [:index] do resources :library_applies, only: [:index] do
member do member do
post :agree post :agree

@ -0,0 +1,5 @@
class ModidyHiddenDefaultForDiscusses < ActiveRecord::Migration[5.2]
def change
change_column :discusses, :hidden, :boolean, :default => false
end
end

@ -5,7 +5,7 @@ namespace :sync_evaluate do
School.find_each do |school| School.find_each do |school|
puts school.id puts school.id
evaluate_count = Game.find_by_sql("select sum(g.evaluate_count) as e_count from games g, user_extensions ue where evaluate_count = Game.find_by_sql("select sum(g.evaluate_count) as e_count from games g, user_extensions ue where
g.user_id = ue.user_id and ue.school_id = #{school}.id").firt.try(:e_count) g.user_id = ue.user_id and ue.school_id = '#{school.id}'").first.try(:e_count)
report = SchoolReport.find_or_initialize_by(school_id: school.id) report = SchoolReport.find_or_initialize_by(school_id: school.id)
report.school_name = school.name report.school_name = school.name

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -140264,6 +140264,87 @@ $(document).on('turbolinks:load', function() {
}); });
} }
}); });
$(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);
});
}
});
$(document).on('turbolinks:load', function() { $(document).on('turbolinks:load', function() {
if ($('body.admins-user-statistics-index-page').length > 0) { if ($('body.admins-user-statistics-index-page').length > 0) {
var $form = $('.user-statistic-list-form'); var $form = $('.user-statistic-list-form');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -793,37 +793,37 @@ class App extends Component {
render={ render={
(props) => (<Paperreview {...this.props} {...props} {...this.state} />) (props) => (<Paperreview {...this.props} {...props} {...this.state} />)
}/> }/>
<Route path="/paperlibrary/edit/:id" {/*<Route path="/paperlibrary/edit/:id"*/}
render={ {/* render={*/}
(props) => (<Paperlibraryeditid {...this.props} {...props} {...this.state} />) {/* (props) => (<Paperlibraryeditid {...this.props} {...props} {...this.state} />)*/}
}/> {/* }/>*/}
<Route path="/paperlibrary/see/:id" {/*<Route path="/paperlibrary/see/:id"*/}
render={ {/* render={*/}
(props) => (<Paperlibraryseeid {...this.props} {...props} {...this.state} />) {/* (props) => (<Paperlibraryseeid {...this.props} {...props} {...this.state} />)*/}
}/> {/* }/>*/}
<Route path="/myproblems/:id/:tab?" <Route path="/myproblems/:id/:tab?"
render={ render={
(props) => (<StudentStudy {...this.props} {...props} {...this.state} />) (props) => (<StudentStudy {...this.props} {...props} {...this.state} />)
} /> } />
<Route path="/question/edit/:id" {/*<Route path="/question/edit/:id"*/}
render={ {/* render={*/}
(props) => (<Questionitem_banks {...this.props} {...props} {...this.state} />) {/* (props) => (<Questionitem_banks {...this.props} {...props} {...this.state} />)*/}
} /> {/* } />*/}
<Route path="/question/newitem" {/*<Route path="/question/newitem"*/}
render={ {/* render={*/}
(props) => (<Questionitem_banks {...this.props} {...props} {...this.state} />) {/* (props) => (<Questionitem_banks {...this.props} {...props} {...this.state} />)*/}
} /> {/* } />*/}
<Route path="/question/:type" {/*<Route path="/question/:type"*/}
render={ {/* render={*/}
(props) => (<Headplugselection {...this.props} {...props} {...this.state} />) {/* (props) => (<Headplugselection {...this.props} {...props} {...this.state} />)*/}
} /> {/* } />*/}
<Route path="/paperlibrary" {/*<Route path="/paperlibrary"*/}
render={ {/* render={*/}
(props) => (<Testpaperlibrary {...this.props} {...props} {...this.state} />) {/* (props) => (<Testpaperlibrary {...this.props} {...props} {...this.state} />)*/}
}/> {/* }/>*/}
<Route path="/Integeneration" <Route path="/Integeneration"
render={ render={
@ -835,15 +835,19 @@ class App extends Component {
(props) => (<Developer {...this.props} {...props} {...this.state} />) (props) => (<Developer {...this.props} {...props} {...this.state} />)
}/> }/>
<Route path="/question" {/*<Route path="/question"*/}
render={ {/* render={*/}
(props) => (<Headplugselection {...this.props} {...props} {...this.state} />) {/* (props) => (<Headplugselection {...this.props} {...props} {...this.state} />)*/}
}/> {/* }/>*/}
{/*<Route path="/wxcode/:identifier?" component={WXCode}*/} {/*<Route path="/wxcode/:identifier?" component={WXCode}*/}
{/* render={*/} {/* render={*/}
{/* (props)=>(<WXCode {...this.props} {...props} {...this.state}></WXCode>)*/} {/* (props)=>(<WXCode {...this.props} {...props} {...this.state}></WXCode>)*/}
{/* }*/} {/* }*/}
{/*/>*/} {/*/>*/}
<Route exact path="/" <Route exact path="/"
// component={ShixunsHome} // component={ShixunsHome}
render={ render={

@ -9,7 +9,7 @@ import axios from 'axios';
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
const array=['腾讯课堂','B站','斗鱼','威佰通']; const array=['腾讯课堂','斗鱼直播','Bilibili','威佰通'];
function range(start, end) { function range(start, end) {
const result = []; const result = [];
@ -212,6 +212,19 @@ class LiveNew extends Component{
className="liveModal" className="liveModal"
> >
<Spin spinning={isSpining}> <Spin spinning={isSpining}>
<style>
{`
.ant-select-dropdown-menu-item{
text-align:center;
padding: 10px 0px;
border-bottom: 1px solid #eee;
}
.ant-select-dropdown-menu-item:last-child{
border-bottom:none;
}
`
}
</style>
<div className="task-popup-content"> <div className="task-popup-content">
<Form onSubmit={this.handleSubmit}> <Form onSubmit={this.handleSubmit}>
<Form.Item label={`直播课程`}> <Form.Item label={`直播课程`}>
@ -229,6 +242,7 @@ class LiveNew extends Component{
placeholder="请选择或输入直播平台名称" placeholder="请选择或输入直播平台名称"
onChange={this.ChangePlatform} onChange={this.ChangePlatform}
dataSource={dataSource} dataSource={dataSource}
className="plateAutoComplete"
> >
</AutoComplete> </AutoComplete>
)} )}

@ -1298,10 +1298,12 @@ class CommonWorkSetting extends Component{
{/* 匿评数量 */} {/* 匿评数量 */}
<div className={"h20 mb30 ml60"}> <div className={"h20 mb30 ml60"}>
<span>匿评数量</span> <span>匿评数量</span>
<Tooltip placement="bottom" title={starttimetype===true?this.props.isAdmin()?"发布时间已过,则不能修改":"":""}> <Tooltip placement="bottom" title={this.props.isAdmin()?moment(init_evaluation_start) < this.fetchMoment?"匿评已开启无法修改匿评数量":"":""}>
<span> <span>
<Input type="number" className="mr10" style={{width:"100px" }} value={evaluation_num} onInput={this.evaluation_num_change} <Input type="number" className="mr10" style={{width:"100px" }} value={evaluation_num} onInput={this.evaluation_num_change}
disabled={anonymous_comment && !noAuth? false : true} min={0} max={100} disabled={anonymous_comment && !noAuth?
moment(init_evaluation_start) < this.fetchMoment?true:false
: true} min={0} max={100}
/> />
</span> </span>
</Tooltip> </Tooltip>

@ -525,7 +525,6 @@ class Coursesleftnav extends Component{
} }
saveNavmodapost=(url,value,positiontype,coursesId)=>{ saveNavmodapost=(url,value,positiontype,coursesId)=>{
axios.post(url, axios.post(url,
{name:value}).then((result)=>{ {name:value}).then((result)=>{
if(result!=undefined){ if(result!=undefined){
@ -554,7 +553,8 @@ class Coursesleftnav extends Component{
} }
if(positiontype==="course_groups"){ if(positiontype==="course_groups"){
window.location.href=`/courses/${coursesId}/course_groups/${result.data.group_id}`; this.props.updataleftNavfun();
this.props.history.push(`/courses/${coursesId}/course_groups/${result.data.group_id}`);
} }
} }
@ -585,6 +585,7 @@ class Coursesleftnav extends Component{
} }
saveNavmoda=()=>{ saveNavmoda=()=>{
debugger;
let {Navmodaltypename,setnavid,NavmodalValue}=this.state; let {Navmodaltypename,setnavid,NavmodalValue}=this.state;
let id =setnavid; let id =setnavid;

@ -66,7 +66,7 @@ function CourseGroupListTable(props) {
className:"color-grey-6", className:"color-grey-6",
render: (name, record, index) => { render: (name, record, index) => {
return <WordsBtn title={name.length > 11 ? name : ''} onClick={() => onGoDetail(record)} style={''} return <WordsBtn title={name.length > 11 ? name : ''} onClick={() => onGoDetail(record)} style={''}
className="overflowHidden1" style2={{maxWidth: '180px', verticalAlign: 'bottom'}}> className="overflowHidden1 color-dark" style2={{maxWidth: '180px', verticalAlign: 'bottom'}}>
{name}</WordsBtn> {name}</WordsBtn>
} }
}, },
@ -154,7 +154,7 @@ function CourseGroupListTable(props) {
{!isCourseEnd && isAdmin && <WordsBtn style2={{ marginRight: '12px' }} onClick={() => onDelete(record)} style={'grey'}>删除分班</WordsBtn>} {!isCourseEnd && isAdmin && <WordsBtn style2={{ marginRight: '12px' }} onClick={() => onDelete(record)} style={'grey'}>删除分班</WordsBtn>}
{isStudent && <WordsBtn style2={{ marginRight: '12px' }} onClick={() => addToDir(record)} style={''}>加入分班</WordsBtn>} {isStudent && <WordsBtn style2={{ marginRight: '12px' }} onClick={() => addToDir(record)} style={''}>加入分班</WordsBtn>}
<WordsBtn onClick={() => onGoDetail(record)} style={''}>查看</WordsBtn> <WordsBtn onClick={() => onGoDetail(record)} style={''} className="color-dark">查看</WordsBtn>
</React.Fragment> </React.Fragment>
} }
}) })

@ -87,7 +87,9 @@ class Shixuninformation extends Component {
this.props.form.setFieldsValue({ this.props.form.setFieldsValue({
selectscripts: this.props.data && this.props.data.shixun.standard_scripts[0].id selectscripts: this.props.data && this.props.data.shixun.standard_scripts[0].id
}) })
this.get_mirror_script(this.props.data && this.props.data.shixun.standard_scripts[0].id) console.log(this.props);
// debugger;
// this.get_mirror_script(this.props.data && this.props.data.shixun.standard_scripts[0].id)
} else { } else {
this.props.form.setFieldsValue({ this.props.form.setFieldsValue({
selectscripts: this.props.data && this.props.data.shixun.choice_standard_scripts selectscripts: this.props.data && this.props.data.shixun.choice_standard_scripts

@ -1,48 +1,30 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import {Input, Select, Radio, Checkbox, Popconfirm, message, Modal} from 'antd'; import { getUploadActionUrl, getUrl } from 'educoder';
import {BrowserRouter as Router, Route, Link, Switch} from "react-router-dom";
// import "antd/dist/antd.css";
import { getImageUrl, getUploadActionUrl, getUrl } from 'educoder';
import '../../courses/css/Courses.css' import '../../courses/css/Courses.css'
import axios from 'axios';
import './css/TPMchallengesnew.css'; import './css/TPMchallengesnew.css';
require('codemirror/lib/codemirror.css'); require('codemirror/lib/codemirror.css');
let origin = getUrl();
let path = '/editormd/lib/' let path = '/editormd/lib/'
path = getUrl("/editormd/lib/") path = getUrl("/editormd/lib/")
const $ = window.$; const $ = window.$;
let timeout;
let currentValue;
const Option = Select.Option;
const RadioGroup = Radio.Group;
// 保存数据 // 保存数据
function md_add_data(k,mdu,d){ function md_add_data(k, mdu, d) {
window.sessionStorage.setItem(k+mdu,d); window.sessionStorage.setItem(k + mdu, d);
} }
// 清空保存的数据 // 清空保存的数据
function md_clear_data(k,mdu,id){ function md_clear_data(k, mdu, id) {
window.sessionStorage.removeItem(k+mdu); window.sessionStorage.removeItem(k + mdu);
var id1 = "#e_tip_"+id; var id1 = "#e_tip_" + id;
var id2 = "#e_tips_"+id; var id2 = "#e_tips_" + id;
if(k == 'content'){ if (k == 'content') {
$(id2).html(" "); $(id2).html(" ");
}else{ } else {
$(id1).html(" "); $(id1).html(" ");
} }
} }
@ -59,16 +41,16 @@ function md_rec_data(k, mdu, id) {
} }
window.md_rec_data = md_rec_data; window.md_rec_data = md_rec_data;
function md_elocalStorage(editor,mdu,id){ function md_elocalStorage(editor, mdu, id) {
if (window.sessionStorage){ if (window.sessionStorage) {
var oc = window.sessionStorage.getItem('content'+mdu); var oc = window.sessionStorage.getItem('content' + mdu);
if(oc !== null && oc != editor.getValue()){ if (oc !== null && oc != editor.getValue()) {
console.log("#e_tips_"+id) console.log("#e_tips_" + id)
$("#e_tips_"+id).data('editor', editor); $("#e_tips_" + id).data('editor', editor);
var h = '您上次有已保存的数据,是否<a style="cursor: pointer;" class="link-color-blue" onclick="md_rec_data(\'content\',\''+ mdu + '\',\'' + id + '\')">恢复</a> ? / <a style="cursor: pointer;" class="link-color-blue" onclick="md_clear_data(\'content\',\''+ mdu + '\',\'' + id + '\')">不恢复</a>'; var h = '您上次有已保存的数据,是否<a style="cursor: pointer;" class="link-color-blue" onclick="md_rec_data(\'content\',\'' + mdu + '\',\'' + id + '\')">恢复</a> ? / <a style="cursor: pointer;" class="link-color-blue" onclick="md_clear_data(\'content\',\'' + mdu + '\',\'' + id + '\')">不恢复</a>';
$("#e_tips_"+id).html(h); $("#e_tips_" + id).html(h);
} }
setInterval(function() { setInterval(function () {
var d = new Date(); var d = new Date();
var h = d.getHours(); var h = d.getHours();
var m = d.getMinutes(); var m = d.getMinutes();
@ -76,26 +58,26 @@ function md_elocalStorage(editor,mdu,id){
h = h < 10 ? '0' + h : h; h = h < 10 ? '0' + h : h;
m = m < 10 ? '0' + m : m; m = m < 10 ? '0' + m : m;
s = s < 10 ? '0' + s : s; s = s < 10 ? '0' + s : s;
if(editor.getValue().trim() != ""){ if (editor.getValue().trim() != "") {
md_add_data("content",mdu,editor.getValue()); md_add_data("content", mdu, editor.getValue());
var id1 = "#e_tip_"+id; var id1 = "#e_tip_" + id;
var id2 = "#e_tips_"+id; var id2 = "#e_tips_" + id;
var textStart = " 数据已于 " var textStart = " 数据已于 "
var text = textStart + h + ':' + m + ':' + s +" 保存 "; var text = textStart + h + ':' + m + ':' + s + " 保存 ";
// 占位符 // 占位符
var oldHtml = $(id2).html(); var oldHtml = $(id2).html();
if (oldHtml && oldHtml != ' ' && oldHtml.startsWith(textStart) == false) { if (oldHtml && oldHtml != ' ' && oldHtml.startsWith(textStart) == false) {
$(id2).html( oldHtml.split(' (')[0] + ` (${text})`); $(id2).html(oldHtml.split(' (')[0] + ` (${text})`);
} else { } else {
$(id2).html(text); $(id2).html(text);
} }
// $(id2).html(""); // $(id2).html("");
} }
},10000); }, 10000);
}else{ } else {
$("#e_tip_"+id).after('您的浏览器不支持localStorage.无法开启自动保存草稿服务,请升级浏览器!'); $("#e_tip_" + id).after('您的浏览器不支持localStorage.无法开启自动保存草稿服务,请升级浏览器!');
} }
} }
@ -106,16 +88,16 @@ function create_editorMD(id, width, high, placeholder, imageUrl, callback, initV
var editorName = window.editormd(id, { var editorName = window.editormd(id, {
width: width, width: width,
height: high===undefined?400:high, height: high === undefined ? 400 : high,
path: path, // "/editormd/lib/" path: getUrl("/editormd/lib/"), // "/editormd/lib/"
markdown : initValue, markdown: initValue,
dialogLockScreen: false, dialogLockScreen: false,
watch:watch===undefined?true:watch, watch: watch === undefined ? true : watch,
syncScrolling: "single", syncScrolling: "single",
tex: true, tex: true,
tocm: true, tocm: true,
emoji: !!emoji , emoji: !!emoji,
taskList: true, taskList: true,
codeFold: true, codeFold: true,
searchReplace: true, searchReplace: true,
@ -123,21 +105,35 @@ function create_editorMD(id, width, high, placeholder, imageUrl, callback, initV
sequenceDiagram: true, sequenceDiagram: true,
autoFocus: false, autoFocus: false,
// mine
toolbarIcons: function (mdEditor) { toolbarIcons: function (mdEditor) {
let react_id = `react_${mdEditor.id}`; let react_id = `react_${mdEditor.id}`;
const __that = window[react_id] const __that = window[react_id]
// Or return editormd.toolbarModes[name]; // full, simple, mini
// Using "||" set icons align right. // Using "||" set icons align right.
const icons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "link", "|", "testIcon", "testIcon1", '|', "image", "table", '|', "watch", "clear"]; const icons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "link", "|", "testIcon", "testIcon1", '|', "image", "table", '|', "line-break", "watch", "clear"];
// 试卷处用到的填空题新增按钮 // 试卷处用到的填空题新增按钮
if (__that.props.showNullButton) { if (__that.props.showNullButton) {
icons.push('nullBtton') icons.push('nullBtton')
} }
return icons return icons
}, },
toolbarIconsClass: {
"line-break": "fa-minus"
},
toolbarHandlers: {
/**
* @param {Object} cm CodeMirror对象
* @param {Object} icon 图标按钮jQuery元素对象
* @param {Object} cursor CodeMirror的光标对象可获取光标所在行和位置
* @param {String} selection 编辑器选中的文本
*/
"line-break": function (cm, icon, cursor, selection) {
// 如果当前没有选中的文本,将光标移到要输入的位置
if (selection === "") {
cm.setCursor(cursor.line, cursor.ch + 1)
}
cm.replaceSelection("<br/>");
}
},
toolbarCustomIcons: { toolbarCustomIcons: {
testIcon: "<a type=\"inline\" class=\"latex\" ><div class='zbg'></div></a>", testIcon: "<a type=\"inline\" class=\"latex\" ><div class='zbg'></div></a>",
testIcon1: "<a type=\"latex\" class=\"latex\" ><div class='zbg_latex'></div></a>", testIcon1: "<a type=\"latex\" class=\"latex\" ><div class='zbg_latex'></div></a>",
@ -152,14 +148,11 @@ function create_editorMD(id, width, high, placeholder, imageUrl, callback, initV
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp", "JPG", "JPEG", "GIF", "PNG", "BMP", "WEBP"], imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp", "JPG", "JPEG", "GIF", "PNG", "BMP", "WEBP"],
imageUploadURL: imageUrl,//url imageUploadURL: imageUrl,//url
onchange: onchange, onchange: onchange,
onload: function() { onload: function () {
let _id = this.id // 如果要使用this这里不能使用箭头函数 let _id = this.id // 如果要使用this这里不能使用箭头函数
let _editorName = this; let _editorName = this;
let react_id = `react_${_editorName.id}`; let react_id = `react_${_editorName.id}`;
const __that = window[react_id] const __that = window[react_id]
// this.previewing();
// let _id = id;
$("#" + _id + " [type=\"latex\"]").bind("click", function () { $("#" + _id + " [type=\"latex\"]").bind("click", function () {
_editorName.cm.replaceSelection("```latex"); _editorName.cm.replaceSelection("```latex");
_editorName.cm.replaceSelection("\n"); _editorName.cm.replaceSelection("\n");
@ -180,19 +173,12 @@ function create_editorMD(id, width, high, placeholder, imageUrl, callback, initV
if (__that.props.showNullButton) { if (__that.props.showNullButton) {
const NULL_CH = '▁' const NULL_CH = '▁'
// const NULL_CH = ''
// const NULL_CH = '🈳'
$("#" + _id + " [type=\"nullBtton\"]").bind("click", function () { $("#" + _id + " [type=\"nullBtton\"]").bind("click", function () {
_editorName.cm.replaceSelection(NULL_CH); _editorName.cm.replaceSelection(NULL_CH);
// var __Cursor = _editorName.cm.getDoc().getCursor();
// _editorName.cm.setCursor(__Cursor.line - 1, 0);
}); });
} }
if (noStorage == true) { if (!noStorage == true) {
} else {
md_elocalStorage(_editorName, `MDEditor__${_id}`, _id); md_elocalStorage(_editorName, `MDEditor__${_id}`, _id);
} }
@ -219,7 +205,7 @@ export default class TPMMDEditor extends Component {
// react_mdEditor_ // react_mdEditor_
componentDidMount = () => { componentDidMount = () => {
const { mdID, initValue, placeholder, showNullButton} = this.props; const { mdID, initValue, placeholder, showNullButton } = this.props;
let _id = `mdEditor_${mdID}` let _id = `mdEditor_${mdID}`
this.contentChanged = false; this.contentChanged = false;
@ -260,20 +246,20 @@ export default class TPMMDEditor extends Component {
__editorName.cm.on("change", (_cm, changeObj) => { __editorName.cm.on("change", (_cm, changeObj) => {
that.contentChanged = true; that.contentChanged = true;
if (that.state.showError) { if (that.state.showError) {
that.setState({showError: false}) that.setState({ showError: false })
} }
that.onEditorChange() that.onEditorChange()
}) })
that.props.onCMBlur && __editorName.cm.on('blur', () => { that.props.onCMBlur && __editorName.cm.on('blur', () => {
that.props.onCMBlur() that.props.onCMBlur()
}) })
that.props.onCMBeforeChange && __editorName.cm.on('beforeChange', (cm,change) => { that.props.onCMBeforeChange && __editorName.cm.on('beforeChange', (cm, change) => {
that.props.onCMBeforeChange(cm,change) that.props.onCMBeforeChange(cm, change)
}) })
that.answers_editormd = __editorName; that.answers_editormd = __editorName;
// 这里应该可以去掉了,方便调试加的 // 这里应该可以去掉了,方便调试加的
window[__editorName.id+'_'] = __editorName; window[__editorName.id + '_'] = __editorName;
}, initValue, this.onEditorChange,this.props.watch, { }, initValue, this.onEditorChange, this.props.watch, {
noStorage: this.props.noStorage, noStorage: this.props.noStorage,
showNullButton: this.props.showNullButton, showNullButton: this.props.showNullButton,
emoji: this.props.emoji emoji: this.props.emoji
@ -282,7 +268,7 @@ export default class TPMMDEditor extends Component {
} }
// 用在form里时validate失败时出现一个红色边框 // 用在form里时validate失败时出现一个红色边框
showError = () => { showError = () => {
this.setState({showError: true}) this.setState({ showError: true })
} }
onEditorChange = () => { onEditorChange = () => {
if (!this.answers_editormd) return; if (!this.answers_editormd) return;
@ -290,7 +276,7 @@ export default class TPMMDEditor extends Component {
//console.log('onEditorChange', this.props.id, val) //console.log('onEditorChange', this.props.id, val)
try { try {
this.props.onChange && this.props.onChange(val) this.props.onChange && this.props.onChange(val)
} catch(e) { } catch (e) {
// http://localhost:3007/courses/1309/common_homeworks/6566/setting // http://localhost:3007/courses/1309/common_homeworks/6566/setting
// 从这个页面,跳转到编辑页面,再在编辑页面点击返回的时候,这里会报错 // 从这个页面,跳转到编辑页面,再在编辑页面点击返回的时候,这里会报错
console.error('出错') console.error('出错')
@ -335,10 +321,10 @@ export default class TPMMDEditor extends Component {
} }
return ( return (
<React.Fragment> <React.Fragment>
<div className={`df ${className} ${imageExpand && 'editormd-image-click-expand' }`} > <div className={`df ${className} ${imageExpand && 'editormd-image-click-expand'}`} >
{/* padding10-20 */} {/* padding10-20 */}
<div className="edu-back-greyf5 radius4" id={`mdEditor_${mdID}`} style={{..._style}}> <div className="edu-back-greyf5 radius4" id={`mdEditor_${mdID}`} style={{ ..._style }}>
<textarea style={{display: 'none'}} id={`mdEditors_${mdID}`} name="content"></textarea> <textarea style={{ display: 'none' }} id={`mdEditors_${mdID}`} name="content"></textarea>
<div className="CodeMirror cm-s-defualt"> <div className="CodeMirror cm-s-defualt">
</div> </div>
</div> </div>

@ -206,9 +206,9 @@ render() {
return ( return (
<div className="edu-back-white" > <div className="edu-back-white" >
<div className="educontent"> <div className="educontent">
<div className="pt40 pb40"> <div className="pt_b_26">
<div className="clearfix mb30 shaiContent"> <div className="clearfix mb20 shaiContent">
<span className="shaiTitle fl mt3">方向</span> <span className="shaiTitle fl">方向</span>
<div className="fl pr shaiAllItem"> <div className="fl pr shaiAllItem">
<li className={shixunsearchAllvalue==="a"?"shaiItem shixun_repertoire active":"shaiItem shixun_repertoire"} value= "a" onClick={this.shixunsearchall}>全部</li> <li className={shixunsearchAllvalue==="a"?"shaiItem shixun_repertoire active":"shaiItem shixun_repertoire"} value= "a" onClick={this.shixunsearchall}>全部</li>
<style> <style>
@ -223,7 +223,7 @@ render() {
margin-right: 20px; margin-right: 20px;
color: #999; color: #999;
cursor: pointer; cursor: pointer;
margin-bottom: 10px; margin-bottom:10px;
} }
.ant-dropdown-menu-item, .ant-dropdown-menu-submenu-title{ .ant-dropdown-menu-item, .ant-dropdown-menu-submenu-title{
padding: 0px 12px; padding: 0px 12px;
@ -250,12 +250,23 @@ render() {
</div> </div>
</div> </div>
<div className="clearfix"> <div className="clearfix">
<span className="shaiTitle fl mt6">筛选</span> <span className="shaiTitle fl">筛选</span>
{ {
<style> <style>
{` {`
.shaiContent li.shaiItem{
padding:0px 15px;
line-height:32px;
height:32px
}
.shaiTitle{
height:32px;
line-height:32px;
}
.shaiItems{ .shaiItems{
padding: 3px 15px; padding:0px 15px;
line-height:32px;
height:32px;
float: left; float: left;
border-radius: 4px; border-radius: 4px;
color: #4C4C4C; color: #4C4C4C;

@ -10,7 +10,7 @@ import { Spin } from 'antd';
import { TPMIndexHOC } from '../TPMIndexHOC'; import { TPMIndexHOC } from '../TPMIndexHOC';
import { SnackbarHOC } from 'educoder'; import { SnackbarHOC,getImageUrl} from 'educoder';
import ShixunCardList from './ShixunCardList'; import ShixunCardList from './ShixunCardList';
@ -389,6 +389,35 @@ class ShixunsIndex extends Component {
{this.state.updata===undefined?"":<UpgradeModals {this.state.updata===undefined?"":<UpgradeModals
{...this.state} {...this.state}
/>} />}
<style>
{
` ::-webkit-scrollbar {
width: 0px !important;
}
.myshixin-head{
width: 100%;
height: 240px;
background-image: url(${getImageUrl(this.props.mygetHelmetapi && this.props.mygetHelmetapi.shixun_banner_url === null ?`images/educoder/courses/courses.jpg`:this.props.mygetHelmetapi&&this.props.mygetHelmetapi.shixun_banner_url)});
background-color: #081C4B;
background-position: center;
background-repeat: no-repeat;
}
`
}
</style>
{
this.props.mygetHelmetapi&&this.props.mygetHelmetapi.shixun_banner_url?
<div className="myshixin-head pr" >
<div className="edu-txt-center pathNavLine">
<div className="inline path-nav">
</div>
</div>
</div>
:
""
}
{/*<Spin spinning={typepvisible} size="large" style={{marginTop:'15%'}}>*/} {/*<Spin spinning={typepvisible} size="large" style={{marginTop:'15%'}}>*/}
<ShixunSearchBar <ShixunSearchBar
Updatasearchlist={this.Updatasearchlist.bind(this)} Updatasearchlist={this.Updatasearchlist.bind(this)}

@ -4,6 +4,9 @@
right: 3px; right: 3px;
top: 0px; top: 0px;
} }
.pt_b_26{
padding:26px 0px;
}
.diffSelect{ .diffSelect{
margin-left:20px !important; margin-left:20px !important;

@ -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…
Cancel
Save