diff --git a/app/assets/stylesheets/characters.css b/app/assets/stylesheets/characters.css index 146a709..1ca07f4 100644 --- a/app/assets/stylesheets/characters.css +++ b/app/assets/stylesheets/characters.css @@ -29,3 +29,23 @@ } } } + +.character-sheet-section.top-level { + background-color: var(--inset-bg-color); +} + +.character-sheet-section { + border: 1px solid var(--background-color); + border-radius: var(--border-radius); + margin-top: .3em; + h4 { + background-color: var(--background-color); + color: var(--header-text-color); + margin: 0; + padding: .5em; + } + + .content { + padding: 1em; + } +} diff --git a/app/controllers/character_sheet_sections_controller.rb b/app/controllers/character_sheet_sections_controller.rb new file mode 100644 index 0000000..b550230 --- /dev/null +++ b/app/controllers/character_sheet_sections_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class CharacterSheetSectionsController < ApplicationController + before_action :set_character, only: [ :index, :new, :create ] + before_action :set_section, only: [ :destroy ] + + def index + @sections = @character.character_sheet_sections.top_level + end + + def new + if params[:parent_section_id].present? + @parent_section = @character.character_sheet_sections.find_by(id: params[:parent_section_id]) + end + @section = @character.character_sheet_sections.new + end + + def create + @section = @character.character_sheet_sections.new(character_sheet_section_params) + unless @section.save + @parent_section = @section.parent_section + render :new, status: :unprocessable_entity + end + end + + def destroy + @id = helpers.dom_id(@section) + @section.destroy + end + + private + + def set_character + @character = Current.user.characters.find(params[:character_id]) + end + + def set_section + @section = Current.user.character_sheet_sections.find(params[:id]) + end + + def character_sheet_section_params + params.require(:character_sheet_section).permit( + :name, + :parent_section_id, + ) + end +end diff --git a/app/models/character_sheet_section.rb b/app/models/character_sheet_section.rb index 7797aba..90ca354 100644 --- a/app/models/character_sheet_section.rb +++ b/app/models/character_sheet_section.rb @@ -2,8 +2,8 @@ class CharacterSheetSection < ApplicationRecord belongs_to :character - belongs_to :parent_section, optional: true - has_many :character_sheet_subsections, class_name: "CharacterSheetSection" + belongs_to :parent_section, optional: true, class_name: "CharacterSheetSection" + has_many :character_sheet_subsections, class_name: "CharacterSheetSection", foreign_key: :parent_section_id validates :name, presence: true, length: { maximum: 255 } diff --git a/app/models/user.rb b/app/models/user.rb index 1413f73..71061c7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,6 +6,7 @@ class User < ApplicationRecord has_and_belongs_to_many :site_roles has_many :characters, dependent: :destroy + has_many :character_sheet_sections, through: :characters has_many :owned_tables, foreign_key: :owner_id, class_name: "Table" has_many :players, dependent: :destroy has_many :tables, through: :players diff --git a/app/views/character_sheet_sections/_character_sheet_section.html.erb b/app/views/character_sheet_sections/_character_sheet_section.html.erb new file mode 100644 index 0000000..d6f19dd --- /dev/null +++ b/app/views/character_sheet_sections/_character_sheet_section.html.erb @@ -0,0 +1,15 @@ +
" + id="<%= dom_id(character_sheet_section) %>"> +

<%= character_sheet_section.name %>

+ +
+
+ <%= render character_sheet_section.character_sheet_subsections %> +
+
+ <%= render partial: "edit_links", + locals: { section: character_sheet_section, parent: character_sheet_section, id: nil } %> +
+
+
diff --git a/app/views/character_sheet_sections/_edit_links.html.erb b/app/views/character_sheet_sections/_edit_links.html.erb new file mode 100644 index 0000000..1eb97d7 --- /dev/null +++ b/app/views/character_sheet_sections/_edit_links.html.erb @@ -0,0 +1,8 @@ +<%# locals: (section:, parent:, id:) -%> +<%= link_to t(".add_subsection"), + new_character_character_sheet_section_path(@character, parent_section_id: parent&.id), + data: { turbo_stream: true } %> +<% unless id == "character_sheet_add_section" %> +<%= link_to t(".delete_section", name: section.name), character_sheet_section_path(section), + data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete", name: section.name) } %> +<% end %> diff --git a/app/views/character_sheet_sections/_form.html.erb b/app/views/character_sheet_sections/_form.html.erb new file mode 100644 index 0000000..db1a042 --- /dev/null +++ b/app/views/character_sheet_sections/_form.html.erb @@ -0,0 +1,11 @@ +
+ <%= form_with model: @section, url: character_character_sheet_sections_path(@character) do |f| %> + <%= f.hidden_field :parent_section_id, value: @parent_section&.id %> + + <%= f.label :name %> + <%= f.text_field :name %> + <%= display_form_errors(@section, :name) %> + + <%= f.submit button_text %> + <% end %> +
diff --git a/app/views/character_sheet_sections/create.turbo_stream.erb b/app/views/character_sheet_sections/create.turbo_stream.erb new file mode 100644 index 0000000..adf54e5 --- /dev/null +++ b/app/views/character_sheet_sections/create.turbo_stream.erb @@ -0,0 +1,15 @@ +<% content_target = @section.parent_section.present? ? "#{dom_id(@section.parent_section)}_sections" + : "character_sheet" %> +<%= turbo_stream.append(content_target) do %> + <%= render @section %> +<% end %> + +<% form_target = @section.parent_section.present? ? "#{dom_id(@section.parent_section)}_add_section" + : "character_sheet_add_section" %> +<%= turbo_stream.replace(form_target) do %> + <% id = @section.parent_section.present? ? "#{dom_id(@section.parent_section)}_add_section" + : "character_sheet_add_section" %> +
> + <%= render partial: "edit_links", locals: { section: @section, parent: @section, id: } %> +
+<% end %> diff --git a/app/views/character_sheet_sections/destroy.turbo_stream.erb b/app/views/character_sheet_sections/destroy.turbo_stream.erb new file mode 100644 index 0000000..9fd2cca --- /dev/null +++ b/app/views/character_sheet_sections/destroy.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.remove(@id) %> diff --git a/app/views/character_sheet_sections/index.html.erb b/app/views/character_sheet_sections/index.html.erb new file mode 100644 index 0000000..fdaa23d --- /dev/null +++ b/app/views/character_sheet_sections/index.html.erb @@ -0,0 +1,16 @@ +<% content_for :title, t(".character_sheet", name: @character.name) %> + +

<%= @character.name %>

+ +
+ <% if @sections.any? %> + <%= render @sections %> + <% else %> +

<%= t(".no_sections") %>

+ <% end %> +
+ +
+ <%= link_to t(".add_section"), new_character_character_sheet_section_path(@character), + data: { turbo_stream: true } %> +
diff --git a/app/views/character_sheet_sections/new.turbo_stream.erb b/app/views/character_sheet_sections/new.turbo_stream.erb new file mode 100644 index 0000000..14f1766 --- /dev/null +++ b/app/views/character_sheet_sections/new.turbo_stream.erb @@ -0,0 +1,6 @@ +<% target = @parent_section.present? ? "#{dom_id(@parent_section)}_add_section" : "character_sheet_add_section" %> +<%= turbo_stream.replace(target) do %> +
+ <%= render partial: "form", locals: { button_text: t(".create_section") } %> +
+<% end %> diff --git a/app/views/characters/show.html.erb b/app/views/characters/show.html.erb index df61176..8c3e129 100644 --- a/app/views/characters/show.html.erb +++ b/app/views/characters/show.html.erb @@ -2,6 +2,7 @@

<%= @character.name %>

+<%= link_to t(".sheet"), character_character_sheet_sections_path(@character) %> <%= link_to t(".edit"), edit_character_path(@character) %>
diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 5d799c5..cc888d9 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -33,8 +33,42 @@ 22 ], "note": "" + }, + { + "warning_type": "Dynamic Render Path", + "warning_code": 15, + "fingerprint": "5e59ccbb20f360876317a1d7721a9b32bfae08b038943124e59179505c3325f8", + "check_name": "Render", + "message": "Render path contains parameter value", + "file": "app/views/character_sheet_sections/index.html.erb", + "line": 7, + "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", + "code": "render(action => Current.user.characters.find(params[:character_id]).character_sheet_sections.top_level, {})", + "render_path": [ + { + "type": "controller", + "class": "CharacterSheetSectionsController", + "method": "index", + "line": 9, + "file": "app/controllers/character_sheet_sections_controller.rb", + "rendered": { + "name": "character_sheet_sections/index", + "file": "app/views/character_sheet_sections/index.html.erb" + } + } + ], + "location": { + "type": "template", + "template": "character_sheet_sections/index" + }, + "user_input": "params[:character_id]", + "confidence": "Weak", + "cwe_id": [ + 22 + ], + "note": "" } ], - "updated": "2024-06-05 18:49:27 +0100", + "updated": "2024-06-06 14:18:01 +0100", "brakeman_version": "6.1.2" } diff --git a/config/locales/en.yml b/config/locales/en.yml index cb9b20a..9cb8277 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -55,6 +55,17 @@ en: destroy: success: Successfully deleted “%{name}”. error: “%{name}” could not be deleted. + character_sheet_sections: + edit_links: + add_subsection: Add subsection + delete_section: Delete %{name} + confirm_delete: Are you sure you want to delete this section? + index: + character_sheet: "%{name}’s character sheet" + no_sections: This character sheet has no content + add_section: Add section + new: + create_section: Create section characters: index: my_characters: My characters @@ -67,6 +78,7 @@ en: success: “%{name}” has been created. error: Your character could not be created. show: + sheet: Character sheet edit: Edit character owner: Player game_system: Game system diff --git a/config/routes.rb b/config/routes.rb index a7b4187..af74565 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,10 @@ Rails.application.routes.draw do resources :password_resets, only: [ :new, :create, :edit, :update ] resources :sessions, only: [ :new, :create, :destroy ] - resources :characters + resources :character_sheet_sections, only: [ :destroy ] + resources :characters do + resources :character_sheet_sections, only: [ :index, :new, :create ] + end resources :table_invites, only: [ :index, :edit, :update ] resources :tables do resources :table_invites, only: [ :new, :create ] diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d91e781..0134f20 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -6,4 +6,11 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :headless_firefox, screen_size: [ 1400, 1400 ] + + def system_sign_in(user, password: "password") + visit new_session_url + fill_in attr_name(User, :username), with: users(:trevor).username + fill_in attr_name(User, :password), with: "password" + click_button I18n.t("sessions.new.log_in") + end end diff --git a/test/controllers/character_sheet_sections_controller_test.rb b/test/controllers/character_sheet_sections_controller_test.rb new file mode 100644 index 0000000..e2aecd2 --- /dev/null +++ b/test/controllers/character_sheet_sections_controller_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" + +class CharacterSheetSectionsControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + sign_in users(:trevor) + + get character_character_sheet_sections_url(characters(:nardren)) + assert_response :success + end + + test "should render new turbo_stream" do + sign_in users(:trevor) + get new_character_character_sheet_section_url(characters(:nardren)), as: :turbo_stream + assert_response :success + end + + test "should create character_sheet_section" do + sign_in users(:trevor) + assert_difference "CharacterSheetSection.count", 1 do + post character_character_sheet_sections_url(characters(:nardren)), + params: { character_sheet_section: { name: "test" } }, + as: :turbo_stream + end + end + + test "should destroy character_sheet_section" do + user = users(:trevor) + sign_in user + assert_difference "CharacterSheetSection.count", -1 do + delete character_sheet_section_url(user.character_sheet_sections.first), + as: :turbo_stream + end + end +end diff --git a/test/system/character_sheet_test.rb b/test/system/character_sheet_test.rb new file mode 100644 index 0000000..99085bc --- /dev/null +++ b/test/system/character_sheet_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "application_system_test_case" + +class CharacterSheetTest < ApplicationSystemTestCase + test "can add and remove sections on a character sheet" do + system_sign_in users(:trevor) + + character = characters(:nardren) + visit character_path(characters) + click_on I18n.t("characters.show.sheet") + assert_text character.name + + click_link(I18n.t("character_sheet_sections.index.add_section")) + fill_in attr_name(CharacterSheetSection, :name), with: "Test Section" + click_button(I18n.t("character_sheet_sections.new.create_section")) + assert_text "Test Section" + + click_link(I18n.t("character_sheet_sections.edit_links.delete_section", name: "Test Section")) + accept_confirm + assert_no_text "Test Section" + end +end