From 858b33d1ae274307a7d37605243a47b68337d836 Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Tue, 4 Jul 2023 18:24:18 +0300
Subject: [PATCH 1/6] refactor: remove excess check in AchievementService
---
app/services/AchievementService.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/services/AchievementService.rb b/app/services/AchievementService.rb
index 0ee824a..6b3f634 100644
--- a/app/services/AchievementService.rb
+++ b/app/services/AchievementService.rb
@@ -18,7 +18,7 @@ def reward(badge)
end
def first_try?(_)
- @user.test_passages.where(test_id: @test.id, finished: true).count == 1
+ @user.test_passages.where(test_id: @test.id).count == 1
end
def category_complete?(category)
From cdfb0a3d3c144babbe85e83d3a02c8af4186a883 Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Wed, 5 Jul 2023 01:35:02 +0300
Subject: [PATCH 2/6] feat: migration add duration time in the table tests
---
db/migrate/20230704222539_add_timer_to_tests.rb | 5 +++++
db/schema.rb | 3 ++-
2 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 db/migrate/20230704222539_add_timer_to_tests.rb
diff --git a/db/migrate/20230704222539_add_timer_to_tests.rb b/db/migrate/20230704222539_add_timer_to_tests.rb
new file mode 100644
index 0000000..3f31743
--- /dev/null
+++ b/db/migrate/20230704222539_add_timer_to_tests.rb
@@ -0,0 +1,5 @@
+class AddTimerToTests < ActiveRecord::Migration[6.1]
+ def change
+ add_column :tests, :duration_time, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0b748b8..8e0d797 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_06_29_235442) do
+ActiveRecord::Schema.define(version: 2023_07_04_222539) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -93,6 +93,7 @@
t.datetime "updated_at", precision: 6, null: false
t.bigint "author_id"
t.boolean "active", default: false
+ t.integer "duration_time"
t.index ["author_id"], name: "index_tests_on_author_id"
t.index ["category_id"], name: "index_tests_on_category_id"
t.index ["title", "level"], name: "index_tests_on_title_and_level", unique: true
From bf346bf89fe7de8edf3015ae2dc9d11296134e17 Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Wed, 5 Jul 2023 02:23:04 +0300
Subject: [PATCH 3/6] feat: add function setup timer in test
---
app/controllers/admin/tests_controller.rb | 2 +-
app/views/admin/tests/_form.html.erb | 4 ++++
app/views/admin/tests/_test.html.erb | 1 +
app/views/admin/tests/index.html.erb | 1 +
app/views/tests/_test.html.erb | 1 +
app/views/tests/index.html.erb | 1 +
config/locales/activerecord.en.yml | 1 +
config/locales/activerecord.ru.yml | 1 +
config/locales/en.yml | 3 +++
config/locales/ru.yml | 3 +++
10 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/app/controllers/admin/tests_controller.rb b/app/controllers/admin/tests_controller.rb
index 2d191f6..4a14408 100644
--- a/app/controllers/admin/tests_controller.rb
+++ b/app/controllers/admin/tests_controller.rb
@@ -42,7 +42,7 @@ def destroy
private
def test_params
- params.require(:test).permit(:title, :level, :category_id, :active)
+ params.require(:test).permit(:title, :level, :category_id, :active, :duration_time)
end
def find_test
diff --git a/app/views/admin/tests/_form.html.erb b/app/views/admin/tests/_form.html.erb
index 1d1eece..d651262 100644
--- a/app/views/admin/tests/_form.html.erb
+++ b/app/views/admin/tests/_form.html.erb
@@ -11,6 +11,10 @@
<%= form.label :level %>
<%= form.number_field :level, id: :test_level, class: 'form-control' %>
+
+ <%= form.label :duration_time %>
+ <%= form.number_field :duration_time, id: :test_duration_time, class: 'form-control' %>
+
<%= form.label :category_id %>
<%= form.collection_select :category_id, Category.all, :id, :title, prompt: true, class: 'dropdown-menu' %>
diff --git a/app/views/admin/tests/_test.html.erb b/app/views/admin/tests/_test.html.erb
index 8bde97e..8f64a50 100644
--- a/app/views/admin/tests/_test.html.erb
+++ b/app/views/admin/tests/_test.html.erb
@@ -5,6 +5,7 @@
<%= render 'form_inline', test: test == @test ? @test : test %>
<%= test.level %> |
+ <%= test.duration_time %> |
<%= link_to test.questions.count, admin_test_questions_path(test.id) %> |
<%= test.active %> |
diff --git a/app/views/admin/tests/index.html.erb b/app/views/admin/tests/index.html.erb
index 366f7ee..3fc7fdf 100644
--- a/app/views/admin/tests/index.html.erb
+++ b/app/views/admin/tests/index.html.erb
@@ -7,6 +7,7 @@
| <%= t('.id') %> |
<%= t('.name') %> |
<%= t('.level') %> |
+ <%= t('.duration_time') %> |
<%= t('.count_question') %> |
<%= t('.active') %> |
<%= t('.action') %> |
diff --git a/app/views/tests/_test.html.erb b/app/views/tests/_test.html.erb
index f981457..235c11f 100644
--- a/app/views/tests/_test.html.erb
+++ b/app/views/tests/_test.html.erb
@@ -2,6 +2,7 @@
<%= test.id %> |
<%= test.title %> |
<%= test.level %> |
+ <%= test.duration_time %> |
<%= test.questions.count %> |
<%= button_to t('.start'), start_test_path(test), class: 'btn btn-primary' %> |
diff --git a/app/views/tests/index.html.erb b/app/views/tests/index.html.erb
index 01d1ca5..1e2cd0a 100644
--- a/app/views/tests/index.html.erb
+++ b/app/views/tests/index.html.erb
@@ -10,6 +10,7 @@
<%= octicon 'arrow-down', class: 'text-success hide' %>
<%= t('.level')%> |
+ <%= t('.duration_time')%> |
<%= t('.count_question')%> |
<%= t('.start')%> |
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index 0cde948..07edfe0 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -18,6 +18,7 @@ en:
level: 'Level'
category: 'Category'
active: 'Active'
+ duration_time: 'Duration time'
test_passage:
correct_questions: 'Correct questions'
feedback:
diff --git a/config/locales/activerecord.ru.yml b/config/locales/activerecord.ru.yml
index d7b1951..b15f343 100644
--- a/config/locales/activerecord.ru.yml
+++ b/config/locales/activerecord.ru.yml
@@ -18,6 +18,7 @@ ru:
level: 'Сложность'
category: 'Категория'
active: 'Активный'
+ duration_time: 'Время продолжительности'
test_passage:
correct_questions: 'Правильные ответы'
feedback:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index a6dec42..88bf066 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -60,6 +60,7 @@ en:
level: 'Level'
category_id: 'Category'
active: 'Active'
+ duration_time: 'Duration time (minutes)'
feedback:
title: 'Topic'
body: 'Feedback'
@@ -107,6 +108,7 @@ en:
header: 'Test list:'
gists: 'Show Gists'
badges: 'Show Badges'
+ duration_time: 'Duration time (minutes)'
show:
id: 'Id'
question: 'Question'
@@ -140,6 +142,7 @@ en:
start: 'Start'
header: 'List of available tests:'
my_badges: 'Show my Badges'
+ duration_time: 'Duration time (minutes)'
test:
start: 'Start'
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index de88c09..545f781 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -28,6 +28,7 @@ ru:
level: 'Сложность'
category_id: 'Категория'
active: 'Активный'
+ duration_time: 'Время продолжительности (минут)'
feedback:
title: 'Тема отзыва'
body: 'Обратная связь'
@@ -75,6 +76,7 @@ ru:
header: 'Список тестов:'
gists: 'Посмотреть Гисты'
badges: 'Посмотреть Бейджи'
+ duration_time: 'Время продолжительности (минут)'
show:
id: 'Номер'
question: 'Вопрос'
@@ -108,6 +110,7 @@ ru:
start: 'Пройти тест'
header: 'Список доступных тестов:'
my_badges: 'Показать мои Бейджи'
+ duration_time: 'Время продолжительности (минут)'
test:
start: 'Начать'
From 85a3d1a7fb3e7b313c1e70e2e970f62130160df5 Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Wed, 5 Jul 2023 03:26:08 +0300
Subject: [PATCH 4/6] feat: add a test duration timer to the session
---
app/controllers/test_passages_controller.rb | 16 +++++++++++++++-
app/controllers/tests_controller.rb | 1 +
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/app/controllers/test_passages_controller.rb b/app/controllers/test_passages_controller.rb
index 08f20d6..64b8418 100644
--- a/app/controllers/test_passages_controller.rb
+++ b/app/controllers/test_passages_controller.rb
@@ -1,9 +1,13 @@
class TestPassagesController < ApplicationController
before_action :find_test_passage, only: %i[show result update gist]
+ before_action :get_end_time, only: %i[update result]
+ before_action :check_end_time, only: %i[update]
def show; end
- def result; end
+ def result
+ session[:"end_time_#{@test_passage.id}"] = nil
+ end
def update
@test_passage.accept!(params[:answer_ids])
@@ -41,6 +45,16 @@ def gist
private
+ def get_end_time
+ @end_time = session[:"end_time_#{@test_passage.id}"]
+ end
+
+ def check_end_time
+ if @end_time.present? && @end_time <= Time.now
+ redirect_to result_test_passage_path(@test_passage)
+ end
+ end
+
def find_test_passage
@test_passage = TestPassage.find(params[:id])
end
diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb
index 2e2b2a5..5de8715 100644
--- a/app/controllers/tests_controller.rb
+++ b/app/controllers/tests_controller.rb
@@ -7,5 +7,6 @@ def start
@test = Test.find(params[:id])
current_user.tests.push(@test)
redirect_to current_user.test_passage(@test)
+ session[:"end_time_#{TestPassage.last.id}"] = Time.now + @test.duration_time.minute if @test.duration_time.present?
end
end
From 29485240f96996ca9f4268f1e1246a19de8c488a Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Thu, 6 Jul 2023 03:44:48 +0300
Subject: [PATCH 5/6] feat: add js timer for test_passages
---
app/controllers/test_passages_controller.rb | 2 +-
app/javascript/packs/application.js | 1 +
app/javascript/utilities/timer.js | 20 ++++++++++++++++++++
app/views/test_passages/show.html.erb | 3 +++
config/locales/en.yml | 1 +
config/locales/ru.yml | 1 +
package.json | 2 +-
yarn.lock | 6 +++---
8 files changed, 31 insertions(+), 5 deletions(-)
create mode 100644 app/javascript/utilities/timer.js
diff --git a/app/controllers/test_passages_controller.rb b/app/controllers/test_passages_controller.rb
index 64b8418..e8939b0 100644
--- a/app/controllers/test_passages_controller.rb
+++ b/app/controllers/test_passages_controller.rb
@@ -1,6 +1,6 @@
class TestPassagesController < ApplicationController
before_action :find_test_passage, only: %i[show result update gist]
- before_action :get_end_time, only: %i[update result]
+ before_action :get_end_time, only: %i[update result show]
before_action :check_end_time, only: %i[update]
def show; end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 61d5c72..fa67b59 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -11,6 +11,7 @@ import "utilities/sorting"
import "utilities/password_check"
import "utilities/form_inline"
import "utilities/progress_bar"
+import "utilities/timer"
Rails.start()
Turbolinks.start()
diff --git a/app/javascript/utilities/timer.js b/app/javascript/utilities/timer.js
new file mode 100644
index 0000000..02c5bb7
--- /dev/null
+++ b/app/javascript/utilities/timer.js
@@ -0,0 +1,20 @@
+document.addEventListener('turbolinks:load', function() {
+ var timerView = document.querySelector('.timer')
+
+ if (timerView) {
+ var timeLeft = timerView.dataset.timer * 60
+ var id = timerView.dataset.id
+ var countdownInterval = setInterval(function() {
+ if (timeLeft > 0) {
+ timeLeft -= 1
+ } else {
+ clearInterval(countdownInterval)
+ var domain = window.location.protocol + '//' + window.location.host
+ window.location.href = `${domain}/test_passages/${id}/result`
+ }
+
+ var resultTime = parseInt(timeLeft / 60) + ':' + timeLeft % 60
+ timerView.textContent = resultTime
+ }, 1000)
+ }
+})
diff --git a/app/views/test_passages/show.html.erb b/app/views/test_passages/show.html.erb
index 95c0207..4058049 100644
--- a/app/views/test_passages/show.html.erb
+++ b/app/views/test_passages/show.html.erb
@@ -3,6 +3,9 @@
<%= content_tag :div, class: "progress-bar", data: { questions_count: @test_passage.test.questions.count, current_question_number: @test_passage.current_question.number } do %>
<% end %>
+
+ <%= t('.time_left') %> <%= content_tag :span, '', class: 'timer', data: { timer: @test_passage.test.duration_time, id: @test_passage.id } %>
+
<%= t('.total', count: @test_passage.test.questions.count) %>
<% unless @test_passage.completed? %>
<%= t('.currently', number: @test_passage.current_question.number) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 88bf066..e66d9ac 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -155,6 +155,7 @@ en:
currently: "You are currently taking a test: %{number}"
next: 'Next'
create_gist: 'Create gist'
+ time_left: 'Time left:'
gist:
success: 'Gist was successfully created, link: %{url}'
failure: 'An error occurred while saving gist'
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 545f781..755eba2 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -123,6 +123,7 @@ ru:
currently: "Вы сейчас проходите тест: %{number}"
next: 'Далее'
create_gist: 'Создать gist'
+ time_left: 'Оставшееся время:'
gist:
success: 'Gist успешно сохранен, ссылка: %{url}'
failure: 'Во время сохранения gist произошла ошибка'
diff --git a/package.json b/package.json
index 2d614c3..1991701 100644
--- a/package.json
+++ b/package.json
@@ -17,4 +17,4 @@
"webpack-dev-server": "^3"
},
"packageManager": "yarn@1.22.19"
-}
\ No newline at end of file
+}
diff --git a/yarn.lock b/yarn.lock
index 33e2e58..d51178f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1861,9 +1861,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001400:
- version "1.0.30001429"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz#70cdae959096756a85713b36dd9cb82e62325639"
- integrity sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==
+ version "1.0.30001512"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz"
+ integrity sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
From 41012e87525a84fa288cf4798cbab2b3edbb1c2c Mon Sep 17 00:00:00 2001
From: Evil Potato
Date: Fri, 7 Jul 2023 05:42:24 +0300
Subject: [PATCH 6/6] feat: change logic timer
---
app/controllers/test_passages_controller.rb | 17 ++++-------------
app/controllers/tests_controller.rb | 1 -
app/javascript/utilities/timer.js | 7 +++----
app/models/test_passage.rb | 16 ++++++++++++++++
app/views/test_passages/show.html.erb | 2 +-
...706230219_add_timestamps_to_test_passages.rb | 6 ++++++
...003843_add_default_duration_time_to_tests.rb | 9 +++++++++
db/schema.rb | 6 ++++--
8 files changed, 43 insertions(+), 21 deletions(-)
create mode 100644 db/migrate/20230706230219_add_timestamps_to_test_passages.rb
create mode 100644 db/migrate/20230707003843_add_default_duration_time_to_tests.rb
diff --git a/app/controllers/test_passages_controller.rb b/app/controllers/test_passages_controller.rb
index e8939b0..4333fd9 100644
--- a/app/controllers/test_passages_controller.rb
+++ b/app/controllers/test_passages_controller.rb
@@ -1,13 +1,10 @@
class TestPassagesController < ApplicationController
before_action :find_test_passage, only: %i[show result update gist]
- before_action :get_end_time, only: %i[update result show]
- before_action :check_end_time, only: %i[update]
+ before_action :check_timer, only: :update
def show; end
- def result
- session[:"end_time_#{@test_passage.id}"] = nil
- end
+ def result; end
def update
@test_passage.accept!(params[:answer_ids])
@@ -45,14 +42,8 @@ def gist
private
- def get_end_time
- @end_time = session[:"end_time_#{@test_passage.id}"]
- end
-
- def check_end_time
- if @end_time.present? && @end_time <= Time.now
- redirect_to result_test_passage_path(@test_passage)
- end
+ def check_timer
+ redirect_to result_test_passage_path(@test_passage) if @test_passage.time_over?
end
def find_test_passage
diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb
index 5de8715..2e2b2a5 100644
--- a/app/controllers/tests_controller.rb
+++ b/app/controllers/tests_controller.rb
@@ -7,6 +7,5 @@ def start
@test = Test.find(params[:id])
current_user.tests.push(@test)
redirect_to current_user.test_passage(@test)
- session[:"end_time_#{TestPassage.last.id}"] = Time.now + @test.duration_time.minute if @test.duration_time.present?
end
end
diff --git a/app/javascript/utilities/timer.js b/app/javascript/utilities/timer.js
index 02c5bb7..ec055d5 100644
--- a/app/javascript/utilities/timer.js
+++ b/app/javascript/utilities/timer.js
@@ -1,16 +1,15 @@
document.addEventListener('turbolinks:load', function() {
var timerView = document.querySelector('.timer')
- if (timerView) {
+ if (timerView && timerView.dataset.timer !== "0") {
var timeLeft = timerView.dataset.timer * 60
- var id = timerView.dataset.id
var countdownInterval = setInterval(function() {
if (timeLeft > 0) {
timeLeft -= 1
} else {
clearInterval(countdownInterval)
- var domain = window.location.protocol + '//' + window.location.host
- window.location.href = `${domain}/test_passages/${id}/result`
+ alert('Time is over!')
+ document.querySelector('form').submit()
}
var resultTime = parseInt(timeLeft / 60) + ':' + timeLeft % 60
diff --git a/app/models/test_passage.rb b/app/models/test_passage.rb
index 3292043..5932ba8 100644
--- a/app/models/test_passage.rb
+++ b/app/models/test_passage.rb
@@ -29,6 +29,22 @@ def finished?
def success?
percent_correct_answers >= SUCCESS_RATE
end
+
+ def timer_on?
+ !test.duration_time.zero?
+ end
+
+ def time_over?
+ if timer_on?
+ (Time.current - created_at) / 60 >= test.duration_time
+ else
+ false
+ end
+ end
+
+ def remaining_time
+ remaining_minutes = (test.duration_time - (Time.current - created_at) / 60).to_i if !time_over?
+ end
private
diff --git a/app/views/test_passages/show.html.erb b/app/views/test_passages/show.html.erb
index 4058049..e382a72 100644
--- a/app/views/test_passages/show.html.erb
+++ b/app/views/test_passages/show.html.erb
@@ -4,7 +4,7 @@
<% end %>
- <%= t('.time_left') %> <%= content_tag :span, '', class: 'timer', data: { timer: @test_passage.test.duration_time, id: @test_passage.id } %>
+ <%= t('.time_left') %> <%= content_tag :span, '', class: 'timer', data: { time_left: @test_passage.remaining_time } %>
<%= t('.total', count: @test_passage.test.questions.count) %>
<% unless @test_passage.completed? %>
diff --git a/db/migrate/20230706230219_add_timestamps_to_test_passages.rb b/db/migrate/20230706230219_add_timestamps_to_test_passages.rb
new file mode 100644
index 0000000..a4ac884
--- /dev/null
+++ b/db/migrate/20230706230219_add_timestamps_to_test_passages.rb
@@ -0,0 +1,6 @@
+class AddTimestampsToTestPassages < ActiveRecord::Migration[6.1]
+ def change
+ add_column :test_passages, :created_at, :datetime, default: -> { 'CURRENT_TIMESTAMP' }, null: false, precision: 6
+ add_column :test_passages, :updated_at, :datetime, default: -> { 'CURRENT_TIMESTAMP' }, null: false, precision: 6
+ end
+end
diff --git a/db/migrate/20230707003843_add_default_duration_time_to_tests.rb b/db/migrate/20230707003843_add_default_duration_time_to_tests.rb
new file mode 100644
index 0000000..4cb6636
--- /dev/null
+++ b/db/migrate/20230707003843_add_default_duration_time_to_tests.rb
@@ -0,0 +1,9 @@
+class AddDefaultDurationTimeToTests < ActiveRecord::Migration[6.1]
+ def up
+ change_column :tests, :duration_time, :integer, default: 0, null: false
+ end
+
+ def down
+ change_column :tests, :duration_time, :integer, default: nil, null: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8e0d797..afc8e21 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_07_04_222539) do
+ActiveRecord::Schema.define(version: 2023_07_07_003843) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -80,6 +80,8 @@
t.integer "correct_questions", default: 0
t.bigint "current_question_id"
t.boolean "finished", default: false
+ t.datetime "created_at", precision: 6, default: -> { "CURRENT_TIMESTAMP" }, null: false
+ t.datetime "updated_at", precision: 6, default: -> { "CURRENT_TIMESTAMP" }, null: false
t.index ["current_question_id"], name: "index_test_passages_on_current_question_id"
t.index ["test_id"], name: "index_test_passages_on_test_id"
t.index ["user_id"], name: "index_test_passages_on_user_id"
@@ -93,7 +95,7 @@
t.datetime "updated_at", precision: 6, null: false
t.bigint "author_id"
t.boolean "active", default: false
- t.integer "duration_time"
+ t.integer "duration_time", default: 0, null: false
t.index ["author_id"], name: "index_tests_on_author_id"
t.index ["category_id"], name: "index_tests_on_category_id"
t.index ["title", "level"], name: "index_tests_on_title_and_level", unique: true