|
|
|
|
class MyshixunsController < ApplicationController
|
|
|
|
|
before_action :require_login, :check_auth, :except => [:training_task_status, :code_runinng_message]
|
|
|
|
|
before_action :find_myshixun, :except => [:training_task_status, :code_runinng_message]
|
|
|
|
|
before_action :find_repo_name, :except => [:training_task_status, :code_runinng_message]
|
|
|
|
|
skip_before_action :verify_authenticity_token, :only => [:html_content]
|
|
|
|
|
|
|
|
|
|
## TPI关卡列表
|
|
|
|
|
def challenges
|
|
|
|
|
# @challenges = Challenge.where(shixun_id: params[:shixun_id])
|
|
|
|
|
|
|
|
|
|
@shixun_status = @myshixun.shixun.status
|
|
|
|
|
@games = @myshixun.games.includes(:challenge).reorder("challenges.position")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# For Admin
|
|
|
|
|
# 强制重置实训
|
|
|
|
|
# 前段需要按照操作过程提示
|
|
|
|
|
def reset_my_game
|
|
|
|
|
unless (current_user.admin? || current_user.id == @myshixun.user_id)
|
|
|
|
|
tip_exception("403", "")
|
|
|
|
|
end
|
|
|
|
|
begin
|
|
|
|
|
ActiveRecord::Base.transaction do
|
|
|
|
|
begin
|
|
|
|
|
@shixun = Shixun.select(:id, :identifier).find(@myshixun.shixun_id)
|
|
|
|
|
@myshixun.destroy
|
|
|
|
|
|
|
|
|
|
StudentWork.where(:myshixun_id => @myshixun.id).update_all(:myshixun_id => 0, :work_status => 0)
|
|
|
|
|
|
|
|
|
|
# 实训在申请发布前,是否玩过实训,如果玩过需要更改记录,防止二次重置
|
|
|
|
|
shixun_mod = ShixunModify.where(:shixun_id => @shixun.id, :myshixun_id => @myshixun.id, :status => 1).take
|
|
|
|
|
shixun_mod.update_column(:status, 0) if shixun_mod
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
logger.error("######reset_my_game_failed:#{e.message}")
|
|
|
|
|
raise("ActiveRecord::RecordInvalid")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
# 删除版本库
|
|
|
|
|
GitService.delete_repository(repo_path: @repo_path)
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
if e.message != "ActiveRecord::RecordInvalid"
|
|
|
|
|
logger.error("######delete_repository_error:#{e.message}")
|
|
|
|
|
end
|
|
|
|
|
raise ActiveRecord::Rollback
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 代码运行中的信息接口
|
|
|
|
|
# 这个方法是中间层主动调用的,点击评测后,中间层会发送参数过来,告诉目前Pod的启动情况,一次评测会调用两次请求
|
|
|
|
|
def code_runinng_message
|
|
|
|
|
begin
|
|
|
|
|
jsonTestDetails = JSON.parse(params[:jsonTestDetails])
|
|
|
|
|
game_id = jsonTestDetails['buildID']
|
|
|
|
|
message = jsonTestDetails['textMsg']
|
|
|
|
|
if game_id.present? && message.present?
|
|
|
|
|
game = Game.find game_id
|
|
|
|
|
msg = game.run_code_message
|
|
|
|
|
# 只有评测中的game才会创建和更新代码评测中的信息
|
|
|
|
|
if game.status == 1 || game.status == 2
|
|
|
|
|
if msg.blank?
|
|
|
|
|
RunCodeMessage.create!(:game_id => game_id, :status => 1, :message => message)
|
|
|
|
|
else
|
|
|
|
|
msg.update_attributes(:status => (msg.status + 1), :message => message)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
render :json => {:data => "success"}
|
|
|
|
|
end
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
render :json => {:data => "failed, exception_message: #{e}"}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 中间层评测接口
|
|
|
|
|
# taskId 即返回的game id
|
|
|
|
|
# 返回结果:params [:stauts] 0 表示成功,其它则失败
|
|
|
|
|
# msg 错误信息
|
|
|
|
|
# output 为测试用户编译输出结果
|
|
|
|
|
# myshixun:status 1为完成实训
|
|
|
|
|
# @jenkins: caseId对应test_set的position,passed: 1表示成功,0表示失败
|
|
|
|
|
# resubmit 1:表示已通关后重新评测;0:表示非重新评测
|
|
|
|
|
# retry_status 0:初始值;1:重新评测失败;2:重新评测成功
|
|
|
|
|
# tpiRepoPath 中间层图片的workspace路径
|
|
|
|
|
# params[:jsonTestDetails] = '{"buildID":"19284","compileSuccess":"1",
|
|
|
|
|
# "msg":[{"caseId":"1","expectedOutput":"MSAyIDMNCg","input":"MiAzIDE","output":"MSAyIDMNCg","passed":"1"},
|
|
|
|
|
# {"caseId":"2","expectedOutput":"LTMgMSA2DQo","input":"LTMgNiAx","output":"LTMgMSA2DQo","passed":"1"},
|
|
|
|
|
# {"caseId":"3","expectedOutput":"LTcgLTUgLTMNCg","input":"LTcgLTMgLTU","output":"LTcgLTUgLTMNCg","passed":"1"}],
|
|
|
|
|
# "outPut":"Y29tcGlsZSBzdWNjZXNzZnVsbHk","resubmit":"","status":"0"}'
|
|
|
|
|
# params[:timeCost] = '{"evaluateEnd":"2017-11-24 11:04:37","pull":"0.086",
|
|
|
|
|
# "createPod":"1.610","evaluateAllTime":2820,"evaluateStart":"2017-11-24 11:04:35","execute":"0.294"}'
|
|
|
|
|
# params[:pics] = "a.png,b.png,c.png"
|
|
|
|
|
def training_task_status
|
|
|
|
|
|
|
|
|
|
ActiveRecord::Base.transaction do
|
|
|
|
|
begin
|
|
|
|
|
t1 = Time.now
|
|
|
|
|
Rails.logger.info("@@@222222#{params[:jsonTestDetails]}")
|
|
|
|
|
jsonTestDetails = JSON.parse(params[:jsonTestDetails])
|
|
|
|
|
timeCost = JSON.parse(params[:timeCost])
|
|
|
|
|
brige_end_time = Time.parse(timeCost['evaluateEnd']) if timeCost['evaluateEnd'].present?
|
|
|
|
|
return_back_time = format("%.3f", ( t1.to_f - brige_end_time.to_f)).to_f
|
|
|
|
|
status = jsonTestDetails['status']
|
|
|
|
|
game_id = jsonTestDetails['buildID']
|
|
|
|
|
sec_key = jsonTestDetails['sec_key']
|
|
|
|
|
|
|
|
|
|
logger.info("training_task_status start#1**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}")
|
|
|
|
|
resubmit = jsonTestDetails['resubmit']
|
|
|
|
|
outPut = tran_base64_decode64(jsonTestDetails['outPut'])
|
|
|
|
|
|
|
|
|
|
jenkins_testsets = jsonTestDetails['msg']
|
|
|
|
|
compile_success = jsonTestDetails['compileSuccess']
|
|
|
|
|
# message = Base64.decode64(params[:msg]) unless params[:msg].blank?
|
|
|
|
|
|
|
|
|
|
game = Game.find(game_id)
|
|
|
|
|
myshixun = game.myshixun
|
|
|
|
|
challenge = game.challenge
|
|
|
|
|
# test_sets = challenge.test_sets
|
|
|
|
|
if challenge.picture_path.present?
|
|
|
|
|
#pics = params[:files]
|
|
|
|
|
pics = params[:tpiRepoPath]
|
|
|
|
|
game.update_column(:picture_path, pics)
|
|
|
|
|
end
|
|
|
|
|
logger.info("training_task_status start#2**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}")
|
|
|
|
|
max_query_index = game.outputs ? (game.outputs.first.try(:query_index).to_i + 1) : 1
|
|
|
|
|
test_set_score = 0
|
|
|
|
|
unless jenkins_testsets.blank?
|
|
|
|
|
jenkins_testsets.each_with_index do |j_test_set, i|
|
|
|
|
|
logger.info("j_test_set: ############## #{j_test_set}")
|
|
|
|
|
actual_output = tran_base64_decode64(j_test_set['output'])
|
|
|
|
|
#ts_time += j_test_set['testSetTime'].to_i
|
|
|
|
|
|
|
|
|
|
# is_public = test_sets.where(:position => j_test_set['caseId']).first.try(:is_public)
|
|
|
|
|
logger.info "actual_output:################################################# #{actual_output}"
|
|
|
|
|
ts_time = format("%.2f", j_test_set['testSetTime'].to_f/1000000000).to_f if j_test_set['testSetTime']
|
|
|
|
|
ts_mem = format("%.2f", j_test_set['testSetMem'].to_f/1024/1024).to_f if j_test_set['testSetMem']
|
|
|
|
|
|
|
|
|
|
Output.create!(:code => status, :game_id => game_id, :out_put => outPut, :test_set_position => j_test_set['caseId'],
|
|
|
|
|
:actual_output => actual_output, :result => j_test_set['passed'].to_i, :query_index => max_query_index,
|
|
|
|
|
:compile_success => compile_success.to_i, :sec_key => sec_key, :ts_time => ts_time, :ts_mem => ts_mem)
|
|
|
|
|
# 如果设置了按测试集给分,则需要统计测试集的分值
|
|
|
|
|
if challenge.test_set_score && j_test_set['passed'].to_i == 1
|
|
|
|
|
test_set_score += challenge.test_sets.where(:position => j_test_set['caseId']).pluck(:score).first
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
uid_logger("#############status: #{status}")
|
|
|
|
|
record = EvaluateRecord.where(:identifier => sec_key).first
|
|
|
|
|
logger.info("training_task_status start#3**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}")
|
|
|
|
|
answer_deduction_percentage = (100 - game.answer_deduction) / 100.to_f # 查看答案后剩余分数的百分比.
|
|
|
|
|
# answer_deduction是查看答案的扣分比例
|
|
|
|
|
# status:0表示评测成功
|
|
|
|
|
if status == "0"
|
|
|
|
|
if resubmit.present?
|
|
|
|
|
uid_logger("#############resubmitdaiao: #{resubmit}")
|
|
|
|
|
game.update_attributes!(:retry_status => 2, :resubmit_identifier => resubmit)
|
|
|
|
|
challenge.path.split(";").each do |path|
|
|
|
|
|
game_passed_code(path.try(:strip), myshixun, game_id)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
game.update_attributes!(:status => 2,
|
|
|
|
|
:end_time => Time.now,
|
|
|
|
|
:accuracy => format("%.4f", 1.0 / game.query_index))
|
|
|
|
|
myshixun.update_attributes!(:status => 1) if game.had_done == 1
|
|
|
|
|
challenge.path.split(";").each do |path|
|
|
|
|
|
game_passed_code(path.try(:strip), myshixun, game_id)
|
|
|
|
|
end
|
|
|
|
|
# 如果是已经发布的实训,则需要给出相应的奖励
|
|
|
|
|
if challenge.shixun.try(:status) > 1
|
|
|
|
|
score = (challenge.score * answer_deduction_percentage).to_i
|
|
|
|
|
if score > 0
|
|
|
|
|
reward_attrs = { container_id: game.id, container_type: 'Game', score: score }
|
|
|
|
|
RewardGradeService.call(game.user, reward_attrs)
|
|
|
|
|
RewardExperienceService.call(game.user, reward_attrs)
|
|
|
|
|
end
|
|
|
|
|
# 需要扣除查看答案的分数
|
|
|
|
|
game.update_attributes!(:final_score => score)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 更新实训关联的作品分数 TODO: 更新作品分数
|
|
|
|
|
# HomeworksService.new.update_myshixun_work_score myshixun
|
|
|
|
|
end
|
|
|
|
|
# 如果过关了,下一关的状态是3(为开启),则需要把状态改成1(已开启)
|
|
|
|
|
# next_game = game.next_game
|
|
|
|
|
next_game = game.next_game(myshixun.shixun_id, game.myshixun_id, challenge.position)
|
|
|
|
|
next_game.update_column(:status, 0) if next_game.present? && next_game.status == 3
|
|
|
|
|
# status == "-1" 表示返回结果错误
|
|
|
|
|
else
|
|
|
|
|
if resubmit.present?
|
|
|
|
|
game.update_attributes!(:retry_status => 1, :resubmit_identifier => resubmit)
|
|
|
|
|
else
|
|
|
|
|
# 评测没通关则,测试集对的个数给分,并且还要扣除用户是否查看答案的值
|
|
|
|
|
test_set_percentage = test_set_score / 100.to_f # 测试集得分比
|
|
|
|
|
score = (challenge.score * test_set_percentage * answer_deduction_percentage).to_i
|
|
|
|
|
# 如果分数比上次多,则更新成绩
|
|
|
|
|
game_update =
|
|
|
|
|
if game.final_score < score
|
|
|
|
|
{final_score: score, status: 0}
|
|
|
|
|
else
|
|
|
|
|
{status: 0}
|
|
|
|
|
end
|
|
|
|
|
game.update_attributes!(game_update)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
test_cases_time = format("%.3f", (Time.now.to_f - t1.to_f)).to_f
|
|
|
|
|
if record.present?
|
|
|
|
|
consume_time = format("%.3f", (Time.now - record.created_at)).to_f
|
|
|
|
|
|
|
|
|
|
record.update_attributes!(:consume_time => consume_time, :git_pull => timeCost['pull'] , :create_pod => timeCost['createPod'],
|
|
|
|
|
:pod_execute => timeCost['execute'], :test_cases => test_cases_time,
|
|
|
|
|
:brige => timeCost['evaluateAllTime'], :return_back => return_back_time)
|
|
|
|
|
end
|
|
|
|
|
uid_logger("training_task_status start#4**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}")
|
|
|
|
|
sucess_status
|
|
|
|
|
# rescue Exception => e
|
|
|
|
|
# tip_exception(e.message)
|
|
|
|
|
# uid_logger_error("training_task_status error: #{e}")
|
|
|
|
|
# raise ActiveRecord::Rollback
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 连接webssh
|
|
|
|
|
def open_webssh
|
|
|
|
|
username = edu_setting('webssh_username')
|
|
|
|
|
password = edu_setting('webssh_password')
|
|
|
|
|
old_time = Time.now.to_i
|
|
|
|
|
begin
|
|
|
|
|
shixun_tomcat = edu_setting('tomcat_webssh')
|
|
|
|
|
uri = "#{shixun_tomcat}/bridge/webssh/getConnectInfo"
|
|
|
|
|
params = {tpiID:@myshixun.id, podType:@myshixun.shixun.try(:webssh),
|
|
|
|
|
containers:(Base64.urlsafe_encode64(shixun_container_limit @myshixun.shixun))}
|
|
|
|
|
res = uri_post uri, params
|
|
|
|
|
if res && res['code'].to_i != 0
|
|
|
|
|
tip_exception("实训云平台繁忙(繁忙等级:92)")
|
|
|
|
|
end
|
|
|
|
|
render :json => {:host => res['address'],
|
|
|
|
|
:port => res['port'],
|
|
|
|
|
:ws_url => res['ws_address'],
|
|
|
|
|
:username => username,
|
|
|
|
|
:password => password,
|
|
|
|
|
:game_id => @myshixun.id,
|
|
|
|
|
:webssh_url => "#{shixun_tomcat}/bridge"}
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
logger.error(e)
|
|
|
|
|
render :json => {:error => e.try(:message)}
|
|
|
|
|
ensure
|
|
|
|
|
use_time = Time.now.to_i - old_time
|
|
|
|
|
logger.info "open_webssh tpiID #{@myshixun.id} use time #{use_time}"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
include GitCommon
|
|
|
|
|
|
|
|
|
|
# -----Repository
|
|
|
|
|
# TODO: 之类需要一个resubmit参数,但是是关于games.
|
|
|
|
|
def update_file
|
|
|
|
|
begin
|
|
|
|
|
@hide_code = Shixun.where(id: @myshixun.shixun_id).pluck(:hide_code).first
|
|
|
|
|
tip_exception("技术平台为空!") if @myshixun.mirror_name.blank?
|
|
|
|
|
path = params[:path].strip unless params[:path].blank?
|
|
|
|
|
game_id = params[:game_id]
|
|
|
|
|
game = Game.find(game_id)
|
|
|
|
|
@content_modified = 0
|
|
|
|
|
|
|
|
|
|
# params[:evaluate] 实训评测时更新必须给的参数,需要依据该参数做性能统计,其它类型的更新可以跳过
|
|
|
|
|
# 自动保存的时候evaluate为0;点评测的时候为1
|
|
|
|
|
if params[:evaluate] == 1
|
|
|
|
|
@sec_key = generate_identifier(EvaluateRecord, 12)
|
|
|
|
|
record = EvaluateRecord.create!(:user_id => current_user.id, :shixun_id => @myshixun.shixun_id, :game_id => game_id,
|
|
|
|
|
:identifier => @sec_key)
|
|
|
|
|
uid_logger("-- game build: file update #{@sec_key}, record id is #{record.id}, time is **** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}")
|
|
|
|
|
end
|
|
|
|
|
unless @hide_code
|
|
|
|
|
# 远程版本库文件内容
|
|
|
|
|
last_content = GitService.file_content(repo_path: @repo_path, path: path)["content"]
|
|
|
|
|
content = params[:content]
|
|
|
|
|
Rails.logger.info("###11222333####{content}")
|
|
|
|
|
Rails.logger.info("###222333####{last_content}")
|
|
|
|
|
|
|
|
|
|
if content != last_content
|
|
|
|
|
@content_modified = 1
|
|
|
|
|
|
|
|
|
|
author_name = current_user.real_name
|
|
|
|
|
author_email = current_user.git_mail
|
|
|
|
|
message = params[:evaluate] == 0 ? "System automatically submitted" : "User submitted"
|
|
|
|
|
uid_logger("112233#{author_name}")
|
|
|
|
|
uid_logger("112233#{author_email}")
|
|
|
|
|
@content = GitService.update_file(repo_path: @repo_path,
|
|
|
|
|
file_path: path,
|
|
|
|
|
message: message,
|
|
|
|
|
content: content,
|
|
|
|
|
author_name: author_name,
|
|
|
|
|
author_email: author_email)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if game.status == 2
|
|
|
|
|
@resubmit = Time.now.to_i
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 评测时间记录
|
|
|
|
|
if record.present?
|
|
|
|
|
consume_time = format("%.3f", (Time.now.to_f - record.created_at.to_f)).to_f
|
|
|
|
|
record.update_attributes!(:file_update => consume_time)
|
|
|
|
|
end
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
uid_logger_error(e.message)
|
|
|
|
|
tip_exception("文件内容更新异常,请稍后重试")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 渲染实训代码
|
|
|
|
|
# educodercss: 字符串以 ‘,’分隔,存储的是版本库css的路径
|
|
|
|
|
# educoderscript: 字符串以 ‘,’分隔,存储的是版本库js的路径
|
|
|
|
|
# contents: html实训的整体内容
|
|
|
|
|
def html_content
|
|
|
|
|
@contents = params[:contents] || ""
|
|
|
|
|
edu_css = params[:educodercss]
|
|
|
|
|
edu_js = params[:educoderscript]
|
|
|
|
|
if @contents.present?
|
|
|
|
|
@contents = @contents.gsub("w3equalsign", "=").gsub("w3scrw3ipttag", "script").gsub("edulink", "link").html_safe
|
|
|
|
|
end
|
|
|
|
|
# css
|
|
|
|
|
if edu_css.present?
|
|
|
|
|
css_path = edu_css.split(",")
|
|
|
|
|
css_path.each do |path|
|
|
|
|
|
file_content = GitService.file_content(repo_path: @repo_path, path: path)["content"]
|
|
|
|
|
file_content = tran_base64_decode64(file_content) unless file_content.blank?
|
|
|
|
|
@contents = @contents.sub(/EDUCODERCSS/, "<style>#{file_content}</style>")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
# js
|
|
|
|
|
if edu_js.present?
|
|
|
|
|
js_path = edu_js.split(",")
|
|
|
|
|
js_path.each do |path|
|
|
|
|
|
file_content = GitService.file_content(repo_path: @repo_path, path: path)["content"]
|
|
|
|
|
file_content = tran_base64_decode64(file_content) unless file_content.blank?
|
|
|
|
|
@contents = @contents.sub(/EDUCODERJS/, "<script>#{file_content}</script>")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
respond_to do |format|
|
|
|
|
|
format.json
|
|
|
|
|
format.html{render :layout => false}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 最新可以用的并发测试接口
|
|
|
|
|
def sigle_mul_test
|
|
|
|
|
codes = %W(1 2 3 4 5 6 7 8 9 A B C D E F G H J K L N M O P Q R S T U V W X Y Z)
|
|
|
|
|
begin
|
|
|
|
|
identifiers = Myshixun.where(:shixun_id => params[:shixun_id].split(",")).pluck(:identifier)
|
|
|
|
|
ide = identifiers[rand(identifiers.length)]
|
|
|
|
|
myshixun = Myshixun.where(:identifier => ide).first
|
|
|
|
|
|
|
|
|
|
game = myshixun.games.last
|
|
|
|
|
logger.warn("###2mul test game_build start ")
|
|
|
|
|
identifier = game.try(:identifier)
|
|
|
|
|
if game.status == 2
|
|
|
|
|
code = codes.sample(8).join
|
|
|
|
|
resubmit = "#{code}_#{myshixun.id}"
|
|
|
|
|
end
|
|
|
|
|
logger.warn("###3mul test game_build start ...")
|
|
|
|
|
EvaluateRecord.create!(:user_id => myshixun.user_id, :shixun_id => myshixun.shixun.id, :game_id => game.id)
|
|
|
|
|
redirect_to "/api/games/#{identifier}/game_build?resubmit=#{resubmit}&content_modified=0&first=1"
|
|
|
|
|
rescue Exception => e
|
|
|
|
|
logger.error("mul test failed ===> #{e.message}")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -----End
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
def find_myshixun
|
|
|
|
|
@myshixun = Myshixun.find_by!(identifier: params[:identifier])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def find_repo_name
|
|
|
|
|
@repo_path = @myshixun.try(:repo_path)
|
|
|
|
|
@path = params[:path]
|
|
|
|
|
end
|
|
|
|
|
end
|