diff --git a/app/controllers/api/coding_test_submissions_controller.rb b/app/controllers/api/coding_test_submissions_controller.rb
new file mode 100644
index 00000000000..ce2014962a8
--- /dev/null
+++ b/app/controllers/api/coding_test_submissions_controller.rb
@@ -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
diff --git a/app/controllers/coding_tests/coding_test_submissions_controller.rb b/app/controllers/coding_tests/coding_test_submissions_controller.rb
new file mode 100644
index 00000000000..7f2aa57e401
--- /dev/null
+++ b/app/controllers/coding_tests/coding_test_submissions_controller.rb
@@ -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
diff --git a/app/controllers/coding_tests_controller.rb b/app/controllers/coding_tests_controller.rb
new file mode 100644
index 00000000000..d899295faf0
--- /dev/null
+++ b/app/controllers/coding_tests_controller.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class CodingTestsController < ApplicationController
+ def show
+ @coding_test = CodingTest.find(params[:id])
+ end
+end
diff --git a/app/controllers/mentor/coding_tests_controller.rb b/app/controllers/mentor/coding_tests_controller.rb
new file mode 100644
index 00000000000..177a5d84d54
--- /dev/null
+++ b/app/controllers/mentor/coding_tests_controller.rb
@@ -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
diff --git a/app/javascript/bootcamp.js b/app/javascript/bootcamp.js
index 6429d710bbb..2a357ff2621 100644
--- a/app/javascript/bootcamp.js
+++ b/app/javascript/bootcamp.js
@@ -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)
})
},
diff --git a/app/javascript/coding-test.js b/app/javascript/coding-test.js
new file mode 100644
index 00000000000..e296a9f5c1e
--- /dev/null
+++ b/app/javascript/coding-test.js
@@ -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)
+ }
+ }
+ })
diff --git a/app/javascript/onbrowserjudge.js b/app/javascript/onbrowserjudge.js
new file mode 100644
index 00000000000..84c3afd75f1
--- /dev/null
+++ b/app/javascript/onbrowserjudge.js
@@ -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 = `
+
+ `
+
+ for(const testCase of OnBrowserJudge.tests) {
+ const tr = document.createElement("tr")
+ tr.innerHTML = `
+${this.dict.case_name}
+ 出力
+ エラー
+ ${this.dict.status}
+
${output}
`
+ document.getElementById(`${testCase}_std_error`).innerHTML = `${errorMessage}
`
+ const span = `${this.dict[result]}`
+ 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 }
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index c37e1298744..9242c603772 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -56,6 +56,7 @@ import '../header-dropdown.js'
import '../editor-selection-form.js'
import '../user_mentor_memo.js'
import '../invitation-url-updater.js'
+import '../coding-test.js'
import VueMounter from '../VueMounter.js'
import Books from '../components/books.vue'
diff --git a/app/models/coding_test.rb b/app/models/coding_test.rb
new file mode 100644
index 00000000000..4620a7bd443
--- /dev/null
+++ b/app/models/coding_test.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CodingTest < ApplicationRecord
+ enum language: {
+ ruby: 1,
+ javascript: 2
+ }, _prefix: true
+
+ belongs_to :practice
+ belongs_to :user
+ has_many :coding_test_cases, dependent: :destroy
+ has_many :coding_test_submissions, dependent: :destroy
+
+ accepts_nested_attributes_for :coding_test_cases
+
+ acts_as_list scope: :practice
+
+ validates :language, presence: true
+ validates :title, presence: true
+ validates :description, presence: true
+
+ def passed_by?(user)
+ coding_test_submissions.exists?(user:)
+ end
+end
diff --git a/app/models/coding_test_case.rb b/app/models/coding_test_case.rb
new file mode 100644
index 00000000000..6f526d151aa
--- /dev/null
+++ b/app/models/coding_test_case.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class CodingTestCase < ApplicationRecord
+ belongs_to :coding_test
+end
diff --git a/app/models/coding_test_submission.rb b/app/models/coding_test_submission.rb
new file mode 100644
index 00000000000..34e72643a79
--- /dev/null
+++ b/app/models/coding_test_submission.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class CodingTestSubmission < ApplicationRecord
+ belongs_to :coding_test
+ belongs_to :user
+
+ validates :coding_test_id, uniqueness: { scope: :user_id }
+end
diff --git a/app/models/practice.rb b/app/models/practice.rb
index 3103daa6be0..624470e3a28 100644
--- a/app/models/practice.rb
+++ b/app/models/practice.rb
@@ -42,6 +42,7 @@ class Practice < ApplicationRecord
accepts_nested_attributes_for :practices_books, reject_if: :all_blank, allow_destroy: true
has_one :submission_answer, dependent: :destroy
+ has_many :coding_tests, dependent: :nullify
validates :title, presence: true
validates :description, presence: true
diff --git a/app/models/user.rb b/app/models/user.rb
index 6e00d7e151e..cbdc7c41f84 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -100,6 +100,8 @@ class User < ApplicationRecord
has_many :surveys, dependent: :destroy
has_many :survey_questions, dependent: :destroy
has_many :external_entries, dependent: :destroy
+ has_many :coding_tests, dependent: :destroy
+ has_many :coding_test_submissions, dependent: :destroy
has_one :report_template, dependent: :destroy
has_one :talk, dependent: :destroy
has_one :discord_profile, dependent: :destroy
@@ -167,6 +169,8 @@ class User < ApplicationRecord
through: :regular_event_participations,
source: :regular_event
+ has_many :coding_test_submissions, dependent: :destroy
+
has_one_attached :avatar
has_one_attached :profile_image
@@ -488,6 +492,10 @@ def create_followup_comment(student)
end
end
+ def submitted?(coding_test)
+ coding_test_submissions.exists?(coding_test_id: coding_test.id)
+ end
+
def away?
last_activity_at && (last_activity_at <= 10.minutes.ago)
end
diff --git a/app/views/application/_mentor_menu.html.slim b/app/views/application/_mentor_menu.html.slim
index d0a2a7957f5..838dade47f4 100644
--- a/app/views/application/_mentor_menu.html.slim
+++ b/app/views/application/_mentor_menu.html.slim
@@ -6,6 +6,10 @@
= link_to mentor_root_path,
class: 'header-dropdown__item-link' do
| メンターページ
+ li.header-dropdown__item
+ = link_to [:mentor, :coding_tests],
+ class: 'header-dropdown__item-link' do
+ | コーディングテスト
li.header-dropdown__item
= link_to mentor_practices_path,
class: 'header-dropdown__item-link' do
diff --git a/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim b/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim
new file mode 100644
index 00000000000..e9e310a5d11
--- /dev/null
+++ b/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim
@@ -0,0 +1,28 @@
+- cts = coding_test_submission
+- coding_test = cts.coding_test
+
+.card-list-item
+ .card-list-item__inner
+ .card-list-item__user
+ = render 'users/icon',
+ user: cts.user,
+ link_class: 'card-list-item__user-link',
+ image_class: 'card-list-item__user-icon'
+ .card-list-item__rows
+ .card-list-item__row
+ .card-list-item-title
+ .card-list-item-title__start
+ h2.card-list-item-title__title(itemprop='name')
+ = link_to [coding_test, cts],
+ itemprop: 'url',
+ class: 'card-list-item-title__link a-text-link js-unconfirmed-link' do
+ = coding_test.title
+ .card-list-item__row
+ .card-list-item-meta
+ .card-list-item-meta__items
+ .card-list-item-meta__item
+ = link_to cts.user, class: 'a-user-name' do
+ = cts.user.long_name
+ .card-list-item-meta__item
+ time.a-meta(datetime="#{cts.updated_at}")
+ = l cts.updated_at
diff --git a/app/views/coding_tests/coding_test_submissions/index.html.slim b/app/views/coding_tests/coding_test_submissions/index.html.slim
new file mode 100644
index 00000000000..4a95b05dc16
--- /dev/null
+++ b/app/views/coding_tests/coding_test_submissions/index.html.slim
@@ -0,0 +1,32 @@
+- title '提出コード'
+- description '提出コードの一覧ページです。'
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ .page-header__title = title
+ .page-header__end
+
+hr.a-border
+
+.page-main
+ - if @coding_test_submissions.present?
+ .page-body
+ .container.is-md
+ .page-content.reports
+ = paginate @coding_test_submissions
+ .card-list.a-card
+ .card-list__items
+ = render @coding_test_submissions
+ = paginate @coding_test_submissions
+ - else
+ .o-empty-message
+ .o-empty-message__icon
+ .card-list.a-card
+ .card__description
+ .o-empty-message
+ .o-empty-message__icon
+ i.fa-regular.fa-sad-tear
+ .o-empty-message__text
+ | 提出コードはまだありません。
diff --git a/app/views/coding_tests/coding_test_submissions/show.html.slim b/app/views/coding_tests/coding_test_submissions/show.html.slim
new file mode 100644
index 00000000000..ad7bf09e1fa
--- /dev/null
+++ b/app/views/coding_tests/coding_test_submissions/show.html.slim
@@ -0,0 +1,45 @@
+- ctd = @coding_test_submission
+- title "#{@coding_test.title}の提出コード"
+- description "#{ctd.user.login_name}さんの提出コードです。"
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ h2.page-header__title 提出コード
+
+hr.a-border
+
+.page-body
+ .container.is-md
+ .page-content.is-work
+ header.page-content-header
+ .page-content-header__start
+ .page-content-header__user
+ = link_to ctd.user,
+ itemprop: 'url',
+ class: 'page-content-header__user-link' do
+ span class="a-user-role is-#{ctd.user.primary_role}"
+ = image_tag ctd.user.avatar_url,
+ title: ctd.user.icon_title,
+ class: 'page-content-header__user-icon a-user-icon'
+ .page-content-header__end
+ .page-content-header__row
+ .page-content-header__before-title
+ = link_to ctd.user, class: 'a-user-name' do
+ = ctd.user.login_name
+ h1.page-content-header__title = title
+ .page-content-header__row
+ .page-content-header-metas
+ .page-content-header-metas__start
+ .page-content-header-metas__meta
+ .a-meta
+ .a-meta__label 提出日
+ time.a-meta__value(datetime="#{ctd.created_at}" pubdate='pubdate')
+ = l ctd.created_at
+
+ .a-card
+ .card-body
+ .card__description
+ pre(class="language-#{@coding_test.language}")
+ code(style="display:block;padding: 1em") = ctd.code
diff --git a/app/views/coding_tests/show.html.slim b/app/views/coding_tests/show.html.slim
new file mode 100644
index 00000000000..59481f381ec
--- /dev/null
+++ b/app/views/coding_tests/show.html.slim
@@ -0,0 +1,117 @@
+- title @coding_test.title
+- description "#{@coding_test.title}のコーディングテストです。"
+- practice = @coding_test.practice
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ h2.page-header__title = title
+
+hr.a-border
+
+- if current_user.submitted?(@coding_test)
+ .a-completion-message()
+ .container
+ .a-completion-message__inner
+ .a-completion-message__inner-start
+ h2.a-completion-message__title
+ | このコーディングテストは修了しました🎉
+
+.page-body
+ .container.is-xl
+ .row.is-gutter-width-32
+ .col-lg-8.col-xs-12
+
+ .practice.page-content
+ header.page-content-header
+ .page-content-header__end
+ .page-content-header__row
+ h1.page-content-header__title
+ = "#{@coding_test.title}(#{@coding_test.language})"
+
+ .a-card
+ header.card-header
+ h2.card-header__title
+ = CodingTest.human_attribute_name :description
+ hr.a-border
+ .card-body
+ .card__description
+ .a-long-text.is-md.js-markdown-view
+ = @coding_test.description
+ hr.a-border
+
+ - @coding_test.coding_test_cases.each_with_index do |coding_test_case, i|
+ .coding_test_case
+ header.card-header
+ h2.card-header__title = "入力例#{i + 1}"
+
+ .card-body
+ .card__description
+ pre.sample.coding_test_case-input(id="coding_test_case_#{coding_test_case.id}_input")
+ = coding_test_case.input
+
+ header.card-header
+ h2.card-header__title = "出力例#{i + 1}"
+
+ .card-body
+ .card__description
+ pre.sample.coding_test_case-output(id="coding_test_case_#{coding_test_case.id}_output")
+ = coding_test_case.output
+
+ hr.a-border-tint
+ footer.card-footer
+ .card-main-actions
+
+ - unless current_user.submitted?(@coding_test)
+ .a-card
+ #code_editor(
+ data-language="#{@coding_test.language}" style="width:90%;height:400px"
+ data-coding-test-id="#{@coding_test.id}"
+ data-practice-id="#{@coding_test.practice_id}"
+ )
+
+ hr.a-border-tint
+ footer.card-footer
+ .card-main-actions
+ ul.card-main-actions__items
+ li.card-main-actions__item
+ button#run.a-button.is-lg.is-primary.is-block 提出
+
+ .a-card
+ header.card-header
+ h2.card-header__title 結果
+
+ table.result-table#result
+
+ .col-lg-4.col-xs-12
+ nav.page-nav.a-card
+ header.page-nav__header
+ h2.page-nav__title
+ = link_to practice,
+ class: 'page-nav__title-inner' do
+ = practice.title
+ hr.a-border-tint
+
+ ul.page-nav__items
+ - practice.coding_tests.each do |coding_test|
+ li.page-nav__item(class="#{@coding_test == coding_test ? 'is-current' : ''}")
+ = link_to coding_test, class: 'page-nav__item-link' do
+ span.page-nav__item-link-inner
+ = coding_test.title
+
+css:
+ .result-table {
+ border-top: solid 1px hsl(242, 7%, 89%);
+ margin-top: 0.5em;
+ color: rgb(79, 79, 100);
+ }
+
+ .result-table th {
+ background-color: rgb(241, 241, 243);
+ }
+
+ .result-table th, td {
+ padding: 0.5em;
+ border: solid 1px solid 1px hsl(242, 7%, 89%);
+ }
diff --git a/app/views/mentor/_mentor_page_tabs.html.slim b/app/views/mentor/_mentor_page_tabs.html.slim
index e4f3cadb4fe..66d6f857aad 100644
--- a/app/views/mentor/_mentor_page_tabs.html.slim
+++ b/app/views/mentor/_mentor_page_tabs.html.slim
@@ -5,6 +5,10 @@
= link_to 'メンターページ',
mentor_root_path,
class: "page-tabs__item-link #{current_link(/^mentor-home/)}"
+ li.page-tabs__item
+ = link_to 'コーディングテスト',
+ mentor_coding_tests_path,
+ class: "page-tabs__item-link #{current_link(/^mentor-coding_tests/)}"
li.page-tabs__item
= link_to 'プラクティス',
mentor_practices_path,
diff --git a/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim b/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim
new file mode 100644
index 00000000000..7da66e44062
--- /dev/null
+++ b/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim
@@ -0,0 +1,13 @@
+.nested-fields
+ .field
+ = f.label :input
+ br
+ = f.text_area :input,
+ style: 'border:solid 1px black;background:white;margin:12px 0'
+ .field
+ = f.label :output
+ br
+ = f.text_area :output,
+ style: 'border:solid 1px black;background:white;margin:12px 0'
+ = link_to_remove_association '削除',
+ f, style: 'border:solid 1px black;background:white;padding:3px;margin:6px 0'
diff --git a/app/views/mentor/coding_tests/_form.html.slim b/app/views/mentor/coding_tests/_form.html.slim
new file mode 100644
index 00000000000..f96ad5c14a2
--- /dev/null
+++ b/app/views/mentor/coding_tests/_form.html.slim
@@ -0,0 +1,60 @@
+= form_with model: [:mentor, coding_test], local: true, html: { name: 'coding_test', class: 'form' } do |f|
+ = render 'errors', object: coding_test
+ = f.hidden_field :course_id, value: params[:course_id]
+ .form__items
+ .form-item
+ .row
+ .col-lg-6.col-xs-12
+ = f.label :practice, class: 'a-form-label'
+ .select-practices
+ = f.collection_select :practice_id,
+ Practice.all,
+ :id,
+ :title,
+ { include_blank: 'プラクティスを選択してください' },
+ { id: 'js-choices-single-select' }
+ .col-md-3.col-xs-6
+ = f.label :language, class: 'a-form-label'
+ .select-users
+ = f.select :language,
+ CodingTest.languages.keys,
+ { include_blank: '言語を選択してください' },
+ { class: 'js-select2' }
+ .form-item
+ .row
+ .col-md-6.col-xs-12
+ = f.label :title, class: 'a-form-label'
+ = f.text_field :title, class: 'a-text-input js-warning-form', placeholder: '文字列操作'
+ .col-md-3.col-xs-6
+ = f.label :user, class: 'a-form-label'
+ .select-users
+ = f.select :user_id,
+ User.where(retired_on: nil).pluck(:login_name, :id).sort,
+ { include_blank: '作成者を選択してください' },
+ { class: 'js-select2' }
+ .form-item
+ .row.js-markdown-parent
+ .col-md-6.col-xs-12
+ = f.label :description, class: 'a-form-label'
+ = f.text_area :description, class: 'a-text-input js-warning-form js-markdown markdown-form__text-area practices-edit__input', data: { 'preview': '.js-preview' }
+ .col-md-6.col-xs-12
+ .a-form-label プレビュー
+ .js-preview.a-long-text.is-md.practices-edit__input.markdown-form__preview
+
+ h2(style='font-weight:bold;margin:12px 0;') テストケース
+
+ .form-item
+ = f.fields_for :coding_test_cases do |coding_test_case|
+ = render 'coding_test_case_fields', f: coding_test_case
+ .links
+ = link_to_add_association '追加', f, :coding_test_cases, style: 'border:solid 1px black;background:white;padding:3px;margin:6px 0'
+
+ .form-actions
+ ul.form-actions__items
+ li.form-actions__item.is-main
+ = f.submit nil, class: 'a-button is-lg is-primary is-block'
+ li.form-actions__item.is-sub
+ = link_to 'キャンセル', mentor_categories_path, class: 'a-button is-sm is-text'
+ - if coding_test.id.present?
+ li.form-actions__item.is-muted
+ = link_to '削除', mentor_coding_test_path(coding_test), method: :delete, data: { confirm: '本当によろしいですか?' }, class: 'a-button is-sm is-muted-text'
diff --git a/app/views/mentor/coding_tests/edit.html.slim b/app/views/mentor/coding_tests/edit.html.slim
new file mode 100644
index 00000000000..5ceeb3f0ebc
--- /dev/null
+++ b/app/views/mentor/coding_tests/edit.html.slim
@@ -0,0 +1,27 @@
+- title 'コーディング問題編集'
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ .page-header__title メンターページ
+
+= render 'mentor/mentor_page_tabs'
+
+.page-main
+ header.page-main-header
+ .container
+ .page-main-header__inner
+ .page-main-header__start
+ h1.page-main-header__title
+ = title
+ .page-main-header__end
+ .page-main-header-actions
+ .page-main-header-actions__items
+ .page-main-header-actions__item
+ = link_to mentor_coding_tests_path, class: 'a-button is-md is-secondary is-block is-back' do
+ | コーディング問題
+ hr.a-border
+ .page-body
+ .container.is-xxl
+ = render 'form', coding_test: @coding_test
diff --git a/app/views/mentor/coding_tests/index.html.slim b/app/views/mentor/coding_tests/index.html.slim
new file mode 100644
index 00000000000..88695ac4e99
--- /dev/null
+++ b/app/views/mentor/coding_tests/index.html.slim
@@ -0,0 +1,61 @@
+- title 'コーディング問題'
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ h2.page-header__title メンターページ
+
+= render 'mentor/mentor_page_tabs'
+
+main.page-main
+ header.page-main-header
+ .container
+ .page-main-header__inner
+ .page-main-header__start
+ h1.page-main-header__title = title
+ .page-main-header__end
+ .page-header-actions
+ .page-header-actions__items
+ .page-header-actions__item
+ = link_to new_mentor_coding_test_path, class: 'a-button is-md is-secondary is-block' do
+ i.fa-regular.fa-plus
+ | コーディング問題作成
+
+ hr.a-border
+ .page-body
+ .container.is-lg
+ .admin-table.is-grab id='mentor-practices'
+ table.admin-table__table
+ thead.admin-table__header
+ tr.admin-table__labels
+ th.admin-table__label = CodingTest.human_attribute_name :title
+ th.admin-table__label = CodingTest.human_attribute_name :language
+ th.admin-table__label 入力例・出力例
+ th.admin-table__label = CodingTest.human_attribute_name :practice
+ th.admin-table__label = CodingTest.human_attribute_name :coding_test_submissions
+ th.admin-table__label.actions 編集
+ tbody.admin-table__items
+ - @coding_tests.each do |coding_test|
+ tr.admin-table__item
+ td.admin-table__item-value
+ = link_to coding_test.title, coding_test
+ td.admin-table__item-value
+ = coding_test.language
+ td.admin-table__item-value
+ ul
+ - coding_test.coding_test_cases.each do |coding_test_case|
+ li
+ = coding_test_case.input
+ = ' / '
+ = coding_test_case.output
+ td.admin-table__item-value
+ = link_to coding_test.practice.title, coding_test.practice
+ td.admin-table__item-value.is-text-align-center
+ = link_to '一覧', [coding_test, :coding_test_submissions]
+ td.admin-table__item-value.is-text-align-center
+ ul.is-inline-buttons
+ li
+ = link_to edit_mentor_coding_test_path(coding_test),
+ class: 'a-button is-sm is-secondary is-icon'
+ i.fa-solid.fa-pen
diff --git a/app/views/mentor/coding_tests/new.html.slim b/app/views/mentor/coding_tests/new.html.slim
new file mode 100644
index 00000000000..1113a418602
--- /dev/null
+++ b/app/views/mentor/coding_tests/new.html.slim
@@ -0,0 +1,27 @@
+- title 'メンターページ'
+
+header.page-header
+ .container
+ .page-header__inner
+ .page-header__start
+ h2.page-header__title
+ = title
+
+= render 'mentor/mentor_page_tabs'
+
+.page-main
+ header.page-main-header
+ .container
+ .page-main-header__inner
+ .page-main-header__start
+ h1.page-main-header__title コーディング問題作成
+ .page-main-header__end
+ .page-main-header-actions
+ .page-main-header-actions__items
+ .page-main-header-actions__item
+ = link_to mentor_coding_tests_path, class: 'a-button is-md is-secondary is-block is-back' do
+ | コーディング問題
+ hr.a-border
+ .page-body
+ .container.is-xxl
+ = render 'form', coding_test: @coding_test
diff --git a/app/views/practices/_coding_tests.html.slim b/app/views/practices/_coding_tests.html.slim
new file mode 100644
index 00000000000..2e35c950014
--- /dev/null
+++ b/app/views/practices/_coding_tests.html.slim
@@ -0,0 +1,31 @@
+.a-card.coding_tests
+ header.card-header
+ h2.card-header__title コーディング問題
+
+ table
+ - coding_tests.order(:position).each do |coding_test|
+ tr
+ td = link_to coding_test.title, coding_test
+ td = coding_test.passed_by?(current_user) ? 'O' : 'X'
+ hr.a-border-tint
+ footer.card-footer
+
+css:
+ .coding_tests {
+ margin-bottom: 1.5em;
+ }
+
+ .coding_tests table {
+ border-top: solid 1px hsl(242, 7%, 89%);
+ margin-top: 0.5em;
+ color: rgb(79, 79, 100);
+ }
+
+ .coding_tests th {
+ background-color: rgb(241, 241, 243);
+ }
+
+ .coding_tests th, td {
+ padding: 0.5em;
+ border: solid 1px solid 1px hsl(242, 7%, 89%);
+ }
diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim
index 15072b8e526..71e8d9c34bf 100644
--- a/app/views/practices/show.html.slim
+++ b/app/views/practices/show.html.slim
@@ -111,6 +111,9 @@
br
| 終了条件をクリアしたら修了にしてください。
+ - if @practice.coding_tests.present?
+ = render partial: 'coding_tests', locals: { coding_tests: @practice.coding_tests }
+
- if current_user.admin_or_mentor?
= render 'memo', practice: @practice
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index b359f5f89a9..6412fc8b2b3 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -47,6 +47,7 @@ ja:
book: 参考書籍
regular_event: 定期イベント
request_retirement: 退会申請
+ coding_test: コーディング問題
attributes:
user:
login_name: アカウント
@@ -301,6 +302,16 @@ ja:
request_retirement/keep_data/option:
keep: 残す
delete: 削除する
+ coding_test:
+ title: タイトル
+ description: 問題文
+ user: 作成者
+ practice: 所属プラクティス
+ language: 言語
+ coding_test_submissions: 提出コード
+ coding_test_case:
+ input: 入力
+ output: 出力
enums:
user:
job:
diff --git a/config/routes.rb b/config/routes.rb
index 39da1941562..0fd47290f51 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -50,6 +50,11 @@
resource :completion, only: %i(show), controller: "practices/completion"
resource :submission_answer, only: %i(show), controller: "practices/submission_answer"
end
+ resources :coding_tests, only: %i(show) do
+ resources :coding_test_submissions,
+ only: %i(index show new create show),
+ controller: "coding_tests/coding_test_submissions"
+ end
resources :pages, param: :slug_or_id
namespace :notification do
resource :redirector, only: %i(show), controller: "redirector"
diff --git a/config/routes/api.rb b/config/routes/api.rb
index 8a9d29ff48e..c9c754692f5 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -45,6 +45,7 @@
resource :completion_message, only: %i(update), controller: "practices/learning/completion_message"
end
end
+ resources :coding_test_submissions, only: %i(create)
resources :reports, only: %i(index)
namespace "reports" do
resources :unchecked, only: %i(index) do
diff --git a/config/routes/mentor.rb b/config/routes/mentor.rb
index fba2e993ac6..f3059fc1a1d 100644
--- a/config/routes/mentor.rb
+++ b/config/routes/mentor.rb
@@ -9,6 +9,7 @@
resources :practices, only: %i(index new edit create update) do
resource :submission_answer, only: %i(new edit create update), controller: "practices/submission_answer"
end
+ resources :coding_tests, only: %i(index new edit create update destroy)
resources :courses, only: %i(index new edit create update) do
resources :categories, only: %i(index), controller: "courses/categories"
end
diff --git a/db/fixtures/coding_test_cases.yml b/db/fixtures/coding_test_cases.yml
new file mode 100644
index 00000000000..a06e780e73e
--- /dev/null
+++ b/db/fixtures/coding_test_cases.yml
@@ -0,0 +1,74 @@
+coding_test_case1:
+ coding_test: coding_test1
+ input: ""
+ output: hello
+
+coding_test_case2:
+ coding_test: coding_test2
+ input: world
+ output: hello world
+
+coding_test_case3:
+ coding_test: coding_test2
+ input: programming
+ output: hello programming
+
+coding_test_case4:
+ coding_test: coding_test3
+ input: 1
+ output: 1
+
+coding_test_case5:
+ coding_test: coding_test3
+ input: 3
+ output: Fizz
+
+coding_test_case6:
+ coding_test: coding_test3
+ input: 5
+ output: Buzz
+
+coding_test_case7:
+ coding_test: coding_test3
+ input: 15
+ output: FizzBuzz
+
+coding_test_case8:
+ coding_test: coding_test4
+ input: a b c d
+ output: d c b a
+
+coding_test_case9:
+ coding_test: coding_test4
+ input: hello
+ output: olleh
+
+coding_test_case10:
+ coding_test: coding_test5
+ input: hello
+ output: h e l l o
+
+coding_test_case11:
+ coding_test: coding_test5
+ input: world
+ output: w o r l d
+
+coding_test_case12:
+ coding_test: coding_test6
+ input: ""
+ output: hello
+
+coding_test_case13:
+ coding_test: coding_test7
+ input: 3
+ output: Fizz
+
+coding_test_case14:
+ coding_test: coding_test7
+ input: 5
+ output: Buzz
+
+coding_test_case15:
+ coding_test: coding_test7
+ input: 15
+ output: FizzBuzz
diff --git a/db/fixtures/coding_test_submissions.yml b/db/fixtures/coding_test_submissions.yml
new file mode 100644
index 00000000000..a0248f084e0
--- /dev/null
+++ b/db/fixtures/coding_test_submissions.yml
@@ -0,0 +1,30 @@
+coding_test_submission1:
+ coding_test: coding_test1
+ user: hajime
+ code: |-
+ console.log(\"hello\")
+
+coding_test_submission6:
+ coding_test: coding_test6
+ user: hajime
+ code: |-
+ puts 'hello'
+
+coding_test_submission7:
+ coding_test: coding_test7
+ user: hajime
+ code: |-
+ input = gets.to_i
+
+ output =
+ if input % 15 == 0
+ 'FizzBuzz'
+ elsif input % 3 == 0
+ 'Fizz'
+ elsif input % 5 == 0
+ 'Buzz'
+ else
+ input.to_s
+ end
+
+ puts output
diff --git a/db/fixtures/coding_tests.yml b/db/fixtures/coding_tests.yml
new file mode 100644
index 00000000000..8a173fe1744
--- /dev/null
+++ b/db/fixtures/coding_tests.yml
@@ -0,0 +1,80 @@
+coding_test1:
+ practice: practice62
+ user: komagata
+ language: javascript
+ title: 最初の出力
+ description: |-
+ `hello`という文字列を出力しなさい。
+ hint: ""
+ position: 1
+
+coding_test2:
+ practice: practice62
+ user: komagata
+ language: javascript
+ title: 最初の入力
+ description: |-
+ 文字列sが与えられます。
+ `hello `とsをつなげて出力しなさい。
+ hint: ""
+ position: 2
+
+coding_test3:
+ practice: practice62
+ user: komagata
+ language: javascript
+ title: 条件分岐
+ description: |-
+ 正の整数aが与えられます。
+ aが3の倍数なら`Fizz`と出力してください。
+ aが5の倍数なら`Buzz`と出力してください。
+ aが3の倍数かつ5の倍数なら`FizzBuzz`と出力してください。
+ どれでもないならaを出力してください。
+ hint: 条件分岐にはifが使えます。
+ position: 3
+
+coding_test4:
+ practice: practice62
+ user: komagata
+ language: javascript
+ title: 配列を並び替え
+ description: |-
+ 文字a, b, c, dが与えられます。
+ 逆に並び替えて`d c b a`と出力してください。
+ hint: 逆に並び替えるにはreverseが使えます。
+ position: 4
+
+coding_test5:
+ practice: practice62
+ user: komagata
+ language: javascript
+ title: スペース区切り
+ description: |-
+ 文字列aが与えられます。
+ 1文字ずつスペースを入れて出力してください。
+ hint: 文字列を分割するにはsplitが使えます。
+ position: 5
+
+coding_test6:
+ practice: practice26
+ user: komagata
+ language: ruby
+ title: 最初の出力
+ description: |-
+ `hello`という文字列を出力しなさい。
+ hint: ""
+ position: 1
+
+coding_test7:
+ practice: practice26
+ user: komagata
+ language: ruby
+ title: 条件分岐
+ description: |-
+ 正の整数aが与えられます。
+ aが3の倍数なら`Fizz`と出力してください。
+ aが5の倍数なら`Buzz`と出力してください。
+ aが3の倍数かつ5の倍数なら`FizzBuzz`と出力してください。
+ どれでもないならaを出力してください。
+ hint: 条件分岐にはifが使えます。
+ position: 2
diff --git a/db/migrate/20240601203433_create_coding_tests.rb b/db/migrate/20240601203433_create_coding_tests.rb
new file mode 100644
index 00000000000..81474dec4c8
--- /dev/null
+++ b/db/migrate/20240601203433_create_coding_tests.rb
@@ -0,0 +1,15 @@
+class CreateCodingTests < ActiveRecord::Migration[6.1]
+ def change
+ create_table :coding_tests do |t|
+ t.integer :language, null: false
+ t.string :title, null: false
+ t.text :description
+ t.text :hint
+ t.integer :position
+ t.references :practice, null: false, foreign_key: true
+ t.references :user, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240601212831_create_coding_test_cases.rb b/db/migrate/20240601212831_create_coding_test_cases.rb
new file mode 100644
index 00000000000..554511703ee
--- /dev/null
+++ b/db/migrate/20240601212831_create_coding_test_cases.rb
@@ -0,0 +1,11 @@
+class CreateCodingTestCases < ActiveRecord::Migration[6.1]
+ def change
+ create_table :coding_test_cases do |t|
+ t.text :input
+ t.text :output
+ t.references :coding_test, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240601212854_create_coding_test_submissions.rb b/db/migrate/20240601212854_create_coding_test_submissions.rb
new file mode 100644
index 00000000000..06063ea0cdb
--- /dev/null
+++ b/db/migrate/20240601212854_create_coding_test_submissions.rb
@@ -0,0 +1,12 @@
+class CreateCodingTestSubmissions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :coding_test_submissions do |t|
+ t.text :code, null: false
+ t.references :coding_test, null: false, foreign_key: true
+ t.references :user, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ add_index :coding_test_submissions, [:coding_test_id, :user_id], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 23e1e1716c3..0b20059fc92 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -172,6 +172,40 @@
t.index ["user_id"], name: "index_checks_on_user_id"
end
+ create_table "coding_test_cases", force: :cascade do |t|
+ t.text "input"
+ t.text "output"
+ t.bigint "coding_test_id", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["coding_test_id"], name: "index_coding_test_cases_on_coding_test_id"
+ end
+
+ create_table "coding_test_submissions", force: :cascade do |t|
+ t.text "code", null: false
+ t.bigint "coding_test_id", null: false
+ t.bigint "user_id", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["coding_test_id", "user_id"], name: "index_coding_test_submissions_on_coding_test_id_and_user_id", unique: true
+ t.index ["coding_test_id"], name: "index_coding_test_submissions_on_coding_test_id"
+ t.index ["user_id"], name: "index_coding_test_submissions_on_user_id"
+ end
+
+ create_table "coding_tests", force: :cascade do |t|
+ t.integer "language", null: false
+ t.string "title", null: false
+ t.text "description"
+ t.text "hint"
+ t.integer "position"
+ t.bigint "practice_id", null: false
+ t.bigint "user_id", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["practice_id"], name: "index_coding_tests_on_practice_id"
+ t.index ["user_id"], name: "index_coding_tests_on_user_id"
+ end
+
create_table "comments", id: :serial, force: :cascade do |t|
t.text "description"
t.integer "user_id"
@@ -806,6 +840,11 @@
add_foreign_key "categories_practices", "practices"
add_foreign_key "check_box_choices", "check_boxes"
add_foreign_key "check_boxes", "survey_questions"
+ add_foreign_key "coding_test_cases", "coding_tests"
+ add_foreign_key "coding_test_submissions", "coding_tests"
+ add_foreign_key "coding_test_submissions", "users"
+ add_foreign_key "coding_tests", "practices"
+ add_foreign_key "coding_tests", "users"
add_foreign_key "discord_profiles", "users"
add_foreign_key "external_entries", "users"
add_foreign_key "hibernations", "users"
diff --git a/db/seeds.rb b/db/seeds.rb
index acc291f14b4..515cbb34d41 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -60,6 +60,9 @@
survey_question_listings
buzzes
inquiries
+ coding_tests
+ coding_test_cases
+ coding_test_submissions
]
ActiveRecord::FixtureSet.create_fixtures 'db/fixtures', tables
diff --git a/package.json b/package.json
index 29d589e3ffd..d535e139e3d 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,11 @@
},
"dependencies": {
"@babel/preset-react": "^7.18.6",
+ "@codemirror/basic-setup": "^0.20.0",
"@johmun/vue-tags-input": "^2.0.1",
"@rails/webpacker": "5.4.3",
"@yaireo/tagify": "^4.17.6",
+ "ace-builds": "^1.35.0",
"autosize": "^4.0.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"choices.js": "^10.1.0",
diff --git a/public/javascript.js b/public/javascript.js
new file mode 100644
index 00000000000..40892a3ec70
--- /dev/null
+++ b/public/javascript.js
@@ -0,0 +1,48 @@
+/**
+ * @file OnBrowserJudge - for JavaScript
+ * @author nodai2hITC
+ * @license MIT License
+ * @version v1.0.0
+ */
+
+"use strict"
+
+self.postMessage(["init"])
+
+self.addEventListener("message", async function(e) {
+ switch (e.data[0]) {
+ case "init":
+ this.self.postMessage(["ready"])
+ break
+ case "execute":
+ const data = e.data[1]
+ const testCase = data.testCase
+ const program = data.program
+ const input = data.input
+
+ let output = ""
+ const _readFileSync = {
+ readFileSync: function(stdin, encoding){
+ if (stdin != "/dev/stdin" || encoding != "utf8") throw "readFileSync error"
+ return input
+ }
+ }
+ const _console = { log: function(text) { output += text + "\n" } }
+ const _require = function(fs) {
+ if (fs != "fs") throw "require error"
+ return _readFileSync
+ }
+
+ let error = 0
+ let errorMessage = ""
+ const startTime = performance.now()
+ try {
+ (new Function("console", "require", program))(_console, _require)
+ } catch(err) {
+ errorMessage = err.toString()
+ error = 2
+ }
+ const execTime = performance.now() - startTime
+ self.postMessage(["executed", { testCase, output, error, errorMessage, execTime }])
+ }
+})
diff --git a/public/ruby.js b/public/ruby.js
new file mode 100644
index 00000000000..bc82c1f1a01
--- /dev/null
+++ b/public/ruby.js
@@ -0,0 +1,54 @@
+/**
+ * @file OnBrowserJudge - for Ruby
+ * @author nodai2hITC
+ * @license MIT License
+ * @version v1.0.0
+ */
+
+"use strict"
+
+const script = "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0-2022-12-19-a/dist/browser.umd.js"
+const wasm = "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0-2022-12-19-a/dist/ruby+stdlib.wasm"
+
+importScripts(script)
+let RubyModule
+const { DefaultRubyVM } = this["ruby-wasm-wasi"]
+const main = async () => {
+ const response = await fetch(wasm)
+ const buffer = await response.arrayBuffer()
+ RubyModule = await WebAssembly.compile(buffer)
+ self.postMessage(["init"])
+}
+main()
+
+self.addEventListener("message", async function(e) {
+ switch (e.data[0]) {
+ case "init":
+ this.self.postMessage(["ready"])
+ break
+ case "execute":
+ const data = e.data[1]
+ const testCase = data.testCase
+ const program = data.program
+ const input = data.input
+ const { vm } = await DefaultRubyVM(RubyModule)
+ vm.eval(`
+require 'stringio'
+$stdin = StringIO.new('${input.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}', 'r')
+$stdout = $stderr = StringIO.new(+'', 'w')
+ `)
+ let output = ""
+ let error = 0
+ let errorMessage = ""
+ const startTime = performance.now()
+ try {
+ vm.eval(program)
+ output = vm.eval("$stdout.string").toString()
+ } catch(err) {
+ errorMessage = err.toString()
+ error = 2
+ }
+ const execTime = performance.now() - startTime
+ self.postMessage(["executed", { testCase, output, error, errorMessage, execTime }])
+ }
+})
diff --git a/test/models/coding_test_case_test.rb b/test/models/coding_test_case_test.rb
new file mode 100644
index 00000000000..3bc9066f115
--- /dev/null
+++ b/test/models/coding_test_case_test.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class CodingTestCaseTest < ActiveSupport::TestCase
+end
diff --git a/test/models/coding_test_language_test.rb b/test/models/coding_test_language_test.rb
new file mode 100644
index 00000000000..0d10dd69f13
--- /dev/null
+++ b/test/models/coding_test_language_test.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class CodingTestLanguageTest < ActiveSupport::TestCase
+end
diff --git a/test/models/coding_test_submission_test.rb b/test/models/coding_test_submission_test.rb
new file mode 100644
index 00000000000..5c722c9316c
--- /dev/null
+++ b/test/models/coding_test_submission_test.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class CodingTestSubmissionTest < ActiveSupport::TestCase
+end
diff --git a/test/models/coding_test_test.rb b/test/models/coding_test_test.rb
new file mode 100644
index 00000000000..c6038643fcd
--- /dev/null
+++ b/test/models/coding_test_test.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class CodingTestTest < ActiveSupport::TestCase
+end
diff --git a/yarn.lock b/yarn.lock
index 60be03a5b5d..9278b0b2904 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1034,6 +1034,83 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
+"@codemirror/autocomplete@^0.20.0":
+ version "0.20.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz#affe2d7e2b2e0be42ee1ac5fb74a1c84a6f1bfd7"
+ integrity sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==
+ dependencies:
+ "@codemirror/language" "^0.20.0"
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.0"
+ "@lezer/common" "^0.16.0"
+
+"@codemirror/basic-setup@^0.20.0":
+ version "0.20.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz#ed331e0b2d29efc0a09317de9e10467b992b0c7b"
+ integrity sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==
+ dependencies:
+ "@codemirror/autocomplete" "^0.20.0"
+ "@codemirror/commands" "^0.20.0"
+ "@codemirror/language" "^0.20.0"
+ "@codemirror/lint" "^0.20.0"
+ "@codemirror/search" "^0.20.0"
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.0"
+
+"@codemirror/commands@^0.20.0":
+ version "0.20.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-0.20.0.tgz#51405d442e6b8687b63e8fa27effc28179917c88"
+ integrity sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==
+ dependencies:
+ "@codemirror/language" "^0.20.0"
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.0"
+ "@lezer/common" "^0.16.0"
+
+"@codemirror/language@^0.20.0":
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-0.20.2.tgz#31c3712eac2251810986272dcd6a50510e0c1529"
+ integrity sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==
+ dependencies:
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.0"
+ "@lezer/common" "^0.16.0"
+ "@lezer/highlight" "^0.16.0"
+ "@lezer/lr" "^0.16.0"
+ style-mod "^4.0.0"
+
+"@codemirror/lint@^0.20.0":
+ version "0.20.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-0.20.3.tgz#34c0fd45c5acd522637f68602e3a416162e03a15"
+ integrity sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==
+ dependencies:
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.2"
+ crelt "^1.0.5"
+
+"@codemirror/search@^0.20.0":
+ version "0.20.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-0.20.1.tgz#9eba0514218a673e29501a889a4fcb7da7ce24ad"
+ integrity sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==
+ dependencies:
+ "@codemirror/state" "^0.20.0"
+ "@codemirror/view" "^0.20.0"
+ crelt "^1.0.5"
+
+"@codemirror/state@^0.20.0":
+ version "0.20.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.20.1.tgz#de5c6dc0de3e216eaa3a9ee9391c926b766f6b46"
+ integrity sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==
+
+"@codemirror/view@^0.20.0", "@codemirror/view@^0.20.2":
+ version "0.20.7"
+ resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-0.20.7.tgz#1d0acc740f71f92abef4b437c030d4e6c39ab6dc"
+ integrity sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==
+ dependencies:
+ "@codemirror/state" "^0.20.0"
+ style-mod "^4.0.0"
+ w3c-keyname "^2.2.4"
+
"@csstools/convert-colors@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
@@ -1226,6 +1303,25 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
+"@lezer/common@^0.16.0":
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.16.1.tgz#3b98b42fdb11454b89e8a340da10bee1b0f94071"
+ integrity sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==
+
+"@lezer/highlight@^0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-0.16.0.tgz#95f7b7ee3c32c8a0f6ce499c085e8b1f927ffbdc"
+ integrity sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==
+ dependencies:
+ "@lezer/common" "^0.16.0"
+
+"@lezer/lr@^0.16.0":
+ version "0.16.3"
+ resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.16.3.tgz#1e4cc581d2725c498e6a731fc83c379114ba3a70"
+ integrity sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==
+ dependencies:
+ "@lezer/common" "^0.16.0"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1562,6 +1658,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
+ace-builds@^1.35.0:
+ version "1.35.0"
+ resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.35.0.tgz#141fd103016d1a1a7febab57a223af28c75a89bc"
+ integrity sha512-bwDKqjqNccC/MSujqnYTeAS5dIR8UmGLP0R90mvsJY0FRC8NUWBSTfj34+EIzo2NWc/gV8IZTqv4fXaiZJpCtA==
+
acorn-jsx@^5.2.0, acorn-jsx@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -2768,6 +2869,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+crelt@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
+ integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
+
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -8774,6 +8880,11 @@ style-loader@^1.3.0:
loader-utils "^2.0.0"
schema-utils "^2.7.0"
+style-mod@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
+ integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
+
stylehacks@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@@ -9460,6 +9571,11 @@ vuex@^3.1.1:
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
+w3c-keyname@^2.2.4:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
+ integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
+
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"