You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

486 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

class Exercise < ApplicationRecord
# exercise_status : 1 未发布 2 已发布 3 已截止
# difficulty: 1 简单 2 适中 3 困难
belongs_to :course, counter_cache: true
belongs_to :exercise_bank, optional: true
belongs_to :user
has_many :exercise_users, -> { where("exercise_users.is_delete = 0") }, :dependent => :delete_all
has_many :score_exercise_users, -> { where("is_delete = 0 and commit_status != 0") }, class_name: "ExerciseUser"
has_many :exercise_questions, :dependent => :delete_all
has_many :exercise_answers, through: :exercise_questions
has_many :exercise_shixun_answers, through: :exercise_questions
has_many :exercise_group_settings, :dependent => :delete_all
has_many :published_settings, -> { exercise_group_published }, class_name: "ExerciseGroupSetting"
has_many :subjective_exercise_questions, -> {subjectives}, class_name: "ExerciseQuestion" # 主观题
has_many :subjective_exercise_answers, through: :subjective_exercise_questions, source: :exercise_answers # 主观题回答
# 随机组卷随机规则
has_many :exercise_random_settings
# 随机组卷赋分规则
has_many :exercise_score_settings
has_many :tidings, as: :container
has_many :course_acts, class_name: 'CourseActivity', as: :course_act, :dependent => :delete_all
belongs_to :examination_bank, optional: true
scope :is_exercise_published, -> { where("exercise_status > ? ",1)}
scope :unified_setting, -> { where("unified_setting = ?",true) }
scope :exercise_by_ids, lambda { |ids| where(id: ids) unless ids.blank? }
scope :exercise_by_status, lambda { |s| where(exercise_status: s) unless s.blank? }
scope :exercise_search, lambda { |keywords|
where("exercise_name LIKE ?", "%#{keywords}%") unless keywords.blank?}
validates :exercise_name, length: { maximum: 200, too_long: "不能超过200个字符" }
validates :exercise_description, length: { maximum: 20000, too_long: "不能超过20000个字符" }
after_create :create_exercise_list
# 试卷的问题类型
SINGLE = 0 #单选题
MULTIPLE = 1 #多选题
JUDGMENT = 2 #判断题
COMPLETION = 3 # 填空题
SUBJECTIVE = 4 # 主观题
PRACTICAL = 5 #实训题
PROGRAM = 6 #编程题
# 试卷的状态
UNPUBLISHED = 1 #未发布
PUBLISHED = 2 #已发布
DEADLINE = 3 #已截止
ENDED = 4 #课堂已结束
def subject_question_ids
exercise_questions.where(question_type: [4]).pluck(:id)
end
# 试卷总分
def question_scores
if is_random?
total = 0
grouped_exercise_random_settings = exercise_random_settings.group_by{|rs| rs.item_type}
exercise_score_settings.each do |ess|
total += ess.score * grouped_exercise_random_settings[ess.item_type].sum(&:quanlity)
end
total
else
exercise_questions.pluck(:question_score).sum
end
end
def subjective_score
exercise_questions.subjectives.pluck(:question_score).sum
end
def objective_score
exercise_questions.find_objective_questions.pluck(:question_score).sum
end
def create_exercise_list
str = ""
# TODO: 一次性为所有学生创建数据是否存在问题?
self.course.students.find_each do |student|
str += "," if str != ""
str += "(#{student.user_id}, #{self.id}, 0, '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}', '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}')"
end
if str != ""
sql = "insert into exercise_users (user_id, exercise_id, commit_status, created_at, updated_at) values" + str
ActiveRecord::Base.connection.execute sql
end
end
#获取学生视角的试卷用户
def get_stu_exercise_users
if unified_setting #试卷统一设置
exercise_users
else
ex_group_setting_ids = published_settings.pluck(:course_group_id)
exercise_users.joins("join course_members on exercise_users.user_id=course_members.user_id").
where(course_members: {course_group_id: ex_group_setting_ids})
end
end
def get_exercise_end_time(user_id)
if unified_setting
self&.end_time
else
user_course_group = course.students.find_by(user_id: user_id)&.course_group_id
exercise_group_settings.find_by(course_group_id:user_course_group)&.end_time
end
end
# 根据是否统一发布获取作业的作品列表
def all_works
ex_users = self.unified_setting ? exercise_users :
exercise_users.joins("join course_members on exercise_users.user_id=course_members.user_id").
where(course_members: {course_group_id: published_settings.pluck(:course_group_id)})
end
# 分班权限的老师可见的作品列表
def all_exercise_users(user_id)
# member = course.course_member(user_id)
teacher_course_groups = course.teacher_course_groups.where(user_id:user_id)
ex_users = self.all_works
# 有分班权限的统计管理的分班且已发布的学生情况
if teacher_course_groups.exists?
group_ids = teacher_course_groups.pluck(:course_group_id)
ex_users = ex_users.joins("join course_members on exercise_users.user_id=course_members.user_id").
where(course_members: {course_group_id: group_ids})
end
ex_users
end
# #统一设置,为当前老师有权限的分班学生,分班设置,也为当前老师有权限的分班的学生
# def all_exercise_users(user_id)
# ex_users = exercise_users
# group_ids = common_published_ids(user_id)
# if group_ids.present?
# ex_users = ex_users.joins("join course_members on exercise_users.user_id=course_members.user_id").
# where(course_members: {course_group_id: group_ids})
# end
# ex_users
# end
#当前用户已发布的班级id和试卷分组已发布的班级id的交集
def common_published_ids(user_id)
current_user_groups = course.teacher_course_ids(user_id)
if unified_setting
published_group_ids = current_user_groups
else
ex_group_setting = exercise_group_settings.select(:course_group_id).pluck("course_group_id").uniq
common_all_ids = ex_group_setting & current_user_groups #当前用户有权限的已发布的分班id #非统一设置时为当前用户有权限的且已发布分班的id
published_group_ids = common_all_ids.uniq
end
published_group_ids
end
#判断用户是否属于试卷分班的学生中
def check_user_in_course(user_id,user_identity)
ex_group_settings = exercise_group_settings.pluck(:course_group_id)
member = course.course_members.course_find_by_ids("user_id",user_id)
member_group_id = member.pluck(:course_group_id).uniq
((member_group_id & ex_group_settings).size > 0 || user_identity < Course::STUDENT) ? true : false
end
# 获取指定用户相对于该试卷的状态
# 如果课程已结束 -> 返回状态 4 Exercise::ENDED
# 如果指定用户是该试卷所属课程下的学生
# -> 如果发布时间已过且截止时间还未到 -> 已发布
# -> 如果结束时间存在且已过 -> 已截止 Exercise::ENDED
# -> 否则,未发布
# 否则 -> 取试卷的总状态
# 判断是否为分班,如果分班,试卷的截止时间为当前分班时间,否则为试卷的截止时间
def get_exercise_status(user)
if course.is_end
status = Exercise::ENDED
else
if user.present? && user.student_of_course?(course) #当为学生的时候,需根据分班来判断试卷状态
ex_time = get_exercise_times(user.id,false)
pb_time = ex_time[:publish_time]
ed_time = ex_time[:end_time]
# 发布时间已过,截止时间还未到
if pb_time.present? && ed_time.present? && pb_time <= Time.now && ed_time > Time.now
status = Exercise::PUBLISHED
# 截止时间已过
elsif ed_time.present? && ed_time <= Time.now
status = Exercise::DEADLINE
else
status = Exercise::UNPUBLISHED
end
else
status = exercise_status #当为老师的时候,则为试卷的总状态
end
end
status
end
#获取试卷的发布时间和截止时间。teacher 为boolean,当为true时表示的是当前为老师
def get_exercise_times(user_id,teacher)
if unified_setting || teacher #当试卷为统一设置或当前为老师的时候
pb_time = publish_time
en_time = end_time
# if (exercise_status != 3) && en_time.present? && (en_time <= Time.now)
# update_column("exercise_status",3)
# end
else
# ex_group_setting = exercise_group_settings
user_group = course.students.where(user_id:user_id).select(:course_group_id)
if user_group.exists?
user_group_id = user_group.first&.course_group_id
user_ex_group_setting = exercise_group_settings.where(course_group_id:user_group_id).select(:publish_time,:end_time)
pb_time = user_ex_group_setting.first&.publish_time
en_time = user_ex_group_setting.first&.end_time
else
pb_time = nil
en_time = nil
end
end
{
"publish_time":pb_time,
"end_time":en_time
}
end
#判断当前用户的答题状态
def check_user_answer_status(user)
ex_answer_user = exercise_users.where(user_id: user.id).select(:start_at,:end_at,:commit_status)
user_ex_status = get_exercise_status(user)
user_status = 2
if ex_answer_user.present? && (ex_answer_user.first&.start_at.present? || ex_answer_user.first&.end_at.present?) #学生有过答题的,或者立即截止,但学生未做试卷的
user_status = ExerciseUser.commit_statuses[ex_answer_user.first.commit_status]
if user_status == 2
user_status = 1
end
end
if ex_answer_user.present? && ex_answer_user.first&.start_at.blank? && user_ex_status == 3
user_status = 4
end
user_status
end
# 单独为一个试卷用户创建随机组卷的试题关联(用于在试卷发布后再加入课堂并开始答题的学生)
def create_user_question_list exercise_user
ActiveRecord::Base.transaction do
exercise_user.exercise_user_questions.where(exercise_user_id: exercise_user.id).destroy_all
user_questions = {}
# 根据试题来源先筛选试题,1私有题库,0公共加私有题库
if source == 1
item_banks = ItemBank.where(user_id: user_id)
hacks = Hack.where(status: 1)
elsif source == 0
item_banks = ItemBank.where(user_id: user_id).or(ItemBank.where(public: 1))
hacks = Hack.where(status: 1)
end
item_banks = item_banks.where(container_id: hacks.pluck(:id), container_type: "Hack")
.or(ItemBank.where(id: item_banks.pluck(:id)).where("item_type != '6'"))
# 按照随机规则的配置,将试题分好组
grouped_item_banks = {}
# 集中存储将要复制的题目的id
item_bank_ids = []
# 赋分设置,各个题型设置的分数组成的hash
grouped_score_settings = exercise_score_settings.select(:item_type, :score).group_by{|setting| setting.item_type}
# 用于存储exercise_question与examination_items的对应关系
exercise_question_item_bank_hash = {}
exercise_random_settings.each do |exercise_random_setting|
# 是否正确筛选了?
grouped_item_banks[exercise_random_setting.id] = item_banks.select do |examination_item|
examination_item.difficulty == exercise_random_setting.difficulty &&
examination_item.sub_discipline_id == exercise_random_setting.sub_discipline_id &&
examination_item.item_type == exercise_random_setting.item_type
end
raise Educoder::TipException.new("题库数量小于随机规则配置的数量!") if grouped_item_banks[exercise_random_setting.id].size < exercise_random_setting.quanlity
end
# 存储使用过的item_bank,避免一个用户抽到重复的
used_item_bank_ids = []
exercise_random_settings.each do |exercise_random_setting|
qualified_item_banks = grouped_item_banks[exercise_random_setting.id].select{|i| !i.id.in?(used_item_bank_ids)}
sampled_item_banks = qualified_item_banks.sample(exercise_random_setting.quanlity)
used_item_bank_ids + sampled_item_banks.map(&:id)
# 是否有题库
if user_questions["#{exercise_user.id}-#{exercise_user.user_id}"].is_a? Array
user_questions["#{exercise_user.id}-#{exercise_user.user_id}"] += sampled_item_banks
else
user_questions["#{exercise_user.id}-#{exercise_user.user_id}"] = sampled_item_banks
end
# 将符合规则试题id的集合放入数组中
item_bank_ids+= sampled_item_banks.map(&:id)
end
# 试题复制-----根据所有符合规则的试题来统一复制试题到试卷中
item_banks = ItemBank.where(id: item_bank_ids)
item_banks.each_with_index do |item_bank, index|
exercise_question = self.exercise_questions.new(
question_type: ItemBank.item_types[item_bank.item_type],
question_title: item_bank.name,
question_number: index,
question_score: grouped_score_settings[item_bank.item_type].first.score
)
if item_bank.PROGRAM?
new_hack = item_bank.container.fork
new_hack.update!(status: 1)
exercise_question.hack_id = new_hack.id
exercise_question.save!
else
exercise_question.save!
if item_bank.item_type.in?(%W(COMPLETION SUBJECTIVE))
item_bank.item_bank_standard_answers.each do |standard_answer|
exercise_question.exercise_standard_answers << ExerciseStandardAnswer.new(
answer_text: standard_answer.answer_text, exercise_choice_id: standard_answer.position
)
end
end
ExerciseQuestionAnalysis.new(analysis: item_bank.analysis, exercise_question_id: exercise_question.id)
item_bank.item_choices.each_with_index do |choice, position|
exercise_question.exercise_choices << ExerciseChoice.new(choice_text: choice.choice_text, choice_position: position+1)
if choice.is_answer
exercise_question.exercise_standard_answers << ExerciseStandardAnswer.new(exercise_choice_id: position+1)
end
end
end
exercise_question_item_bank_hash[item_bank.id] = exercise_question.id
end
attrs = []
# 试卷用户关联-----试题复制完成后,开始将试卷用户和试题进行关联
user_questions.each do |exercise_id_user_id, item_banks|
item_banks.each do |item_bank|
attrs.push(
{
exercise_id: self.id,
exercise_user_id: exercise_id_user_id.split("-")[0],
user_id: exercise_id_user_id.split("-")[1],
exercise_question_id: exercise_question_item_bank_hash[item_bank.id]
}
)
end
end
ExerciseUserQuestion.bulk_insert values: attrs
end
end
def update_user_question_list ex_user
exercise_random_settings.each do |setting|
question_count = ex_user.exercise_questions.where(question_type: ExerciseRandomSetting.item_types[setting.item_type], sub_discipline_id: setting.sub_discipline_id).count
if question_count < setting.quanlity
absence_count = setting.num - question_count
exercise_questions.where.not(id: ex_user.exercise_question_ids)
.where(question_type: setting.question_type, sub_discipline_id: setting.sub_discipline_id).sample(absence_count).each do |question|
ExerciseUserQuestion.create!(exercise: self, exercise_user_id: ex_user.id, user_id: ex_user.user_id, exercise_question_id: question.id)
end
elsif question_count > setting.quanlity
over_count = question_count - setting.num
ex_user.exercise_user_questions.last(over_count).each do |user_question|
user_question.delete
end
end
end
end
# 根据随机规则为试卷创建题目,没发布一次都按照配置的随机规则从最新的题库中取出题目
def create_exercise_questions_by_random_settings
raise Educoder::TipException.new("该试卷不为随机试卷") unless is_random?
user_questions = {}
# 根据试题来源先筛选试题,1私有题库,0公共加私有题库
if source == 1
item_banks = ItemBank.where(user_id: user_id)
hacks = Hack.where(status: 1)
elsif source == 0
item_banks = ItemBank.where(user_id: user_id).or(ItemBank.where(public: 1))
hacks = Hack.where(status: 1)
end
item_banks = item_banks.where(container_id: hacks.pluck(:id), container_type: "Hack")
.or(ItemBank.where(id: item_banks.pluck(:id)).where("item_type != '6'"))
# 按照随机规则的配置,将试题分好组
grouped_item_banks = {}
# 集中存储将要复制的题目的id
item_bank_ids = []
# 赋分设置,各个题型设置的分数组成的hash
grouped_score_settings = exercise_score_settings.select(:item_type, :score).group_by{|setting| setting.item_type}
# 用于存储exercise_question与examination_items的对应关系
exercise_question_item_bank_hash = {}
exercise_random_settings.each do |exercise_random_setting|
# 是否正确筛选了?
grouped_item_banks[exercise_random_setting.id] = item_banks.select do |examination_item|
examination_item.difficulty == exercise_random_setting.difficulty &&
examination_item.sub_discipline_id == exercise_random_setting.sub_discipline_id &&
examination_item.item_type == exercise_random_setting.item_type
end
raise Educoder::TipException.new("题库数量小于随机规则配置的数量!") if grouped_item_banks[exercise_random_setting.id].size < exercise_random_setting.quanlity
end
# 按照随机规则给每位学生分配指定数量的试题,用于之后创建学生试题
# 注意,只需要给已发布的分班发送即可或者是统一发布的试卷才给所有学生发送试题
if unified_setting?
published_exercise_users = exercise_users
else
published_exercise_users = exercise_users.joins("join course_members on exercise_users.user_id=course_members.user_id").
where(course_members: {course_group_id: published_settings.pluck(:course_group_id)})
end
published_exercise_users.each do |exercise_user|
# 存储使用过的item_bank,避免一个用户抽到重复的
used_item_bank_ids = []
exercise_random_settings.each do |exercise_random_setting|
qualified_item_banks = grouped_item_banks[exercise_random_setting.id].select{|i| !i.id.in?(used_item_bank_ids)}
sampled_item_banks = qualified_item_banks.sample(exercise_random_setting.quanlity)
used_item_bank_ids + sampled_item_banks.map(&:id)
# 是否有题库
if user_questions["#{exercise_user.id}-#{exercise_user.user_id}"].is_a? Array
user_questions["#{exercise_user.id}-#{exercise_user.user_id}"] += sampled_item_banks
else
user_questions["#{exercise_user.id}-#{exercise_user.user_id}"] = sampled_item_banks
end
# 将符合规则试题id的集合放入数组中
item_bank_ids+= sampled_item_banks.map(&:id)
end
end
analysis_attrs = []
# 试题复制-----根据所有符合规则的试题来统一复制试题到试卷中
item_banks = ItemBank.where(id: item_bank_ids)
item_banks.each_with_index do |item_bank, index|
exercise_choice_attrs = []
exercise_standard_answer_attrs = []
exercise_question = self.exercise_questions.new(
question_type: ItemBank.item_types[item_bank.item_type],
question_title: item_bank.name,
question_number: index,
question_score: grouped_score_settings[item_bank.item_type].first.score,
is_ordered: item_bank.is_ordered
)
if item_bank.PROGRAM?
new_hack = item_bank.container.fork
new_hack.update!(status: 1)
exercise_question.hack_id = new_hack.id
exercise_question.save!
else
exercise_question.save!
if item_bank.item_type.in?(%W(COMPLETION SUBJECTIVE))
ExerciseStandardAnswer.bulk_insert(*%i[answer_text exercise_choice_id exercise_question_id created_at updated_at]) do |worker|
item_bank.item_bank_standard_answers.each do |standard_answer|
worker.add(answer_text: standard_answer.answer_text, exercise_choice_id: standard_answer.position, exercise_question_id: exercise_question.id)
end
end
end
analysis_attrs.push({analysis: item_bank.analysis, exercise_question_id: exercise_question.id})
item_bank.item_choices.each_with_index do |choice, position|
exercise_choice_attrs.push({
choice_text: choice.choice_text, choice_position: position+1, exercise_question_id: exercise_question.id
})
if choice.is_answer
exercise_standard_answer_attrs.push({ exercise_choice_id: position+1, exercise_question_id: exercise_question.id })
end
end
ExerciseChoice.bulk_insert values: exercise_choice_attrs
ExerciseStandardAnswer.bulk_insert values: exercise_standard_answer_attrs
end
exercise_question_item_bank_hash[item_bank.id] = exercise_question.id
end
# 试题分析批量插入
ExerciseQuestionAnalysis.bulk_insert values: analysis_attrs
attrs = []
# 试卷用户关联-----试题复制完成后,开始将试卷用户和试题进行关联
user_questions.each do |exercise_id_user_id, item_banks|
item_banks.each do |item_bank|
attrs.push(
{
exercise_id: self.id,
exercise_user_id: exercise_id_user_id.split("-")[0],
user_id: exercise_id_user_id.split("-")[1],
exercise_question_id: exercise_question_item_bank_hash[item_bank.id]
}
)
end
end
ExerciseUserQuestion.bulk_insert values: attrs
end
end