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