brut 0.0.29 → 0.1.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/Gemfile.lock +1 -1
- data/README.md +23 -2
- data/assets/LogoStop.pxd +0 -0
- data/assets/MetroLogo.graffle +0 -0
- data/assets/SocialImage.png +0 -0
- data/assets/SocialImage.pxd +0 -0
- data/brutrb.com/.vitepress/config.mjs +45 -8
- data/brutrb.com/.vitepress/theme/style.css +6 -5
- data/brutrb.com/ai.md +10 -15
- data/brutrb.com/assets.md +2 -9
- data/brutrb.com/brut-js.md +12 -2
- data/brutrb.com/cli.md +9 -13
- data/brutrb.com/components.md +118 -96
- data/brutrb.com/configuration.md +3 -4
- data/brutrb.com/css.md +2 -2
- data/brutrb.com/custom-element-tests.md +3 -4
- data/brutrb.com/database-access.md +1 -1
- data/brutrb.com/database-schema.md +29 -41
- data/brutrb.com/dev-environment.md +7 -7
- data/brutrb.com/dir-structure.md +120 -0
- data/brutrb.com/doc-conventions.md +18 -15
- data/brutrb.com/dx +1 -0
- data/brutrb.com/end-to-end-tests.md +12 -10
- data/brutrb.com/features.md +373 -0
- data/brutrb.com/flash-and-session.md +115 -131
- data/brutrb.com/form-constraints.md +266 -0
- data/brutrb.com/forms.md +140 -765
- data/brutrb.com/getting-started.md +10 -11
- data/brutrb.com/handlers.md +119 -95
- data/brutrb.com/hooks.md +18 -20
- data/brutrb.com/i18n.md +6 -4
- data/brutrb.com/images/LogoStop.png +0 -0
- data/brutrb.com/instrumentation.md +7 -10
- data/brutrb.com/javascript.md +14 -14
- data/brutrb.com/keyword-injection.md +72 -114
- data/brutrb.com/layouts.md +20 -52
- data/brutrb.com/lsp.md +1 -1
- data/brutrb.com/overview.md +30 -372
- data/brutrb.com/pages.md +119 -207
- data/brutrb.com/public/SocialImage.png +0 -0
- data/brutrb.com/public/favicon.ico +0 -0
- data/brutrb.com/recipes/alternate-layouts.md +32 -0
- data/brutrb.com/recipes/authentication.md +315 -6
- data/brutrb.com/recipes/blank-layouts.md +22 -0
- data/brutrb.com/recipes/custom-flash.md +51 -0
- data/brutrb.com/recipes/indexed-forms.md +149 -0
- data/brutrb.com/recipes/text-field-component.md +182 -0
- data/brutrb.com/routes.md +56 -82
- data/brutrb.com/security.md +0 -3
- data/brutrb.com/space-time-continuum.md +8 -12
- data/brutrb.com/tutorial.md +1 -1
- data/brutrb.com/why.md +19 -0
- data/docs/404.html +8 -3
- data/docs/SocialImage.png +0 -0
- data/docs/ai.html +11 -6
- data/docs/api/Brut/BackEnd/SeedData.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
- data/docs/api/Brut/BackEnd/Validators.html +1 -1
- data/docs/api/Brut/BackEnd.html +1 -1
- data/docs/api/Brut/CLI/App.html +1 -1
- data/docs/api/Brut/CLI/AppRunner.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB.html +1 -1
- data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
- data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test.html +1 -1
- data/docs/api/Brut/CLI/Apps.html +1 -1
- data/docs/api/Brut/CLI/Command.html +1 -1
- data/docs/api/Brut/CLI/Error.html +1 -1
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
- data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
- data/docs/api/Brut/CLI/Executor.html +1 -1
- data/docs/api/Brut/CLI/InvalidOption.html +1 -1
- data/docs/api/Brut/CLI/Options.html +1 -1
- data/docs/api/Brut/CLI/Output.html +1 -1
- data/docs/api/Brut/CLI/SystemExecError.html +1 -1
- data/docs/api/Brut/CLI.html +1 -1
- data/docs/api/Brut/FactoryBot.html +1 -1
- data/docs/api/Brut/Framework/App.html +1 -1
- data/docs/api/Brut/Framework/Config.html +1 -1
- data/docs/api/Brut/Framework/Container.html +1 -1
- data/docs/api/Brut/Framework/Error.html +1 -1
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
- data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
- data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
- data/docs/api/Brut/Framework/Errors.html +1 -1
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
- data/docs/api/Brut/Framework/MCP.html +1 -1
- data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
- data/docs/api/Brut/Framework.html +1 -1
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
- data/docs/api/Brut/FrontEnd/Component.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
- data/docs/api/Brut/FrontEnd/Components.html +1 -1
- data/docs/api/Brut/FrontEnd/Download.html +1 -1
- data/docs/api/Brut/FrontEnd/Flash.html +1 -1
- data/docs/api/Brut/FrontEnd/Form.html +9 -11
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +135 -20
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +135 -20
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms.html +1 -1
- data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
- data/docs/api/Brut/FrontEnd/Handler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
- data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
- data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
- data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
- data/docs/api/Brut/FrontEnd/Layout.html +1 -1
- data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
- data/docs/api/Brut/FrontEnd/Page.html +1 -1
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
- data/docs/api/Brut/FrontEnd/Pages.html +1 -1
- data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing.html +1 -1
- data/docs/api/Brut/FrontEnd/Session.html +1 -1
- data/docs/api/Brut/FrontEnd.html +1 -1
- data/docs/api/Brut/I18n/BaseMethods.html +1 -1
- data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
- data/docs/api/Brut/I18n/ForCLI.html +1 -1
- data/docs/api/Brut/I18n/ForHTML.html +1 -1
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
- data/docs/api/Brut/I18n.html +1 -1
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
- data/docs/api/Brut/Instrumentation.html +1 -1
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
- data/docs/api/Brut/SinatraHelpers.html +1 -1
- data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
- data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
- data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
- data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
- data/docs/api/Brut/SpecSupport.html +1 -1
- data/docs/api/Brut.html +1 -1
- data/docs/api/Clock.html +1 -1
- data/docs/api/RichString.html +1 -1
- data/docs/api/SemanticLogger/Appender/Async.html +1 -1
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
- data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
- data/docs/api/Sequel/Extensions.html +1 -1
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
- data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
- data/docs/api/Sequel/Plugins/FindBang.html +1 -1
- data/docs/api/Sequel/Plugins.html +1 -1
- data/docs/api/Sequel.html +1 -1
- data/docs/api/_index.html +1 -1
- data/docs/api/file.README.html +22 -3
- data/docs/api/index.html +22 -3
- data/docs/api/method_list.html +16 -0
- data/docs/api/top-level-namespace.html +1 -1
- data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
- data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
- data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
- data/docs/assets/{app.BhrfSt68.js → app.ClaS47Ru.js} +1 -1
- data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
- data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
- data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
- data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
- data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
- data/docs/assets/chunks/{VPLocalSearchBox.Dpot_2H4.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
- data/docs/assets/chunks/{theme.N2SNVLgU.js → theme.B45bvibT.js} +2 -2
- data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
- data/docs/assets/components.md.DatoNgFo.js +96 -0
- data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
- data/docs/assets/{configuration.md.LG-zIBww.js → configuration.md.DeyhpqEx.js} +3 -3
- data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
- data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
- data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
- data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
- data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
- data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
- data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
- data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
- data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
- data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
- data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
- data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
- data/docs/assets/features.md.DPFXsy0z.js +154 -0
- data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
- data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
- data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
- data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
- data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
- data/docs/assets/forms.md.C2Dizvzq.js +64 -0
- data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
- data/docs/assets/{getting-started.md.Dj0qtZI2.js → getting-started.md.C93e0odB.js} +5 -5
- data/docs/assets/{getting-started.md.Dj0qtZI2.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
- data/docs/assets/handlers.md.Chyri6KA.js +54 -0
- data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
- data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
- data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
- data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
- data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
- data/docs/assets/{index.md.CuBB-BdM.js → index.md.CAMqGBJE.js} +1 -1
- data/docs/assets/{index.md.CuBB-BdM.lean.js → index.md.CAMqGBJE.lean.js} +1 -1
- data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
- data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
- data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
- data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
- data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
- data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
- data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
- data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
- data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
- data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
- data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
- data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
- data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
- data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
- data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
- data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
- data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
- data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
- data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
- data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
- data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
- data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
- data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
- data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
- data/docs/assets/routes.md.B8kfUPHU.js +21 -0
- data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
- data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
- data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
- data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
- data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
- data/docs/assets/{style.B2o1L9eN.css → style.prAgp4yQ.css} +1 -1
- data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
- data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
- data/docs/assets/why.md.C-hk5xgJ.js +1 -0
- data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
- data/docs/assets.html +12 -7
- data/docs/brut-js/api/AjaxSubmit.html +1 -1
- data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
- data/docs/brut-js/api/Autosubmit.html +1 -1
- data/docs/brut-js/api/Autosubmit.js.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
- data/docs/brut-js/api/BrutCustomElements.html +1 -1
- data/docs/brut-js/api/BufferedLogger.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
- data/docs/brut-js/api/Form.html +1 -1
- data/docs/brut-js/api/Form.js.html +1 -1
- data/docs/brut-js/api/I18nTranslation.html +1 -1
- data/docs/brut-js/api/I18nTranslation.js.html +1 -1
- data/docs/brut-js/api/LocaleDetection.html +1 -1
- data/docs/brut-js/api/LocaleDetection.js.html +1 -1
- data/docs/brut-js/api/Logger.html +1 -1
- data/docs/brut-js/api/Logger.js.html +1 -1
- data/docs/brut-js/api/Message.html +1 -1
- data/docs/brut-js/api/Message.js.html +1 -1
- data/docs/brut-js/api/PrefixedLogger.html +1 -1
- data/docs/brut-js/api/RichString.html +1 -1
- data/docs/brut-js/api/RichString.js.html +1 -1
- data/docs/brut-js/api/Tabs.html +1 -1
- data/docs/brut-js/api/Tabs.js.html +1 -1
- data/docs/brut-js/api/Tracing.html +1 -1
- data/docs/brut-js/api/Tracing.js.html +1 -1
- data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
- data/docs/brut-js/api/external-Performance.html +1 -1
- data/docs/brut-js/api/external-Promise.html +1 -1
- data/docs/brut-js/api/external-ValidityState.html +1 -1
- data/docs/brut-js/api/external-Window.html +1 -1
- data/docs/brut-js/api/external-fetch.html +1 -1
- data/docs/brut-js/api/global.html +1 -1
- data/docs/brut-js/api/index.html +1 -1
- data/docs/brut-js/api/index.js.html +1 -1
- data/docs/brut-js/api/module-testing.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
- data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
- data/docs/brut-js/api/testing.DOMCreator.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
- data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
- data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
- data/docs/brut-js/api/testing_index.js.html +1 -1
- data/docs/brut-js.html +12 -7
- data/docs/business-logic.html +10 -5
- data/docs/cli.html +26 -26
- data/docs/components.html +61 -64
- data/docs/configuration.html +13 -8
- data/docs/css.html +14 -9
- data/docs/custom-element-tests.html +14 -9
- data/docs/database-access.html +12 -7
- data/docs/database-schema.html +15 -10
- data/docs/deployment.html +10 -5
- data/docs/dev-environment.html +12 -7
- data/docs/dir-structure.html +74 -0
- data/docs/doc-conventions.html +11 -6
- data/docs/end-to-end-tests.html +15 -8
- data/docs/favicon.ico +0 -0
- data/docs/features.html +182 -0
- data/docs/flash-and-session.html +73 -82
- data/docs/form-constraints.html +118 -0
- data/docs/forms.html +57 -367
- data/docs/getting-started.html +15 -10
- data/docs/handlers.html +51 -61
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +14 -9
- data/docs/i18n.html +12 -7
- data/docs/index.html +11 -6
- data/docs/instrumentation.html +12 -7
- data/docs/javascript.html +17 -12
- data/docs/jobs.html +10 -5
- data/docs/keyword-injection.html +22 -21
- data/docs/layouts.html +12 -20
- data/docs/lsp.html +11 -6
- data/docs/markdown-examples.html +10 -5
- data/docs/middleware.html +10 -5
- data/docs/not-released.html +10 -5
- data/docs/overview.html +11 -138
- data/docs/pages.html +49 -121
- data/docs/recipes/alternate-layouts.html +50 -0
- data/docs/recipes/authentication.html +166 -6
- data/docs/recipes/blank-layouts.html +43 -0
- data/docs/recipes/custom-flash.html +54 -0
- data/docs/recipes/indexed-forms.html +102 -0
- data/docs/recipes/text-field-component.html +129 -0
- data/docs/routes.html +16 -19
- data/docs/security.html +11 -6
- data/docs/seed-data.html +10 -5
- data/docs/space-time-continuum.html +11 -6
- data/docs/tutorial.html +11 -6
- data/docs/unit-tests.html +10 -5
- data/docs/why.html +29 -0
- data/lib/brut/front_end/form.rb +8 -8
- data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
- data/lib/brut/front_end/forms/select_input.rb +8 -1
- data/lib/brut/version.rb +1 -1
- data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
- data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
- metadata +117 -75
- data/brutrb.com/public/images/logo-300.png +0 -0
- data/brutrb.com/public/images/logo.png +0 -0
- data/docs/assets/LogoStop.X8x-4riz.png +0 -0
- data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
- data/docs/assets/chunks/@localSearchIndexroot.CeRAdP1K.js +0 -1
- data/docs/assets/components.md.CRUMdRoN.js +0 -104
- data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
- data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
- data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
- data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
- data/docs/assets/forms.md.B-koVgyw.js +0 -379
- data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
- data/docs/assets/handlers.md.089DVD3v.js +0 -69
- data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
- data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
- data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
- data/docs/assets/overview.md.DVKRM8zl.js +0 -133
- data/docs/assets/overview.md.DVKRM8zl.lean.js +0 -1
- data/docs/assets/pages.md.BE3kfOc5.js +0 -122
- data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
- data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
- data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
- data/docs/assets/routes.md.BMM7peut.js +0 -29
- data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
- data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
- data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
- data/docs/images/logo-300.png +0 -0
- data/docs/images/logo.png +0 -0
- /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
- /data/docs/assets/{configuration.md.LG-zIBww.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
- /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
- /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
- /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
- /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
@@ -7,15 +7,16 @@ this recipe will demonstrate all the moving parts:
|
|
7
7
|
* How to design pages that require authentication
|
8
8
|
* How to manage the signed-in user in code
|
9
9
|
|
10
|
-
## Feature
|
10
|
+
## Feature
|
11
11
|
|
12
|
-
* Visitors can
|
13
|
-
* Visitors can
|
14
|
-
* Visitors cannot access the
|
15
|
-
* Visitors can access the about page without logging in
|
12
|
+
* Visitors can log in with an email, that is assumed to have been inserted previously (no passwords or signup, just to simplify the recipe)
|
13
|
+
* Visitors can access the home page without logging in
|
14
|
+
* Visitors cannot access the dashboard page without logging in
|
16
15
|
|
17
16
|
## Recipe
|
18
17
|
|
18
|
+
### Set up Database and Seed Data
|
19
|
+
|
19
20
|
First, we'll make a database table called `accounts` that will have an email field
|
20
21
|
and a password hash field.
|
21
22
|
|
@@ -23,4 +24,312 @@ and a password hash field.
|
|
23
24
|
bin/db new-migration accounts
|
24
25
|
```
|
25
26
|
|
26
|
-
This will create a file in `app/src/back_end/data_models/migrations
|
27
|
+
This will create a file in `app/src/back_end/data_models/migrations`. We'll edit it
|
28
|
+
to create a new table called `accounts`:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
Sequel.migration do
|
32
|
+
up do
|
33
|
+
create_table :accounts, comment: "People or systems who can access this system", external_id: true do
|
34
|
+
column :email, :text, unique: true
|
35
|
+
column :deactivated_at, :timestamptz, null: true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
We'll also create `app/src/back_end/data_models/db/account.rb`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class DB::Account < AppDataModel
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Next, we'll create a factory for it in `specs/factories/db/account.factory.rb`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require "bcrypt"
|
52
|
+
FactoryBot.define do
|
53
|
+
factory :account, class: "DB::Account" do
|
54
|
+
email { Faker::Internet.unique.email }
|
55
|
+
trait :inactive do
|
56
|
+
deactivated_at { Time.now }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Next, we'll make seed data in `app/src/back_end/data_models/seed/app_seed_data.rb`
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
require "brut/back_end/seed_data"
|
66
|
+
class AppSeedData < Brut::BackEnd::SeedData
|
67
|
+
include FactoryBot::Syntax::Methods
|
68
|
+
def seed!
|
69
|
+
create(:account, email: "pat@example.com")
|
70
|
+
create(:account, :inactive, email: "chris@example.com")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Now, let's apply this to the database and load the seed data:
|
76
|
+
|
77
|
+
```
|
78
|
+
> bin/db migrate
|
79
|
+
> bin/db migrate -e test
|
80
|
+
> bin/db seed
|
81
|
+
```
|
82
|
+
|
83
|
+
### Create a Login Page
|
84
|
+
|
85
|
+
To make this UI work, we'll need a login page and a dashboard page.
|
86
|
+
|
87
|
+
```
|
88
|
+
> bin/scaffold page /login
|
89
|
+
> bin/scaffold page /dashboard
|
90
|
+
```
|
91
|
+
|
92
|
+
We'll also need a login form:
|
93
|
+
|
94
|
+
```
|
95
|
+
> bin/scaffold form /login
|
96
|
+
```
|
97
|
+
|
98
|
+
We'll add a link on the HomePage to log in:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# app/src/front_end/pages/home_page.rb
|
102
|
+
class HomePage < AppPage
|
103
|
+
def page_template
|
104
|
+
h1 { "Welcome!" }
|
105
|
+
a(href: LoginPage.routing) {
|
106
|
+
"Log in"
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
Before building the login page, we'll need the form. It'll just have one field:
|
113
|
+
email:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# app/src/front_end/forms/login_form.rb
|
117
|
+
class LoginForm < AppForm
|
118
|
+
input :email # Brut will make this type=email and required
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
Now, we can create the login page:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# app/src/front_end/pages/login_page.rb
|
126
|
+
class LoginPage < AppPage
|
127
|
+
|
128
|
+
include Brut::FrontEnd::Components
|
129
|
+
|
130
|
+
# An existing form can be passed in, so that this
|
131
|
+
# page can be shown with form errors from a previous
|
132
|
+
# login attempt
|
133
|
+
def initialize(form: nil)
|
134
|
+
@form = form || LoginForm.new
|
135
|
+
end
|
136
|
+
|
137
|
+
def page_template
|
138
|
+
h1 { "Login, please!" }
|
139
|
+
brut_form do
|
140
|
+
FormTag(for: @form) do
|
141
|
+
label do
|
142
|
+
Inputs::TextField(form: @form, input_name: :email)
|
143
|
+
div { "Email" }
|
144
|
+
ConstraintViolations(form: @form, input_name: :email)
|
145
|
+
end
|
146
|
+
button do
|
147
|
+
"Login"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
Let's style the constraint violations in `app/src/front_end/css/index.css`:
|
156
|
+
|
157
|
+
```css
|
158
|
+
/* app/src/front_end/css/index.css */
|
159
|
+
brut-cv {
|
160
|
+
display: none;
|
161
|
+
}
|
162
|
+
|
163
|
+
brut-cv[server-side],
|
164
|
+
brut-form[submitted-invalid] brut-cv {
|
165
|
+
display: block;
|
166
|
+
color var(--red-300);
|
167
|
+
}
|
168
|
+
```
|
169
|
+
|
170
|
+
Now, you can click on "Login", and you should see a client-side error message.
|
171
|
+
|
172
|
+
### Handle Logins
|
173
|
+
|
174
|
+
Now, we'll build out the login handler. An email must exist and be active to be
|
175
|
+
allowed in.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# app/src/front_end/handlers/login_handler.rb
|
179
|
+
class LoginHandler < AppHandler
|
180
|
+
def initialize(form:, session:, flash:)
|
181
|
+
@form = form
|
182
|
+
@session = session
|
183
|
+
@flash = flash
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle
|
187
|
+
if !form.constraint_violations? # no client-side issues
|
188
|
+
account = DB::Account.find(email: form.email, deactivated_at: nil)
|
189
|
+
if !account
|
190
|
+
form.server_side_constraint_violation(
|
191
|
+
input_name: :email,
|
192
|
+
key: :no_such_account
|
193
|
+
)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
if form.constraint_violations?
|
197
|
+
LoginPage.new(form: @form)
|
198
|
+
else
|
199
|
+
session.login!(account:)
|
200
|
+
redirect_to(DashboardPage)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
Hopefully, this logic is straightforward. We'll need to allow `AppSession` to
|
207
|
+
implement `login!`. We'll also need to have it fetch the `DB::Account` from the
|
208
|
+
session, we'll add that, too.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
# app/src/front_end/support/app_session.rb
|
212
|
+
class AppSession < Brut::FrontEnd::Session
|
213
|
+
def login!(account:)
|
214
|
+
self[:account_id] = account.id
|
215
|
+
end
|
216
|
+
def account
|
217
|
+
DB::Account.find(id: self[:account_id])
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
Now, we can build the dashboard page to greet them. Instead of injecting the
|
223
|
+
session, however, we're going to inject the account as `current_account:`. We'll
|
224
|
+
set this up in a minute.
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
# app/src/front_end/pages/dashboard_page.rb
|
228
|
+
class DashboardPage < AppPage
|
229
|
+
def initialize(current_account:)
|
230
|
+
@current_account = current_account
|
231
|
+
end
|
232
|
+
|
233
|
+
def page_template
|
234
|
+
h1 { "Dashboard" }
|
235
|
+
h2 { "Hello #{@current_account.email}!" }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
```
|
239
|
+
|
240
|
+
### Injecting the Current Account
|
241
|
+
|
242
|
+
We want the current account to be in the `Brut::FrontEnd::RequestContext` if the
|
243
|
+
visitor is logged in. We'll do that in a route hook.
|
244
|
+
|
245
|
+
First, we'll declare it in `App`:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
# app/src/app.rb
|
249
|
+
class App < Brut::Framework::App
|
250
|
+
|
251
|
+
# ...
|
252
|
+
|
253
|
+
before :SetupCurrentAccount
|
254
|
+
|
255
|
+
# ...
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
Now, we can build the `SetupCurrentAccount` route hook. Since it'll run after
|
260
|
+
`Brut::FrontEnd::RouteHooks::SetupRequestContext`, we can assume a `RequestContext`
|
261
|
+
will be available for injection. The session will be, too, of course:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
# app/src/front_end/hooks/setup_current_account.rb
|
265
|
+
class SetupCurrentAccount < Brut::FrontEnd::RouteHook
|
266
|
+
def before(request_context:, session:)
|
267
|
+
logged_in = !!session.account
|
268
|
+
# NOTE: we do not insert nil. Either insert a value or don't insert.
|
269
|
+
if logged_in
|
270
|
+
request_context[:current_account] = session.account
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
```
|
275
|
+
|
276
|
+
At this point, the code we've written should work. The only problem is that anyone
|
277
|
+
can access the Dashboard page. Granted, doing so without being logged in will cause
|
278
|
+
an error, but we don't want that.
|
279
|
+
|
280
|
+
### Requiring Login
|
281
|
+
|
282
|
+
To require login, we'll add to the `SetupCurrentAccount` hook we created. We want to
|
283
|
+
allow access to the login page as well as any Brut-owned paths. If a logged-out
|
284
|
+
user access a restricted page, we'll redirect them to the login page.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
# app/src/front_end/hooks/setup_current_account.rb
|
288
|
+
class SetupCurrentAccount < Brut::FrontEnd::RouteHook
|
289
|
+
def before(request_context:, session:)
|
290
|
+
logged_in = !!session.account
|
291
|
+
if logged_in
|
292
|
+
request_context[:current_account] = session.account
|
293
|
+
end
|
294
|
+
|
295
|
+
is_login_page = request.path_info.match(/#{Regexp.escape(LoginPage.routing)}/
|
296
|
+
is_brut_owned_path = env["brut.owned_path"]
|
297
|
+
|
298
|
+
path_requires_login = !is_login_page &&
|
299
|
+
!is_brut_owned_path
|
300
|
+
|
301
|
+
if !logged_in && path_requires_login
|
302
|
+
redirect_to(LoginPage)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
```
|
307
|
+
|
308
|
+
And that's it! The visitor should be redirected if they aren't logged in, but
|
309
|
+
should be allowed to restricted pages like the dashboard page if they are.
|
310
|
+
|
311
|
+
### You Don't Need Page Hooks for This
|
312
|
+
|
313
|
+
Implementing something like this in Rails would usually involve similar code to what
|
314
|
+
we just did, but pages requiring login would have some sort of `before_action`:
|
315
|
+
|
316
|
+
```ruby{2}
|
317
|
+
class WidgetsController < ApplicationController
|
318
|
+
before_action :require_login!
|
319
|
+
|
320
|
+
# ...
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
This could be shared in a parent page, but you essentially have to remember to do this on every page that requires login (or do the opposite - allow specific pages to be accessed without logging in).
|
325
|
+
|
326
|
+
In Rails, this is a good practice, because even though your views won't route a
|
327
|
+
logged-out visitor to a logged-in page, URL hacking or bugs could result in an
|
328
|
+
attempt to do so. You need the failsafe.
|
329
|
+
|
330
|
+
In Brut, the very definition of the page's class includes the requirement for the
|
331
|
+
`current_account`. The page cannot be instantiated without it.
|
332
|
+
|
333
|
+
Thus, there is no need for a failsafe. `SetupCurrentAccount` handles checking the
|
334
|
+
routes, and that's it. If someone hacks a URL or a bug in the code sends a
|
335
|
+
logged-out visitor to the dashboard page, Brut literally cannot handle the request, since the `current_account` will be missing.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Blank or No Layout
|
2
|
+
|
3
|
+
If you don't want a layout, you are encouraged to create a blank layout, for example:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class BlankLayout < Brut::FrontEnd::Layout
|
7
|
+
def view_template
|
8
|
+
yield
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# use like so:
|
13
|
+
|
14
|
+
class NakedPage < AppPage
|
15
|
+
def layout = "blank"
|
16
|
+
|
17
|
+
def page_template
|
18
|
+
# ...
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Custom Flash Class
|
2
|
+
|
3
|
+
If you want to have a more sophisticated [Flash](/flash-and-session), you can do
|
4
|
+
this by overriding Brut's [configuration](/configuration).
|
5
|
+
|
6
|
+
## Recipe
|
7
|
+
|
8
|
+
First, create your new class in `app/support/app_flash.rb`. You can implement your
|
9
|
+
new methods using `[]` and `[]=`.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class AppFlash < Brut::FrontEnd::Flash
|
13
|
+
def debug = self[:debug]
|
14
|
+
def debug? = !!self.debug
|
15
|
+
|
16
|
+
def debug=(debug_message)
|
17
|
+
self[:debug] = debug_message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Now, in `app/src/app.rb`'s initializer, use `Brut.container.override`:
|
23
|
+
|
24
|
+
```ruby {6}
|
25
|
+
class App < Brut::Framework::App
|
26
|
+
def initialize
|
27
|
+
|
28
|
+
# ...
|
29
|
+
|
30
|
+
Brut.container.override("flash_class",AppFlash)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
Now, any time you inject `flash:` into a component, it'll be an instance of
|
36
|
+
`AppFlash`:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class HomePage < AppPage
|
40
|
+
def initialize(flash:)
|
41
|
+
@flash = flash
|
42
|
+
end
|
43
|
+
|
44
|
+
def page_template
|
45
|
+
h1 { "Welcome!" }
|
46
|
+
if @flash.debug?
|
47
|
+
aside { @flash.debug }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# Indexed Forms
|
2
|
+
|
3
|
+
HTTP allows a form to have any number of elements with the same name. HTTP will
|
4
|
+
make all values available to you. Rack supports this, too, but not in a standard
|
5
|
+
way.
|
6
|
+
|
7
|
+
This recipe will show a form that has more than one set of text fields for the same
|
8
|
+
conceptual field.
|
9
|
+
|
10
|
+
## Feature
|
11
|
+
|
12
|
+
Allow editing a single form that has 10 sets of name/quantity fields, in order to
|
13
|
+
bulk create up to 10 widgets at a time.
|
14
|
+
|
15
|
+
## Recipe
|
16
|
+
|
17
|
+
We'll create a form, a handler, and a page to do this.
|
18
|
+
|
19
|
+
### Creating a Form with Indexes
|
20
|
+
|
21
|
+
First, we'll scaffold the form and handler:
|
22
|
+
|
23
|
+
```
|
24
|
+
bin/scaffold form /bulk_create_widgets
|
25
|
+
```
|
26
|
+
|
27
|
+
Next, we'll create the form in `app/src/front_end/forms/bulk_create_widgets_form.rb`
|
28
|
+
This will look like a normal form except each field will have `array: true`, to
|
29
|
+
indicate there will be an arbitrary number of these fields. Since they are not all
|
30
|
+
going to be required, we'll set `required: false`.
|
31
|
+
|
32
|
+
When you specify `array:true`, the method created by `input` accepts an index as an
|
33
|
+
argument. For example, `form.name(3)` would retrieve the fourth name submitted.
|
34
|
+
|
35
|
+
We'll also implement the method `each_widget` that will yield each name/quantity
|
36
|
+
pair. The reason Brut doesn't provide this is that your form could have non-array
|
37
|
+
values as well, so there is no obvious implementation.
|
38
|
+
|
39
|
+
Brut *does* provide an `_each` method for every array field. We can use that to
|
40
|
+
iterate over however many values were submitted.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class BulkCreateWidgetsForm < AppForm
|
44
|
+
input :name, array: true, required: false
|
45
|
+
input :quantity, type: :number, array: true, required: false,
|
46
|
+
min: 1
|
47
|
+
|
48
|
+
def each_widget(&block)
|
49
|
+
name_each do |name, index|
|
50
|
+
block.(name, self.quantity(index), index)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Processing a Form with Array Values
|
57
|
+
|
58
|
+
When Brut sends this data to us, each field will be an array of values. We'll see
|
59
|
+
how to generate the HTML for that in a moment. Before that, let's implement the
|
60
|
+
handler.
|
61
|
+
|
62
|
+
We want to require a name and quantity if either is present. If not, it's fine.
|
63
|
+
When we detect a problem, we'll use the `index:` parameter on
|
64
|
+
`server_side_constraint_violation` to indicate which index has the issue.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# app/src/front_end/handlers/bulk_create_widgets_handledr.rb
|
68
|
+
class BulkCreateWidgetsHandler < AppHandler
|
69
|
+
def initialize(form:)
|
70
|
+
@form = form
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle
|
74
|
+
@form.each_widget(name, quantity, index)
|
75
|
+
name_blank = name.to_s.strip == ""
|
76
|
+
quantity_blank = name.to_s.strip == ""
|
77
|
+
if name_blank && quantity_blank
|
78
|
+
# fine
|
79
|
+
elsif !name_blank && !quantity_blank
|
80
|
+
# fine
|
81
|
+
elsif name_blank
|
82
|
+
@form.server_side_constraint_violation(
|
83
|
+
input_name: :name,
|
84
|
+
key: :required_with_quantity,
|
85
|
+
index: index
|
86
|
+
)
|
87
|
+
else
|
88
|
+
@form.server_side_constraint_violation(
|
89
|
+
input_name: :quantity,
|
90
|
+
key: :required_with_name,
|
91
|
+
index: index
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if @form.constraint_violations?
|
96
|
+
BulkCreateWidgetsPage.new(form: @form)
|
97
|
+
else
|
98
|
+
redirect_to(WidgetsPage)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Generating a Form with Array Values
|
105
|
+
|
106
|
+
Whew! Now, let's see our HTML for the form. Note the use
|
107
|
+
of the `index:` parameter when creating the form elements.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# app/src/front_end/pages/bulk_create_widgets_page.rb
|
111
|
+
class BulkCreateWidgetsPage < AppPage
|
112
|
+
|
113
|
+
include Brut::FrontEnd::Components
|
114
|
+
|
115
|
+
def initialize(form:)
|
116
|
+
@form = form || BulkCreateWidgetsForm.new
|
117
|
+
@num_widgets = 10
|
118
|
+
end
|
119
|
+
|
120
|
+
def page_template
|
121
|
+
brut_form do
|
122
|
+
FormTag(for: @form) do
|
123
|
+
@num_widgets.each do |index|
|
124
|
+
label do
|
125
|
+
Inputs::TextField(form: @form, input_name: :name, index: index)
|
126
|
+
div { "Name #{index + 1}" }
|
127
|
+
ConstraintViolations(form: @form, input_name: :name, index: index)
|
128
|
+
end
|
129
|
+
label do
|
130
|
+
Inputs::TextField(form: @form, input_name: :quantity, index: index)
|
131
|
+
div { "Quantity for Widget #{index + 1}" }
|
132
|
+
ConstraintViolations(form: @form, input_name: :quantity, index: index)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
button { "Save Widgets }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
Even if the form doesn't have 10 entries, the code above will create 10 pairs of
|
143
|
+
fields. If there are server-side constraint violations, they will be shown for the
|
144
|
+
appropriate index. Lastly, Brut's components (like `TextField`) will use the Rack
|
145
|
+
non-standard HTML for arrays of values. Instead of `name="quantity"`, Brut will
|
146
|
+
render `name="quantity[]"`.
|
147
|
+
|
148
|
+
|
149
|
+
|