class ShixunsController < ApplicationController include ShixunsHelper include ApplicationHelper include ElasticsearchAble include CoursesHelper include GitCommon before_action :require_login, :check_auth, except: [:download_file, :index, :menus, :show, :show_right, :ranking_list, :discusses, :collaborators, :fork_list, :propaedeutics] before_action :check_account, only: [:new, :create, :shixun_exec, :jupyter_exec] before_action :find_shixun, except: [:index, :new, :create, :menus, :get_recommend_shixuns, :propaedeutics, :departments, :apply_shixun_mirror, :get_mirror_script, :download_file, :shixun_list, :batch_send_to_course] before_action :shixun_access_allowed, except: [:index, :new, :create, :menus, :get_recommend_shixuns, :propaedeutics, :departments, :apply_shixun_mirror, :jupyter_exec, :get_mirror_script, :download_file, :shixun_list, :batch_send_to_course] before_action :find_repo_name, only: [:repository, :commits, :file_content, :update_file, :shixun_exec, :copy, :add_file, :jupyter_exec, :upload_git_file, :delete_git_file, :upload_git_folder] before_action :allowed, only: [:update, :close, :update_propaedeutics, :settings, :publish, :apply_public, :upload_git_folder, :shixun_members_added, :change_manager, :collaborators_delete, :upload_git_file, :cancel_apply_public, :cancel_publish, :add_collaborators, :add_file, :delete_git_file] before_action :portion_allowed, only: [:copy] before_action :special_allowed, only: [:send_to_course, :search_user_courses] before_action :shixun_marker, only: [:new, :create] #before_action :validate_wachat_support, only: [:shixun_exec] skip_before_action :check_sign, only: [:download_file] ## 获取课程列表 def index @shixuns = current_laboratory.shixuns.unhidden.publiced ## 方向 if params[:tag_level].present? && params[:tag_id].present? @shixuns = @shixuns.filter_tag(params[:tag_level].to_i, params[:tag_id].to_i) case params[:tag_level].to_i when 1 #大类 @search_tags = Repertoire.find(params[:tag_id].to_i).name when 2 #子类 @search_tags = SubRepertoire.find(params[:tag_id].to_i).name when 3 #tag tag = TagRepertoire.find(params[:tag_id].to_i) @search_tags = "#{tag.sub_repertoire.name} / #{tag.name}" end end ## 搜索关键字 匹配关卡名, 用户名, 实训名 和 空格多搜索 if params[:keyword].present? keyword = params[:keyword].strip @shixuns = @shixuns.joins(:user, challenges: :challenge_tags). where("challenge_tags.name like :keyword or challenges.subject like :keyword or concat(lastname, firstname) like :keyword or shixuns.name like :name", keyword: "%#{keyword}%", name: "%#{keyword.split(" ").join("%")}%").distinct end ## 筛选 状态 if params[:status].present? && params[:status].to_i != 0 params[:status] = [0, 1] if params[:status].to_i == 1 @shixuns = @shixuns.where(status: params[:status]) end ## 筛选 难度 if params[:diff].present? && params[:diff].to_i != 0 @shixuns = @shixuns.where(trainee: params[:diff]) end ## 排序参数 bsort = (params[:sort] == "desc" ? "desc" : "asc") case params[:order_by] || 'new' when 'hot' @shixuns = @shixuns.order("shixuns.public = 2 desc, shixuns.myshixuns_count #{bsort}") else @shixuns = @shixuns.order("shixuns.public = 2 desc, shixuns.publish_time #{bsort}") end # 用id计数会快10+MS左右,对于搜索的内容随着数据的增加,性能会提升一些。 @total_count = @shixuns.count("shixuns.id") ## 分页参数 page = params[:page] || 1 limit = params[:limit] || 16 @shixuns = @shixuns.includes(:tag_repertoires, :challenges).page(page).per(limit) #@tag_name_map = TagRepertoire.joins(:shixun_tag_repertoires) # .where(shixun_tag_repertoires: { shixun_id: @shixuns.map(&:id) }) # .group('shixun_tag_repertoires.shixun_id') # .select('shixun_id, tag_repertoires.name') # .each_with_object({}) { |r, obj| obj[r.shixun_id] = r.name } end def shixun_list # 全部实训/我的实训 type = params[:type] || "all" # 状态:已发布/未发布 status = params[:status] || "all" # 超级管理员用户显示所有未隐藏的实训、非管理员显示所有已发布的实训(对本单位公开且未隐藏未关闭) if type == "mine" @shixuns = current_user.shixuns.none_closed else if current_user.admin? @shixuns = Shixun.none_closed.where(hidden: 0) else none_shixun_ids = ShixunSchool.where("school_id != #{current_user.school_id}").pluck(:shixun_id) @shixuns = Shixun.where.not(id: none_shixun_ids).none_closed.where(hidden: 0) end end unless status == "all" @shixuns = status == "published" ? @shixuns.where(status: 2) : @shixuns.where(status: [0, 1]) end ## 筛选 难度 if params[:diff].present? && params[:diff].to_i != 0 @shixuns = @shixuns.where(trainee: params[:diff]) end page = params[:page] || 1 limit = params[:limit] || 10 offset = (page.to_i - 1) * (limit.to_i) order = params[:order] || "desc" ## 搜索关键字创建者、实训名称、院校名称 keyword = params[:keyword].to_s.strip.presence || '*' model_options = { index_name: [Shixun], model_includes: Shixun.searchable_includes } model_options.merge(where: { id: @shixuns.pluck(:id) }).merge(order: {"myshixuns_count" => order}).merge(limit: limit, offset: offset) model_options.merge(default_options) @shixuns = Searchkick.search(keyword, model_options) # @shixuns = Shixun.search keyword, where: {id: @shixuns.pluck(:id)}, order: {"myshixuns_count" => order}, limit: limit, offset: offset @total_count = @shixuns.total_count end ## 获取顶部菜单 def menus @repertoires = current_laboratory.shixun_repertoires end ## 实训详情 def show # 当前用户开启的实训 can_fork = current_user.is_certification_teacher || current_user.admin_or_business? unless can_fork @can_fork = {can_fork: "已经职业认证的教师才能fork实训", certi_url: "/account/certification"} end @current_myshixun = @shixun.current_myshixun(current_user.id) if @shixun.fork_from fork_shixun = Shixun.select(:id, :user_id, :name, :identifier).where(id: @shixun.fork_from).first @fork_from = {name: fork_shixun.name, username: fork_shixun.owner.try(:full_name), fork_identifier: fork_shixun.identifier} if fork_shixun end @power = current_user.manager_of_shixun?(@shixun) # 更新是为了首页的排序,myshixun的动静需要在实训中展现出来 if @current_myshixun && params[:exit] && !current_user.admin? @current_myshixun.update_column(:updated_at, Time.now) end end def show_right owner = @shixun.owner @user_own_shixuns = owner.shixuns.published.count @user_tags = @shixun.user_tags_name(current_user) @shixun_tags = @shixun.challenge_tags_name @myshixun = @shixun.myshixuns.find_by(user_id: current_user.id) end # 排行榜 def ranking_list if @shixun.status == 2 sql = " select m.user_id, u.login, u.lastname, m.updated_at, (select sum(cost_time) from games g where g.myshixun_id = m.id) as time, (select sum(final_score) from games g where g.myshixun_id = m.id) as score from (users u left join myshixuns m on m.user_id = u.id) where u.is_test =0 and m.shixun_id = #{@shixun.id} and m.status = 1 order by score desc, time asc limit 10 " @myshixuns = Myshixun.find_by_sql(sql) end end #评论 def discusses new_params = params.merge(container_id: @shixun.id, container_type: 'Shixun') discusses = ShixunsService.new.shixun_discuss new_params, current_user if discusses.present? @children_list = discusses[:children_list] else @children_list = [] end end def copy ActiveRecord::Base.transaction do begin @new_shixun = Shixun.new @new_shixun.attributes = @shixun.attributes.dup.except("id","user_id","visits","gpid","status", "identifier", "averge_star", "homepage_show","repo_name", "myshixuns_count", "challenges_count", "can_copy", "created_at", "updated_at", "public") @new_shixun.user_id = User.current.id @new_shixun.averge_star = 5 @new_shixun.identifier = generate_identifier Shixun, 8 @new_shixun.fork_from = @shixun.id @new_shixun.save! # 同步shixun_info的信息 if @shixun.shixun_info.present? ShixunInfo.create!(shixun_id: @new_shixun.id, description: @shixun.description, evaluate_script: @shixun.evaluate_script, fork_reason: params[:reason].to_s.strip) end # 同步私密版本库 if @shixun.shixun_secret_repository # 源仓库的的私密版本库地址 repo_name = @shixun.shixun_secret_repository.repo_name # 新生成的地址 fork_repository_name = "#{current_user.login}/secret_#{@new_shixun.identifier}" ShixunSecretRepository.create!(shixun_id: @new_shixun.id, repo_name: "#{fork_repository_name}", secret_dir_path: @shixun.shixun_secret_repository.secret_dir_path) GitService.fork_repository(repo_path: "#{repo_name}.git", fork_repository_path: (fork_repository_name + ".git")) end # 同步镜像 if @shixun.mirror_repositories.present? @shixun.mirror_repositories.each do |mirror| ShixunMirrorRepository.create!(:shixun_id => @new_shixun.id, :mirror_repository_id => mirror.id) end end # 同步技术标签 @shixun.shixun_tag_repertoires.each do |str| ShixunTagRepertoire.create!(:tag_repertoire_id => str.tag_repertoire_id, :shixun_id => @new_shixun.id) end # 同步配置 logger.info("########-shixun_service_configs_count: #{@shixun.shixun_service_configs.pluck(:id, :shixun_id)}") @shixun.shixun_service_configs.each do |config| ShixunServiceConfig.create!(:shixun_id => @new_shixun.id, :cpu_limit => config.cpu_limit, :lower_cpu_limit => config.lower_cpu_limit, :memory_limit => config.memory_limit, :request_limit => config.request_limit, :mirror_repository_id => config.mirror_repository_id) end # 同步高校限制 @shixun.shixun_schools.each do |school| ShixunSchool.create!(shixun_id: @new_shixun.id, school_id: school.school_id) end # fork版本库 logger.info("###########fork_repo_path: ######{@repo_path}") project_fork(@new_shixun, @repo_path, current_user.login) ShixunMember.create!(:user_id => User.current.id, :shixun_id => @new_shixun.try(:id), :role => 1) # 如果是jupyter,先创建一个目录,为了挂载(因为后续数据集,开启Pod后环境在没销毁前,你上传数据集是挂载不上目录的,因此要先创建目录,方便中间层挂载) if @new_shixun.is_jupyter? folder = EduSetting.get('shixun_folder') raise "存储目录未定义" unless folder.present? path = "#{folder}/#{@new_shixun.identifier}" FileUtils.mkdir_p(path, :mode => 0777) unless File.directory?(path) # 复制数据集 save_path = File.join(folder, @shixun.identifier) @shixun.data_sets.each do |set| new_date_set = Attachment.new new_date_set.attributes = set.attributes.dup.except("id", "container_id", "disk_directory") new_date_set.container_id = @new_shixun.id new_date_set.disk_directory = @new_shixun.identifier new_date_set.save! FileUtils.cp("#{save_path}/#{set.relative_path_filename}", path) end end # 同步复制关卡 if @shixun.challenges.present? @shixun.challenges.each do |challenge| new_challenge = Challenge.new new_challenge.attributes = challenge.attributes.dup.except("id","shixun_id","user_id", "challenge_tags_count") new_challenge.user_id = User.current.id new_challenge.shixun_id = @new_shixun.id new_challenge.save!(validate: false) # 同步参考答案 challenge.challenge_answers.each do |answer| new_answer = ChallengeAnswer.new new_answer.attributes = answer.attributes.dup.except("id","challenge_id") new_answer.challenge_id = new_challenge.id new_answer.save!(validate: false) end if challenge.st == 0 # 评测题 # 同步测试集 if challenge.test_sets.present? challenge.test_sets.each do |test_set| new_test_set = TestSet.new new_test_set.attributes = test_set.attributes.dup.except("id","challenge_id") new_test_set.challenge_id = new_challenge.id new_test_set.save!(validate: false) end end # 同步关卡标签 challenge_tags = ChallengeTag.where("challenge_id =? and challenge_choose_id is null", challenge.id) if challenge_tags.present? challenge_tags.each do |challenge_tag| ChallengeTag.create!(:challenge_id => new_challenge.id, :name => challenge_tag.try(:name)) end end elsif challenge.st == 1 # 选择题 if challenge.challenge_chooses.present? challenge.challenge_chooses.each do |challenge_choose| new_challenge_choose = ChallengeChoose.new new_challenge_choose.attributes = challenge_choose.attributes.dup.except("id","challenge_id") new_challenge_choose.challenge_id = new_challenge.id new_challenge_choose.save! # 每一题的选项 if challenge_choose.challenge_questions.present? challenge_choose.challenge_questions.each do |challenge_question| new_challenge_question = ChallengeQuestion.new new_challenge_question.attributes = challenge_question.attributes.dup.except("id","challenge_choose_id") new_challenge_question.challenge_choose_id = new_challenge_choose.id new_challenge_question.save! end end # 每一题的知识标签 st_challenge_tags = ChallengeTag.where(:challenge_id => challenge.id, :challenge_choose_id => challenge_choose.id) if st_challenge_tags.present? st_challenge_tags.each do |st_challenge_tag| ChallengeTag.create!(:challenge_id => new_challenge.id, :name => st_challenge_tag.try(:name), :challenge_choose_id => new_challenge_choose.id) end end end end end end end # 将实训标志为该云上实验室建立 Laboratory.current.laboratory_shixuns.create!(shixun: @shixun, ownership: true) rescue Exception => e uid_logger_error("copy shixun failed ##{e.message}") # 删除版本库 # 删除私密版本库 GitService.delete_repository(repo_path: "#{fork_repository_name}.git") if @new_shixun.shixun_secret_repository&.repo_name GitService.delete_repository(repo_path: @new_shixun.repo_path) if @new_shixun.repo_path tip_exception("实训Fork失败") raise ActiveRecord::Rollback end end end #合作者 def collaborators @user = current_user ## 分页参数 page = params[:page] || 1 limit = params[:limit] || 10 @member_count = @shixun.shixun_members.count @members = @shixun.shixun_members.order("role = 1 desc, created_at asc").includes(:user).page(page).per(limit) end def fork_list @shixuns = Shixun.where(:fork_from => @shixun.id) @shixuns_count = @shixuns.count ## 分页参数 page = params[:page] || 1 limit = params[:limit] || 20 @shixuns = @shixuns.page(page).per(limit) end def new @introduction_sample = PlatformSample.where(samples_type: ['introduction', 'knowledge']).pluck([:samples_type, :contents]) @main_type = shixun_main_type @small_type = shixun_small_type end def create @shixun = CreateShixunService.call(current_user, shixun_params, params) end # 保存jupyter到版本库 def update_jupyter jupyter_save_with_shixun(@shixun, params[:jupyter_port]) end def update # 镜像方面 mirror_ids = MirrorRepository.where(id: params[:main_type]) .or( MirrorRepository.where(id: params[:sub_type])).pluck(:id).uniq old_mirror_ids = @shixun.shixun_mirror_repositories .where(mirror_repository_id: params[:main_type]) .or(@shixun.shixun_mirror_repositories.where(mirror_repository_id: params[:sub_type])) .pluck(:mirror_repository_id).uniq new_mirror_id = (mirror_ids - old_mirror_ids).map{|id| {mirror_repository_id: id}} # 转换成数组hash方便操作 logger.info("##########new_mirror_id: #{new_mirror_id}") logger.info("##########old_mirror_ids: #{old_mirror_ids}") logger.info("##########mirror_ids: #{mirror_ids}") # 服务配置方面 service_create_params = service_config_params[:shixun_service_configs] .select{|config| !old_mirror_ids.include?(config[:mirror_repository_id]) && MirrorRepository.find(config[:mirror_repository_id]).name.present?} service_update_params = service_config_params[:shixun_service_configs] .select{|config| old_mirror_ids.include?(config[:mirror_repository_id])} logger.info("#########service_create_params: #{service_create_params}") logger.info("#########service_update_params: #{service_update_params}") begin ActiveRecord::Base.transaction do @shixun.update_attributes!(shixun_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.create!(new_mirror_id) if new_mirror_id.present? # 镜像变动要更换服务配置 @shixun.shixun_service_configs.where.not(mirror_repository_id: old_mirror_ids).destroy_all @shixun.shixun_service_configs.create!(service_create_params) if service_create_params.present? service_update_params&.map do |service| smr = @shixun.shixun_service_configs.find_by(mirror_repository_id: service[:mirror_repository_id]) logger.info("########smr: #{smr}") smr.update_attributes(service) if smr.present? end # 添加第二仓库(管理员权限) if params[:is_secret_repository] add_secret_repository if @shixun.shixun_secret_repository.blank? else # 如果有仓库,就要删 if @shixun.shixun_secret_repository&.repo_name @shixun.shixun_secret_repository.lock! GitService.delete_repository(repo_path: @shixun.shixun_secret_repository.repo_path) @shixun.shixun_secret_repository.destroy end end end rescue => e uid_logger_error(e.message) tip_exception("基本信息更新失败:#{e.message}") end end # 实训权限设置 def update_permission_setting # 查找需要增删的高校id school_id = School.where(:name => params[:scope_partment]).pluck(:id) old_school_ids = @shixun.shixun_schools.pluck(:school_id) school_params = (school_id - old_school_ids).map{|id| {school_id: id}} begin ActiveRecord::Base.transaction do @shixun.update_attributes!(shixun_params) @shixun.shixun_schools.where.not(school_id: school_id).destroy_all if school_id.present? @shixun.shixun_schools.create!(school_params) end rescue => e uid_logger_error("实训权限设置失败--------#{e.message}") tip_exception("实训权限设置失败") end end # 实训学习页面设置 def update_learn_setting begin ActiveRecord::Base.transaction do update_params = if params[:shixun][:vnc] shixun_params.merge(vnc_evaluate: 1) else shixun_params.merge(vnc_evaluate: 0) end @shixun.update_attributes!(update_params) end rescue => e uid_logger_error("实训学习页面设置失败--------#{e.message}") tip_exception("实训学习页面设置失败") end end # Jupyter数据集 def get_data_sets page = params[:page] || 1 limit = params[:limit] || 10 data_sets = @shixun.data_sets @data_count = data_sets.count @data_sets= data_sets.order("created_on desc").page(page).per(limit) @absolute_folder = edu_setting('shixun_folder') end # 实训测试集附件 def upload_data_sets begin upload_file = params["file"] raise "未上传文件" unless upload_file folder = edu_setting('shixun_folder') raise "存储目录未定义" unless folder.present? rep_name = @shixun.data_sets.pluck(:filename).include?(upload_file.original_filename) raise "文件名已经存在\"#{upload_file.original_filename}\", 请删除后再上传" if rep_name tpm_folder = params[:identifier] # 这个是实训的identifier save_path = File.join(folder, tpm_folder) ext = file_ext(upload_file.original_filename) local_path, digest = file_save_to_local(save_path, upload_file.tempfile, ext) content_type = upload_file.content_type.presence || 'application/octet-stream' disk_filename = local_path[save_path.size + 1, local_path.size] @attachment = Attachment.where(disk_filename: disk_filename, author_id: current_user.id).first if @attachment.blank? @attachment = Attachment.new @attachment.filename = upload_file.original_filename @attachment.disk_filename = local_path[save_path.size + 1, local_path.size] @attachment.filesize = upload_file.tempfile.size @attachment.content_type = content_type @attachment.digest = digest @attachment.author_id = current_user.id @attachment.disk_directory = tpm_folder @attachment.container_id = @shixun.id @attachment.container_type = @shixun.class.name @attachment.attachtype = 2 @attachment.save! else logger.info "文件已存在,id = #{@attachment.id}, filename = #{@attachment.filename}" end render_ok rescue => e uid_logger_error(e.message) tip_exception(e.message) end end # 多文件删除 def destroy_data_sets files = Attachment.where(id: params[:id]) shixun_folder= edu_setting("shixun_folder") begin files.each do |file| file_path = "#{shixun_folder}/#{file.relative_path_filename}" delete_file(file_path) end files.destroy_all render_ok rescue => e uid_logger_error(e.message) tip_exception(e.message) end end def apply_shixun_mirror form_params = params.permit(*%i[language runtime run_method attachment_id]) form = ApplyShixunMirrorForm.new(form_params) form.validate! tiding = Tiding.new( user_id: 1, trigger_user_id: current_user.id, container_type: 'SendMessage', viewed: 0, tiding_type: 'Apply', extra: form.to_json ) ActiveRecord::Base.transaction do # TODO: 由于tiding是多态,而SendMessage却没有对应的model,因此,如果不跳过验证会报 container must exist的错. tiding.save(validate: false) form.attachment.update!(container: tiding) end sucess_status rescue ActiveModel::ValidationError => ex tip_exception(ex.message) rescue Exception => e uid_logger_error(e.message) tip_exception("申请失败") end # 永久关闭实训 def close @shixun.update_attributes(status: 3, closer_id: current_user.id, end_time: Time.now) sucess_status end def propaedeutics @content = Shixun.find_by_identifier!(params[:identifier]).propaedeutics end # 更新背景知识 def update_propaedeutics @shixun.shixun_info.update_column(:propaedeutics, params[:content]) end # 获取推荐实训接口 2个热门实训 + 2个最新实训 def get_recommend_shixuns hot_shixuns = Shixun.field_for_recommend.published.order("myshixuns_count desc").limit(2) newest_shixuns = Shixun.field_for_recommend.published.order("created_at desc").limit(2) @recommend_shixuns = hot_shixuns + newest_shixuns end def settings @choice_main_type = @shixun.main_mirror_id @choice_small_type = @shixun.small_mirror_id @main_type = shixun_main_type @small_type = shixun_small_type @configs = @shixun.shixun_service_configs #@mirror_script = MirrorScript.select([:id, :script_type]).find(@shixun.mirror_script_id).attributes if @shixun.mirror_script_id && @shixun.mirror_script_id != 0 # @shixun_main_mirror = @shixun.show_shixun_mirror # @script_type = @shixun.script_tag.try(:script_type) || "无" # @evaluate_scirpt = @shixun.evaluate_script || "无" end # 获取脚本内容 def get_script_contents mirrir_script = MirrorScript.find(params[:script_id]) script = mirrir_script.try(:script) @description = mirrir_script.try(:description) @script = modify_shixun_script @shixun, script end def get_custom_script shixun_script = PlatformSample.where(:samples_type => "script").first.try(:contents) compile = params[:compile] execute = params[:executive] if shixun_script shixun_script = compile.blank? ? shixun_script.gsub("COMPILEFUNCTION", "").gsub("CHALLENGEFIELPATH", "") : shixun_script.gsub("COMPILEFUNCTION", "#{compile_command}").gsub("COMPILECOMMAND", "#{compile}") shixun_script = execute.blank? ? shixun_script.gsub("EXECUTEFUNCTION", "") : shixun_script.gsub("EXECUTECOMMAND", "#{execute}") shixun_script = modify_shixun_script @shixun, shixun_script end @shixun_script = shixun_script end def departments @scope = [] q = params[:q] if q && q.strip.present? @scope = School.where("name like ?", "%#{q.strip}%").pluck(:name) end end def get_mirror_script mirror = MirrorRepository.find(params[:mirror_id]) @script = mirror.mirror_scripts end # TODO: 目前实训只做软删除. def destroy apply_records = ApplyAction.where(container_id: @shixun.id, container_type: "ApplyShixun") apply_records.delete_all if apply_records # HomeworkCommonShixuns.where(shixun_id: @shixun).delete_all # @shixun.destroy @shixun.update_column(:status, -1) end # 开启挑战 # 以前在开启挑战的时候检测实训是否更新,更新则重置,觉得应该放在TPI更好 # 中间需要一个过渡动画 # TODO: 第一次开启实训都会去判断是否是纯选择题类型,感觉做成在创建关卡的时候就判断该实训是否是纯选择题更加合适 def shixun_exec if is_shixun_opening? tip_show_exception(-3, "#{@shixun.opening_time.strftime('%Y-%m-%d %H:%M:%S')}") end current_myshixun = @shixun.current_myshixun(current_user.id) min_challenges = @shixun.challenges.pluck(:id , :st) Rails.logger.info("11111111112#{current_myshixun.try(:id)}") Rails.logger.info("111111111102#{params[:reset] != 1}") # 因为读写分离有延迟,所以如果是重置来的请求可以先跳过,重置过来的params[:reset]为1 if current_myshixun && params[:reset] != "1" games = current_myshixun.games # 如果TPM和TPI的管卡数不相等或者关卡顺序错了,说明实训被极大的改动,需要重置,实训发布前打过的实训都需要重置 if is_shixun_reset?(games, min_challenges, current_myshixun) # 这里页面弹框要收到 当前用户myshixun的identifier. tip_show_exception("/myshixuns/#{current_myshixun.try(:identifier)}/reset_my_game") end # 如果存在实训,则直接进入实训 # 如果实训允许跳关,传参params[:challenge_id]跳入具体的关卡 @current_task = if params[:challenge_id] game = games.where(challenge_id: params[:challenge_id]).take if @shixun.task_pass || game.status != 3 game else current_myshixun.current_task(games) end else current_myshixun.current_task(games) end else # 如果未创建关卡一定不能开启实训,否则TPI没法找到当前的关卡 if @shixun.challenges_count == 0 tip_exception("开启实战前请先创建实训关卡") end # 判断实训是否全为选择题 is_choice_type = (min_challenges.size == min_challenges.select{|challenge| challenge.last == 1}.count) if !is_choice_type commit = GitService.commits(repo_path: @repo_path).try(:first) uid_logger("First comit########{commit}") tip_exception("开启实战前请先在版本库中提交代码") if commit.blank? commit_id = commit["id"] end # 如果该实训是金课中的实训,则将当前用户加入到当期开课的课堂 if StageShixun.exists?(shixun_id: @shixun.id, subject_id: Subject.where(excellent: 1)) subject = Subject.find_by(id: StageShixun.where(shixun_id: @shixun.id).pluck(:subject_id), excellent: 1) course = subject.courses.where("start_date is not null and start_date <= '#{Date.today}' and end_date is not null and end_date >= '#{Date.today}'").take if course.present? && !CourseMember.exists?(course_id: course.id, user_id: current_user.id) # 为了不影响后续操作,用create而不是create! CourseMember.create(course_id: course.id, user_id: current_user.id, role: 4) CourseAddStudentCreateWorksJob.perform_later(course.id, [current_user.id]) end end ActiveRecord::Base.transaction do begin cloud_bridge = edu_setting('cloud_bridge') myshixun_identifier = generate_identifier Myshixun, 10 myshixun = @shixun.myshixuns.create!(user_id: current_user.id, identifier: myshixun_identifier, modify_time: @shixun.modify_time, reset_time: @shixun.reset_time, onclick_time: Time.now, commit_id: commit_id, repo_name: (is_choice_type ? "-1" : nil)) uid_logger("myshixun_id is #{myshixun.id}") # 其它创建关卡等操作 challenges = @shixun.challenges # 之所以增加user_id是为了方便统计查询性能 game_attrs = %i[challenge_id myshixun_id status user_id open_time identifier modify_time created_at updated_at] Game.bulk_insert(*game_attrs) do |worker| base_attr = { myshixun_id: myshixun.id, user_id: myshixun.user_id } challenges.each_with_index do |challenge, index| status = (index == 0 ? 0 : 3) game_identifier = generate_identifier(Game, 12) worker.add(base_attr.merge(challenge_id: challenge.id, status: status, identifier: game_identifier, modify_time: challenge.modify_time)) end end # 如果实训是纯选择题,则不需要去fork仓库以及中间层的相关操作了 unless is_choice_type # fork仓库 project_fork(myshixun, @repo_path, current_user.login) rep_url = Base64.urlsafe_encode64(repo_ip_url @repo_path ) uid_logger("start openGameInstance") uri = "#{cloud_bridge}/bridge/game/openGameInstance" logger.info("end openGameInstance") params = {tpiID: "#{myshixun.id}", tpmGitURL:rep_url, tpiRepoName: myshixun.repo_name.split("/").last} uid_logger("openGameInstance params is #{params}") interface_post uri, params, 83, "实训云平台繁忙(繁忙等级:83)" end @current_task = myshixun.current_task(myshixun.games) uid_logger("## shixun exec: myshixun id is #{myshixun.id}") rescue Exception => e uid_logger_error(e.message) tip_exception("实训云平台繁忙(繁忙等级:81)") raise ActiveRecord::Rollback end end end end # jupyter开启挑战 def jupyter_exec if is_shixun_opening? tip_show_exception(-3, "#{@shixun.opening_time.strftime('%Y-%m-%d %H:%M:%S')}") end current_myshixun = @shixun.current_myshixun(current_user.id) if current_myshixun @myshixun = current_myshixun else commit = GitService.commits(repo_path: @repo_path).try(:first) uid_logger("First comit########{commit}") tip_exception("开启挑战前,请先在Jupyter中填写内容并保存") if commit.blank? commit_id = commit["id"] cloud_bridge = edu_setting('cloud_bridge') myshixun_identifier = generate_identifier Myshixun, 10 begin ActiveRecord::Base.transaction do @myshixun = @shixun.myshixuns.create!(user_id: current_user.id, identifier: myshixun_identifier, modify_time: @shixun.modify_time, reset_time: @shixun.reset_time, onclick_time: Time.now, commit_id: commit_id) # fork仓库 project_fork(@myshixun, @repo_path, current_user.login) rep_url = Base64.urlsafe_encode64(repo_ip_url @repo_path) uri = "#{cloud_bridge}/bridge/game/openGameInstance" params = {tpiID: "#{@myshixun.id}", tpmGitURL: rep_url, tpiRepoName: @myshixun.repo_name.split("/").last} interface_post uri, params, 83, "服务器出现问题,请重置环境" end rescue => e uid_logger_error(e.message) tip_exception("服务器出现问题,请重置环境") end end end def publish @status = 0 @position = [] begin unless @shixun.is_jupyter? if @shixun.challenges.count == 0 @status = 4 else @shixun.challenges.each do |challenge| if challenge.challenge_tags.count == 0 @status = 3 @position << challenge.position end end unfinish_challenge = @shixun.challenges.where(:st => 0, :path => nil) if unfinish_challenge.count > 0 && !@shixun.is_choice_type? @status = 2 @pos = [] unfinish_challenge.each do |challenge| @pos << challenge.position end end end end if @status == 0 @shixun.update_attributes!(:status => 2) end rescue Exception => e logger.error("pushlish game #{e}") end end def apply_public tip_exception(-1, "请先发布实训再申请公开") if @shixun.status != 2 ActiveRecord::Base.transaction do @shixun.update_attributes!(public: 1) apply = ApplyAction.where(:container_type => "ApplyShixun", :container_id => @shixun.id).order("created_at desc").first if apply && apply.status == 0 @status = 0 else ApplyAction.create(:container_type => "ApplyShixun", :container_id => @shixun.id, :user_id => current_user.id, :status => 0) end end normal_status(0, "申请成功") end # 设置私密版本库的在tpm中的目录 def set_secret_dir raise("设置路径不能为空") if params[:secret_dir_path].blank? raise("请先配置私密版本库") if @shixun.shixun_secret_repository.blank? @shixun.shixun_secret_repository.update_attributes(:secret_dir_path => params[:secret_dir_path]) normal_status("设置成功") end def secret_repository begin @repo_path = @shixun.shixun_secret_repository&.repo_path @repo_url = repo_url @repo_path @trees = GitService.file_tree(repo_path: @repo_path, path: params[:path]) logger.info("#11@@#@#@#@111#@@@@###{@trees}") if @trees logger.info("#@@#@#@#@#@@@@###{@trees.try(:count)}") @latest_commit = [GitService.commits(repo_path: @repo_path).first] Rails.logger.info("########## #{@latest_commit}") end rescue Exception => e logger.error(e.message) end end def update_file content = params[:content] author_name = current_user.real_name author_email = current_user.git_mail @content = update_file_content content, @repo_path, @path, author_email, author_name, "Edit by browser" end def upload_git_file upload_file = params["file"] uid_logger("#########################file_params##: #{params["file"]}") raise "未上传文件" unless upload_file content = upload_file.tempfile.read uid_logger("#########################content####{content}") author_name = current_user.real_name author_email = current_user.git_mail message = params[:message] || "upload file by browser" uid_logger("-----author_email: #{author_email}") path = @path.present? ? "#{@path}/#{upload_file.original_filename}" : "#{upload_file.original_filename}" update_file_base64_content(content, @repo_path, path, author_email, author_name, message) render_ok end # 上传目录 def upload_git_folder author_name = current_user.real_name author_email = current_user.git_mail message = params[:message] || "upload folder by browser" git_add_folder(@repo_path, @path, author_name, author_email, message) render_ok end def delete_git_file author_name = current_user.real_name author_email = current_user.git_mail message = params[:message] || "delete file by browser" git_delete_file(@repo_path, @path, author_name, author_email, message) render_ok end def add_collaborators member_ids = "(" + @shixun.shixun_members.map(&:user_id).join(',') + ")" user_name = "%#{params[:user_name].to_s.strip}%" school_name = "%#{params[:school_name].to_s.strip}%" if user_name.present? || school_name.present? @users = User.where("users.id not in #{member_ids} AND users.status = 1 AND (LOWER(concat(users.lastname, users.firstname)) LIKE ? or users.phone like ?)", user_name, user_name) @users = @users.joins(user_extension: :school).where("schools.name like '%#{school_name}%'") if params[:school_name].present? else @users = User.none end @users = @users.where(laboratory_id: current_laboratory.id) unless current_laboratory.main_site? page = params[:page] || 1 limit = params[:limit] || 20 @user_count = @users.count @users = @users.page(page).per(20) end def shixun_members_added raise("user_ids 不能为空!") if params[:user_ids].blank? memberships = params[:user_ids] memberships.each do |member| ShixunMember.create!(:user_id => member, :shixun_id => @shixun.id, :role => 2) end end def change_manager # 搜索成员 if request.get? @collaborators = @shixun.shixun_members.where("user_id != #{@shixun.user_id}") else begin raise("请先选择成员") if params[:user_id].blank? man_member = ShixunMember.where(:shixun_id => @shixun.id, :user_id => @shixun.user_id).first cha_member = ShixunMember.where(:user_id => params[:user_id], :shixun_id => @shixun.id).first if man_member && cha_member man_member.update_attribute(:role, 2) cha_member.update_attribute(:role, 1) @shixun.update_attribute(:user_id, cha_member.user_id) end rescue => e logger.error("######change_manager_error: #{e.message}") render_error(e.message) end end end # 刪除合作者 def collaborators_delete raise("user_id不能为空") if params[:user_id].blank? shixun_member = ShixunMember.where(:user_id => params[:user_id], :shixun_id => @shixun.id, :role => 2).first shixun_member.delete end # 实训的发送至课堂:搜索课堂 def search_user_courses ## 分页参数 page = params[:page] || 1 limit = params[:limit] || 20 if params[:search] search = "%#{params[:search].to_s.strip.downcase}%" course_ids = Course.find_by_sql("SELECT c.id FROM courses c, course_members m WHERE m.course_id = c.id AND m.role in (1,2,3) AND m.user_id=#{current_user.id} AND c.is_delete = 0 AND c.is_end = 0 AND c.name like '#{search}' ").map(&:id) else course_ids = Course.find_by_sql("SELECT c.id, c.name FROM courses c, course_members m WHERE m.course_id = c.id AND m.role in (1,2,3) AND m.user_id=#{current_user.id} AND c.is_delete = 0 AND c.is_end = 0").map(&:id) end @courses = Course.where(:id => course_ids) ## 云上实验室过滤 @courses = @courses.where(id: current_laboratory.all_courses) @course_count = @courses.count @courses = @courses.page(page).per(10) end # 将实训发送到课程 def send_to_course @course = Course.find(params[:course_id]) homework = HomeworksService.new.create_homework @shixun, @course, nil, current_user CreateStudentWorkJob.perform_later(homework.id) end # 批量发送 def batch_send_to_course @course = Course.find_by!(id: params[:course_id]) shixuns = Shixun.where(id: params[:shixun_ids]).unhidden shixuns.each do |shixun| homework = HomeworksService.new.create_homework shixun, @course, nil, current_user CreateStudentWorkJob.perform_later(homework.id) end end # 二维码扫描下载 def download_file file_path = params[:file_name] send_file "#{Rails.root}/#{file_path}", :filename => "#{file_path}", :type => 'shixun', :disposition => 'attachment' #inline can open in browser end # 撤销申请公开 def cancel_apply_public tip_exception("实训已经公开,无法撤销") if @shixun.public == 2 ActiveRecord::Base.transaction do apply = ApplyAction.where(:container_type => "ApplyShixun", :container_id => @shixun.id).order("created_at desc").first if apply && apply.status == 0 apply.update_attributes!(status: 3) apply.tidings&.destroy_all end @shixun.update_column(:public, 0) end normal_status(0, "成功撤销申请") end # 撤销发布 def cancel_publish tip_exception("请先撤销申请公开,再撤销发布") if @shixun.public == 1 tip_exception("实训已经公开,无法撤销") if @shixun.public == 2 @shixun.update_column(:status, 0) end # 创建实训审核 def review_shixun validate_review_shixun_params # 没有记录就创建记录, 如果有记录就 @shixun.shixun_reviews.create!(user_id: current_user.id, status: params[:status], review_type: params[:review_type], evaluate_content: params[:evaluate_content]) normal_status("审核完成") end # 实训审核最新记录 def review_newest_record @content_record = @shixun.shixun_reviews.where(review_type: "Content").first @perfer_record = @shixun.shixun_reviews.where(review_type: "Performance").first end private def shixun_params params.require(:shixun).permit(:name, :trainee, :webssh, :can_copy, :use_scope, :vnc, :test_set_permission, :task_pass, :multi_webssh, :opening_time, :mirror_script_id, :code_hidden, :hide_code, :forbid_copy, :vnc_evaluate, :code_edit_permission, :is_jupyter) end def validate_review_shixun_params tip_exception("只有平台管理员或运营人员才能审核") if !admin_or_business? tip_exception("审核类型参数不对") unless ["Content", "Performance"].include?(params[:review_type]) end def shixun_info_params params.require(:shixun_info).permit(:description, :evaluate_script) end def service_config_params params.permit(shixun_service_configs: [:cpu_limit, :lower_cpu_limit, :memory_limit, :request_limit, :mirror_repository_id]) end def find_shixun @shixun = Shixun.find_by_identifier(params[:identifier]) if @shixun.blank? normal_status(404, "...") return end end def find_repo_name # 有私密版本库的参数时,需要拿私密仓库 @repo_path = if params[:secret_repository] @shixun.shixun_secret_repository&.repo_path else @shixun.repo_path end @path = params[:path] end def allowed unless current_user.manager_of_shixun?(@shixun) tip_exception(403, "..") end end def portion_allowed if current_user.shixun_identity(@shixun) > User::EDU_CERTIFICATION_TEACHER raise Educoder::TipException.new(403, "..") end end def special_allowed if @shixun.status != 2 tip_exception(403, "..") end end # 实训是否需要开启 def is_shixun_opening? @shixun.opening_time.present? && @shixun.opening_time > Time.now && current_user.shixun_identity(@shixun) > User::EDU_SHIXUN_MEMBER end # 实训是否需要重置 def is_shixun_reset?(games, min_challenges, current_myshixun) # 用户在申请发布之前,是否玩过实训 TODO: 重置的字段应该迁移到myshixuns表比较合适 modify_shixun = ShixunModify.exists?(:myshixun_id => current_myshixun.id, :shixun_id => @shixun.id, :status => 1) games.size != min_challenges.size || modify_shixun end # 添加私密仓库 def add_secret_repository # 防止跟tpm版本库重名,加了前缀secret repo_path = repo_namespace(current_user.login, "secret_#{@shixun.identifier}") GitService.add_repository(repo_path: repo_path) ShixunSecretRepository.create!(repo_name: repo_path.split(".")[0], shixun_id: @shixun.id) end def file_save_to_local(save_path, temp_file, ext) unless Dir.exists?(save_path) FileUtils.mkdir_p(save_path) ##不成功这里会抛异常 end digest = md5_file(temp_file) digest = "#{digest}_#{(Time.now.to_f * 1000).to_i}" local_file_path = File.join(save_path, digest) + ext save_temp_file(temp_file, local_file_path) [local_file_path, digest] end def save_temp_file(temp_file, save_file_path) File.open(save_file_path, 'wb') do |f| temp_file.rewind while (buffer = temp_file.read(8192)) f.write(buffer) end end end def file_ext(file_name) ext = '' exts = file_name.split(".") if exts.size > 1 ext = ".#{exts.last}" end ext end def delete_file(file_path) File.delete(file_path) if File.exist?(file_path) end def md5_file(temp_file) md5 = Digest::MD5.new temp_file.rewind while (buffer = temp_file.read(8192)) md5.update(buffer) end md5.hexdigest end # def validate_wachat_support # # if (params[:wechat].present? && !@shixun.is_wechat_support?) # tip_exception(-5, "..") # end # end end