Skip to content

Commit

Permalink
コーディングテスト機能を追加
Browse files Browse the repository at this point in the history
  • Loading branch information
komagata committed Aug 12, 2024
1 parent e58207f commit d98b5fd
Show file tree
Hide file tree
Showing 46 changed files with 1,404 additions and 2 deletions.
23 changes: 23 additions & 0 deletions app/controllers/api/coding_test_submissions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class API::CodingTestSubmissionsController < API::BaseController
def create
cts = CodingTestSubmission.new(coding_test_submission_params)
cts.user = current_user

if cts.save
head :ok
else
render json: { errors: cts.errors }, status: :unprocessable_entity
end
end

private

def coding_test_submission_params
params.require(:coding_test_submission).permit(
:coding_test_id,
:code
)
end
end
19 changes: 19 additions & 0 deletions app/controllers/coding_tests/coding_test_submissions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class CodingTests::CodingTestSubmissionsController < ApplicationController
before_action :set_coding_test

def index
@coding_test_submissions = @coding_test.coding_test_submissions.page(params[:page])
end

def show
@coding_test_submission = @coding_test.coding_test_submissions.find(params[:id])
end

private

def set_coding_test
@coding_test = CodingTest.find(params[:coding_test_id])
end
end
7 changes: 7 additions & 0 deletions app/controllers/coding_tests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class CodingTestsController < ApplicationController
def show
@coding_test = CodingTest.find(params[:id])
end
end
57 changes: 57 additions & 0 deletions app/controllers/mentor/coding_tests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

class Mentor::CodingTestsController < ApplicationController
before_action :require_admin_or_mentor_login, only: %i[index new create edit update]
before_action :set_coding_test, only: %i[show edit update destroy]

def index
@coding_tests = CodingTest.joins(:practice).order('practices.id, coding_tests.position')
end

def show; end

def new
@coding_test = CodingTest.new(user: current_user)
end

def edit; end

def create
@coding_test = CodingTest.new(coding_test_params)
if @coding_test.save
redirect_to @coding_test, notice: 'コーディング問題を作成しました。'
else
render :new
end
end

def update
if @coding_test.update(coding_test_params)
redirect_to @coding_test, notice: 'コーディング問題を更新しました。'
else
render :edit
end
end

def destroy
@coding_test.destroy!
redirect_to mentor_coding_tests_path, notice: 'コーディング問題を削除しました。'
end

private

def coding_test_params
params.require(:coding_test).permit(
:title,
:description,
:practice_id,
:user_id,
:language,
coding_test_cases_attributes: %i[id input output _destroy]
)
end

def set_coding_test
@coding_test = CodingTest.find(params[:id])
end
end
5 changes: 3 additions & 2 deletions app/javascript/bootcamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ export default {
})
},

post(path) {
post(path, params = {}) {
return fetch(path, {
method: 'POST',
headers: headers(),
credentials: 'same-origin',
redirect: 'manual'
redirect: 'manual',
body: JSON.stringify(params)
})
},

Expand Down
64 changes: 64 additions & 0 deletions app/javascript/coding-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { OnBrowserJudge } from './onbrowserjudge.js'
import ace from 'ace-builds'
import 'ace-builds/webpack-resolver'
import 'ace-builds/src-noconflict/mode-javascript'
import 'ace-builds/src-noconflict/mode-ruby'
import 'ace-builds/src-noconflict/theme-github'
import Bootcamp from 'bootcamp'

document.addEventListener('DOMContentLoaded', () => {
const id = 'code_editor'
const element = document.getElementById(id)
if (!element) {
return null
}

const codingTestId = element.dataset.codingTestId
const practiceId = element.dataset.practiceId
const language = element.dataset.language
const editor = ace.edit(id)

editor.session.setMode(`ace/mode/${language}`)
editor.setTheme('ace/theme/github')

OnBrowserJudge.workerFile = `../${language}.js`
OnBrowserJudge.getProgram = () => editor.getValue()
OnBrowserJudge.dict.ready = "提出"
OnBrowserJudge.dict.running = "停止"
OnBrowserJudge.dict.preparation = "準備中"
OnBrowserJudge.dict.case_name = "テストケース名"
OnBrowserJudge.dict.status = "結果"
OnBrowserJudge.dict.AC = "正解"
OnBrowserJudge.dict.WA = "不正解"
OnBrowserJudge.dict.RE = "エラー"
OnBrowserJudge.dict.TLE = "時間超過"
OnBrowserJudge.dict.WJ = "ジャッジ待ち"
OnBrowserJudge.timeLimit = 2001
OnBrowserJudge.process = (program, _casename, _input) => program
OnBrowserJudge.assertEqual = (expected, actual) => {
console.log(`expected: ${expected}, actual: ${actual}`)
console.log(expected === actual.trimEnd())
return expected === actual.trimEnd()
}
OnBrowserJudge.congratulations = async () => {
alert("正解!")

const params = {
coding_test_submission: {
coding_test_id: codingTestId,
code: editor.getValue()
}
}

try {
const response = await Bootcamp.post('/api/coding_test_submissions', params)
if (response.ok) {
location.href = `/practices/${practiceId}`
} else {
console.warn('提出に失敗しました。')
}
} catch (error) {
console.error(error)
}
}
})
205 changes: 205 additions & 0 deletions app/javascript/onbrowserjudge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
const OnBrowserJudge = {
dict: {
ready: "▶Run",
running: "■Stop",
preparation: "In preparation",
case_name: "Case Name",
status: "Status",
AC: "AC",
WA: "WA",
RE: "RE",
CE: "CE",
IE: "IE",
TLE: "TLE",
MLE: "MLE",
OLE: "OLE",
WJ: "WJ"
},

timeLimit: 2000,

initialData: null,

congratulations: () => {},

process: (program, _casename, _input) => program,

assertEqual: (expected, actual) => expected === actual.trimEnd(),


status: "preparation",

updateStatus: function(status) {
this.status = status
const button = document.getElementById("run")
button.disabled = status === "preparation"
button.innerHTML = this.dict[status]
},

runButtonPressed: function() {
switch (this.status) {
case "ready":
this.run()
break
case "running":
this.stop()
break
}
},

worker: null,

timer: null,

workerEvent: function(e) {
let d = null
switch (e.data[0]) {
case "init":
this.worker.postMessage(["init", this.initialData])
break
case "ready":
this.updateStatus("ready")
break
case "executed":
d = e.data[1]
this.executed(d.testCase, d.output, d.error, d.errorMessage, d.execTime)
break
}
},

loadWorker: function(path) {
const baseURL = window.location.href.replaceAll("\\", "/").replace(/\/[^/]*$/, "/")
const array = [`importScripts("${baseURL}${path}");`]
const blob = new Blob(array, { type: "text/javascript" })
const url = window.URL.createObjectURL(blob)
return new Worker(url)
},

resetWorker: function() {
if (this.worker) this.worker.terminate()
this.worker = this.loadWorker(this.workerFile)
this.worker.addEventListener("message", event => { this.workerEvent(event) }, false)
},

run: async function() {
if (this.status !== "ready") return
this.updateStatus("running")
const autocopy = document.getElementById("autocopy")
if (!autocopy || autocopy.checked) this.copyProgram()
this.initializeResult()
this.restTests = Array.from(this.tests)
this.allPassed = true
this.program = this.getProgram()
this.nextTest()
},

nextTest: function() {
const testCase = this.restTests.shift()
const input = document.getElementById(`${testCase}_input`).innerText.trim()
const program = this.process(this.getProgram(), testCase, input)
this.timer = setTimeout(() => this.tle(testCase), this.timeLimit * 2)
this.worker.postMessage(["execute", { testCase, program, input }])
},

tle: function(testCase) {
this.updateResult(testCase, "TLE", '', '', '', this.timeLimit * 2)
this.stop()
},

executed: function(testCase, output, error, errorMessage, execTime) {
clearTimeout(this.timer)
let result = "AC"
if (error !== 0) {
result = error === 1 ? "CE" : "RE"
} else {
if (execTime > this.timeLimit) result = "TLE"
const expected = document.getElementById(`${testCase}_output`).innerText.trim()
if (! this.assertEqual(expected, output)) result = "WA"
}

console.log('output:', output);
console.log('error:', error);
console.log('errorMessage:', errorMessage);

this.updateResult(testCase, result, output, error, errorMessage, execTime)
if (result !== "AC") this.allPassed = false
if (this.restTests.length === 0) {
if (this.allPassed) setTimeout(this.congratulations, 20)
this.updateStatus("ready")
} else {
this.nextTest()
}
},

initializeResult: function() {
document.getElementById("result").innerHTML = `
<thead><tr>
<th>${this.dict.case_name}</th>
<th>出力</th>
<th>エラー</th>
<th>${this.dict.status}</th>
</tr></thead>`

for(const testCase of OnBrowserJudge.tests) {
const tr = document.createElement("tr")
tr.innerHTML = `
<td id="${testCase}">${testCase}</td>
<td id="${testCase}_std_output"><pre><code></code></pre></td></td>
<td id="${testCase}_std_error"><pre><code></code></pre></td>
<td id="${testCase}_status"><span class="status wj">${this.dict.WJ}</span></td>`
document.getElementById("result").appendChild(tr)
}
document.getElementById("result").scrollIntoView({ behavior: "smooth" })
},

updateResult: function(testCase, result, output, _error, errorMessage, _execTime) {
document.getElementById(`${testCase}_std_output`).innerHTML = `<pre><code>${output}</code></pre>`
document.getElementById(`${testCase}_std_error`).innerHTML = `<pre><code>${errorMessage}</code></pre>`
const span = `<span class="status ${result.toLowerCase()}` +
`" title="${result}">${this.dict[result]}</span>`
document.getElementById(`${testCase}_status`).innerHTML = span
},

stop: function() {
window.clearTimeout(this.timer)
Array.from(document.getElementsByClassName("wj")).forEach((elm) => {
elm.innerText = ""
})

this.updateStatus("preparation")
this.resetWorker()
},

copyProgram: function() {
navigator.clipboard.writeText(this.getProgram())
}
}


window.addEventListener("DOMContentLoaded", () => {
const editor = document.getElementById('code_editor')
if (!editor) return null

function getTestNames() {
return Array.from(document.getElementsByTagName("pre")).map(elm =>
elm.id
).filter(id =>
id.match(/_input$/) && document.getElementById(id.replace(/_input$/, "_output"))
).map(id =>
id.replace(/_input$/, "")
)
}
OnBrowserJudge.tests = getTestNames()

function trimAllSampleCases() {
const samples = document.getElementsByClassName("sample")
for (const elm of samples) elm.innerText = elm.innerText.trim()
}
trimAllSampleCases()

OnBrowserJudge.updateStatus("preparation")
OnBrowserJudge.resetWorker()
document.getElementById("run").onclick = () => OnBrowserJudge.runButtonPressed()
})

export { OnBrowserJudge }
Loading

0 comments on commit d98b5fd

Please sign in to comment.