brut 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/CHANGELOG.md +7 -0
- data/Dockerfile.dx +19 -0
- data/Gemfile.lock +1 -1
- data/README.md +19 -0
- data/assets/YouTubeThumb.pxd +0 -0
- data/bin/build +86 -0
- data/bin/ci +36 -0
- data/bin/docs +39 -9
- data/bin/publish +61 -0
- data/bin/setup +6 -0
- data/brut-css/bin/build +19 -0
- data/brut-css/bin/ci +19 -0
- data/brut-css/bin/docs +19 -0
- data/brut-css/bin/publish +21 -0
- data/brut-css/bin/setup +1 -0
- data/brut-css/package-lock.json +2 -2
- data/brut-css/package.json +1 -1
- data/brut-js/bin/build +15 -6
- data/brut-js/bin/docs +25 -0
- data/brut-js/bin/publish +21 -0
- data/brut-js/bin/setup +1 -0
- data/brut-js/dx +1 -0
- data/brut-js/package-lock.json +2 -2
- data/brut-js/package.json +1 -1
- data/brut.gemspec +2 -2
- data/brutrb.com/bin/setup +1 -0
- data/brutrb.com/getting-started.md +3 -0
- data/brutrb.com/overview.md +6 -0
- data/brutrb.com/tutorial.md +7 -3
- data/docs/404.html +2 -2
- data/docs/adrs.html +3 -3
- data/docs/ai.html +3 -3
- data/docs/assets/{app.D6BuVHo9.js → app.DyQLb4Ot.js} +1 -1
- data/docs/assets/chunks/@localSearchIndexroot.CmtZyrFA.js +1 -0
- data/docs/assets/chunks/{VPLocalSearchBox.BpvHMbx6.js → VPLocalSearchBox.T1iA-eJx.js} +1 -1
- data/docs/assets/chunks/{theme.wlAOvi2f.js → theme.ChwsbWjK.js} +2 -2
- data/docs/assets/{components.md.iLiv2E9X.js → components.md.DHh-NwKs.js} +3 -3
- data/docs/assets/{configuration.md.DmuAdsli.js → configuration.md.D8Wz3oJU.js} +1 -1
- data/docs/assets/{forms.md.D8aa_qI-.js → forms.md.BRE85eju.js} +1 -1
- data/docs/assets/{getting-started.md.DLplsDUd.js → getting-started.md.2ioiTe-B.js} +6 -3
- data/docs/assets/{getting-started.md.DLplsDUd.lean.js → getting-started.md.2ioiTe-B.lean.js} +1 -1
- data/docs/assets/overview.md.DlKiRRG_.js +1 -0
- data/docs/assets/overview.md.DlKiRRG_.lean.js +1 -0
- data/docs/assets/tutorial.md.BIb7XT6j.js +1 -0
- data/docs/assets/tutorial.md.BIb7XT6j.lean.js +1 -0
- data/docs/assets.html +3 -3
- data/docs/brut-js.html +3 -3
- data/docs/business-logic.html +3 -3
- data/docs/cli.html +3 -3
- data/docs/components.html +7 -7
- data/docs/configuration.html +5 -5
- data/docs/css.html +3 -3
- data/docs/custom-element-tests.html +3 -3
- data/docs/database-access.html +3 -3
- data/docs/database-schema.html +3 -3
- data/docs/deployment.html +3 -3
- data/docs/dev-environment.html +3 -3
- data/docs/dir-structure.html +3 -3
- data/docs/doc-conventions.html +3 -3
- data/docs/end-to-end-tests.html +3 -3
- data/docs/features.html +3 -3
- data/docs/flash-and-session.html +3 -3
- data/docs/form-constraints.html +3 -3
- data/docs/forms.html +5 -5
- data/docs/getting-started.html +9 -6
- data/docs/handlers.html +3 -3
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +3 -3
- data/docs/i18n.html +3 -3
- data/docs/index.html +3 -3
- data/docs/instrumentation.html +3 -3
- data/docs/javascript.html +3 -3
- data/docs/jobs.html +3 -3
- data/docs/keyword-injection.html +3 -3
- data/docs/layouts.html +3 -3
- data/docs/lsp.html +3 -3
- data/docs/markdown-examples.html +3 -3
- data/docs/middleware.html +3 -3
- data/docs/overview.html +5 -5
- data/docs/pages.html +3 -3
- data/docs/recipes/alternate-layouts.html +3 -3
- data/docs/recipes/authentication.html +3 -3
- data/docs/recipes/blank-layouts.html +3 -3
- data/docs/recipes/custom-flash.html +3 -3
- data/docs/recipes/indexed-forms.html +3 -3
- data/docs/recipes/migrations.html +3 -3
- data/docs/recipes/text-field-component.html +3 -3
- data/docs/roadmap.html +3 -3
- data/docs/routes.html +3 -3
- data/docs/security.html +3 -3
- data/docs/seed-data.html +3 -3
- data/docs/space-time-continuum.html +3 -3
- data/docs/tutorial.html +5 -5
- data/docs/unit-tests.html +3 -3
- data/docs/why.html +3 -3
- data/lib/brut/framework/mcp.rb +1 -1
- data/lib/brut/front_end/components/form_tag.rb +2 -2
- data/lib/brut/version.rb +1 -1
- data/mkbrut/.gitignore +16 -0
- data/mkbrut/CODE_OF_CONDUCT.txt +100 -0
- data/mkbrut/Gemfile +3 -0
- data/mkbrut/Gemfile.lock +19 -0
- data/mkbrut/LICENSE.txt +370 -0
- data/mkbrut/README.md +145 -0
- data/mkbrut/Rakefile +2 -0
- data/mkbrut/bin/build +36 -0
- data/mkbrut/bin/ci +19 -0
- data/mkbrut/bin/docs +19 -0
- data/mkbrut/bin/publish +129 -0
- data/mkbrut/bin/rake +16 -0
- data/mkbrut/bin/setup +30 -0
- data/mkbrut/brut-welcome.png +0 -0
- data/mkbrut/deploy/.dockerignore +2 -0
- data/mkbrut/deploy/Dockerfile +25 -0
- data/mkbrut/exe/mkbrut +5 -0
- data/mkbrut/lib/mkbrut/app.rb +79 -0
- data/mkbrut/lib/mkbrut/app_id.rb +8 -0
- data/mkbrut/lib/mkbrut/app_name.rb +29 -0
- data/mkbrut/lib/mkbrut/app_options.rb +36 -0
- data/mkbrut/lib/mkbrut/base.rb +57 -0
- data/mkbrut/lib/mkbrut/cli.rb +107 -0
- data/mkbrut/lib/mkbrut/erb_binding_delegate.rb +20 -0
- data/mkbrut/lib/mkbrut/internet_identifier.rb +32 -0
- data/mkbrut/lib/mkbrut/invalid_identifier.rb +4 -0
- data/mkbrut/lib/mkbrut/ops/add_css_import.rb +42 -0
- data/mkbrut/lib/mkbrut/ops/add_i18n_message.rb +74 -0
- data/mkbrut/lib/mkbrut/ops/add_method.rb +48 -0
- data/mkbrut/lib/mkbrut/ops/append_to_file.rb +20 -0
- data/mkbrut/lib/mkbrut/ops/base_op.rb +21 -0
- data/mkbrut/lib/mkbrut/ops/copy_file.rb +12 -0
- data/mkbrut/lib/mkbrut/ops/insert_code_in_method.rb +58 -0
- data/mkbrut/lib/mkbrut/ops/insert_route.rb +52 -0
- data/mkbrut/lib/mkbrut/ops/mkdir.rb +13 -0
- data/mkbrut/lib/mkbrut/ops/prism_parsing_op.rb +70 -0
- data/mkbrut/lib/mkbrut/ops/render_template.rb +26 -0
- data/mkbrut/lib/mkbrut/ops/skip_file.rb +10 -0
- data/mkbrut/lib/mkbrut/ops.rb +16 -0
- data/mkbrut/lib/mkbrut/organization.rb +5 -0
- data/mkbrut/lib/mkbrut/prefix.rb +26 -0
- data/mkbrut/lib/mkbrut/prefixed_io.rb +16 -0
- data/mkbrut/lib/mkbrut/segments/bare_bones.rb +185 -0
- data/mkbrut/lib/mkbrut/segments/demo.rb +121 -0
- data/mkbrut/lib/mkbrut/segments/heroku.rb +30 -0
- data/mkbrut/lib/mkbrut/segments/sidekiq.rb +3 -0
- data/mkbrut/lib/mkbrut/segments.rb +8 -0
- data/mkbrut/lib/mkbrut/version.rb +3 -0
- data/mkbrut/lib/mkbrut/versions.rb +13 -0
- data/mkbrut/lib/mkbrut.rb +18 -0
- data/mkbrut/mkbrut.gemspec +32 -0
- data/mkbrut/templates/Base/.dockerignore +25 -0
- data/mkbrut/templates/Base/.env.development.erb +60 -0
- data/mkbrut/templates/Base/.env.test.erb +8 -0
- data/mkbrut/templates/Base/.gitignore +31 -0
- data/mkbrut/templates/Base/.projections.json +59 -0
- data/mkbrut/templates/Base/Dockerfile.dx +205 -0
- data/mkbrut/templates/Base/Gemfile.erb +53 -0
- data/mkbrut/templates/Base/Procfile.development +5 -0
- data/mkbrut/templates/Base/Procfile.test +1 -0
- data/mkbrut/templates/Base/README.md +4 -0
- data/mkbrut/templates/Base/README.md.erb +40 -0
- data/mkbrut/templates/Base/app/bootstrap.rb +61 -0
- data/mkbrut/templates/Base/app/config/i18n/en/1_defaults.rb +128 -0
- data/mkbrut/templates/Base/app/config/i18n/en/2_app.rb +24 -0
- data/mkbrut/templates/Base/app/public/static/manifest.json.erb +33 -0
- data/mkbrut/templates/Base/app/src/app.rb.erb +37 -0
- data/mkbrut/templates/Base/app/src/back_end/data_models/app_data_model.rb +5 -0
- data/mkbrut/templates/Base/app/src/back_end/data_models/db.rb +19 -0
- data/mkbrut/templates/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb +6 -0
- data/mkbrut/templates/Base/app/src/back_end/data_models/seed/seed_data.rb +9 -0
- data/mkbrut/templates/Base/app/src/front_end/components/app_component.rb +8 -0
- data/mkbrut/templates/Base/app/src/front_end/components/custom_element_registration.rb.erb +7 -0
- data/mkbrut/templates/Base/app/src/front_end/css/index.css +2 -0
- data/mkbrut/templates/Base/app/src/front_end/css/svgs.css +12 -0
- data/mkbrut/templates/Base/app/src/front_end/forms/app_form.rb +4 -0
- data/mkbrut/templates/Base/app/src/front_end/handlers/app_handler.rb +4 -0
- data/mkbrut/templates/Base/app/src/front_end/images/LogoPylon.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/LogoTransit.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/favicon.ico +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/icon.png +0 -0
- data/mkbrut/templates/Base/app/src/front_end/images/mkicons.sh +6 -0
- data/mkbrut/templates/Base/app/src/front_end/js/index.js +6 -0
- data/mkbrut/templates/Base/app/src/front_end/layouts/default_layout.rb.erb +73 -0
- data/mkbrut/templates/Base/app/src/front_end/pages/app_page.rb +11 -0
- data/mkbrut/templates/Base/app/src/front_end/pages/home_page.rb +62 -0
- data/mkbrut/templates/Base/app/src/front_end/support/app_session.rb +6 -0
- data/mkbrut/templates/Base/app/src/front_end/svgs/README.md +5 -0
- data/mkbrut/templates/Base/app/src/front_end/svgs/comment-button.svg +59 -0
- data/mkbrut/templates/Base/bin/README.md.erb +5 -0
- data/mkbrut/templates/Base/bin/build-assets +7 -0
- data/mkbrut/templates/Base/bin/ci +39 -0
- data/mkbrut/templates/Base/bin/console +31 -0
- data/mkbrut/templates/Base/bin/db +9 -0
- data/mkbrut/templates/Base/bin/dbconsole +51 -0
- data/mkbrut/templates/Base/bin/dev +25 -0
- data/mkbrut/templates/Base/bin/release +26 -0
- data/mkbrut/templates/Base/bin/run +86 -0
- data/mkbrut/templates/Base/bin/scaffold +9 -0
- data/mkbrut/templates/Base/bin/setup +256 -0
- data/mkbrut/templates/Base/bin/startup-message +65 -0
- data/mkbrut/templates/Base/bin/test +9 -0
- data/mkbrut/templates/Base/bin/test-server +29 -0
- data/mkbrut/templates/Base/bin/watch-and-build-assets +37 -0
- data/mkbrut/templates/Base/config.ru +16 -0
- data/mkbrut/templates/Base/docker-compose.dx.yml +92 -0
- data/mkbrut/templates/Base/dx/README.md +28 -0
- data/mkbrut/templates/Base/dx/bash_customizations +12 -0
- data/mkbrut/templates/Base/dx/bash_customizations.local +8 -0
- data/mkbrut/templates/Base/dx/build +107 -0
- data/mkbrut/templates/Base/dx/docker-compose.env.erb +25 -0
- data/mkbrut/templates/Base/dx/dx.sh.lib +137 -0
- data/mkbrut/templates/Base/dx/exec +68 -0
- data/mkbrut/templates/Base/dx/prune +19 -0
- data/mkbrut/templates/Base/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/mkbrut/templates/Base/dx/start +30 -0
- data/mkbrut/templates/Base/dx/stop +23 -0
- data/mkbrut/templates/Base/package.json.erb +37 -0
- data/mkbrut/templates/Base/puma.config.rb +53 -0
- data/mkbrut/templates/Base/specs/e2e/home_page.spec.rb.erb +23 -0
- data/mkbrut/templates/Base/specs/front_end/js/SpecHelper.js +24 -0
- data/mkbrut/templates/Base/specs/front_end/pages/home_page.spec.rb +22 -0
- data/mkbrut/templates/Base/specs/lint_factories.spec.rb +7 -0
- data/mkbrut/templates/Base/specs/spec_helper.rb +78 -0
- data/mkbrut/templates/Base/specs/support.rb +2 -0
- data/mkbrut/templates/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +24 -0
- data/mkbrut/templates/segments/BareBones/app/src/front_end/js/Example.js.erb +49 -0
- data/mkbrut/templates/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +41 -0
- data/mkbrut/templates/segments/BareBones/specs/front_end/js/Example.spec.js.erb +38 -0
- data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +3 -0
- data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +14 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/components/flash_component.rb +36 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/css/constraint-violations.css +18 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/css/fonts.css +19 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +4 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +64 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +41 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page.rb +43 -0
- data/mkbrut/templates/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +64 -0
- data/mkbrut/templates/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +5 -0
- data/mkbrut/templates/segments/Demo/specs/e2e/guest_message.spec.rb +54 -0
- data/mkbrut/templates/segments/Demo/specs/factories/db/guestbook_message.factory.rb +7 -0
- data/mkbrut/templates/segments/Demo/specs/front_end/components/flash_component.spec.rb +5 -0
- data/mkbrut/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +122 -0
- data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +5 -0
- data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +52 -0
- data/mkbrut/templates/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +5 -0
- data/mkbrut/templates/segments/Heroku/bin/deploy +11 -0
- data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +125 -0
- data/mkbrut/templates/segments/Heroku/deploy/docker-entrypoint +15 -0
- data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +26 -0
- metadata +185 -21
- data/docs/assets/chunks/@localSearchIndexroot.COP2Bcmp.js +0 -1
- data/docs/assets/overview.md.iMnwLO4x.js +0 -1
- data/docs/assets/overview.md.iMnwLO4x.lean.js +0 -1
- data/docs/assets/tutorial.md.BYXj4cOu.js +0 -1
- data/docs/assets/tutorial.md.BYXj4cOu.lean.js +0 -1
- /data/docs/assets/{components.md.iLiv2E9X.lean.js → components.md.DHh-NwKs.lean.js} +0 -0
- /data/docs/assets/{configuration.md.DmuAdsli.lean.js → configuration.md.D8Wz3oJU.lean.js} +0 -0
- /data/docs/assets/{forms.md.D8aa_qI-.lean.js → forms.md.BRE85eju.lean.js} +0 -0
data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
class GuestbookPage::MessageComponent < AppComponent
|
2
|
+
def initialize(message:, index:, current_visitor_message: false)
|
3
|
+
@message = message
|
4
|
+
@current_visitor_message = current_visitor_message
|
5
|
+
@background_color = [
|
6
|
+
"bg-gray-800",
|
7
|
+
"bg-blue-800",
|
8
|
+
"bg-green-800",
|
9
|
+
"bg-yellow-800",
|
10
|
+
][index % 4]
|
11
|
+
end
|
12
|
+
|
13
|
+
def view_template
|
14
|
+
div_classes = [
|
15
|
+
"measure",
|
16
|
+
"mh-auto",
|
17
|
+
"pa-3",
|
18
|
+
"br-4",
|
19
|
+
"br-tr-0",
|
20
|
+
"br-bl-0",
|
21
|
+
"mt-3",
|
22
|
+
@background_color,
|
23
|
+
]
|
24
|
+
border_class = if @current_visitor_message
|
25
|
+
div_classes << "ba" << "bc-gray-600" << "bw-2"
|
26
|
+
else
|
27
|
+
div_classes << "bn"
|
28
|
+
end
|
29
|
+
div(class: div_classes, id: @message.external_id) do
|
30
|
+
blockquote(class: "ff-cursive p mh-auto i f-4") do
|
31
|
+
@message.message
|
32
|
+
end
|
33
|
+
p(class: "p mh-auto tr f-2") do
|
34
|
+
plain("—")
|
35
|
+
plain(@message.name)
|
36
|
+
plain(" on ")
|
37
|
+
TimeTag(date: @message.created_at, format: :date, class: "gray-400 f-1 ttu")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class GuestbookPage < AppPage
|
2
|
+
|
3
|
+
def initialize(session:)
|
4
|
+
@session = session
|
5
|
+
end
|
6
|
+
|
7
|
+
def page_template
|
8
|
+
div(class: "flex flex-column items-center ff-sans") do
|
9
|
+
article(class: "w-50 pv-3 bt bc-gray-700", id: "guestbook") do
|
10
|
+
h2(class: "ff-sans ma-0 lh-title f-4 tc") do
|
11
|
+
"Check out the Guestbook!"
|
12
|
+
end
|
13
|
+
if !@session.signed_guestbook?
|
14
|
+
a(
|
15
|
+
href: NewGuestbookMessagePage.routing,
|
16
|
+
class: "db tc mv-3 f-2 fw-5 blue-300"
|
17
|
+
) do
|
18
|
+
"Sign it, yourself!"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
a(
|
22
|
+
href: "##{@session.guestbook_message.external_id}",
|
23
|
+
class: "db tc mv-3 f-2 fw-5 blue-300"
|
24
|
+
) do
|
25
|
+
"Check out your message"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
DB::GuestbookMessage.order(
|
30
|
+
Sequel.function(:random)
|
31
|
+
).each_with_index do |message,index|
|
32
|
+
render(
|
33
|
+
GuestbookPage::MessageComponent.new(
|
34
|
+
message:,
|
35
|
+
index:,
|
36
|
+
current_visitor_message: message == @session.guestbook_message
|
37
|
+
)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class NewGuestbookMessagePage < AppPage
|
2
|
+
def initialize(form: nil, session:)
|
3
|
+
@form = form || GuestbookMessageForm.new
|
4
|
+
@session = session
|
5
|
+
end
|
6
|
+
|
7
|
+
def before_generate
|
8
|
+
if @session.signed_guestbook?
|
9
|
+
redirect_to(GuestbookPage)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def page_template
|
14
|
+
div(class: "flex flex-column items-center ff-sans") do
|
15
|
+
render global_component(FlashComponent)
|
16
|
+
div(class: "w-50 mv-5") do
|
17
|
+
brut_form(class: "w-100") do
|
18
|
+
FormTag(for: @form, class: "flex flex-column gap-3 items-end") do
|
19
|
+
h1(class: "ff-sans ma-0 lh-title f-5 self-start flex items-center gap-3") do
|
20
|
+
img(src: "/static/images/icon.png", class: "w-5")
|
21
|
+
span { "Sign The Guestbook" }
|
22
|
+
end
|
23
|
+
label(class: "flex flex-column gap-1 w-100") do
|
24
|
+
Inputs::InputTag(
|
25
|
+
form: @form,
|
26
|
+
input_name: "name",
|
27
|
+
placeholder: "e.g. Pat",
|
28
|
+
class: "f-3 pa-2 inset-shadow-1 br-2",
|
29
|
+
"data-1p-ignore": true
|
30
|
+
)
|
31
|
+
div(class: "flex items-start gap-3") do
|
32
|
+
span { "Your Message" }
|
33
|
+
ConstraintViolations(
|
34
|
+
form: @form,
|
35
|
+
input_name: "name"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
label(class: "flex flex-column gap-1 w-100") do
|
41
|
+
Inputs::TextareaTag(
|
42
|
+
form: @form,
|
43
|
+
input_name: "message",
|
44
|
+
placeholder: "e.g. What a great site!",
|
45
|
+
rows: 6,
|
46
|
+
class: "f-3 pa-2 inset-shadow-1 br-2"
|
47
|
+
)
|
48
|
+
div(class: "flex items-start gap-3") do
|
49
|
+
span { "Your Message" }
|
50
|
+
ConstraintViolations(
|
51
|
+
form: @form,
|
52
|
+
input_name: "message"
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
button(class: "bg-green-900 green-300 ph-5 pv-3 f-3 ba bc-green-600 br-pill shadow-1 pointer") do
|
57
|
+
"Sign!"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "You can leave a guestbook message" do
|
4
|
+
it "accepts your message after validation, then shows it with the others" do
|
5
|
+
page.goto(HomePage.routing)
|
6
|
+
|
7
|
+
link = page.locator("a[href='#{NewGuestbookMessagePage.routing}']")
|
8
|
+
|
9
|
+
link.click
|
10
|
+
|
11
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
12
|
+
|
13
|
+
button = page.locator("form button")
|
14
|
+
|
15
|
+
button.click
|
16
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
17
|
+
|
18
|
+
name_error_message = page.locator("brut-cv-messages[input-name='name']")
|
19
|
+
message_error_message = page.locator("brut-cv-messages[input-name='message']")
|
20
|
+
|
21
|
+
this_field = t("cv.this_field")
|
22
|
+
|
23
|
+
expect(name_error_message).to have_text(
|
24
|
+
t("cv.cs.valueMissing", field: this_field)
|
25
|
+
)
|
26
|
+
expect(message_error_message).to have_text(
|
27
|
+
t("cv.cs.valueMissing", field: this_field)
|
28
|
+
)
|
29
|
+
|
30
|
+
name_field = page.locator("form input[name='name']")
|
31
|
+
message_field = page.locator("form textarea[name='message']")
|
32
|
+
|
33
|
+
name_field.fill("Pat")
|
34
|
+
message_field.fill("FIRSTPOST!")
|
35
|
+
|
36
|
+
button.click
|
37
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
38
|
+
|
39
|
+
flash = page.locator("[role='alert']")
|
40
|
+
expect(flash).to have_text(t(:guestbook_not_saved))
|
41
|
+
expect(message_error_message).to have_text(
|
42
|
+
t("cv.ss.not_enough_words", field: this_field)
|
43
|
+
)
|
44
|
+
|
45
|
+
message_field.fill("OK, fine, this site is awesome!")
|
46
|
+
button.click
|
47
|
+
|
48
|
+
expect(page).to be_page_for(GuestbookPage)
|
49
|
+
|
50
|
+
quotes = page.locator("blockquote")
|
51
|
+
expect(quotes).to have_text("OK, fine, this site is awesome!")
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/mkbrut/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe GuestbookMessageHandler do
|
4
|
+
describe "#handle!" do
|
5
|
+
context "there are client-side constraint violations" do
|
6
|
+
it "generates the new guestbook message page" do
|
7
|
+
form = GuestbookMessageForm.new(params: {})
|
8
|
+
flash = empty_flash
|
9
|
+
session = empty_session
|
10
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
11
|
+
|
12
|
+
confidence_check do
|
13
|
+
expect(form).to have_constraint_violation(:name,key: :valueMissing)
|
14
|
+
end
|
15
|
+
|
16
|
+
handler = described_class.new(
|
17
|
+
form:,
|
18
|
+
flash:,
|
19
|
+
session:,
|
20
|
+
rack_request_ip:
|
21
|
+
)
|
22
|
+
result = nil
|
23
|
+
expect {
|
24
|
+
result = handler.handle
|
25
|
+
}.not_to change { DB::GuestbookMessage.count }
|
26
|
+
|
27
|
+
expect(result).to have_generated(NewGuestbookMessagePage)
|
28
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
context "there are server-side constraint violations" do
|
32
|
+
it "indicates a server-side error and generates the new guestbook message page" do
|
33
|
+
form = GuestbookMessageForm.new(params: {
|
34
|
+
name: "Pat",
|
35
|
+
message: "FIRSTPOST!!!!",
|
36
|
+
})
|
37
|
+
flash = empty_flash
|
38
|
+
session = empty_session
|
39
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
40
|
+
|
41
|
+
handler = described_class.new(
|
42
|
+
form:,
|
43
|
+
flash:,
|
44
|
+
session:,
|
45
|
+
rack_request_ip:
|
46
|
+
)
|
47
|
+
result = nil
|
48
|
+
expect {
|
49
|
+
result = handler.handle
|
50
|
+
}.not_to change { DB::GuestbookMessage.count }
|
51
|
+
|
52
|
+
expect(result).to have_generated(NewGuestbookMessagePage)
|
53
|
+
expect(form).to have_constraint_violation(:message, key: :not_enough_words)
|
54
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
context "the ip address has already been used" do
|
58
|
+
|
59
|
+
it "indicates a server-side error and generates the new guestbook message page" do
|
60
|
+
existing_message = create(:guestbook_message)
|
61
|
+
|
62
|
+
form = GuestbookMessageForm.new(params: {
|
63
|
+
name: "Pat",
|
64
|
+
message: "This website is amazing!",
|
65
|
+
})
|
66
|
+
flash = empty_flash
|
67
|
+
session = empty_session
|
68
|
+
rack_request_ip = existing_message.ip_address
|
69
|
+
|
70
|
+
handler = described_class.new(
|
71
|
+
form:,
|
72
|
+
flash:,
|
73
|
+
session:,
|
74
|
+
rack_request_ip:
|
75
|
+
)
|
76
|
+
result = nil
|
77
|
+
expect {
|
78
|
+
result = handler.handle
|
79
|
+
}.not_to change { DB::GuestbookMessage.count }
|
80
|
+
|
81
|
+
expect(result).to have_generated(NewGuestbookMessagePage)
|
82
|
+
expect(form).to have_constraint_violation(:name, key: :already_posted)
|
83
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
context "no constraint violations, unused ip address" do
|
87
|
+
it "saves the message and redirects back to the guestbook page" do
|
88
|
+
form = GuestbookMessageForm.new(params: {
|
89
|
+
name: "Pat",
|
90
|
+
message: "This website is amazing!",
|
91
|
+
})
|
92
|
+
flash = empty_flash
|
93
|
+
session = empty_session
|
94
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
95
|
+
|
96
|
+
handler = described_class.new(
|
97
|
+
form:,
|
98
|
+
flash:,
|
99
|
+
session:,
|
100
|
+
rack_request_ip:
|
101
|
+
)
|
102
|
+
result = nil
|
103
|
+
expect {
|
104
|
+
result = handler.handle
|
105
|
+
}.to change { DB::GuestbookMessage.count }.by(1)
|
106
|
+
|
107
|
+
message = DB::GuestbookMessage.order(Sequel.desc(:created_at)).first
|
108
|
+
|
109
|
+
expect(message.name).to eq("Pat")
|
110
|
+
expect(message.message).to eq("This website is amazing!")
|
111
|
+
expect(message.ip_address).to eq(rack_request_ip)
|
112
|
+
|
113
|
+
expect(result).to have_redirected_to(GuestbookPage,
|
114
|
+
anchor: message.external_id)
|
115
|
+
expect(form).not_to have_constraint_violation(:name,
|
116
|
+
key: :already_posted)
|
117
|
+
expect(flash.notice).to eq("guestbook_saved")
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe GuestbookPage do
|
4
|
+
it "shows current guestbook messages" do
|
5
|
+
messages = 5.times.map {
|
6
|
+
create(:guestbook_message)
|
7
|
+
}
|
8
|
+
confidence_check { expect(DB::GuestbookMessage.all.length).to eq(5) }
|
9
|
+
|
10
|
+
result = generate_and_parse(self.described_class.new(session: empty_session))
|
11
|
+
|
12
|
+
message_elements = result.css("div:has(> blockquote)")
|
13
|
+
expect(message_elements.length).to eq(5)
|
14
|
+
messages.each do |message|
|
15
|
+
element = message_elements.select {
|
16
|
+
it.css("blockquote").text.strip == message.message
|
17
|
+
}
|
18
|
+
expect(element.size).to eq(1),
|
19
|
+
"Found #{element.size} blockquotes for message #{message.external_id}, instead of exactly 1"
|
20
|
+
expect(element[0].css("p").text).to include(message.name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
context "when you've signed the guestbook" do
|
24
|
+
it "shows a link to view your message and not a link to make one" do
|
25
|
+
message = create(:guestbook_message)
|
26
|
+
session = empty_session
|
27
|
+
|
28
|
+
session.signed_guestbook(message)
|
29
|
+
|
30
|
+
result = generate_and_parse(self.described_class.new(session:))
|
31
|
+
|
32
|
+
add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
|
33
|
+
expect(add_link).to eq(nil)
|
34
|
+
|
35
|
+
view_link = result.e("a[href='##{message.external_id}']")
|
36
|
+
expect(view_link).not_to eq(nil)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
context "when you have not signed the guestbook" do
|
40
|
+
it "shows a link to view your message and not a link to make one" do
|
41
|
+
message = create(:guestbook_message)
|
42
|
+
|
43
|
+
result = generate_and_parse(self.described_class.new(session: empty_session))
|
44
|
+
|
45
|
+
add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
|
46
|
+
expect(add_link).not_to eq(nil)
|
47
|
+
|
48
|
+
view_link = result.e("a[href='##{message.external_id}']")
|
49
|
+
expect(view_link).to eq(nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# This is a multi-stage build, meaning one image will be built that
|
2
|
+
# has necessarily dev tools in it, and that image is used to create artifacts
|
3
|
+
# that will be copied into the second build, which will create the image
|
4
|
+
# you deploy to production.
|
5
|
+
#
|
6
|
+
# # Maintaing this file
|
7
|
+
#
|
8
|
+
# You own this file now. While there could be a way to create it from
|
9
|
+
# the Dockerfile.dx, for now, you'll need to stay on top of it:
|
10
|
+
#
|
11
|
+
# 1 - ensure the versions of Ruby match
|
12
|
+
# 2 - ensure the versions of NodeJS match
|
13
|
+
# 3 - ensure that anything you installed in Dockerfile.dx to make
|
14
|
+
# the app work or to pre-generate assets is set up here as well.
|
15
|
+
# You are advised to make that setup identical.
|
16
|
+
#
|
17
|
+
# You can test this locally but using the `--plaform` flag to `bin/deploy deploy`
|
18
|
+
# and running a container locally. You'll need all infrastructure available
|
19
|
+
# but it can be done to test things before you deploy. If you need to.
|
20
|
+
|
21
|
+
# Use Ruby 3.4 as a base.
|
22
|
+
FROM docker.io/library/ruby:3.4 AS base
|
23
|
+
|
24
|
+
# bin/deploy will inject this value so that your app's GIT SHA1
|
25
|
+
# is in the environment in production, thus allowing you to be more
|
26
|
+
# sure of what's actually running.
|
27
|
+
ARG app_git_sha1
|
28
|
+
|
29
|
+
WORKDIR /brut-app
|
30
|
+
|
31
|
+
# Install base packages
|
32
|
+
#
|
33
|
+
# - ca-certificates is needed for other installs
|
34
|
+
# - curl is needed generally for other installs and by Heroku
|
35
|
+
# - gnupg is a PGP replacement used in making sure new APT repos work
|
36
|
+
# - libjemalloc2 in theory speeds up Ruby
|
37
|
+
# - lsb-release is used to generically access information for this OS's version
|
38
|
+
# - wget allows us to copy/paste commands from vendors about how to install
|
39
|
+
# software even though it does the same thing as curl
|
40
|
+
RUN apt-get update --quiet --yes && \
|
41
|
+
apt-get install --no-install-recommends --quiet --yes \
|
42
|
+
ca-certificates \
|
43
|
+
curl \
|
44
|
+
gnupg \
|
45
|
+
libjemalloc2 \
|
46
|
+
lsb-release \
|
47
|
+
rsync \
|
48
|
+
wget && \
|
49
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
50
|
+
|
51
|
+
# Install the PostgreSQL client. The latest version is not available
|
52
|
+
# from Debian, so we set up our own. This should match what's in Dockerfile.dx
|
53
|
+
# and ideally the version of Postgres used in production.
|
54
|
+
#
|
55
|
+
# Incancation is based on: https://www.postgresql.org/download/linux/debian/
|
56
|
+
RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
|
57
|
+
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
58
|
+
apt-get update && \
|
59
|
+
apt-get --yes --quiet install postgresql-client-16 && \
|
60
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
61
|
+
|
62
|
+
# Set basic env vars for production
|
63
|
+
ENV RACK_ENV="production" \
|
64
|
+
BUNDLE_DEPLOYMENT="1" \
|
65
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
66
|
+
BUNDLE_WITHOUT="development" \
|
67
|
+
APP_GIT_SHA1="${app_git_sha1}"
|
68
|
+
|
69
|
+
# This makes a new image that we'll throw away after building
|
70
|
+
# needed artifacts.
|
71
|
+
FROM base AS build
|
72
|
+
|
73
|
+
# - build-essential is needed for almost any build tool we have to install
|
74
|
+
# - git is needed to install some things
|
75
|
+
# - libpq-dev is needed by postgres
|
76
|
+
# - pkg-config is, I guess, not considered "essential" (as in build-essential),
|
77
|
+
# but still needed to install downstream stuff
|
78
|
+
RUN apt-get update --quiet --yes && \
|
79
|
+
apt-get install --no-install-recommends --quiet --yes \
|
80
|
+
build-essential \
|
81
|
+
git \
|
82
|
+
libpq-dev \
|
83
|
+
pkg-config
|
84
|
+
|
85
|
+
# Install NodeJS, per https://nodejs.org/en/download
|
86
|
+
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
|
87
|
+
\. "$HOME/.nvm/nvm.sh" && \
|
88
|
+
nvm install 22 && \
|
89
|
+
node -v && nvm current && npm -v
|
90
|
+
|
91
|
+
# Copy the app into the file, excluding the contents of .dockerignore
|
92
|
+
COPY . .
|
93
|
+
|
94
|
+
# Install RubyGems from app's Gemfile
|
95
|
+
RUN bundle install --verbose && \
|
96
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
|
97
|
+
|
98
|
+
# Install Node Modules from package.json
|
99
|
+
RUN \. "$HOME/.nvm/nvm.sh" && \
|
100
|
+
nvm use default && \
|
101
|
+
npm clean-install --no-audit --no-fund --verbose
|
102
|
+
|
103
|
+
# Build all assets
|
104
|
+
RUN \. "$HOME/.nvm/nvm.sh" && \
|
105
|
+
nvm use default && \
|
106
|
+
bin/build-assets && \
|
107
|
+
rm -rf node_modules
|
108
|
+
|
109
|
+
# We are now switching back to building the image that will be deployed.
|
110
|
+
|
111
|
+
FROM base
|
112
|
+
# Copy built artifacts from the throwaway image: gems, application
|
113
|
+
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
114
|
+
COPY --from=build /brut-app /brut-app
|
115
|
+
|
116
|
+
# For security, set directories that will be written to be owned by non-root
|
117
|
+
RUN groupadd --system --gid 1000 brut && \
|
118
|
+
useradd brut --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
119
|
+
chown -R brut:brut logs tmp
|
120
|
+
USER 1000:1000
|
121
|
+
|
122
|
+
# This is used to execute other commands. When the app is run in production,
|
123
|
+
# this script is used to run it.
|
124
|
+
ENTRYPOINT ["/brut-app/deploy/docker-entrypoint"]
|
125
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash -e
|
2
|
+
|
3
|
+
# This is the Docker entrypoint, mean this script is used to execute
|
4
|
+
# the command given to the RUN directive.
|
5
|
+
#
|
6
|
+
# The reason this exists is to load jemalloc. If you don't want to use
|
7
|
+
# jemalloc, you can delete this script, as well as the ENTRYPOINT directive
|
8
|
+
# in deploy/Dockerfile.
|
9
|
+
|
10
|
+
# Enable jemalloc for reduced memory usage and latency.
|
11
|
+
if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then
|
12
|
+
export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)"
|
13
|
+
fi
|
14
|
+
|
15
|
+
exec "${@}"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# This contains settings for your Heroku setup.
|
2
|
+
# You own and maintain this file. It is required by
|
3
|
+
# build/deploy
|
4
|
+
class HerokuConfig
|
5
|
+
|
6
|
+
# Return a Hash oif additional images to run, beyond "web" and "release".
|
7
|
+
#
|
8
|
+
# The format of this Hash is:
|
9
|
+
#
|
10
|
+
# {
|
11
|
+
# "«image name» => {
|
12
|
+
# cmd: "«command line for Dockerfile RUN directive»",
|
13
|
+
# }
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
# For example, if you have the Sidekiq segment installed, `bin/run-sidekiq`
|
17
|
+
# runs Sidekiq, so you would return this hash:
|
18
|
+
#
|
19
|
+
# {
|
20
|
+
# "sidekiq" => {
|
21
|
+
# cmd: "bin/run-sidekiq",
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
def self.additional_images = {}
|
25
|
+
end
|
26
|
+
|