diff --git a/app/controllers/mentor/practices_controller.rb b/app/controllers/mentor/practices_controller.rb index 504b4014f94..a1d548eae6f 100644 --- a/app/controllers/mentor/practices_controller.rb +++ b/app/controllers/mentor/practices_controller.rb @@ -12,9 +12,12 @@ def index def new @practice = Practice.new(pjord_review: true, pjord_auto_check: false) + @practice.build_template end - def edit; end + def edit + @practice.build_template unless @practice.template + end def create @practice = Practice.new(practice_params) @@ -63,7 +66,8 @@ def practice_params :summary, :ogp_image, category_ids: [], - practices_books_attributes: %i[id book_id must_read _destroy] + practices_books_attributes: %i[id book_id must_read _destroy], + template_attributes: %i[id description _destroy] ) end diff --git a/app/models/practice.rb b/app/models/practice.rb index 412d5ed5ac7..5c56075afc0 100644 --- a/app/models/practice.rb +++ b/app/models/practice.rb @@ -25,6 +25,13 @@ class Practice < ApplicationRecord # rubocop:todo Metrics/ClassLength source: :user has_many :skipped_practices, dependent: :destroy has_many :products, dependent: :destroy + + has_one :template, as: :templatable, dependent: :destroy + accepts_nested_attributes_for :template, update_only: true, allow_destroy: true, + reject_if: proc { |attributes| + attributes['_destroy'] != '1' && attributes['description'].blank? + } + has_many :questions, dependent: :nullify has_many :pages, -> { order(updated_at: :desc, id: :desc) }, diff --git a/app/models/template.rb b/app/models/template.rb new file mode 100644 index 00000000000..c9d19efd539 --- /dev/null +++ b/app/models/template.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Template < ApplicationRecord + belongs_to :templatable, polymorphic: true + + validates :description, presence: true +end diff --git a/app/views/mentor/practices/_form.html.slim b/app/views/mentor/practices/_form.html.slim index 9e2869249c6..ccf4aebd039 100644 --- a/app/views/mentor/practices/_form.html.slim +++ b/app/views/mentor/practices/_form.html.slim @@ -109,6 +109,26 @@ = f.check_box :include_progress, class: 'a-toggle-checkbox' = f.label :include_progress | 進捗の計算に含むようにする場合はチェック + + .form-item + label.a-form-label + | 提出物のテンプレート + = f.fields_for :template do |template_form| + - if template_form.object.persisted? + .checkboxes + ul.checkboxes__items + li.checkboxes__item + = template_form.check_box :_destroy, class: 'a-toggle-checkbox' + = template_form.label :_destroy do + | 提出物のテンプレートを削除する + .row.js-markdown-parent + .col-md-6.col-xs-12 + = template_form.text_area :description, + class: 'a-text-input js-warning-form markdown-form__text-area js-markdown', + data: { 'preview': '.js-product-template-preview' } + .col-md-6.col-xs-12 + .js-product-template-preview.a-long-text.is-md.markdown-form__preview + .form-item .row.js-markdown-parent .col-md-6.col-xs-12 diff --git a/app/views/products/_form.html.slim b/app/views/products/_form.html.slim index 821fbcd2132..6fd023fba13 100644 --- a/app/views/products/_form.html.slim +++ b/app/views/products/_form.html.slim @@ -9,8 +9,10 @@ | 提出物 .form-textarea .form-textarea__body + - template = @product.practice.template&.description = f.text_area :body, class: 'a-text-input js-warning-form js-markdown markdown-form__text-area', - data: { 'preview': '.js-preview', 'input': '.file-input' } + data: { 'preview': '.js-preview', 'input': '.file-input' }, + value: @product.body.presence || template .form-textarea__footer .form-textarea__insert label.a-file-insert.a-button.is-xs.is-text-reversal.is-block diff --git a/db/migrate/20260520061059_create_templates.rb b/db/migrate/20260520061059_create_templates.rb new file mode 100644 index 00000000000..5f08c8ab8bb --- /dev/null +++ b/db/migrate/20260520061059_create_templates.rb @@ -0,0 +1,10 @@ +class CreateTemplates < ActiveRecord::Migration[8.1] + def change + create_table :templates do |t| + t.references :templatable, polymorphic: true, null: false, index: { unique: true } + t.text :description, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 99e52cbd229..2ad96e6add0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1025,6 +1025,15 @@ t.index ["user_id"], name: "index_talks_on_user_id" end + create_table "templates", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "description", null: false + t.bigint "templatable_id", null: false + t.string "templatable_type", null: false + t.datetime "updated_at", null: false + t.index ["templatable_type", "templatable_id"], name: "index_templates_on_templatable", unique: true + end + create_table "users", id: :serial, force: :cascade do |t| t.datetime "accessed_at", precision: nil t.boolean "admin", default: false, null: false diff --git a/test/fixtures/templates.yml b/test/fixtures/templates.yml new file mode 100644 index 00000000000..fbb7cdfdeba --- /dev/null +++ b/test/fixtures/templates.yml @@ -0,0 +1,3 @@ +one: + templatable: practice1 (Practice) + description: 提出物のテンプレート diff --git a/test/models/practice_test.rb b/test/models/practice_test.rb index a1a63c97d47..183060fbf94 100644 --- a/test/models/practice_test.rb +++ b/test/models/practice_test.rb @@ -148,4 +148,36 @@ class PracticeTest < ActiveSupport::TestCase assert_equal initial_count + 1, practice.reports_count(include_source: false) assert_equal initial_count_including_source + 1, practice.reports_count(include_source: true) end + + test 'does not create product template when description is blank' do + practice = practices(:practice1) + practice.template&.destroy! + + assert_no_difference 'Template.count' do + practice.update!( + template_attributes: { + description: '' + } + ) + end + + assert_nil practice.reload.template + end + + test 'destroys product template with nested attributes' do + practice = practices(:practice1) + practice.template&.destroy! + template = practice.create_template!(description: '提出物のテンプレート') + + assert_difference 'Template.count', -1 do + practice.update!( + template_attributes: { + id: template.id, + _destroy: '1' + } + ) + end + + assert_nil practice.reload.template + end end diff --git a/test/system/practice/products_test.rb b/test/system/practice/products_test.rb index e620b0806ae..3599df705b1 100644 --- a/test/system/practice/products_test.rb +++ b/test/system/practice/products_test.rb @@ -7,4 +7,10 @@ class Practice::ProductsTest < ApplicationSystemTestCase visit_with_auth "/practices/#{practices(:practice1).id}/products", 'komagata' assert_equal 'OS X Mountain Lionをクリーンインストールするの提出物 | FBC', title end + + test 'shows product template when product template exists' do + practice = practices(:practice1) + visit_with_auth "/mentor/practices/#{practice.id}/edit", 'komagata' + assert_field 'practice_template_attributes_description', with: '提出物のテンプレート' + end end diff --git a/test/system/practices/mentor_test.rb b/test/system/practices/mentor_test.rb index 823668207d5..0deeb064109 100644 --- a/test/system/practices/mentor_test.rb +++ b/test/system/practices/mentor_test.rb @@ -21,6 +21,7 @@ class MentorTest < ApplicationSystemTestCase end fill_in 'practice[goal]', with: 'テストのゴールの内容です' fill_in 'practice[memo]', with: 'テストのメンター向けメモの内容です' + fill_in 'practice_template_attributes_description', with: 'テストテンプレート' uncheck 'practice[pjord_review]', allow_label_click: true click_button '登録する' end @@ -28,6 +29,24 @@ class MentorTest < ApplicationSystemTestCase assert_not_predicate Practice.order(:created_at).last, :pjord_review? end + test 'can create practice without product template' do + visit_with_auth '/mentor/practices/new', 'komagata' + + within 'form[name=practice]' do + fill_in 'practice[title]', with: 'テンプレなし' + check categories(:category1).name, allow_label_click: true + fill_in 'practice[description]', with: 'テストの内容です' + fill_in 'practice[goal]', with: 'テストのゴールの内容です' + click_button '登録する' + end + + assert_text 'プラクティスを作成しました' + + practice = Practice.find_by!(title: 'テンプレなし') + visit_with_auth "/mentor/practices/#{practice.id}/edit", 'komagata' + assert_empty find_field('practice_template_attributes_description').value + end + test 'create practice as a mentor' do visit_with_auth '/mentor/practices/new', 'mentormentaro' within 'form[name=practice]' do @@ -39,6 +58,7 @@ class MentorTest < ApplicationSystemTestCase end fill_in 'practice[goal]', with: 'テストのゴールの内容です' fill_in 'practice[memo]', with: 'テストのメンター向けメモの内容です' + fill_in 'practice_template_attributes_description', with: 'テストテンプレート' click_button '登録する' end assert_text 'プラクティスを作成しました' @@ -51,6 +71,7 @@ class MentorTest < ApplicationSystemTestCase within 'form[name=practice]' do fill_in 'practice[title]', with: 'テストプラクティス' fill_in 'practice[memo]', with: 'メンター向けのメモの内容です' + fill_in 'practice_template_attributes_description', with: 'テストテンプレート' within '#reference_books' do click_link '書籍を選択' end @@ -60,6 +81,34 @@ class MentorTest < ApplicationSystemTestCase visit "/products/#{product.id}" check 'toggle-mentor-memo-body', allow_label_click: true, visible: false assert_text 'メンター向けのメモの内容です' + + visit new_product_path(practice_id: practice.id) + assert_field 'product[body]', with: 'テストテンプレート' + end + + test 'update product template' do + practice = practices(:practice1) + visit_with_auth "/mentor/practices/#{practice.id}/edit", 'komagata' + assert_field 'practice_template_attributes_description', with: '提出物のテンプレート' + fill_in 'practice_template_attributes_description', with: '更新後テンプレート' + click_button '更新する' + assert_text 'プラクティスを更新しました' + + visit new_product_path(practice_id: practice.id) + assert_field 'product[body]', with: '更新後テンプレート' + end + + test 'deletes product template from practice edit form' do + practice = practices(:practice1) + visit_with_auth "/mentor/practices/#{practice.id}/edit", 'komagata' + check '提出物のテンプレートを削除する', allow_label_click: true + + assert_difference 'Template.count', -1 do + click_button '更新する' + end + + assert_text 'プラクティスを更新しました' + assert_nil practice.reload.template end test 'add a book' do