├── .github └── workflows │ └── build.yml ├── .gitignore ├── .rubocop.yml ├── Gemfile ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── VERSION ├── app ├── assets │ ├── images │ │ └── actions │ │ │ ├── add.png │ │ │ ├── delete.png │ │ │ ├── edit.png │ │ │ ├── list.png │ │ │ └── show.png │ └── stylesheets │ │ ├── crud.scss │ │ └── sample.scss ├── controllers │ ├── crud_controller.rb │ ├── dry_crud │ │ ├── generic_model.rb │ │ ├── nestable.rb │ │ ├── rememberable.rb │ │ ├── render_callbacks.rb │ │ ├── searchable.rb │ │ └── sortable.rb │ └── list_controller.rb ├── helpers │ ├── actions_helper.rb │ ├── dry_crud │ │ ├── form │ │ │ ├── builder.rb │ │ │ └── control.rb │ │ └── table │ │ │ ├── actions.rb │ │ │ ├── builder.rb │ │ │ ├── col.rb │ │ │ └── sorting.rb │ ├── form_helper.rb │ ├── format_helper.rb │ ├── i18n_helper.rb │ ├── table_helper.rb │ └── utility_helper.rb └── views │ ├── crud │ ├── _actions_edit.html.erb │ ├── _actions_edit.html.haml │ ├── _actions_index.html.erb │ ├── _actions_index.html.haml │ ├── _actions_show.html.erb │ ├── _actions_show.html.haml │ ├── _attrs.html.erb │ ├── _attrs.html.haml │ ├── _form.html.erb │ ├── _form.html.haml │ ├── _list.html.erb │ ├── _list.html.haml │ ├── edit.html.erb │ ├── edit.html.haml │ ├── new.html.erb │ ├── new.html.haml │ ├── show.html.erb │ ├── show.html.haml │ └── show.json.jbuilder │ ├── layouts │ ├── _flash.html.erb │ ├── _flash.html.haml │ ├── _nav.html.erb │ ├── _nav.html.haml │ ├── application.html.erb │ └── application.html.haml │ ├── list │ ├── _actions_index.html.erb │ ├── _actions_index.html.haml │ ├── _list.html.erb │ ├── _list.html.haml │ ├── _search.html.erb │ ├── _search.html.haml │ ├── index.html.erb │ ├── index.html.haml │ └── index.json.jbuilder │ └── shared │ ├── _error_messages.html.erb │ ├── _error_messages.html.haml │ ├── _labeled.html.erb │ └── _labeled.html.haml ├── config └── locales │ ├── crud.de.yml │ ├── crud.en.yml │ └── crud.it.yml ├── dry_crud.gemspec ├── lib ├── dry_crud.rb ├── dry_crud │ └── engine.rb └── generators │ └── dry_crud │ ├── USAGE │ ├── dry_crud_generator.rb │ ├── dry_crud_generator_base.rb │ ├── file_generator.rb │ └── templates │ ├── INSTALL │ ├── config │ └── initializers │ │ └── field_error_proc.rb │ ├── spec │ ├── controllers │ │ └── crud_test_models_controller_spec.rb │ ├── helpers │ │ ├── dry_crud │ │ │ ├── form │ │ │ │ └── builder_spec.rb │ │ │ └── table │ │ │ │ └── builder_spec.rb │ │ ├── form_helper_spec.rb │ │ ├── format_helper_spec.rb │ │ ├── i18n_helper_spec.rb │ │ ├── table_helper_spec.rb │ │ └── utility_helper_spec.rb │ └── support │ │ ├── crud_controller_examples.rb │ │ └── crud_controller_test_helper.rb │ └── test │ ├── controllers │ └── crud_test_models_controller_test.rb │ ├── helpers │ ├── custom_assertions_test.rb │ ├── dry_crud │ │ ├── form │ │ │ └── builder_test.rb │ │ └── table │ │ │ └── builder_test.rb │ ├── form_helper_test.rb │ ├── format_helper_test.rb │ ├── i18n_helper_test.rb │ ├── table_helper_test.rb │ └── utility_helper_test.rb │ └── support │ ├── crud_controller_test_helper.rb │ ├── crud_test_helper.rb │ ├── crud_test_model.rb │ ├── crud_test_models_controller.rb │ └── custom_assertions.rb ├── template.rb └── test └── templates ├── Gemfile.append ├── app ├── controllers │ ├── admin │ │ ├── cities_controller.rb │ │ └── countries_controller.rb │ ├── people_controller.rb │ ├── turbo_controller.rb │ └── vips_controller.rb ├── helpers │ ├── cities_helper.rb │ └── people_helper.rb ├── models │ ├── city.rb │ ├── country.rb │ └── person.rb └── views │ ├── admin │ ├── cities │ │ ├── _actions_index.html.erb │ │ ├── _actions_index.html.haml │ │ ├── _attrs.html.erb │ │ ├── _attrs.html.haml │ │ ├── _form.html.erb │ │ ├── _form.html.haml │ │ ├── _hello.html.erb │ │ ├── _hello.html.haml │ │ ├── _list.html.erb │ │ └── _list.html.haml │ └── countries │ │ ├── _form.html.erb │ │ ├── _form.html.haml │ │ ├── _list.html.erb │ │ └── _list.html.haml │ ├── layouts │ ├── _nav.html.erb │ └── _nav.html.haml │ ├── people │ ├── _attrs.html.erb │ ├── _attrs.html.haml │ ├── _list.html.erb │ └── _list.html.haml │ └── turbo │ ├── _actions_index.html.erb │ ├── _actions_index.html.haml │ ├── _actions_show.html.erb │ ├── _actions_show.html.haml │ ├── _hello.html.erb │ ├── _hello.html.haml │ ├── edit.turbo_stream.erb │ ├── edit.turbo_stream.haml │ ├── show.turbo_stream.erb │ ├── show.turbo_stream.haml │ ├── turbo.turbo_stream.erb │ ├── turbo.turbo_stream.haml │ ├── update.turbo_stream.erb │ └── update.turbo_stream.haml ├── config ├── database.yml ├── initializers │ └── deprecations.rb ├── locales │ ├── cities.en.yml │ └── de.yml └── routes.rb ├── db ├── migrate │ └── 20100511174904_create_people_and_cities.rb └── seeds.rb ├── spec ├── controllers │ ├── admin │ │ ├── cities_controller_spec.rb │ │ └── countries_controller_spec.rb │ └── people_controller_spec.rb └── routing │ ├── cities_routing_spec.rb │ └── countries_routing_spec.rb └── test ├── controllers ├── admin │ ├── cities_controller_test.rb │ └── countries_controller_test.rb └── people_controller_test.rb └── fixtures ├── cities.yml ├── countries.yml └── people.yml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build 3 | 4 | on: 5 | push: 6 | pull_request: 7 | branches: [ $default-branch ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['3.3', '3.4'] 16 | haml: [true, false] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 24 | - name: Run tests 25 | run: bundle exec rake 26 | env: 27 | HAML: ${{ matrix.haml }} 28 | 29 | lint: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3 38 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 39 | - name: Run rubocop 40 | run: bundle exec rubocop 41 | 42 | coverage: 43 | name: Coverage 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Set up Ruby 48 | uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: 3.3 51 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 52 | - uses: paambaati/codeclimate-action@v5.0.0 53 | env: 54 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 55 | with: 56 | coverageCommand: bundle exec rake 57 | coverageLocations: | 58 | ${{github.workspace}}/coverage/spec/coverage.json:simplecov 59 | ${{github.workspace}}/coverage/test/coverage.json:simplecov 60 | debug: true 61 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | .loadpath 4 | Gemfile.lock 5 | .tmp* 6 | coverage 7 | test/test_app 8 | tmp 9 | pkg 10 | rdoc 11 | *.gemfile.lock 12 | .byebug_history 13 | # RVM files 14 | .ruby-gemset 15 | .ruby-version 16 | .vscode 17 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 8.0.0" 4 | 5 | gem "puma" 6 | 7 | gem "rake" 8 | 9 | gem "rspec-rails" 10 | 11 | gem "haml" 12 | gem "jbuilder" 13 | 14 | gem "kaminari" 15 | 16 | gem "propshaft" 17 | gem "jsbundling-rails" 18 | gem "cssbundling-rails" 19 | gem "turbo-rails" 20 | gem "stimulus-rails" 21 | 22 | gem "bootsnap", require: false 23 | 24 | gem "tzinfo-data", platforms: [ :windows, :jruby ] 25 | 26 | group :development do 27 | gem "web-console" 28 | gem "rubocop" 29 | gem "rubocop-rails-omakase" 30 | gem "sdoc" 31 | gem "spring" 32 | end 33 | 34 | gem "simplecov", require: false 35 | gem "debug", platforms: [ :mri, :windows ], require: "debug/prelude" 36 | 37 | # platform specific gems 38 | 39 | platforms :ruby do 40 | gem "sqlite3" 41 | end 42 | 43 | platforms :jruby do 44 | gem "jdbc-sqlite3" 45 | gem "activerecord-jdbcsqlite3-adapter" 46 | gem "jruby-openssl" 47 | end 48 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2023 Pascal Zumkehr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 8.0.0 2 | -------------------------------------------------------------------------------- /app/assets/images/actions/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/add.png -------------------------------------------------------------------------------- /app/assets/images/actions/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/delete.png -------------------------------------------------------------------------------- /app/assets/images/actions/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/edit.png -------------------------------------------------------------------------------- /app/assets/images/actions/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/list.png -------------------------------------------------------------------------------- /app/assets/images/actions/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/show.png -------------------------------------------------------------------------------- /app/assets/stylesheets/crud.scss: -------------------------------------------------------------------------------- 1 | 2 | h1 { 3 | margin-bottom: 20px; 4 | } 5 | 6 | .right { 7 | text-align: right; 8 | } 9 | 10 | .center { 11 | text-align: center; 12 | } 13 | 14 | #content { 15 | padding-top: 10px; 16 | } 17 | 18 | #flash { 19 | clear: both; 20 | padding-top: 5px; 21 | } 22 | 23 | table.table td.action { 24 | width: 20px; 25 | text-align: center; 26 | } 27 | 28 | .cancel { 29 | font-size: 80%; 30 | margin-left: 7px; 31 | } 32 | 33 | #error_explanation h2 { 34 | font-size: 100%; 35 | margin-top: 0px; 36 | } 37 | 38 | #error_explanation ul { 39 | margin-bottom: 5px; 40 | } 41 | 42 | footer { 43 | clear: both; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/sample.scss: -------------------------------------------------------------------------------- 1 | $container_width: 1000px; 2 | $theme_color: #2580a2; 3 | 4 | body, 5 | div, 6 | p, 7 | td, 8 | th { 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | font-size: 14px; 11 | line-height: 1.42857; 12 | } 13 | 14 | body { 15 | text-align: center; 16 | margin: 0; 17 | background-color: #ddf; 18 | } 19 | 20 | .container { 21 | padding: 20px 20px; 22 | margin: 0 auto; 23 | text-align: left; 24 | max-width: $container_width; 25 | min-width: $container_width - 200px; 26 | height: 100%; 27 | background-color: #f6f6ff; 28 | -moz-box-shadow: 0px 0px 5px $theme_color; 29 | -webkit-box-shadow: 0px 0px 5px $theme_color; 30 | box-shadow: 0px 0px 5px $theme_color; 31 | } 32 | 33 | .navbar { 34 | background: #333; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | max-width: $container_width; 38 | min-width: $container_width - 200px; 39 | height: 33px; 40 | -moz-box-shadow: 0px 0px 5px $theme_color; 41 | -webkit-box-shadow: 0px 0px 5px $theme_color; 42 | box-shadow: 0px 1px 5px $theme_color; 43 | } 44 | 45 | .navbar-brand { 46 | font-weight: bold; 47 | font-size: 130%; 48 | color: #ddd !important; 49 | padding: 5px; 50 | float: left; 51 | } 52 | 53 | a.navbar-brand:hover { 54 | text-decoration: none; 55 | color: #ddd; 56 | } 57 | 58 | .navbar-nav { 59 | list-style: none; 60 | margin: 0; 61 | float: left; 62 | } 63 | 64 | .navbar-nav li.nav-item { 65 | float: left; 66 | font-size: 110%; 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | .navbar-nav a.nav-link { 72 | color: #ddd !important; 73 | display: block; 74 | float: left; 75 | margin: 0; 76 | padding: 7px 12px 7px; 77 | text-decoration: none; 78 | height: 19px; 79 | } 80 | 81 | .navbar-nav a.nav-link:hover { 82 | background: $theme_color bottom center no-repeat; 83 | color: #fff !important; 84 | text-decoration: none; 85 | } 86 | 87 | .actions { 88 | height: 25px; 89 | } 90 | 91 | #content { 92 | clear: both; 93 | width: 100%; 94 | } 95 | 96 | h1 { 97 | font-size: 150%; 98 | margin: 0px 0 20px 0; 99 | } 100 | 101 | table.table { 102 | border-collapse: collapse; 103 | width: 100%; 104 | padding: 0; 105 | } 106 | 107 | /* div rendered if no entries available for list */ 108 | div.table { 109 | } 110 | 111 | table.table th { 112 | background-color: $theme_color; 113 | color: white; 114 | font-weight: bold; 115 | padding: 4px 4px; 116 | } 117 | 118 | table.table th a { 119 | color: white; 120 | text-decoration: none; 121 | } 122 | 123 | table.table td { 124 | padding: 4px 4px; 125 | } 126 | 127 | .table-striped thead tr:nth-child(odd), 128 | .table thead tr:nth-child(even) { 129 | background-color: #d0d0d0; 130 | } 131 | 132 | .table-striped tr:nth-child(odd) { 133 | background-color: #f8f8f8; 134 | } 135 | 136 | .table-striped tr:nth-child(even) { 137 | background-color: #f0f0f0; 138 | } 139 | 140 | .table-hover tr:hover { 141 | background-color: #ffffe0; 142 | } 143 | 144 | td { 145 | vertical-align: top; 146 | } 147 | 148 | td p { 149 | margin: 0; 150 | } 151 | 152 | a { 153 | color: #2580a2; 154 | text-decoration: none; 155 | } 156 | 157 | a:hover { 158 | text-decoration: underline; 159 | } 160 | 161 | a:visited { 162 | color: #2580a2; 163 | } 164 | 165 | a img { 166 | border: none; 167 | } 168 | 169 | dl { 170 | vertical-align: top; 171 | margin: 0; 172 | clear: both; 173 | } 174 | 175 | dt { 176 | width: 120px; 177 | padding-right: 5px; 178 | margin-bottom: 5px; 179 | float: left; 180 | font-style: italic; 181 | } 182 | 183 | dd { 184 | margin-left: 130px; 185 | margin-bottom: 5px; 186 | } 187 | 188 | dd p { 189 | margin: 0; 190 | } 191 | 192 | label { 193 | font-style: italic; 194 | text-align: right; 195 | } 196 | 197 | .form-actions { 198 | margin-left: 130px; 199 | } 200 | 201 | a.action { 202 | padding: 0 5px; 203 | } 204 | 205 | a.icon { 206 | margin-right: 5px; 207 | margin-left: 5px; 208 | } 209 | 210 | a.icon img { 211 | vertical-align: text-top; 212 | } 213 | 214 | .icon { 215 | width: 16px; 216 | height: 16px; 217 | display: inline-block; 218 | background: no-repeat; 219 | vertical-align: top; 220 | } 221 | 222 | .icon-plus { 223 | background-image: image-url("actions/add.png"); 224 | } 225 | .icon-trash { 226 | background-image: image-url("actions/delete.png"); 227 | } 228 | .icon-pencil { 229 | background-image: image-url("actions/edit.png"); 230 | } 231 | .icon-list { 232 | background-image: image-url("actions/list.png"); 233 | } 234 | .icon-zoom-in { 235 | background-image: image-url("actions/show.png"); 236 | } 237 | 238 | .form-group { 239 | clear: both; 240 | padding: 4px 0 4px; 241 | } 242 | 243 | .form-group > label { 244 | float: left; 245 | width: 120px; 246 | padding-right: 10px; 247 | padding-top: 3px; 248 | } 249 | 250 | input, 251 | textarea, 252 | select { 253 | font-family: Verdana, Geneva, Helvetica, Arial, sans-serif; 254 | font-size: 14px; 255 | } 256 | 257 | input[type="text"], 258 | input[type="password"], 259 | input[type="email"] { 260 | width: 300px; 261 | } 262 | 263 | input[type="number"] { 264 | width: 100px; 265 | } 266 | 267 | textarea, 268 | select[multiple] { 269 | width: 300px; 270 | height: 80px; 271 | } 272 | 273 | [role="search"] [type="search"] { 274 | width: 220px; 275 | } 276 | 277 | .has-error .control-label { 278 | color: #d88; 279 | } 280 | 281 | .has-error .form-control { 282 | border-color: #d88; 283 | } 284 | 285 | .input-group-append { 286 | font-size: 80%; 287 | vertical-align: top; 288 | margin-left: 4px; 289 | display: inline-block; 290 | } 291 | 292 | .help-block { 293 | margin-top: 0; 294 | } 295 | 296 | .alert { 297 | margin: 15px; 298 | padding: 5px 10px; 299 | clear: both; 300 | } 301 | 302 | .alert-success { 303 | border: solid 2px #6a6; 304 | background-color: #afa; 305 | } 306 | 307 | .alert-danger { 308 | border: solid 2px #d88; 309 | background-color: #fec; 310 | } 311 | 312 | .close { 313 | float: right; 314 | } 315 | 316 | .float-end { 317 | float: right; 318 | } 319 | 320 | .float-start { 321 | float: left; 322 | } 323 | 324 | footer { 325 | margin: auto; 326 | text-align: right; 327 | max-width: $container_width; 328 | min-width: $container_width - 200px; 329 | } 330 | 331 | .col-md-offset-2 { 332 | margin-left: 130px; 333 | } 334 | 335 | .col-md-8 { 336 | margin-left: 130px; 337 | } 338 | 339 | .col-md-5 { 340 | float: left; 341 | width: 350px; 342 | } 343 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/generic_model.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Connects the including controller to the model whose name corrsponds to 3 | # the controller's name. 4 | # 5 | # The two main methods are +model_class+ and +model_scope+. 6 | # Additional helper methods store and retrieve values in instance variables 7 | # named after their class. 8 | module GenericModel 9 | extend ActiveSupport::Concern 10 | 11 | included do 12 | helper_method :model_class, :models_label, :path_args 13 | 14 | delegate :model_class, :models_label, :model_identifier, to: "self.class" 15 | end 16 | 17 | # The scope where model entries will be listed and created. 18 | # This is mainly used for nested models to provide the 19 | # required context. 20 | def model_scope 21 | model_class.all 22 | end 23 | 24 | # The path arguments to link to the given model entry. 25 | # If the controller is nested, this provides the required context. 26 | def path_args(last) 27 | last 28 | end 29 | 30 | # Get the instance variable named after the +model_class+. 31 | # If the collection variable is required, pass true as the second argument. 32 | def model_ivar_get(plural: false) 33 | name = ivar_name(model_class) 34 | name = name.pluralize if plural 35 | name = :"@#{name}" 36 | instance_variable_get(name) if instance_variable_defined?(name) 37 | end 38 | 39 | # Sets an instance variable with the underscored class name if the given 40 | # value. If the value is a collection, sets the plural name. 41 | def model_ivar_set(value) 42 | name = if value.respond_to?(:klass) # ActiveRecord::Relation 43 | ivar_name(value.klass).pluralize 44 | elsif value.respond_to?(:each) # Array 45 | ivar_name(value.first.class).pluralize 46 | else 47 | ivar_name(value.class) 48 | end 49 | instance_variable_set(:"@#{name}", value) 50 | end 51 | 52 | def ivar_name(klass) 53 | klass.model_name.param_key 54 | end 55 | 56 | # Class methods from GenericModel. 57 | module ClassMethods 58 | # The ActiveRecord class of the model. 59 | def model_class 60 | @model_class ||= controller_name.classify.constantize 61 | end 62 | 63 | # The identifier of the model used for form parameters. 64 | # I.e., the symbol of the underscored model name. 65 | def model_identifier 66 | @model_identifier ||= model_class.model_name.param_key 67 | end 68 | 69 | # A human readable plural name of the model. 70 | def models_label(plural: true) 71 | opts = { count: (plural ? 3 : 1) } 72 | opts[:default] = model_class.model_name.human.titleize 73 | opts[:default] = opts[:default].pluralize if plural 74 | 75 | model_class.model_name.human(opts) 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/nestable.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Provides functionality to nest controllers/resources. 3 | # If a controller is nested, the parent classes and namespaces 4 | # may be defined as an array in the +nesting+ class attribute. 5 | # 6 | # For example, a cities controller, nested in country and a admin 7 | # namespace, may define this attribute as follows: 8 | # self.nesting = :admin, Country 9 | module Nestable 10 | # Adds the :nesting class attribute and parent helper methods 11 | # to the including controller. 12 | def self.prepended(klass) 13 | klass.class_attribute :nesting 14 | 15 | klass.helper_method :parent, :parents 16 | end 17 | 18 | private 19 | 20 | # Returns the direct parent ActiveRecord of the current request, if any. 21 | def parent 22 | parents.reverse.find { |p| p.is_a?(ActiveRecord::Base) } 23 | end 24 | 25 | # Returns the parent entries of the current request, if any. 26 | # These are ActiveRecords or namespace symbols, corresponding 27 | # to the defined nesting attribute. 28 | def parents 29 | @parents ||= Array(nesting).map do |p| 30 | if p.is_a?(Class) && p < ActiveRecord::Base 31 | parent_entry(p) 32 | else 33 | p 34 | end 35 | end 36 | end 37 | 38 | # Loads the parent entry for the given ActiveRecord class. 39 | # By default, performs a find with the class_name_id param. 40 | def parent_entry(clazz) 41 | model_ivar_set(clazz.find(params["#{clazz.name.underscore}_id"])) 42 | end 43 | 44 | # An array of objects used in url_for and related functions. 45 | def path_args(last) 46 | parents + [ last ] 47 | end 48 | 49 | # Uses the parent entry (if any) to constrain the model scope. 50 | def model_scope 51 | if parent.present? 52 | parent_scope 53 | else 54 | super 55 | end 56 | end 57 | 58 | # The model scope for the current parent resource. 59 | def parent_scope 60 | parent.send(model_class.name.underscore.pluralize) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/rememberable.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Remembers certain params of the index action in order to return 3 | # to the same list after an entry was viewed or edited. 4 | # If the index is called with a param :returning, the remembered params 5 | # will be re-used to present the user the same list as she left it. 6 | # 7 | # Define a list of param keys that should be remembered for the list action 8 | # with the class attribute +remember_params+. 9 | # 10 | # The params are stored separately for each different +remember_key+, which 11 | # defaults to the current request's path. 12 | module Rememberable 13 | extend ActiveSupport::Concern 14 | 15 | included do 16 | class_attribute :remember_params 17 | self.remember_params = %w[q sort sort_dir page] 18 | 19 | before_action :handle_remember_params, only: [ :index ] 20 | end 21 | 22 | private 23 | 24 | # Store and restore the corresponding params. 25 | def handle_remember_params 26 | remembered = remembered_params 27 | 28 | restore_params_on_return(remembered) 29 | store_current_params(remembered) 30 | clear_void_params(remembered) 31 | end 32 | 33 | def restore_params_on_return(remembered) 34 | if params[:returning] 35 | remember_params.each { |p| params[p] ||= remembered[p] } 36 | end 37 | end 38 | 39 | def store_current_params(remembered) 40 | remember_params.each do |p| 41 | remembered[p] = params[p].presence 42 | remembered.delete(p) if remembered[p].nil? 43 | end 44 | end 45 | 46 | def clear_void_params(remembered) 47 | session[:list_params].delete(remember_key) if remembered.blank? 48 | end 49 | 50 | # Get the params stored in the session. 51 | def remembered_params 52 | session[:list_params] ||= {} 53 | session[:list_params][remember_key] ||= {} 54 | session[:list_params][remember_key] 55 | end 56 | 57 | # Params are stored by request path to play nice when a controller 58 | # is used in different routes. 59 | def remember_key 60 | request.path 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/render_callbacks.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Provide +before_render+ callbacks. 3 | module RenderCallbacks 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | extend ActiveModel::Callbacks 8 | prepend Prepends 9 | end 10 | 11 | # Prepended methods for callbacks. 12 | module Prepends 13 | # Helper method to run +before_render+ callbacks and render the action. 14 | # If a callback renders or redirects, the action is not rendered. 15 | def render(...) 16 | options = _normalize_render(...) 17 | callback = "render_#{options[:template] || options[:action] || action_name}" 18 | run_callbacks(callback) if respond_to?(:"_#{callback}_callbacks", true) 19 | 20 | super unless performed? 21 | end 22 | 23 | private 24 | 25 | # Helper method the run the given block in between the before and after 26 | # callbacks of the given kinds. 27 | def with_callbacks(*kinds, &block) 28 | kinds.reverse.reduce(block) do |a, e| 29 | -> { run_callbacks(e, &a) } 30 | end.call 31 | end 32 | end 33 | 34 | # Class methods for callbacks. 35 | module ClassMethods 36 | # Defines before callbacks for the render actions. 37 | def define_render_callbacks(*actions) 38 | args = actions.map { |a| :"render_#{a}" } 39 | args << { only: :before, terminator: render_callback_terminator } 40 | define_model_callbacks(*args) 41 | end 42 | 43 | private 44 | 45 | def render_callback_terminator 46 | proc do |ctrl, result_lambda| 47 | terminate = true 48 | catch(:abort) do 49 | result_lambda.call if result_lambda.is_a?(Proc) 50 | terminate = !ctrl.response_body.nil? 51 | end 52 | terminate 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/searchable.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # The search functionality for the index table. 3 | # Define an array of searchable string columns in your subclassing 4 | # controllers using the class attribute +search_columns+. 5 | module Searchable 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | class_attribute :search_columns 10 | self.search_columns = [] 11 | 12 | helper_method :search_support? 13 | 14 | prepend Prepends 15 | end 16 | 17 | # Prepended methods for searching. 18 | module Prepends 19 | private 20 | 21 | # Enhance the list entries with an optional search criteria 22 | def list_entries 23 | super.where(search_conditions) 24 | end 25 | 26 | # Concat the word clauses with AND. 27 | def search_conditions 28 | if search_support? && params[:q].present? 29 | search_word_conditions.reduce do |query, condition| 30 | query.and(condition) 31 | end 32 | end 33 | end 34 | 35 | # Split the search query in single words and create a list of word 36 | # clauses. 37 | def search_word_conditions 38 | params[:q].split(/\s+/).map { |w| search_word_condition(w) } 39 | end 40 | 41 | # Concat the column queries of the given word with OR. 42 | def search_word_condition(word) 43 | search_column_condition(word).reduce do |query, condition| 44 | query.or(condition) 45 | end 46 | end 47 | 48 | # Create a list of Arel #matches queries for each column and the given 49 | # word. 50 | def search_column_condition(word) 51 | self.class.search_tables_and_fields.map do |table_name, field| 52 | table = Arel::Table.new(table_name) 53 | table[field].matches(Arel::Nodes::Quoted.new("%#{word}%")) 54 | end 55 | end 56 | 57 | # Returns true if this controller has searchable columns. 58 | def search_support? 59 | search_columns.present? 60 | end 61 | end 62 | 63 | # Class methods for Searchable. 64 | module ClassMethods 65 | # All search columns divided in table and field names. 66 | def search_tables_and_fields 67 | @search_tables_and_fields ||= search_columns.map do |f| 68 | if f.to_s.include?(".") 69 | f.split(".", 2) 70 | else 71 | [ model_class.table_name, f ] 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/controllers/dry_crud/sortable.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Sort functionality for the index table. 3 | # Define a default sort expression that is always appended to the 4 | # current sort params with the class attribute +default_sort+. 5 | module Sortable 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | class_attribute :sort_mappings_with_indifferent_access 10 | self.sort_mappings = {} 11 | 12 | class_attribute :default_sort 13 | 14 | helper_method :sortable? 15 | 16 | prepend Prepends 17 | end 18 | 19 | # Class methods for sorting. 20 | module ClassMethods 21 | # Define a map of (virtual) attributes to SQL order expressions. 22 | # May be used for sorting table columns that do not appear directly 23 | # in the database table. E.g., map city_id: 'cities.name' to 24 | # sort the displayed city names. 25 | def sort_mappings=(hash) 26 | self.sort_mappings_with_indifferent_access = 27 | hash.with_indifferent_access 28 | end 29 | end 30 | 31 | # Prepended methods for sorting. 32 | module Prepends 33 | private 34 | 35 | # Enhance the list entries with an optional sort order. 36 | def list_entries 37 | sortable = sortable?(params[:sort]) 38 | if sortable || default_sort 39 | clause = [ sortable ? sort_expression : nil, default_sort ] 40 | super.reorder(Arel.sql(clause.compact.join(", "))) 41 | else 42 | super 43 | end 44 | end 45 | 46 | # Return the sort expression to be used in the list query. 47 | def sort_expression 48 | col = sort_mappings_with_indifferent_access[params[:sort]] || 49 | "#{model_class.table_name}.#{params[:sort]}" 50 | "#{col} #{sort_dir}" 51 | end 52 | 53 | # The sort direction, either 'asc' or 'desc'. 54 | def sort_dir 55 | params[:sort_dir] == "desc" ? "DESC" : "ASC" 56 | end 57 | 58 | # Returns true if the passed attribute is sortable. 59 | def sortable?(attr) 60 | attr.present? && ( 61 | model_class.column_names.include?(attr.to_s) || 62 | sort_mappings_with_indifferent_access.include?(attr)) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/controllers/list_controller.rb: -------------------------------------------------------------------------------- 1 | # Abstract controller providing a basic list action. 2 | # The loaded model entries are available in the view as an instance variable 3 | # named after the +model_class+ or by the helper method +entries+. 4 | # 5 | # The +index+ action lists all entries of a certain model and provides 6 | # functionality to search and sort this list. 7 | # Furthermore, it remembers the last search and sort parameters after the 8 | # user returns from a displayed or edited entry. 9 | class ListController < ApplicationController 10 | include DryCrud::GenericModel 11 | prepend DryCrud::Nestable 12 | include DryCrud::RenderCallbacks 13 | include DryCrud::Rememberable 14 | 15 | define_render_callbacks :index 16 | 17 | helper_method :entries 18 | 19 | ############## ACTIONS ############################################ 20 | 21 | # GET /entries 22 | # GET /entries.json 23 | # 24 | # List all entries of this model. 25 | def index 26 | entries 27 | end 28 | 29 | private 30 | 31 | # Helper method to access the entries to be displayed in the current index 32 | # page in an uniform way. 33 | def entries 34 | model_ivar_get(plural: true) || model_ivar_set(list_entries) 35 | end 36 | 37 | # The base relation used to filter the entries. 38 | # Calls the #list scope if it is defined on the model class. 39 | # 40 | # This method may be adapted as long it returns an 41 | # ActiveRecord::Relation. 42 | # Some of the modules included extend this method. 43 | def list_entries 44 | model_class.respond_to?(:list) ? model_scope.list : model_scope 45 | end 46 | 47 | # Include these modules after the #list_entries method is defined. 48 | include DryCrud::Searchable 49 | include DryCrud::Sortable 50 | end 51 | -------------------------------------------------------------------------------- /app/helpers/actions_helper.rb: -------------------------------------------------------------------------------- 1 | # Helpers to create action links. This default implementation supports 2 | # regular links with an icon and a label. To change the general style 3 | # of action links, change the method #action_link, e.g. to generate a button. 4 | # The common crud actions show, edit, destroy, index and add are provided here. 5 | module ActionsHelper 6 | # A generic helper method to create action links. 7 | # These link could be styled to look like buttons, for example. 8 | def action_link(label, icon = nil, url = {}, html_options = {}) 9 | add_css_class html_options, "action btn btn-light" 10 | link_to(icon ? action_icon(icon, label) : label, 11 | url, html_options) 12 | end 13 | 14 | # Outputs an icon for an action with an optional label. 15 | def action_icon(icon, label = nil) 16 | html = tag.i("", class: "bi-#{icon}") 17 | html << " " << label if label 18 | html 19 | end 20 | 21 | # Standard show action to the given path. 22 | # Uses the current +entry+ if no path is given. 23 | def show_action_link(path = nil) 24 | path ||= path_args(entry) 25 | action_link(ti("link.show"), "zoom-in", path) 26 | end 27 | 28 | # Standard edit action to given path. 29 | # Uses the current +entry+ if no path is given. 30 | def edit_action_link(path = nil) 31 | path ||= path_args(entry) 32 | path = edit_polymorphic_path(path) unless path.is_a?(String) 33 | action_link(ti("link.edit"), "pencil", path) 34 | end 35 | 36 | # Standard destroy action to the given path. 37 | # Uses the current +entry+ if no path is given. 38 | def destroy_action_link(path = nil) 39 | path ||= path_args(entry) 40 | action_link(ti("link.delete"), "trash", path, 41 | data: { 'turbo-confirm': ti(:confirm_delete), 42 | 'turbo-method': :delete }) 43 | end 44 | 45 | # Standard list action to the given path. 46 | # Uses the current +model_class+ if no path is given. 47 | def index_action_link(path = nil, url_options = { returning: true }) 48 | path ||= path_args(model_class) 49 | path = polymorphic_path(path, url_options) unless path.is_a?(String) 50 | action_link(ti("link.list"), "list", path) 51 | end 52 | 53 | # Standard add action to given path. 54 | # Uses the current +model_class+ if no path is given. 55 | def add_action_link(path = nil, url_options = {}) 56 | path ||= path_args(model_class) 57 | path = new_polymorphic_path(path, url_options) unless path.is_a?(String) 58 | action_link(ti("link.add"), "plus", path) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/helpers/dry_crud/form/control.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | module Form 3 | # Internal class to handle the rendering of a single form control, 4 | # consisting of a label, input field, addon, help text or 5 | # required mark. 6 | class Control 7 | attr_reader :builder, :attr, :args, :options, :addon, :help 8 | 9 | delegate :tag, :object, :add_css_class, 10 | to: :builder 11 | 12 | # Html displayed to mark an input as required. 13 | REQUIRED_MARK = "*".freeze 14 | 15 | # Number of default input field span columns depending 16 | # on the #field_method. 17 | INPUT_SPANS = Hash.new(8) 18 | INPUT_SPANS[:number_field] = 19 | INPUT_SPANS[:integer_field] = 20 | INPUT_SPANS[:float_field] = 21 | INPUT_SPANS[:decimal_field] = 2 22 | INPUT_SPANS[:date_field] = 23 | INPUT_SPANS[:time_field] = 3 24 | 25 | # Create a new control instance. 26 | # Takes the form builder, the attribute to build the control for 27 | # as well as any additional arguments for the field method. 28 | # This includes an options hash as the last argument, that 29 | # may contain the following special options: 30 | # 31 | # * :addon - Addon content displayed just after the input field. 32 | # * :help - A help text displayed below the input field. 33 | # * :span - Number of columns the input field should span. 34 | # * :caption - Different caption for the label. 35 | # * :field_method - Different method to create the input field. 36 | # * :required - Sets the field as required 37 | # (The value for this option usually is 'required'). 38 | # 39 | # All the other options will go to the field_method. 40 | def initialize(builder, attr, *args, **options) 41 | @builder = builder 42 | @attr = attr 43 | @options = options 44 | @args = args 45 | 46 | @addon = options.delete(:addon) 47 | @help = options.delete(:help) 48 | @span = options.delete(:span) 49 | @caption = options.delete(:caption) 50 | @field_method = options.delete(:field_method) 51 | @required = options[:required] 52 | end 53 | 54 | # Renders only the content of the control. 55 | # I.e. no label and span divs. 56 | def render_content 57 | content 58 | end 59 | 60 | # Renders the complete control with label and everything. 61 | # Render the content given or the default one. 62 | def render_labeled(content = nil) 63 | @content = content if content 64 | labeled 65 | end 66 | 67 | private 68 | 69 | # Create the HTML markup for any labeled content. 70 | def labeled 71 | tag.div(class: "row mb-3") do 72 | builder.label(attr, caption, class: "col-md-2 col-form-label") + 73 | tag.div(content, class: "col-md-#{span}") 74 | end 75 | end 76 | 77 | # Return the currently set content or create it 78 | # based on the various options given. 79 | # 80 | # Optionally renders addon, required mark and/or a help block 81 | # additionally to the input field. 82 | def content 83 | @content ||= begin 84 | content = input 85 | if addon 86 | content = builder.with_addon(content, addon) 87 | elsif required 88 | content = builder.with_addon(content, REQUIRED_MARK) 89 | end 90 | content << builder.help_block(help) if help.present? 91 | content 92 | end 93 | end 94 | 95 | # Return the currently set input field or create it 96 | # depending on the attribute. 97 | def input 98 | @input ||= begin 99 | options[:required] = "required" if required 100 | add_css_class(options, "is-invalid") if errors? 101 | builder.send(field_method, attr, *args, **options) 102 | end 103 | end 104 | 105 | # The field method used to create the input. 106 | # If none is set, detect it from the attribute type. 107 | def field_method 108 | @field_method ||= detect_field_method 109 | end 110 | 111 | # True if the attr is required, false otherwise. 112 | def required 113 | @required = @required.nil? ? builder.required?(attr) : @required 114 | end 115 | 116 | # Number of grid columns the input field should span. 117 | def span 118 | @span ||= INPUT_SPANS[field_method] 119 | end 120 | 121 | # The caption of the label. 122 | # If none is set, uses the I18n value of the attribute. 123 | def caption 124 | @caption ||= builder.captionize(attr, object.class) 125 | end 126 | 127 | # Returns true if any errors are found on the passed attribute or its 128 | # association. 129 | def errors? 130 | attr_plain, attr_id = builder.assoc_and_id_attr(attr) 131 | object.errors.key?(attr_plain.to_sym) || 132 | object.errors.key?(attr_id.to_sym) 133 | end 134 | 135 | # Defines the field method to use based on the attribute 136 | # type, association or name. 137 | def detect_field_method 138 | if type == :text 139 | :text_area 140 | elsif association_kind?(:belongs_to) 141 | :belongs_to_field 142 | elsif association_kind?(:has_and_belongs_to_many, :has_many) 143 | :has_many_field 144 | elsif attr.to_s.include?("password") 145 | :password_field 146 | elsif attr.to_s.include?("email") 147 | :email_field 148 | elsif builder.respond_to?(:"#{type}_field") 149 | :"#{type}_field" 150 | else 151 | :text_field 152 | end 153 | end 154 | 155 | # The column type of the attribute. 156 | def type 157 | @type ||= builder.column_type(object, attr) 158 | end 159 | 160 | # Returns true if attr is a non-polymorphic association. 161 | # If one or more macros are given, the association must be of this kind. 162 | def association_kind?(*macros) 163 | if type == :integer || type.nil? 164 | assoc = builder.association(object, attr, *macros) 165 | 166 | assoc.present? && assoc.options[:polymorphic].nil? 167 | else 168 | false 169 | end 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /app/helpers/dry_crud/table/actions.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | module Table 3 | # Adds action columns to the table builder. 4 | # Predefined actions are available for show, edit and destroy. 5 | # Additionally, a special col type to define cells linked to the show page 6 | # of the row entry is provided. 7 | module Actions 8 | extend ActiveSupport::Concern 9 | 10 | included do 11 | delegate :link_to, :path_args, :edit_polymorphic_path, :ti, 12 | to: :template 13 | end 14 | 15 | # Renders the passed attr with a link to the show action for 16 | # the current entry. 17 | # A block may be given to define the link path for the row entry. 18 | def attr_with_show_link(attr, &block) 19 | sortable_attr(attr) do |entry| 20 | link_to(format_attr(entry, attr), action_path(entry, &block)) 21 | end 22 | end 23 | 24 | # Action column to show the row entry. 25 | # A block may be given to define the link path for the row entry. 26 | # If the block returns nil, no link is rendered. 27 | def show_action_col(**html_options, &block) 28 | action_col do |entry| 29 | path = action_path(entry, &block) 30 | if path 31 | table_action_link("zoom-in", 32 | path, 33 | **html_options.clone) 34 | end 35 | end 36 | end 37 | 38 | # Action column to edit the row entry. 39 | # A block may be given to define the link path for the row entry. 40 | # If the block returns nil, no link is rendered. 41 | def edit_action_col(**html_options, &block) 42 | action_col do |entry| 43 | path = action_path(entry, &block) 44 | if path 45 | path = edit_polymorphic_path(path) unless path.is_a?(String) 46 | table_action_link("pencil", path, **html_options.clone) 47 | end 48 | end 49 | end 50 | 51 | # Action column to destroy the row entry. 52 | # A block may be given to define the link path for the row entry. 53 | # If the block returns nil, no link is rendered. 54 | def destroy_action_col(**html_options, &block) 55 | action_col do |entry| 56 | path = action_path(entry, &block) 57 | if path 58 | table_action_link("trash", 59 | path, 60 | **html_options, 61 | data: { 'turbo-confirm': ti(:confirm_delete), 62 | 'turbo-method': :delete }) 63 | end 64 | end 65 | end 66 | 67 | # Action column inside a table. No header. 68 | # The cell content should be defined in the passed block. 69 | def action_col(&block) 70 | col("", class: "action", &block) 71 | end 72 | 73 | # Generic action link inside a table. 74 | def table_action_link(icon, url, **html_options) 75 | add_css_class(html_options, "bi-#{icon}") 76 | link_to("", url, html_options) 77 | end 78 | 79 | private 80 | 81 | # If a block is given, call it to get the path for the current row entry. 82 | # Otherwise, return the standard path args. 83 | def action_path(entry) 84 | block_given? ? yield(entry) : path_args(entry) 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /app/helpers/dry_crud/table/builder.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | module Table 3 | # A simple helper to easily define tables listing several rows of the same 4 | # data type. 5 | # 6 | # Example Usage: 7 | # DryCrud::Table::Builder.table(entries, template) do |t| 8 | # t.col('My Header', class: 'css') {|e| link_to 'Show', e } 9 | # t.attrs :name, :city 10 | # end 11 | class Builder 12 | include Sorting 13 | include Actions 14 | 15 | attr_reader :entries, :cols, :options, :template 16 | 17 | delegate :tag, :format_attr, :column_type, :association, :dom_id, 18 | :captionize, :add_css_class, :content_tag_nested, 19 | to: :template 20 | 21 | def initialize(entries, template, **options) 22 | @entries = entries 23 | @template = template 24 | @options = options 25 | @cols = [] 26 | end 27 | 28 | # Convenience method to directly generate a table. Renders a row for each 29 | # entry in entries. Takes a block that gets the table object as parameter 30 | # for configuration. Returns the generated html for the table. 31 | def self.table(entries, template, **options) 32 | t = new(entries, template, **options) 33 | yield t 34 | t.to_html 35 | end 36 | 37 | # Define a column for the table with the given header, the html_options 38 | # used for each td and a block rendering the contents of a cell for the 39 | # current entry. The columns appear in the order they are defined. 40 | def col(header = "", **html_options, &block) 41 | @cols << Col.new(header, html_options, @template, block) 42 | end 43 | 44 | # Convenience method to add one or more attribute columns. 45 | # The attribute name will become the header, the cells will contain 46 | # the formatted attribute value for the current entry. 47 | def attrs(*attrs) 48 | attrs.each do |a| 49 | attr(a) 50 | end 51 | end 52 | 53 | # Define a column for the given attribute and an optional header. 54 | # If no header is given, the attribute name is used. The cell will 55 | # contain the formatted attribute value for the current entry. 56 | def attr(attr, header = nil, **html_options, &block) 57 | header ||= attr_header(attr) 58 | block ||= ->(e) { format_attr(e, attr) } 59 | add_css_class(html_options, align_class(attr)) 60 | col(header, **html_options, &block) 61 | end 62 | 63 | # Renders the table as HTML. 64 | def to_html 65 | tag.table(**options) do 66 | tag.thead(html_header) + 67 | content_tag_nested(:tbody, entries) { |e| html_row(e) } 68 | end 69 | end 70 | 71 | # Returns css classes used for alignment of the cell data. 72 | # Based on the column type of the attribute. 73 | def align_class(attr) 74 | entry = entries.present? ? entry_class.new : nil 75 | case column_type(entry, attr) 76 | when :integer, :float, :decimal 77 | "right" unless association(entry, attr, :belongs_to) 78 | when :boolean 79 | "center" 80 | end 81 | end 82 | 83 | # Creates a header string for the given attr. 84 | def attr_header(attr) 85 | captionize(attr, entry_class) 86 | end 87 | 88 | private 89 | 90 | # Renders the header row of the table. 91 | def html_header 92 | content_tag_nested(:tr, cols, &:html_header) 93 | end 94 | 95 | # Renders a table row for the given entry. 96 | def html_row(entry) 97 | attrs = {} 98 | attrs[:id] = dom_id(entry) if entry.respond_to?(:to_key) 99 | content_tag_nested(:tr, cols, **attrs) { |c| c.html_cell(entry) } 100 | end 101 | 102 | # Determines the class of the table entries. 103 | # All entries should be of the same type. 104 | def entry_class 105 | if entries.respond_to?(:klass) 106 | entries.klass 107 | else 108 | entries.first.class 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /app/helpers/dry_crud/table/col.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | module Table 3 | # Helper class to store column information. 4 | class Col # :nodoc: 5 | delegate :tag, :capture, to: :template 6 | 7 | attr_reader :header, :html_options, :template, :block 8 | 9 | def initialize(header, html_options, template, block) 10 | @header = header 11 | @html_options = html_options 12 | @template = template 13 | @block = block 14 | end 15 | 16 | # Runs the Col block for the given entry. 17 | def content(entry) 18 | entry.nil? ? "" : capture(entry, &block) 19 | end 20 | 21 | # Renders the header cell of the Col. 22 | def html_header 23 | tag.th(header, **html_options) 24 | end 25 | 26 | # Renders a table cell for the given entry. 27 | def html_cell(entry) 28 | tag.td(content(entry), **html_options) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/helpers/dry_crud/table/sorting.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | module Table 3 | # Provides headers with sort links. Expects a method :sortable?(attr) 4 | # in the template/controller to tell if an attribute is sortable or not. 5 | # Extracted into an own module for convenience. 6 | module Sorting 7 | # Create a header with sort links and a mark for the current sort 8 | # direction. 9 | def sort_header(attr, label = nil) 10 | label ||= attr_header(attr) 11 | template.link_to(label, sort_params(attr)) + current_mark(attr) 12 | end 13 | 14 | # Same as :attrs, except that it renders a sort link in the header 15 | # if an attr is sortable. 16 | def sortable_attrs(*attrs) 17 | attrs.each { |a| sortable_attr(a) } 18 | end 19 | 20 | # Renders a sort link header, otherwise similar to :attr. 21 | def sortable_attr(attr, header = nil, &block) 22 | if template.sortable?(attr) 23 | attr(attr, sort_header(attr, header), &block) 24 | else 25 | attr(attr, header, &block) 26 | end 27 | end 28 | 29 | private 30 | 31 | # Request params for the sort link. 32 | def sort_params(attr) 33 | result = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params 34 | result.merge(sort: attr, sort_dir: sort_dir(attr), only_path: true) 35 | end 36 | 37 | # The sort mark, if any, for the given attribute. 38 | def current_mark(attr) 39 | if current_sort?(attr) 40 | # rubocop:disable Rails/OutputSafety 41 | (sort_dir(attr) == "asc" ? " ↑" : " ↓").html_safe 42 | # rubocop:enable Rails/OutputSafety 43 | else 44 | "" 45 | end 46 | end 47 | 48 | # Returns true if the given attribute is the current sort column. 49 | def current_sort?(attr) 50 | params[:sort] == attr.to_s 51 | end 52 | 53 | # The sort direction to use in the sort link for the given attribute. 54 | def sort_dir(attr) 55 | current_sort?(attr) && params[:sort_dir] == "asc" ? "desc" : "asc" 56 | end 57 | 58 | # Delegate to template. 59 | def params 60 | template.params 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/helpers/form_helper.rb: -------------------------------------------------------------------------------- 1 | # Defines forms to edit models. The helper methods come in different 2 | # granularities: 3 | # * #plain_form - A form using Crud::FormBuilder. 4 | # * #standard_form - A #plain_form for a given object and attributes with error 5 | # messages and save and cancel buttons. 6 | # * #crud_form - A #standard_form for the current +entry+, with the given 7 | # attributes or default. 8 | module FormHelper 9 | # Renders a form using Crud::FormBuilder. 10 | def plain_form(object, **options, &block) 11 | options[:html] ||= {} 12 | add_css_class(options[:html], "form-horizontal") 13 | options[:html][:role] ||= "form" 14 | options[:builder] ||= DryCrud::Form::Builder 15 | options[:cancel_url] ||= polymorphic_path(object, returning: true) 16 | 17 | form_for(object, **options, &block) 18 | end 19 | 20 | # Renders a standard form for the given entry and attributes. 21 | # The form is rendered with a basic save and cancel button. 22 | # If a block is given, custom input fields may be rendered and attrs is 23 | # ignored. Before the input fields, the error messages are rendered, 24 | # if present. An options hash may be given as the last argument. 25 | def standard_form(object, *attrs, **options, &block) 26 | plain_form(object, **options) do |form| 27 | content = [ form.error_messages ] 28 | 29 | content << if block_given? 30 | capture(form, &block) 31 | else 32 | form.labeled_input_fields(*attrs) 33 | end 34 | 35 | content << form.standard_actions 36 | safe_join(content) 37 | end 38 | end 39 | 40 | # Renders a crud form for the current entry with default_crud_attrs or the 41 | # given attribute array. An options hash may be given as the last argument. 42 | # If a block is given, a custom form may be rendered and attrs is ignored. 43 | def crud_form(*attrs, **options, &block) 44 | attrs = default_crud_attrs - %i[created_at updated_at] if attrs.blank? 45 | standard_form(path_args(entry), *attrs, **options, &block) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/helpers/format_helper.rb: -------------------------------------------------------------------------------- 1 | # Provides uniform formatting of basic data types, based on Ruby class (#f) 2 | # or database column type (#format_attr). If other helpers define methods 3 | # with names like 'format_{class}_{attr}', these methods are used for 4 | # formatting. 5 | # 6 | # Futher helpers standartize the layout of multiple attributes (#render_attrs), 7 | # values with labels (#labeled) and simple lists. 8 | module FormatHelper 9 | # Formats a basic value based on its Ruby class. 10 | def f(value) 11 | case value 12 | when Float, BigDecimal 13 | number_with_precision(value, precision: t("number.format.precision"), 14 | delimiter: t("number.format.delimiter")) 15 | when Integer 16 | number_with_delimiter(value, delimiter: t("number.format.delimiter")) 17 | when Date then l(value) 18 | when Time then "#{l(value.to_date)} #{l(value, format: :time)}" 19 | when true then t("global.yes") 20 | when false then t("global.no") 21 | when nil then UtilityHelper::EMPTY_STRING 22 | else value.to_s 23 | end 24 | end 25 | 26 | # Formats an arbitrary attribute of the given ActiveRecord object. 27 | # If no specific format_{class}_{attr} or format_{attr} method is found, 28 | # formats the value as follows: 29 | # If the value is an associated model, renders the label of this object. 30 | # Otherwise, calls format_type. 31 | def format_attr(obj, attr) 32 | format_with_helper(obj, attr) || 33 | format_association(obj, attr) || 34 | format_type(obj, attr) 35 | end 36 | 37 | # Renders a simple unordered list, which will 38 | # simply render all passed items or yield them 39 | # to your block. 40 | def simple_list(items, **ul_options) 41 | content_tag_nested(:ul, items, **ul_options) do |item| 42 | tag.li(block_given? ? yield(item) : f(item)) 43 | end 44 | end 45 | 46 | # Renders a list of attributes with label and value for a given object. 47 | # Optionally surrounded with a div. 48 | def render_attrs(obj, *attrs) 49 | content_tag_nested(:dl, attrs, class: "dl-horizontal") do |a| 50 | labeled_attr(obj, a) 51 | end 52 | end 53 | 54 | # Renders the formatted content of the given attribute with a label. 55 | def labeled_attr(obj, attr) 56 | labeled(captionize(attr, obj.class), format_attr(obj, attr)) 57 | end 58 | 59 | # Renders an arbitrary content with the given label. Used for uniform 60 | # presentation. 61 | def labeled(label, content = nil, &block) 62 | content = capture(&block) if block_given? 63 | render("shared/labeled", label: label, content: content) 64 | end 65 | 66 | # Transform the given text into a form as used by labels or table headers. 67 | def captionize(text, clazz = nil) 68 | text = text.to_s 69 | if clazz.respond_to?(:human_attribute_name) 70 | text_without_id = text.end_with?("_ids") ? text[0..-5].pluralize : text 71 | clazz.human_attribute_name(text_without_id) 72 | else 73 | text.humanize.titleize 74 | end 75 | end 76 | 77 | private 78 | 79 | # Checks whether a format_{class}_{attr} or format_{attr} helper method is 80 | # defined and calls it if is. 81 | def format_with_helper(obj, attr) 82 | class_name = obj.class.name.underscore.tr("/", "_") 83 | format_type_attr_method = :"format_#{class_name}_#{attr}" 84 | format_attr_method = :"format_#{attr}" 85 | 86 | if respond_to?(format_type_attr_method) 87 | send(format_type_attr_method, obj) 88 | elsif respond_to?(format_attr_method) 89 | send(format_attr_method, obj) 90 | else 91 | false 92 | end 93 | end 94 | 95 | # Checks whether the given attr is an association of obj and formats it 96 | # accordingly if it is. 97 | def format_association(obj, attr) 98 | belongs_to = association(obj, attr, :belongs_to, :has_one) 99 | has_many = association(obj, attr, :has_many, :has_and_belongs_to_many) 100 | 101 | if belongs_to 102 | format_belongs_to(obj, belongs_to) 103 | elsif has_many 104 | format_has_many(obj, has_many) 105 | else 106 | false 107 | end 108 | end 109 | 110 | # Formats an arbitrary attribute of the given object depending on its data 111 | # type. For Active Records, take the defined data type into account for 112 | # special types that have no own object class. 113 | def format_type(obj, attr) 114 | val = obj.send(attr) 115 | return UtilityHelper::EMPTY_STRING if val.blank? && val != false 116 | 117 | case column_type(obj, attr) 118 | when :time then l(val, format: :time) 119 | when :date then f(val.to_date) 120 | when :datetime, :timestamp then f(val.time) 121 | when :text then simple_format(h(val)) 122 | when :decimal 123 | number_with_precision(val.to_s.to_f, 124 | precision: column_property(obj, attr, :scale), 125 | delimiter: t("number.format.delimiter")) 126 | else f(val) 127 | end 128 | end 129 | 130 | # Formats an ActiveRecord +belongs_to+ or +has_one+ association. 131 | def format_belongs_to(obj, assoc) 132 | val = obj.send(assoc.name) 133 | if val 134 | assoc_link(assoc, val) 135 | else 136 | ta(:no_entry, assoc) 137 | end 138 | end 139 | 140 | # Formats an ActiveRecord +has_and_belongs_to_many+ or 141 | # +has_many+ association. 142 | def format_has_many(obj, assoc) 143 | values = obj.send(assoc.name) 144 | if values.size == 1 145 | assoc_link(assoc, values.first) 146 | elsif values.present? 147 | simple_list(values) { |val| assoc_link(assoc, val) } 148 | else 149 | ta(:no_entry, assoc) 150 | end 151 | end 152 | 153 | # Renders a link to the given association entry. 154 | def assoc_link(assoc, val) 155 | link_to_if(assoc_link?(assoc, val), val.to_s, val) 156 | end 157 | 158 | # Returns true if no link should be created when formatting the given 159 | # association. 160 | def assoc_link?(_assoc, val) 161 | respond_to?(:"#{val.class.model_name.singular_route_key}_path") 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /app/helpers/i18n_helper.rb: -------------------------------------------------------------------------------- 1 | # Translation helpers extending the Rails +translate+ helper to support 2 | # translation inheritance over the controller class hierarchy. 3 | module I18nHelper 4 | # Translates the passed key by looking it up over the controller hierarchy. 5 | # The key is searched in the following order: 6 | # - {controller}.{current_partial}.{key} 7 | # - {controller}.{current_action}.{key} 8 | # - {controller}.global.{key} 9 | # - {parent_controller}.{current_partial}.{key} 10 | # - {parent_controller}.{current_action}.{key} 11 | # - {parent_controller}.global.{key} 12 | # - ... 13 | # - global.{key} 14 | def translate_inheritable(key, **variables) 15 | partial = defined?(@virtual_path) ? @virtual_path.gsub(/.*\/_?/, "") : nil 16 | defaults = inheritable_translation_defaults(key, partial) 17 | variables[:default] ||= defaults 18 | t(defaults.shift, **variables) 19 | end 20 | 21 | alias ti translate_inheritable 22 | 23 | # Translates the passed key for an active record association. This helper is 24 | # used for rendering association dependent keys in forms like :no_entry, 25 | # :none_available or :please_select. 26 | # The key is looked up in the following order: 27 | # - activerecord.associations.models.{model_name}.{association_name}.{key} 28 | # - activerecord.associations.{association_model_name}.{key} 29 | # - global.associations.{key} 30 | def translate_association(key, assoc = nil, **variables) 31 | if assoc && assoc.options[:polymorphic].nil? 32 | variables[:default] ||= [ association_klass_key(assoc, key).to_sym, 33 | :"global.associations.#{key}" ] 34 | t(association_owner_key(assoc, key), **variables) 35 | else 36 | t("global.associations.#{key}", **variables) 37 | end 38 | end 39 | 40 | alias ta translate_association 41 | 42 | private 43 | 44 | # General translation key based on the klass of the association. 45 | def association_klass_key(assoc, key) 46 | k = "activerecord.associations." 47 | k << assoc.klass.model_name.singular 48 | k << "." 49 | k << key.to_s 50 | end 51 | 52 | # Specific translation key based on the owner model and the name 53 | # of the association. 54 | def association_owner_key(assoc, key) 55 | k = "activerecord.associations.models." 56 | k << assoc.active_record.model_name.singular 57 | k << "." 58 | k << assoc.name.to_s 59 | k << "." 60 | k << key.to_s 61 | end 62 | 63 | def inheritable_translation_defaults(key, partial) 64 | defaults = [] 65 | current = controller.class 66 | while current < ActionController::Base 67 | folder = current.controller_path 68 | if folder.present? 69 | append_controller_translation_keys(defaults, folder, partial, key) 70 | end 71 | current = current.superclass 72 | end 73 | defaults << :"global.#{key}" 74 | end 75 | 76 | def append_controller_translation_keys(defaults, folder, partial, key) 77 | defaults << :"#{folder}.#{partial}.#{key}" if partial 78 | defaults << :"#{folder}.#{action_name}.#{key}" 79 | defaults << :"#{folder}.global.#{key}" 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/helpers/table_helper.rb: -------------------------------------------------------------------------------- 1 | # Defines tables to display a list of entries. The helper methods come in 2 | # different granularities: 3 | # * #plain_table - A basic table for the given entries and attributes using 4 | # the Crud::TableBuilder. 5 | # * #list_table - A sortable #plain_table for the current +entries+, with the 6 | # given attributes or default. 7 | # * #crud_table - A sortable #plain_table for the current +entries+, with the 8 | # given attributes or default and the standard crud action links. 9 | module TableHelper 10 | # Renders a table for the given entries. One column is rendered for each 11 | # attribute passed. If a block is given, the columns defined therein are 12 | # appended to the attribute columns. 13 | # If entries is empty, an appropriate message is rendered. 14 | # An options hash may be given as the last argument. 15 | def plain_table(entries, *attrs, **options) 16 | add_css_class(options, "table table-striped table-hover") 17 | builder = options.delete(:builder) || DryCrud::Table::Builder 18 | builder.table(entries, self, **options) do |t| 19 | t.attrs(*attrs) 20 | yield t if block_given? 21 | end 22 | end 23 | 24 | # Renders a #plain_table for the given entries. 25 | # If entries is empty, an appropriate message is rendered. 26 | def plain_table_or_message(entries, *attrs, **options, &block) 27 | entries.to_a # force evaluation of relations 28 | if entries.present? 29 | plain_table(entries, *attrs, **options, &block) 30 | else 31 | tag.div(ti(:no_list_entries), class: "table") 32 | end 33 | end 34 | 35 | # Create a table of the +entries+ with the default or 36 | # the passed attributes in its columns. An options hash may be given 37 | # as the last argument. 38 | def list_table(*attrs, **options, &block) 39 | attrs = attrs_or_default(attrs, &block) 40 | plain_table_or_message(entries, **options) do |t| 41 | t.sortable_attrs(*attrs) 42 | yield t if block_given? 43 | end 44 | end 45 | 46 | # Create a table of the current +entries+ with the default or the passed 47 | # attributes in its columns. Edit and destroy actions are added to each row. 48 | # If attrs are present, the first column will link to the show 49 | # action. Edit and destroy actions are appended to the end of each row. 50 | # If a block is given, the column defined there will be inserted 51 | # between the given attributes and the actions. 52 | # An options hash for the table builder may be given as the last argument. 53 | def crud_table(*attrs, **options, &block) 54 | attrs = attrs_or_default(attrs, &block) 55 | first = attrs.shift 56 | plain_table_or_message(entries, **options) do |t| 57 | t.attr_with_show_link(first) if first 58 | t.sortable_attrs(*attrs) 59 | yield t if block_given? 60 | standard_table_actions(t) 61 | end 62 | end 63 | 64 | # Adds standard action link columns (edit, destroy) to the given table. 65 | def standard_table_actions(table) 66 | table.edit_action_col 67 | table.destroy_action_col 68 | end 69 | 70 | private 71 | 72 | def attrs_or_default(attrs) 73 | if !block_given? && attrs.blank? 74 | default_crud_attrs 75 | else 76 | attrs 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/helpers/utility_helper.rb: -------------------------------------------------------------------------------- 1 | require "English" 2 | 3 | # View helpers for basic functions used in various other helpers. 4 | module UtilityHelper 5 | # non-breaking space asserts better css. 6 | EMPTY_STRING = " ".html_safe.freeze 7 | 8 | # Render a content tag with the collected contents rendered 9 | # by &block for each item in collection. 10 | def content_tag_nested(tag, collection, **options, &block) 11 | content_tag(tag, safe_join(collection, &block), **options) 12 | end 13 | 14 | # Overridden method that takes a block that is executed for each item in 15 | # array before appending the results. 16 | def safe_join(array, sep = $OUTPUT_FIELD_SEPARATOR, &block) 17 | super(block_given? ? array.map(&block).compact : array, sep) 18 | end 19 | 20 | # Returns the css class for the given flash level. 21 | def flash_class(level) 22 | case level 23 | when :notice then "success" 24 | when :alert then "error" 25 | else level.to_s 26 | end 27 | end 28 | 29 | # Adds a class to the given options, even if there are already classes. 30 | def add_css_class(options, classes) 31 | if options[:class] 32 | options[:class] += " #{classes}" if classes 33 | else 34 | options[:class] = classes 35 | end 36 | end 37 | 38 | # The default attributes to use in attrs, list and form partials. 39 | # These are all defined attributes except certain special ones like 40 | # 'id' or 'position'. 41 | def default_crud_attrs 42 | attrs = model_class.column_names.map(&:to_sym) 43 | attrs - %i[id position password] 44 | end 45 | 46 | # Returns the ActiveRecord column type or nil. 47 | def column_type(obj, attr) 48 | column_property(obj, attr, :type) 49 | end 50 | 51 | # Returns an ActiveRecord column property for the passed attr or nil 52 | def column_property(obj, attr, property) 53 | if obj.respond_to?(:column_for_attribute) && obj.has_attribute?(attr) 54 | obj.column_for_attribute(attr).send(property) 55 | end 56 | end 57 | 58 | # Returns the association proxy for the given attribute. The attr parameter 59 | # may be the _id column or the association name. If a macro (e.g. 60 | # :belongs_to) is given, the association must be of this type, otherwise, 61 | # any association is returned. Returns nil if no association (or not of the 62 | # given macro) was found. 63 | def association(obj, attr, *macros) 64 | if obj.class.respond_to?(:reflect_on_association) 65 | name = assoc_and_id_attr(attr).first.to_sym 66 | assoc = obj.class.reflect_on_association(name) 67 | assoc if assoc && (macros.blank? || macros.include?(assoc.macro)) 68 | end 69 | end 70 | 71 | # Returns the name of the attr and it's corresponding field 72 | def assoc_and_id_attr(attr) 73 | attr = attr.to_s 74 | if attr.end_with?("_id") 75 | [ attr[0..-4], attr ] 76 | elsif attr.end_with?("_ids") 77 | [ attr[0..-5].pluralize, attr ] 78 | else 79 | [ attr, "#{attr}_id" ] 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/views/crud/_actions_edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= index_action_link %> 2 | <%= show_action_link %> 3 | <%= destroy_action_link %> -------------------------------------------------------------------------------- /app/views/crud/_actions_edit.html.haml: -------------------------------------------------------------------------------- 1 | = index_action_link 2 | = show_action_link 3 | = destroy_action_link -------------------------------------------------------------------------------- /app/views/crud/_actions_index.html.erb: -------------------------------------------------------------------------------- 1 | <%= add_action_link %> -------------------------------------------------------------------------------- /app/views/crud/_actions_index.html.haml: -------------------------------------------------------------------------------- 1 | = add_action_link 2 | -------------------------------------------------------------------------------- /app/views/crud/_actions_show.html.erb: -------------------------------------------------------------------------------- 1 | <%= index_action_link %> 2 | <%= edit_action_link %> 3 | <%= destroy_action_link %> -------------------------------------------------------------------------------- /app/views/crud/_actions_show.html.haml: -------------------------------------------------------------------------------- 1 | = index_action_link 2 | = edit_action_link 3 | = destroy_action_link -------------------------------------------------------------------------------- /app/views/crud/_attrs.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_attrs entry, *default_crud_attrs %> -------------------------------------------------------------------------------- /app/views/crud/_attrs.html.haml: -------------------------------------------------------------------------------- 1 | = render_attrs entry, *default_crud_attrs -------------------------------------------------------------------------------- /app/views/crud/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_form %> -------------------------------------------------------------------------------- /app/views/crud/_form.html.haml: -------------------------------------------------------------------------------- 1 | = crud_form -------------------------------------------------------------------------------- /app/views/crud/_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_table %> -------------------------------------------------------------------------------- /app/views/crud/_list.html.haml: -------------------------------------------------------------------------------- 1 | = crud_table -------------------------------------------------------------------------------- /app/views/crud/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% @title ||= ti(:title, model: full_entry_label).html_safe -%> 2 | 3 | <% content_for(:actions, render('actions_edit')) %> 4 | 5 | <%= render 'form' %> 6 | -------------------------------------------------------------------------------- /app/views/crud/edit.html.haml: -------------------------------------------------------------------------------- 1 | - @title ||= ti(:title, model: full_entry_label).html_safe 2 | 3 | - content_for(:actions, render('actions_edit')) 4 | 5 | = render 'form' 6 | -------------------------------------------------------------------------------- /app/views/crud/new.html.erb: -------------------------------------------------------------------------------- 1 | <% @title ||= ti(:title, model: models_label(plural: false)) -%> 2 | 3 | <% content_for(:actions, index_action_link) %> 4 | 5 | <%= render 'form' %> 6 | -------------------------------------------------------------------------------- /app/views/crud/new.html.haml: -------------------------------------------------------------------------------- 1 | - @title ||= ti(:title, model: models_label(plural: false)) 2 | 3 | - content_for(:actions, index_action_link) 4 | 5 | = render 'form' 6 | -------------------------------------------------------------------------------- /app/views/crud/show.html.erb: -------------------------------------------------------------------------------- 1 | <% @title ||= ti(:title, model: full_entry_label).html_safe -%> 2 | 3 | <% content_for(:actions, render('actions_show')) %> 4 | 5 | <%= render 'attrs' %> 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/crud/show.html.haml: -------------------------------------------------------------------------------- 1 | - @title ||= ti(:title, model: full_entry_label).html_safe 2 | 3 | - content_for(:actions, render('actions_show')) 4 | 5 | = render 'attrs' 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/crud/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! entry, :id, *default_crud_attrs 2 | -------------------------------------------------------------------------------- /app/views/layouts/_flash.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash[level].present? %> 2 |
3 | <%= flash[level].html_safe %> 4 |
5 | <% end %> -------------------------------------------------------------------------------- /app/views/layouts/_flash.html.haml: -------------------------------------------------------------------------------- 1 | - if flash[level].present? 2 | %div{class: "alert alert-#{flash_class(level)}"}= flash[level].html_safe -------------------------------------------------------------------------------- /app/views/layouts/_nav.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to 'MyApp', root_path, class: 'navbar-brand' %> 2 | 7 | -------------------------------------------------------------------------------- /app/views/layouts/_nav.html.haml: -------------------------------------------------------------------------------- 1 | = link_to 'MyApp', root_path, class: 'navbar-brand' 2 | %ul.navbar-nav 3 | %li.nav-item= link_to "Link1", "/path1", class: 'nav-link' 4 | %li.nav-item= link_to "Link2", "/path2", class: 'nav-link' 5 | %li.nav-item= link_to "Link3", "/path3", class: 'nav-link' 6 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= strip_tags(@title) %> - MyApp 7 | 8 | 9 | <%= csrf_meta_tag %> 10 | <%= csp_meta_tag %> 11 | 12 | <%= stylesheet_link_tag 'application', 'data-turbo-track': 'reload' %> 13 | <%= javascript_include_tag 'application', 'data-turbo-track': 'reload', type: 'module', defer: true %> 14 | 15 | <%= yield :head %> 16 | 17 | 18 | 19 | 24 | 25 |
26 | 27 |

<%= @title %>

28 | 29 |
30 |
31 | <%= yield :tools %> 32 |
33 |
34 |
35 | <%= yield :actions %> 36 |
37 |
38 |
39 | 40 |
41 | <%= render partial: 'layouts/flash', collection: [:notice, :alert], as: :level %> 42 |
43 | 44 |
45 | <%= yield %> 46 |
47 | 48 |
49 | 50 | 53 | 54 | <%= javascript_tag yield(:javascripts) if content_for?(:javascripts) %> 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | 3 | %html 4 | %head 5 | %meta{:charset => 'utf-8'} 6 | %title 7 | = strip_tags(@title) 8 | \- MyApp 9 | 10 | %meta{name: 'viewport', content: 'width=device-width, initial-scale=1.0'} 11 | = csrf_meta_tag 12 | = csp_meta_tag 13 | 14 | = stylesheet_link_tag 'application', 'data-turbo-track': 'reload' 15 | = javascript_include_tag 'application', 'data-turbo-track': 'reload', type: 'module', defer: true 16 | 17 | = yield :head 18 | 19 | %body 20 | %nav.navbar.navbar-expand-lg.navbar-light.bg-light.mb-4 21 | .container-fluid 22 | = render 'layouts/nav' 23 | 24 | .container 25 | %h1= @title 26 | 27 | .row.actions.mb-3 28 | .col-md-5 29 | = yield :tools 30 | .col-md-7 31 | .btn-toolbar.float-end 32 | = yield :actions 33 | 34 | #flash= render partial: 'layouts/flash', collection: [:notice, :alert], as: :level 35 | 36 | #content= yield 37 | 38 | %footer 39 | %p 40 | © code!z #{Time.zone.now.year} 41 | 42 | = javascript_tag yield(:javascripts) if content_for?(:javascripts) 43 | -------------------------------------------------------------------------------- /app/views/list/_actions_index.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/views/list/_actions_index.html.erb -------------------------------------------------------------------------------- /app/views/list/_actions_index.html.haml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/views/list/_actions_index.html.haml -------------------------------------------------------------------------------- /app/views/list/_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= list_table %> -------------------------------------------------------------------------------- /app/views/list/_list.html.haml: -------------------------------------------------------------------------------- 1 | = list_table -------------------------------------------------------------------------------- /app/views/list/_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag(nil, method: :get, role: 'search', class: 'form-inline') do %> 2 | <%= hidden_field_tag :returning, true %> 3 | <%= hidden_field_tag :page, 1 %> 4 |
5 | <%= search_field_tag :q, params[:q], class: 'form-control' %> 6 | <%= submit_tag ti(:"button.search"), class: 'btn btn-outline-secondary' %> 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/list/_search.html.haml: -------------------------------------------------------------------------------- 1 | = form_tag(nil, { method: :get, class: 'form-inline', role: 'search' }) do 2 | = hidden_field_tag :returning, true 3 | = hidden_field_tag :page, 1 4 | .input-group 5 | = search_field_tag :q, params[:q], class: 'form-control' 6 | = submit_tag ti(:"button.search"), class: 'btn btn-outline-secondary' 7 | -------------------------------------------------------------------------------- /app/views/list/index.html.erb: -------------------------------------------------------------------------------- 1 | <% @title ||= ti(:title, models: models_label) -%> 2 | 3 | <% content_for(:tools, render('search')) if search_support? %> 4 | 5 | <% content_for(:actions, render('actions_index')) %> 6 | 7 | <%= render 'list' %> 8 | -------------------------------------------------------------------------------- /app/views/list/index.html.haml: -------------------------------------------------------------------------------- 1 | - @title ||= ti(:title, models: models_label) 2 | 3 | - content_for(:tools, render('search')) if search_support? 4 | 5 | - content_for(:actions, render('actions_index')) 6 | 7 | = render 'list' 8 | -------------------------------------------------------------------------------- /app/views/list/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(entries) do |entry| 2 | json.extract! entry, :id, *default_crud_attrs 3 | json.url polymorphic_url(path_args(entry), format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if errors.any? %> 2 |
3 |

4 | <%= ti(:"errors.header", count: errors.count, model: object.to_s) %> 5 |

6 | 11 |
12 | <% end -%> 13 | -------------------------------------------------------------------------------- /app/views/shared/_error_messages.html.haml: -------------------------------------------------------------------------------- 1 | - if errors.any? 2 | #error_explanation.alert.alert-danger 3 | %h2= ti(:"errors.header", count: errors.count, model: object.to_s) 4 | %ul 5 | - errors.full_messages.each do |msg| 6 | %li= msg 7 | -------------------------------------------------------------------------------- /app/views/shared/_labeled.html.erb: -------------------------------------------------------------------------------- 1 |
<%= label.presence || raw(UtilityHelper::EMPTY_STRING) %>
2 |
<%= content.presence || raw(UtilityHelper::EMPTY_STRING) %>
3 | -------------------------------------------------------------------------------- /app/views/shared/_labeled.html.haml: -------------------------------------------------------------------------------- 1 | %dt= label.presence || raw(UtilityHelper::EMPTY_STRING) 2 | %dd.value= content.presence || raw(UtilityHelper::EMPTY_STRING) -------------------------------------------------------------------------------- /config/locales/crud.de.yml: -------------------------------------------------------------------------------- 1 | # Translations of all crud strings. 2 | # See also I18nHelper#translate_inheritable and #translate_association. 3 | 4 | de: 5 | # global scope 6 | global: 7 | "yes": "ja" 8 | "no": "nein" 9 | no_list_entries: Keine Einträge gefunden. 10 | confirm_delete: Wollen Sie diesen Eintrag wirklich löschen? 11 | 12 | associations: 13 | # association keys may be customized per model with the prefix 14 | # 'activerecord.associations.{model}.' or even per actual association with 15 | # 'activerecord.associations.models.{holder_model}.{assoc_name}.' 16 | no_entry: (keine) 17 | none_available: (keine verfügbar) 18 | please_select: Bitte auswählen 19 | 20 | button: 21 | save: Speichern 22 | cancel: Abbrechen 23 | search: Suchen 24 | 25 | link: 26 | show: Anzeigen 27 | edit: Bearbeiten 28 | add: Erstellen 29 | delete: Löschen 30 | list: Liste 31 | 32 | errors: 33 | header: 34 | one: "Ein Fehler verhinderte das Speichern dieses Eintrages:" 35 | other: "%{count} Fehler verhinderten das Speichern dieses Eintrages:" 36 | 37 | # formats 38 | time: 39 | formats: 40 | time: "%H:%M" 41 | 42 | # list controller 43 | list: 44 | index: 45 | title: "%{models}" 46 | 47 | # crud controller 48 | crud: 49 | show: 50 | title: "%{model}" 51 | new: 52 | title: "%{model} erstellen" 53 | edit: 54 | title: "%{model} bearbeiten" 55 | create: 56 | flash: 57 | success: "%{model} wurde erfolgreich erstellt." 58 | update: 59 | flash: 60 | success: "%{model} wurde erfolgreich aktualisiert." 61 | destroy: 62 | flash: 63 | success: "%{model} wurde erfolgreich gelöscht." 64 | failure: "%{model} konnte nicht gelöscht werden." 65 | -------------------------------------------------------------------------------- /config/locales/crud.en.yml: -------------------------------------------------------------------------------- 1 | # Translations of all crud strings. 2 | # See also StandardHelper#translate_inheritable and #translate_association. 3 | 4 | en: 5 | # global scope 6 | global: 7 | "yes": "yes" 8 | "no": "no" 9 | no_list_entries: No entries found. 10 | confirm_delete: Do you really want to delete this entry? 11 | 12 | associations: 13 | # association keys may be customized per model with the prefix 14 | # 'activerecord.associations.{model}.' or even per actual association with 15 | # 'activerecord.associations.models.{holder_model}.{assoc_name}.' 16 | no_entry: (none) 17 | none_available: (none available) 18 | please_select: Please select 19 | 20 | button: 21 | save: Save 22 | cancel: Cancel 23 | search: Search 24 | 25 | link: 26 | show: Show 27 | edit: Edit 28 | add: Add 29 | delete: Delete 30 | list: List 31 | 32 | errors: 33 | header: 34 | one: "1 error prohibited this entry from being saved:" 35 | other: "%{count} errors prohibited this entry from being saved:" 36 | 37 | # formats 38 | time: 39 | formats: 40 | time: "%H:%M" 41 | 42 | # list controller 43 | list: 44 | index: 45 | title: Listing %{models} 46 | 47 | # crud controller 48 | crud: 49 | show: 50 | title: "%{model}" 51 | new: 52 | title: "New %{model}" 53 | edit: 54 | title: "Edit %{model}" 55 | create: 56 | flash: 57 | success: "%{model} was successfully created." 58 | update: 59 | flash: 60 | success: "%{model} was successfully updated." 61 | destroy: 62 | flash: 63 | success: "%{model} was successfully deleted." 64 | failure: "%{model} could not be deleted." 65 | -------------------------------------------------------------------------------- /config/locales/crud.it.yml: -------------------------------------------------------------------------------- 1 | # Translations of all crud strings by mberlanda 2 | # See also StandardHelper#translate_inheritable and #translate_association. 3 | 4 | it: 5 | # global scope 6 | global: 7 | "yes": "si" 8 | "no": "no" 9 | no_list_entries: Nessun elemento trovato. 10 | confirm_delete: Vuoi davvero eliminare questo elemento? 11 | 12 | associations: 13 | # association keys may be customized per model with the prefix 14 | # 'activerecord.associations.{model}.' or even per actual association with 15 | # 'activerecord.associations.models.{holder_model}.{assoc_name}.' 16 | no_entry: (nessuno) 17 | none_available: (non disponibile) 18 | please_select: Prego selezionare 19 | 20 | button: 21 | save: Salva 22 | cancel: Annulla 23 | search: Cerca 24 | 25 | link: 26 | show: Mostra 27 | edit: Modifica 28 | add: Aggiungi 29 | delete: Elimina 30 | list: Elenco 31 | 32 | errors: 33 | header: 34 | one: "1 errore impedisce il salvataggio di questo elemento:" 35 | other: "%{count} errori impediscono il salvataggio di questo elemento:" 36 | 37 | # formats 38 | time: 39 | formats: 40 | time: "%H:%M" 41 | 42 | # list controller 43 | list: 44 | index: 45 | title: Elenco %{models} 46 | 47 | # crud controller 48 | crud: 49 | show: 50 | title: "%{model}" 51 | new: 52 | title: "Nuovo %{model}" 53 | edit: 54 | title: "Modifica %{model}" 55 | create: 56 | flash: 57 | success: "%{model} è stato creato con successo." 58 | update: 59 | flash: 60 | success: "%{model} è stato aggiornato con successo." 61 | destroy: 62 | flash: 63 | success: "%{model} è stato eliminato con successo." 64 | failure: "Non è stato possibile eliminare %{model}." 65 | -------------------------------------------------------------------------------- /dry_crud.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'rubygems' 4 | require 'rake' 5 | require 'date' 6 | 7 | DRY_CRUD_GEMSPEC = Gem::Specification.new do |spec| 8 | spec.name = 'dry_crud' 9 | spec.version = File.read('VERSION').strip 10 | spec.date = Date.today.to_s 11 | 12 | spec.author = 'Pascal Zumkehr' 13 | spec.email = 'pascal+github@codez.ch' 14 | spec.homepage = 'http://github.com/codez/dry_crud' 15 | 16 | spec.summary = <<-END 17 | Generates DRY and specifically extendable CRUD controller, views and helpers 18 | for Rails applications. 19 | END 20 | spec.description = <<-END 21 | Generates simple and extendable controller, views and helpers that support you 22 | to DRY up the CRUD code in your Rails project. Start with these elements and 23 | build a clean base to efficiently develop your application upon. 24 | END 25 | 26 | spec.add_dependency 'rails', '>= 8.0' 27 | 28 | readmes = FileList.new('*') do |list| 29 | list.exclude(/(^|[^.a-z])[a-z]+/) 30 | list.exclude('TODO') 31 | end.to_a 32 | 33 | spec.files = FileList['app/**/*'].to_a + 34 | FileList['config/**/*'].to_a + 35 | FileList['lib/**/*'].to_a + 36 | readmes 37 | 38 | spec.extra_rdoc_files = readmes 39 | spec.rdoc_options << '--title' << '"Dry Crud"' << 40 | '--main' << 'README.rdoc' << 41 | '--line-numbers' 42 | end 43 | -------------------------------------------------------------------------------- /lib/dry_crud.rb: -------------------------------------------------------------------------------- 1 | require "dry_crud/engine" 2 | 3 | # Base namespace 4 | module DryCrud 5 | end 6 | -------------------------------------------------------------------------------- /lib/dry_crud/engine.rb: -------------------------------------------------------------------------------- 1 | module DryCrud 2 | # Dry Crud Rails engine 3 | class Engine < Rails::Engine 4 | # Fields with errors are directly styled in DryCrud::FormBuilder. 5 | # Rails should just output the plain html tag. 6 | initializer "dry_crud.field_error_proc" do |_app| 7 | ActionView::Base.field_error_proc = 8 | proc { |html_tag, _instance| html_tag } 9 | end 10 | 11 | # Load dry_crud engine helpers first so that the application may override 12 | # them. 13 | config.to_prepare do 14 | paths = ApplicationController.helpers_path 15 | helper_path = "#{File::SEPARATOR}app#{File::SEPARATOR}helpers" 16 | regexp = /dry_crud(-\d+\.\d+\.\d+)?#{helper_path}\z/ 17 | dry_crud_helpers = paths.detect { |p| p =~ regexp } 18 | if dry_crud_helpers 19 | paths.delete(dry_crud_helpers) 20 | paths.prepend(dry_crud_helpers) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/USAGE: -------------------------------------------------------------------------------- 1 | rails g dry_crud -------------------------------------------------------------------------------- /lib/generators/dry_crud/dry_crud_generator.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "generators/dry_crud/dry_crud_generator_base" 3 | rescue LoadError => _e 4 | # ok, we are in the rake task 5 | end 6 | 7 | # Copies all dry_crud files to the rails application. 8 | class DryCrudGenerator < DryCrudGeneratorBase 9 | desc "Copy all dry_crud files to the application." 10 | 11 | class_options %w[templates -t] => "erb" 12 | class_options %w[tests] => "testunit" 13 | 14 | # copy everything to application 15 | def install_dry_crud 16 | copy_files(all_template_files) 17 | 18 | Dir.chdir(self.class.template_root) do 19 | copy_crud_test_model 20 | end 21 | 22 | readme "INSTALL" 23 | end 24 | 25 | private 26 | 27 | def should_copy?(file_source) 28 | !file_source.end_with?(exclude_template) && 29 | !file_source.start_with?(exclude_test_dir) && 30 | file_source != "INSTALL" 31 | end 32 | 33 | def copy_crud_test_model 34 | unless exclude_test_dir == "spec" 35 | template(File.join("test", "support", "crud_test_model.rb"), 36 | File.join("spec", "support", "crud_test_model.rb")) 37 | template(File.join("test", "support", "crud_test_models_controller.rb"), 38 | File.join("spec", "support", "crud_test_models_controller.rb")) 39 | template(File.join("test", "support", "crud_test_helper.rb"), 40 | File.join("spec", "support", "crud_test_helper.rb")) 41 | end 42 | end 43 | 44 | def exclude_template 45 | options[:templates].casecmp("haml").zero? ? ".erb" : ".haml" 46 | end 47 | 48 | def exclude_test_dir 49 | case options[:tests].downcase 50 | when "rspec" then "test" 51 | when "all" then "exclude_nothing" 52 | else "spec" 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/dry_crud_generator_base.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | 3 | # Generates all dry crud files 4 | class DryCrudGeneratorBase < Rails::Generators::Base 5 | def self.template_root 6 | File.join(File.dirname(__FILE__), "templates") 7 | end 8 | 9 | def self.gem_root 10 | File.join(File.dirname(__FILE__), "..", "..", "..") 11 | end 12 | 13 | def self.source_paths 14 | [ gem_root, 15 | template_root ] 16 | end 17 | 18 | private 19 | 20 | def all_template_files 21 | { self.class.gem_root => 22 | template_files(self.class.gem_root, "app", "config"), 23 | self.class.template_root => 24 | template_files(self.class.template_root) } 25 | end 26 | 27 | def template_files(root, *folders) 28 | pattern = File.join("**", "**") 29 | pattern = File.join("{#{folders.join(',')}}", pattern) if folders.present? 30 | Dir.chdir(root) do 31 | Dir.glob(pattern).sort.reject { |f| File.directory?(f) } 32 | end 33 | end 34 | 35 | def copy_files(root_files) 36 | root_files.each do |root, files| 37 | Dir.chdir(root) do 38 | files.each do |file_source| 39 | copy_file_source(file_source) if should_copy?(file_source) 40 | end 41 | end 42 | end 43 | end 44 | 45 | def should_copy?(_file_source) 46 | true 47 | end 48 | 49 | def copy_file_source(file_source) 50 | if file_source.end_with?(".erb") 51 | copy_file(file_source) 52 | else 53 | template(file_source) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/file_generator.rb: -------------------------------------------------------------------------------- 1 | require "generators/dry_crud/dry_crud_generator_base" 2 | 3 | module DryCrud 4 | # Copies one file of dry_crud to the rails application. 5 | class FileGenerator < ::DryCrudGeneratorBase 6 | desc "Copy one file from dry_crud to the application.\n" \ 7 | "FILENAME is a part of the name of the file to copy. " \ 8 | "Must match exactly one file." 9 | 10 | argument :filename, 11 | type: :string, 12 | desc: "Name or part of the filename to copy. " \ 13 | "Must match exactly one file." 14 | 15 | # rubocop:disable Rails/Output 16 | def copy_matching_file 17 | files = matching_files 18 | case files.size 19 | when 1 20 | copy_files(@root_folder => files) 21 | when 0 22 | puts "No file containing '#{filename}' found in dry_crud." 23 | else 24 | puts "Please be more specific. " \ 25 | "All the following files match '#{filename}':" 26 | files.each do |f| 27 | puts " * #{f}" 28 | end 29 | end 30 | end 31 | # rubocop:enable Rails/Output 32 | 33 | private 34 | 35 | def matching_files 36 | all_template_files.collect do |root, files| 37 | files.select do |f| 38 | included = f.include?(filename) 39 | @root_folder = root if included 40 | included 41 | end 42 | end.flatten 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/INSTALL: -------------------------------------------------------------------------------- 1 | 2 | Thank you for using dry_crud. Feel free to adapt all generated classes to 3 | your needs. To integrate dry_crud into your code, only a few additions are 4 | required: 5 | 6 | * For uniform CRUD functionallity, just subclass your controllers from 7 | CrudController and define the #permitted_attrs (for StrongParameters). 8 | * Overwrite the #to_s method of your models for a human-friendly 9 | representation. 10 | 11 | 12 | Enjoy dry_crud and stay DRY! 13 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/config/initializers/field_error_proc.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # Fields with errors are directly styled in Crud::FormBuilder. 4 | # Rails should just output the plain html tag. 5 | ActionView::Base.field_error_proc = proc { |html_tag, instance| html_tag } 6 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/helpers/dry_crud/form/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "DryCrud::Form::Builder" do 4 | include FormatHelper 5 | include FormHelper 6 | include UtilityHelper 7 | include I18nHelper 8 | include CrudTestHelper 9 | 10 | before(:all) do 11 | reset_db 12 | setup_db 13 | create_test_data 14 | end 15 | 16 | after(:all) { reset_db } 17 | 18 | let(:entry) { CrudTestModel.first } 19 | let(:form) { DryCrud::Form::Builder.new(:entry, entry, self, {}) } 20 | 21 | describe "#input_field" do 22 | it "dispatches name attr to string field" do 23 | expect(form).to receive(:string_field) 24 | .with(:name, required: "required") 25 | .and_return("") 26 | form.input_field(:name) 27 | end 28 | 29 | it { expect(form.input_field(:name)).to be_html_safe } 30 | 31 | { password: :password_field, 32 | email: :email_field, 33 | remarks: :text_area, 34 | children: :integer_field, 35 | human: :boolean_field, 36 | birthdate: :date_field, 37 | gets_up_at: :time_field, 38 | last_seen: :datetime_field, 39 | companion_id: :belongs_to_field, 40 | other_ids: :has_many_field, 41 | more_ids: :has_many_field }.each do |attr, method| 42 | it "dispatches #{attr} attr to #{method}" do 43 | expect(form).to receive(method).with(attr) 44 | form.input_field(attr) 45 | end 46 | 47 | it { expect(form.input_field(attr)).to be_html_safe } 48 | end 49 | end 50 | 51 | describe "#labeled_input_fields" do 52 | subject { form.labeled_input_fields(:name, :remarks, :children) } 53 | 54 | it { is_expected.to be_html_safe } 55 | it { is_expected.to include(form.input_field(:name, required: "required")) } 56 | it { is_expected.to include(form.input_field(:remarks)) } 57 | it { is_expected.to include(form.input_field(:children)) } 58 | end 59 | 60 | describe "#labeled_input_field" do 61 | context "when required" do 62 | subject { form.labeled_input_field(:name) } 63 | it { is_expected.to include("input-group-text") } 64 | end 65 | 66 | context "when not required" do 67 | subject { form.labeled_input_field(:remarks) } 68 | it { is_expected.not_to include("input-group-text") } 69 | end 70 | 71 | context "with help text" do 72 | subject { form.labeled_input_field(:name, help: "Some Help") } 73 | it { is_expected.to include(form.help_block("Some Help")) } 74 | end 75 | end 76 | 77 | describe "#belongs_to_field" do 78 | it "has all options by default" do 79 | f = form.belongs_to_field(:companion_id) 80 | expect_n_options(f, 7) 81 | end 82 | 83 | it "with has options from :list option" do 84 | list = CrudTestModel.all 85 | f = form.belongs_to_field(:companion_id, 86 | list: [ list.first, list.second ]) 87 | expect_n_options(f, 3) 88 | end 89 | 90 | it "with empty instance list has no select" do 91 | assign(:companions, []) 92 | @companions = [] 93 | f = form.belongs_to_field(:companion_id) 94 | expect(f).to match t("global.associations.none_available") 95 | expect_n_options(f, 0) 96 | end 97 | end 98 | 99 | describe "#has_and_belongs_to_many_field" do 100 | let(:others) { OtherCrudTestModel.all[0..1] } 101 | 102 | it "has all options by default" do 103 | f = form.has_many_field(:other_ids) 104 | expect_n_options(f, 6) 105 | end 106 | 107 | it "uses options from :list option if given" do 108 | f = form.has_many_field(:other_ids, list: others) 109 | expect_n_options(f, 2) 110 | end 111 | 112 | it "uses options form instance variable if given" do 113 | assign(:others, others) 114 | @others = others 115 | f = form.has_many_field(:other_ids) 116 | expect_n_options(f, 2) 117 | end 118 | 119 | it "displays a message for an empty list" do 120 | @others = [] 121 | f = form.has_many_field(:other_ids) 122 | expect(f).to match t("global.associations.none_available") 123 | expect_n_options(f, 0) 124 | end 125 | end 126 | 127 | describe "#string_field" do 128 | it "sets maxlength if attr has a limit" do 129 | expect(form.string_field(:name)).to match(/maxlength="50"/) 130 | end 131 | end 132 | 133 | describe "#label" do 134 | context "only with attr" do 135 | subject { form.label(:gugus_dada) } 136 | 137 | it { is_expected.to be_html_safe } 138 | it "provides the same interface as rails" do 139 | is_expected.to match(/label [^>]*for.+Gugus dada/) 140 | end 141 | end 142 | 143 | context "with attr and text" do 144 | subject { form.label(:gugus_dada, "hoho") } 145 | 146 | it { is_expected.to be_html_safe } 147 | it "provides the same interface as rails" do 148 | is_expected.to match(/label [^>]*for.+hoho/) 149 | end 150 | end 151 | end 152 | 153 | describe "#labeled" do 154 | context "in labeled_ method" do 155 | subject { form.labeled_string_field(:name) } 156 | 157 | it { is_expected.to be_html_safe } 158 | it "provides the same interface as rails" do 159 | is_expected.to match(/label [^>]*for.+input/m) 160 | end 161 | end 162 | 163 | context "with custom content in argument" do 164 | subject do 165 | form.labeled("gugus", "".html_safe) 166 | end 167 | 168 | it { is_expected.to be_html_safe } 169 | it { is_expected.to match(/label [^>]*for.+".html_safe 176 | end 177 | end 178 | 179 | it { is_expected.to be_html_safe } 180 | it { is_expected.to match(/label [^>]*for.+".html_safe, 187 | caption: "Caption") 188 | end 189 | 190 | it { is_expected.to be_html_safe } 191 | it { is_expected.to match(/label [^>]*for.+>Caption<\/label>.*".html_safe 198 | end 199 | end 200 | 201 | it { is_expected.to be_html_safe } 202 | it { is_expected.to match(/label [^>]*for.+>Caption<\/label>.*").size).to eq(count) 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/helpers/dry_crud/table/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "DryCrud::Table::Builder" do 4 | include FormatHelper 5 | include UtilityHelper 6 | 7 | let(:entries) { %w[foo bahr] } 8 | let(:table) { DryCrud::Table::Builder.new(entries, self) } 9 | 10 | def format_size(obj) # :nodoc: 11 | "#{obj.size} chars" 12 | end 13 | 14 | specify "#html_header" do 15 | table.attrs :upcase, :size 16 | dom = "UpcaseSize" 17 | assert_dom_equal dom, table.send(:html_header) 18 | end 19 | 20 | specify "single attr row" do 21 | table.attrs :upcase, :size 22 | dom = "FOO3 chars" 23 | assert_dom_equal dom, table.send(:html_row, entries.first) 24 | end 25 | 26 | specify "custom row" do 27 | table.col("Header", class: "hula") { |e| "Weights #{e.size} kg" } 28 | dom = 'Weights 3 kg' 29 | assert_dom_equal dom, table.send(:html_row, entries.first) 30 | end 31 | 32 | context "attr col" do 33 | let(:col) { table.cols.first } 34 | 35 | context "output" do 36 | before { table.attrs :upcase } 37 | 38 | it { expect(col.html_header).to eq("Upcase") } 39 | it { expect(col.content("foo")).to eq("FOO") } 40 | it { expect(col.html_cell("foo")).to eq("FOO") } 41 | end 42 | 43 | context "content with custom format_size method" do 44 | before { table.attrs :size } 45 | 46 | it { expect(col.content("abcd")).to eq("4 chars") } 47 | end 48 | end 49 | 50 | specify "two x two table" do 51 | dom = <<-FIN 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
UpcaseSize
FOO3 chars
BAHR4 chars
61 | FIN 62 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 63 | 64 | table.attrs :upcase, :size 65 | 66 | assert_dom_equal dom, table.to_html 67 | end 68 | 69 | specify "table with before and after cells" do 70 | dom = <<-FIN 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
headUpcaseSize
fooFOO3 charsNever foo
bahrBAHR4 charsNever bahr
90 | FIN 91 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 92 | 93 | table.col("head", class: "left") { |e| link_to e, "/" } 94 | table.attrs :upcase, :size 95 | table.col { |e| "Never #{e}" } 96 | 97 | assert_dom_equal dom, table.to_html 98 | end 99 | 100 | specify "empty entries collection renders empty table" do 101 | dom = <<-FIN 102 | 103 | 104 | 105 | 106 | 107 | 108 |
headUpcaseSize
109 | FIN 110 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 111 | 112 | table = DryCrud::Table::Builder.new([], self) 113 | table.col("head", class: "left") { |e| link_to e, "/" } 114 | table.attrs :upcase, :size 115 | table.col { |e| "Never #{e}" } 116 | 117 | assert_dom_equal dom, table.to_html 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/helpers/form_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe FormHelper do 4 | include UtilityHelper 5 | include FormatHelper 6 | include I18nHelper 7 | include CrudTestHelper 8 | 9 | before(:all) do 10 | reset_db 11 | setup_db 12 | create_test_data 13 | end 14 | 15 | after(:all) { reset_db } 16 | 17 | describe "#plain_form" do 18 | subject do 19 | with_test_routing do 20 | capture do 21 | plain_form(entry, html: { class: "special" }) do |f| 22 | f.labeled_input_fields :name, :birthdate 23 | end 24 | end 25 | end 26 | end 27 | 28 | context "for existing entry" do 29 | let(:entry) { crud_test_models(:AAAAA) } 30 | 31 | it do 32 | is_expected.to match(/form .*?class="special\ form-horizontal" 33 | .*?action="\/crud_test_models\/#{entry.id}" 34 | .*?method="post"/x) 35 | end 36 | 37 | it do 38 | is_expected.to match(/input .*?type="hidden" 39 | .*?name="_method" 40 | .*?value="(put|patch)"/x) 41 | end 42 | 43 | it do 44 | is_expected.to match(/input .*?type="text" 45 | .*?value="AAAAA" 46 | .*?name="crud_test_model\[name\]"/x) 47 | end 48 | 49 | it do 50 | is_expected.to match(/input .*?value="1910-01-01" 51 | .*?type="date" 52 | .*?name="crud_test_model\[birthdate\]"/x) 53 | end 54 | end 55 | end 56 | 57 | describe "#standard_form" do 58 | subject do 59 | with_test_routing do 60 | capture do 61 | standard_form(entry, 62 | :name, :children, :birthdate, :human, 63 | cancel_url: "/somewhere", 64 | html: { class: "special" }) 65 | end 66 | end 67 | end 68 | 69 | context "for existing entry" do 70 | let(:entry) { crud_test_models(:AAAAA) } 71 | 72 | it do 73 | is_expected.to match(/form .*?class="special\ form-horizontal" 74 | .*?action="\/crud_test_models\/#{entry.id}" 75 | .*?method="post"/x) 76 | end 77 | 78 | it do 79 | is_expected.to match(/input .*?type="hidden" 80 | .*?name="_method" 81 | .*?value="(put|patch)"/x) 82 | end 83 | 84 | it do 85 | is_expected.to match(/input .*?type="text" 86 | .*?value="AAAAA" 87 | .*?name="crud_test_model\[name\]"/x) 88 | end 89 | 90 | it do 91 | is_expected.to match(/input .*?value="1910-01-01" 92 | .*?type="date" 93 | .*?name="crud_test_model\[birthdate\]"/x) 94 | end 95 | 96 | it do 97 | is_expected.to match(/input .*?type="number" 98 | .*?value="9" 99 | .*?name="crud_test_model\[children\]"/x) 100 | end 101 | 102 | it do 103 | is_expected.to match(/input .*?type="checkbox" 104 | .*?name="crud_test_model\[human\]"/x) 105 | end 106 | 107 | it do 108 | is_expected.to match(/button\ .*?type="submit".*> 109 | #{t('global.button.save')} 110 | <\/button>/x) 111 | end 112 | 113 | it do 114 | is_expected.to match(/a\ .*href="\/somewhere".*> 115 | #{t('global.button.cancel')} 116 | <\/a>/x) 117 | end 118 | end 119 | 120 | context "for invalid entry" do 121 | let(:entry) do 122 | e = crud_test_models(:AAAAA) 123 | e.name = nil 124 | e.valid? 125 | e 126 | end 127 | 128 | it do 129 | is_expected.to match(/div[^>]* id='error_explanation'/) 130 | end 131 | 132 | it do 133 | is_expected.to match(/div\ class="row\ mb-3">.*? 134 | 213 | #{t('global.button.cancel')}<\/a>/x) 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/helpers/i18n_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe I18nHelper do 4 | include CrudTestHelper 5 | 6 | describe "#translate_inheritable" do 7 | before { @controller = CrudTestModelsController.new } 8 | 9 | before do 10 | I18n.backend.store_translations( 11 | I18n.locale, 12 | global: { 13 | test_key: "global" 14 | } 15 | ) 16 | end 17 | subject { ti(:test_key) } 18 | 19 | it { is_expected.to eq("global") } 20 | 21 | context "with list key" do 22 | before do 23 | I18n.backend.store_translations( 24 | I18n.locale, 25 | list: { 26 | global: { 27 | test_key: "list global" 28 | } 29 | } 30 | ) 31 | end 32 | it { is_expected.to eq("list global") } 33 | 34 | context "and list action key" do 35 | before do 36 | I18n.backend.store_translations( 37 | I18n.locale, 38 | list: { 39 | index: { 40 | test_key: "list index" 41 | } 42 | } 43 | ) 44 | end 45 | it { is_expected.to eq("list index") } 46 | 47 | context "and crud global key" do 48 | before do 49 | I18n.backend.store_translations( 50 | I18n.locale, 51 | crud: { 52 | global: { 53 | test_key: "crud global" 54 | } 55 | } 56 | ) 57 | end 58 | it { is_expected.to eq("crud global") } 59 | 60 | context "and crud action key" do 61 | before do 62 | I18n.backend.store_translations( 63 | I18n.locale, 64 | crud: { 65 | index: { 66 | test_key: "crud index" 67 | } 68 | } 69 | ) 70 | end 71 | it { is_expected.to eq("crud index") } 72 | 73 | context "and controller global key" do 74 | before do 75 | I18n.backend.store_translations( 76 | I18n.locale, 77 | crud_test_models: { 78 | global: { 79 | test_key: "test global" 80 | } 81 | } 82 | ) 83 | end 84 | it { is_expected.to eq("test global") } 85 | 86 | context "and controller action key" do 87 | before do 88 | I18n.backend.store_translations( 89 | I18n.locale, 90 | crud_test_models: { 91 | index: { 92 | test_key: "test index" 93 | } 94 | } 95 | ) 96 | end 97 | it { is_expected.to eq("test index") } 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | 106 | describe "#translate_association" do 107 | let(:assoc) { CrudTestModel.reflect_on_association(:companion) } 108 | subject { ta(:test_key, assoc) } 109 | 110 | before do 111 | I18n.backend.store_translations( 112 | I18n.locale, 113 | global: { 114 | associations: { 115 | test_key: "global" 116 | } 117 | } 118 | ) 119 | end 120 | it { is_expected.to eq("global") } 121 | 122 | context "with model key" do 123 | before do 124 | I18n.backend.store_translations( 125 | I18n.locale, 126 | activerecord: { 127 | associations: { 128 | crud_test_model: { 129 | test_key: "model" 130 | } 131 | } 132 | } 133 | ) 134 | end 135 | 136 | it { is_expected.to eq("model") } 137 | 138 | context "and assoc key" do 139 | before do 140 | I18n.backend.store_translations( 141 | I18n.locale, 142 | activerecord: { 143 | associations: { 144 | models: { 145 | crud_test_model: { 146 | companion: { 147 | test_key: "companion" 148 | } 149 | } 150 | } 151 | } 152 | } 153 | ) 154 | end 155 | 156 | it { is_expected.to eq("companion") } 157 | it "uses global without assoc" do 158 | expect(ta(:test_key)).to eq("global") 159 | end 160 | end 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/helpers/utility_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe UtilityHelper do 4 | include CrudTestHelper 5 | 6 | before(:all) do 7 | reset_db 8 | setup_db 9 | create_test_data 10 | end 11 | 12 | after(:all) { reset_db } 13 | 14 | describe "#column_type" do 15 | let(:model) { crud_test_models(:AAAAA) } 16 | 17 | it "recognizes types" do 18 | expect(column_type(model, :name)).to eq(:string) 19 | expect(column_type(model, :children)).to eq(:integer) 20 | expect(column_type(model, :companion_id)).to eq(:integer) 21 | expect(column_type(model, :rating)).to eq(:float) 22 | expect(column_type(model, :income)).to eq(:decimal) 23 | expect(column_type(model, :birthdate)).to eq(:date) 24 | expect(column_type(model, :gets_up_at)).to eq(:time) 25 | expect(column_type(model, :last_seen)).to eq(:datetime) 26 | expect(column_type(model, :human)).to eq(:boolean) 27 | expect(column_type(model, :remarks)).to eq(:text) 28 | expect(column_type(model, :companion)).to be_nil 29 | end 30 | end 31 | 32 | describe "#content_tag_nested" do 33 | it "escapes safe content" do 34 | html = content_tag_nested(:div, %w[a b]) { |e| tag.span(e) } 35 | expect(html).to be_html_safe 36 | expect(html).to eq("
ab
") 37 | end 38 | 39 | it "escapes unsafe content" do 40 | html = content_tag_nested(:div, %w[a b]) { |e| "<#{e}>" } 41 | expect(html).to eq("
<a><b>
") 42 | end 43 | 44 | it "simplys join without block" do 45 | html = content_tag_nested(:div, %w[a b]) 46 | expect(html).to eq("
ab
") 47 | end 48 | end 49 | 50 | describe "#safe_join" do 51 | it "works as super without block" do 52 | html = safe_join([ "", "".html_safe ]) 53 | expect(html).to eq("<a>") 54 | end 55 | 56 | it "collects contents for array" do 57 | html = safe_join(%w[a b]) { |e| tag.span(e) } 58 | expect(html).to eq("ab") 59 | end 60 | end 61 | 62 | describe "#default_crud_attrs" do 63 | it "do not contain id and password" do 64 | expect(default_crud_attrs).to eq( 65 | %i[name email whatever children companion_id rating income 66 | birthdate gets_up_at last_seen human remarks 67 | created_at updated_at] 68 | ) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/spec/support/crud_controller_test_helper.rb: -------------------------------------------------------------------------------- 1 | # Contains assertions for testing common crud controller use cases. 2 | # See crud_controller_examples for use cases. 3 | module CrudControllerTestHelper 4 | extend ActiveSupport::Concern 5 | 6 | # Performs a request based on the metadata of the action example under test. 7 | def perform_request # rubocop:disable Metrics/AbcSize 8 | m = RSpec.current_example.metadata 9 | example_params = respond_to?(:params) ? send(:params) : {} 10 | params = scope_params.dup 11 | params[:format] = m[:format] if m[:format] 12 | params[:id] = test_entry.id if m[:id] 13 | params.merge!(example_params) 14 | if m[:method] == :get && m[:format] == :js 15 | get m[:action], params: params, xhr: true 16 | else 17 | send(m[:method], m[:action], params: params) 18 | end 19 | end 20 | 21 | # If a combine key is given in metadata, only the first request for all 22 | # examples with the same key will be performed. 23 | def perform_combined_request 24 | stack = RSpec.current_example.metadata[:combine] 25 | if stack 26 | @@current_stack ||= nil 27 | if stack == @@current_stack && 28 | described_class == @@current_controller.class 29 | restore_request 30 | else 31 | perform_request 32 | @@current_stack = stack 33 | remember_request 34 | end 35 | else 36 | perform_request 37 | end 38 | end 39 | 40 | def remember_request 41 | @@current_response = @response 42 | @@current_request = @request 43 | @@current_controller = @controller 44 | @@current_templates = @_templates || @templates 45 | 46 | # treat in-memory entry as committed in order to 47 | # avoid rollback of internal state. 48 | entry&.committed! 49 | end 50 | 51 | def restore_request 52 | @response = @@current_response 53 | @_templates = @templates = @@current_templates 54 | @controller = @@current_controller 55 | @request = @@current_request 56 | end 57 | 58 | def ivar(name) 59 | controller.instance_variable_get(:"@#{name}") 60 | end 61 | 62 | # The params defining the nesting of the test entry. 63 | def scope_params 64 | params = {} 65 | # for nested controllers, add parent ids to each request 66 | Array(controller.nesting).reverse.reduce(test_entry) do |parent, p| 67 | if p.is_a?(Class) && p < ActiveRecord::Base 68 | assoc = p.name.underscore 69 | params["#{assoc}_id"] = parent.send(:"#{assoc}_id") 70 | parent.send(assoc) 71 | else 72 | parent 73 | end 74 | end 75 | params 76 | end 77 | 78 | # Helper methods to describe contexts. 79 | module ClassMethods 80 | # Describe a certain action and provide some usefull metadata. 81 | # Tests whether this action is configured to be skipped. 82 | def describe_action(method, action, metadata = {}, &block) 83 | action_defined = described_class.instance_methods 84 | .map(&:to_s) 85 | .include?(action.to_s) 86 | describe("#{method.to_s.upcase} #{action}", 87 | { if: action_defined, 88 | method: method, 89 | action: action }.merge(metadata), 90 | &block) 91 | end 92 | 93 | # Is the current context part of the skip list. 94 | def skip?(options, *contexts) 95 | options ||= {} 96 | contexts = Array(contexts).flatten 97 | skips = Array(options[:skip]) 98 | skips = [ skips ] if skips.blank? || !skips.first.is_a?(Array) 99 | 100 | skips.flatten.present? && 101 | skips.any? { |skip| skip == contexts.take(skip.size) } 102 | end 103 | 104 | # Test the response status, default 200. 105 | def it_is_expected_to_respond(status = 200) 106 | it { expect(response.status).to eq(status) } 107 | end 108 | 109 | # Test that a json response is rendered. 110 | def it_is_expected_to_render_json 111 | it { expect(response.body).to start_with("{") } 112 | end 113 | 114 | # Test that test_entry_attrs are set on entry. 115 | def it_is_expected_to_set_attrs(action = nil) 116 | it "sets params as entry attributes" do 117 | attrs = send(:"#{action}_entry_attrs") 118 | actual = {} 119 | attrs.each_key do |key| 120 | actual[key] = entry.attributes[key.to_s] 121 | end 122 | expect(actual).to eq(attrs) 123 | end 124 | end 125 | 126 | # Test that the response redirects to the index action. 127 | def it_is_expected_to_redirect_to_index 128 | it do 129 | is_expected.to redirect_to scope_params.merge(action: "index", 130 | id: nil, 131 | returning: true) 132 | end 133 | end 134 | 135 | # Test that the response redirects to the show action of the current entry. 136 | def it_is_expected_to_redirect_to_show 137 | it do 138 | is_expected.to redirect_to scope_params.merge(action: "show", 139 | id: entry.id) 140 | end 141 | end 142 | 143 | # Test that the given flash type is present. 144 | def it_is_expected_to_have_flash(type, message = nil) 145 | it "flash(#{type}) is set" do 146 | expect(flash[type]).to(message ? match(message) : be_present) 147 | end 148 | end 149 | 150 | # Test that not flash of the given type is present. 151 | def it_is_expected_to_not_have_flash(type) 152 | it "flash(#{type}) is nil" do 153 | expect(flash[type]).to be_blank 154 | end 155 | end 156 | 157 | # Test that the current entry is persistend and valid, or not. 158 | def it_is_expected_to_persist_entry(persist: true) 159 | context "entry" do 160 | subject { entry } 161 | 162 | if persist 163 | it { is_expected.not_to be_new_record } 164 | it { is_expected.to be_valid } 165 | else 166 | it { is_expected.to be_new_record } 167 | end 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/custom_assertions_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/custom_assertions" 3 | require "support/crud_test_helper" 4 | require "support/crud_test_model" 5 | 6 | # Test CustomAssertions 7 | class CustomAssertionsTest < ActiveSupport::TestCase 8 | include CustomAssertions 9 | include CrudTestHelper 10 | 11 | setup :reset_db, :setup_db, :create_test_data 12 | teardown :reset_db 13 | 14 | test "assert count succeeds if count matches" do 15 | assert_nothing_raised do 16 | assert_count 3, "ba", "barbabapa" 17 | end 18 | end 19 | 20 | test "assert count succeeds if count is zero" do 21 | assert_nothing_raised do 22 | assert_count 0, "bo", "barbabapa" 23 | end 24 | end 25 | 26 | test "assert count fails if count does not match" do 27 | assert_raise(Minitest::Assertion) do 28 | assert_count 2, "ba", "barbabapa" 29 | end 30 | end 31 | 32 | test "assert valid record succeeds" do 33 | assert_nothing_raised do 34 | assert_valid crud_test_models("AAAAA") 35 | end 36 | end 37 | 38 | test "assert valid record fails for invalid" do 39 | assert_raise(Minitest::Assertion) do 40 | assert_valid invalid_record 41 | end 42 | end 43 | 44 | test "assert not valid succeeds if record invalid" do 45 | assert_nothing_raised do 46 | assert_not_valid invalid_record 47 | end 48 | end 49 | 50 | test "assert not valid succeds if record invalid and invalid attrs given" do 51 | assert_nothing_raised do 52 | assert_not_valid invalid_record, :name, :rating 53 | end 54 | end 55 | 56 | test "assert not valid fails if record valid" do 57 | assert_raise(Minitest::Assertion) do 58 | assert_not_valid crud_test_models("AAAAA") 59 | end 60 | end 61 | 62 | test "assert not valid fails if record invalid and valid attrs given" do 63 | assert_raise(Minitest::Assertion) do 64 | assert_not_valid invalid_record, :name, :rating, :children 65 | end 66 | end 67 | 68 | test "assert not valid fails if not all invalid attrs given" do 69 | assert_raise(Minitest::Assertion) do 70 | assert_not_valid invalid_record, :name 71 | end 72 | end 73 | 74 | private 75 | 76 | def invalid_record 77 | m = crud_test_models("AAAAA") 78 | m.name = nil 79 | m.rating = 42 80 | m 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/dry_crud/table/builder_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module DryCrud 4 | module Table 5 | # Test DryCrud::Table::Builder 6 | class BuilderTest < ActionView::TestCase 7 | # set dummy helper class for ActionView::TestCase 8 | self.helper_class = UtilityHelper 9 | 10 | include FormatHelper 11 | 12 | attr_reader :table, :entries 13 | 14 | def setup 15 | @entries = %w[foo bahr] 16 | @table = DryCrud::Table::Builder.new(entries, self) 17 | end 18 | 19 | def format_size(obj) 20 | "#{obj.size} chars" 21 | end 22 | 23 | test "html header" do 24 | table.attrs :upcase, :size 25 | 26 | dom = "UpcaseSize" 27 | 28 | assert_dom_equal dom, table.send(:html_header) 29 | end 30 | 31 | test "single attr row" do 32 | table.attrs :upcase, :size 33 | 34 | dom = "FOO3 chars" 35 | 36 | assert_dom_equal dom, table.send(:html_row, entries.first) 37 | end 38 | 39 | test "custom row" do 40 | table.col("Header", class: "hula") { |e| "Weights #{e.size} kg" } 41 | 42 | dom = 'Weights 3 kg' 43 | 44 | assert_dom_equal dom, table.send(:html_row, entries.first) 45 | end 46 | 47 | test "attr col output" do 48 | table.attrs :upcase 49 | col = table.cols.first 50 | 51 | assert_equal "Upcase", col.html_header 52 | assert_equal "FOO", col.content("foo") 53 | assert_equal "FOO", col.html_cell("foo") 54 | end 55 | 56 | test "attr col content with custom format_size method" do 57 | table.attrs :size 58 | col = table.cols.first 59 | 60 | assert_equal "4 chars", col.content("abcd") 61 | end 62 | 63 | test "two x two table" do 64 | dom = <<-FIN 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
UpcaseSize
FOO3 chars
BAHR4 chars
74 | FIN 75 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 76 | 77 | table.attrs :upcase, :size 78 | 79 | assert_dom_equal dom, table.to_html 80 | end 81 | 82 | test "table with before and after cells" do 83 | dom = <<-FIN 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
headUpcaseSize
fooFOO3 charsNever foo
bahrBAHR4 charsNever bahr
107 | FIN 108 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 109 | 110 | table.col("head", class: "left") { |e| link_to e, "/" } 111 | table.attrs :upcase, :size 112 | table.col { |e| "Never #{e}" } 113 | 114 | assert_dom_equal dom, table.to_html 115 | end 116 | 117 | test "empty entries collection renders empty table" do 118 | dom = <<-FIN 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
headUpcaseSize
131 | FIN 132 | dom.gsub!(/[\n\t]/, "").gsub!(/\s{2,}/, "") 133 | 134 | table = DryCrud::Table::Builder.new([], self) 135 | table.col("head", class: "left") { |e| link_to e, "/" } 136 | table.attrs :upcase, :size 137 | table.col { |e| "Never #{e}" } 138 | 139 | assert_dom_equal dom, table.to_html 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/form_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_test_model" 3 | require "support/crud_test_helper" 4 | 5 | # Test FormHelper 6 | class FormHelperTest < ActionView::TestCase 7 | include UtilityHelper 8 | include FormatHelper 9 | include I18nHelper 10 | include CrudTestHelper 11 | 12 | setup :reset_db, :setup_db, :create_test_data 13 | teardown :reset_db 14 | 15 | test "plain form for existing entry" do 16 | e = crud_test_models("AAAAA") 17 | f = with_test_routing do 18 | capture do 19 | plain_form(e, html: { class: "special" }) do |form| 20 | form.labeled_input_fields :name, :birthdate 21 | end 22 | end 23 | end 24 | 25 | assert_match(/form .*?class="special\ form-horizontal" 26 | .*?action="\/crud_test_models\/#{e.id}" 27 | .*?method="post"/x, f) 28 | assert_match(/input .*?type="hidden" 29 | .*?name="_method" 30 | .*?value="(patch|put)"/x, f) 31 | assert_match(/input .*?type="text" 32 | .*?value="AAAAA" 33 | .*?name="crud_test_model\[name\]"/x, f) 34 | end 35 | 36 | test "standard form" do 37 | e = crud_test_models("AAAAA") 38 | f = with_test_routing do 39 | capture do 40 | standard_form(e, 41 | :name, :children, :birthdate, :human, 42 | cancel_url: "/somewhere", 43 | html: { class: "special" }) 44 | end 45 | end 46 | 47 | assert_match(/form .*?action="\/crud_test_models\/#{e.id}" 48 | .*?method="post"/x, f) 49 | assert_match(/form .*?class="special\ form-horizontal"/x, f) 50 | assert_match(/input .*?type="hidden" 51 | .*?name="_method" 52 | .*?value="(patch|put)"/x, f) 53 | assert_match(/input .*?type="text" 54 | .*?value="AAAAA" 55 | .*?name="crud_test_model\[name\]"/x, f) 56 | assert_match(/input .*?type="date" 57 | .*?name="crud_test_model\[birthdate\]"/x, f) 58 | assert_match(/input .*?type="number" 59 | .*?value="9" 60 | .*?name="crud_test_model\[children\]"/x, f) 61 | assert_match(/input .*?type="checkbox" 62 | .*?name="crud_test_model\[human\]"/x, f) 63 | assert_match(/button\ .*?type="submit".*> 64 | #{t('global.button.save')} 65 | <\/button>/x, f) 66 | assert_match(/ 67 | #{t('global.button.cancel')} 68 | <\/a>/x, f) 69 | end 70 | 71 | test "standard form with errors" do 72 | e = crud_test_models("AAAAA") 73 | e.name = nil 74 | assert_not e.valid? 75 | 76 | f = with_test_routing do 77 | capture do 78 | standard_form(e) do |form| 79 | form.labeled_input_fields(:name, :birthdate) 80 | end 81 | end 82 | end 83 | 84 | assert_match(/form .*?action="\/crud_test_models\/#{e.id}" 85 | .*?method="post"/x, f) 86 | assert_match(/input .*?type="hidden" 87 | .*?name="_method" 88 | .*?value="(patch|put)"/x, f) 89 | assert_match(/div[^>]* id='error_explanation'/, f) 90 | assert_match(/input .*?class="is-invalid\ form-control" 91 | .*?type="text" 92 | .*?name="crud_test_model\[name\]"/x, f) 93 | assert_match(/input .*?value="1910-01-01" 94 | .*?type="date" 95 | .*?name="crud_test_model\[birthdate\]"/x, f) 96 | end 97 | 98 | test "crud form" do 99 | f = with_test_routing do 100 | capture { crud_form } 101 | end 102 | 103 | assert_match(/form .*?action="\/crud_test_models\/#{entry.id}"/, f) 104 | assert_match(/input .*?type="text" 105 | .*?name="crud_test_model\[name\]"/x, f) 106 | assert_match(/input .*?type="text" 107 | .*?name="crud_test_model\[whatever\]"/x, f) 108 | assert_match(/input .*?type="number" 109 | .*?name="crud_test_model\[children\]"/x, f) 110 | assert_match(/input .*?type="number" 111 | .*?name="crud_test_model\[rating\]"/x, f) 112 | assert_match(/input .*?type="number" 113 | .*?name="crud_test_model\[income\]"/x, f) 114 | assert_match(/input .*?type="date" 115 | .*?name="crud_test_model\[birthdate\]"/x, f) 116 | assert_match(/input .*?type="time" 117 | .*?name="crud_test_model\[gets_up_at\]"/x, f) 118 | assert_match(/input .*?type="datetime-local" 119 | .*?name="crud_test_model\[last_seen\]"/x, f) 120 | assert_match(/input .*?type="checkbox" 121 | .*?name="crud_test_model\[human\]"/x, f) 122 | assert_match(/select .*?name="crud_test_model\[companion_id\]"/, f) 123 | assert_match(/textarea .*?name="crud_test_model\[remarks\]"/, f) 124 | assert_match(/a .*href="\/crud_test_models\/#{entry.id}\?returning=true" 125 | .*>#{t('global.button.cancel')}<\/a>/x, f) 126 | end 127 | 128 | def entry 129 | @entry ||= CrudTestModel.first 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/format_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_test_model" 3 | 4 | # Test FormatHelper 5 | class FormatHelperTest < ActionView::TestCase 6 | include UtilityHelper 7 | include I18nHelper 8 | include CrudTestHelper 9 | 10 | setup :reset_db, :setup_db, :create_test_data 11 | teardown :reset_db 12 | 13 | def format_size(obj) 14 | "#{f(obj.size)} items" 15 | end 16 | 17 | def format_string_size(obj) 18 | "#{f(obj.size)} chars" 19 | end 20 | 21 | test "labeled text as block" do 22 | result = labeled("label") { "value" } 23 | 24 | assert result.html_safe? 25 | assert_dom_equal "
label
" \ 26 | "
value
", 27 | result.squish 28 | end 29 | 30 | test "labeled text empty" do 31 | result = labeled("label", "") 32 | 33 | assert result.html_safe? 34 | assert_dom_equal "
label
" \ 35 | "
#{EMPTY_STRING}
", 36 | result.squish 37 | end 38 | 39 | test "labeled text as content" do 40 | result = labeled("label", "value ") 41 | 42 | assert result.html_safe? 43 | assert_dom_equal "
label
" \ 44 | "
value <unsafe>
", 45 | result.squish 46 | end 47 | 48 | test "labeled attr" do 49 | result = labeled_attr("foo", :size) 50 | assert result.html_safe? 51 | assert_dom_equal "
Size
" \ 52 | "
3 chars
", 53 | result.squish 54 | end 55 | 56 | test "format nil" do 57 | assert EMPTY_STRING.html_safe? 58 | assert_equal EMPTY_STRING, f(nil) 59 | end 60 | 61 | test "format Strings" do 62 | assert_equal "blah blah", f("blah blah") 63 | assert_equal "", f("") 64 | assert_not f("").html_safe? 65 | end 66 | 67 | unless ENV["NON_LOCALIZED"] # localization dependent tests 68 | test "format Floats" do 69 | assert_equal "1.000", f(1.0) 70 | assert_equal "1.200", f(1.2) 71 | assert_equal "3.142", f(3.14159) 72 | end 73 | 74 | test "format Booleans" do 75 | assert_equal "yes", f(true) 76 | assert_equal "no", f(false) 77 | end 78 | 79 | test "format attr with fallthrough to f" do 80 | assert_equal "12.234", format_attr("12.23424", :to_f) 81 | end 82 | end 83 | 84 | test "format attr with custom format_string_size method" do 85 | assert_equal "4 chars", format_attr("abcd", :size) 86 | end 87 | 88 | test "format attr with custom format_size method" do 89 | assert_equal "2 items", format_attr([ 1, 2 ], :size) 90 | end 91 | 92 | test "format integer column" do 93 | m = crud_test_models(:AAAAA) 94 | assert_equal "9", format_type(m, :children) 95 | 96 | m.children = 10_000 97 | assert_equal "10,000", format_type(m, :children) 98 | end 99 | 100 | unless ENV["NON_LOCALIZED"] # localization dependent tests 101 | test "format float column" do 102 | m = crud_test_models(:AAAAA) 103 | assert_equal "1.100", format_type(m, :rating) 104 | 105 | m.rating = 3.145001 # you never know with these floats.. 106 | assert_equal "3.145", format_type(m, :rating) 107 | end 108 | 109 | test "format decimal column" do 110 | m = crud_test_models(:AAAAA) 111 | assert_equal "10,000,000.1111", format_type(m, :income) 112 | end 113 | 114 | test "format date column" do 115 | m = crud_test_models(:AAAAA) 116 | assert_equal "1910-01-01", format_type(m, :birthdate) 117 | end 118 | 119 | test "format datetime column" do 120 | m = crud_test_models(:AAAAA) 121 | assert_equal "2010-01-01 11:21", format_type(m, :last_seen) 122 | end 123 | end 124 | 125 | test "format time column" do 126 | m = crud_test_models(:AAAAA) 127 | assert_equal "01:01", format_type(m, :gets_up_at) 128 | end 129 | 130 | test "format text column" do 131 | m = crud_test_models(:AAAAA) 132 | assert_equal "

AAAAA BBBBB CCCCC\n
AAAAA BBBBB CCCCC\n

", 133 | format_type(m, :remarks) 134 | assert format_type(m, :remarks).html_safe? 135 | end 136 | 137 | test "format boolean false column" do 138 | m = crud_test_models(:AAAAA) 139 | m.human = false 140 | assert_equal "no", format_type(m, :human) 141 | end 142 | 143 | test "format boolean true column" do 144 | m = crud_test_models(:AAAAA) 145 | m.human = true 146 | assert_equal "yes", format_type(m, :human) 147 | end 148 | 149 | test "format belongs to column without content" do 150 | m = crud_test_models(:AAAAA) 151 | assert_equal t("global.associations.no_entry"), 152 | format_attr(m, :companion) 153 | end 154 | 155 | test "format belongs to column with content" do 156 | m = crud_test_models(:BBBBB) 157 | assert_equal "AAAAA", format_attr(m, :companion) 158 | end 159 | 160 | test "format has one without content" do 161 | m = crud_test_models(:FFFFF) 162 | assert_equal t("global.associations.no_entry"), 163 | format_attr(m, :comrad) 164 | end 165 | 166 | test "format has one with content" do 167 | m = crud_test_models(:AAAAA) 168 | assert_equal "BBBBB", format_attr(m, :comrad) 169 | end 170 | 171 | test "format has_many column with content" do 172 | m = crud_test_models(:CCCCC) 173 | assert_equal "
  • AAAAA
  • BBBBB
", 174 | format_attr(m, :others) 175 | end 176 | 177 | test "captionize" do 178 | assert_equal "Camel Case", captionize(:camel_case) 179 | assert_equal "All Upper Case", captionize("all upper case") 180 | assert_equal "With Object", captionize("With object", Object.new) 181 | assert_not captionize("bad ").html_safe? 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/i18n_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_test_model" 3 | require "support/crud_test_models_controller" 4 | 5 | # Test I18nHelper 6 | class I18nHelperTest < ActionView::TestCase 7 | include CrudTestHelper 8 | 9 | test "translate inheritable lookup" do 10 | # current controller is :crud_test_models, action is :index 11 | @controller = CrudTestModelsController.new 12 | 13 | I18n.backend.store_translations( 14 | I18n.locale, 15 | global: { test_key: "global" } 16 | ) 17 | assert_equal "global", ti(:test_key) 18 | 19 | I18n.backend.store_translations( 20 | I18n.locale, 21 | list: { global: { test_key: "list global" } } 22 | ) 23 | assert_equal "list global", ti(:test_key) 24 | 25 | I18n.backend.store_translations( 26 | I18n.locale, 27 | list: { index: { test_key: "list index" } } 28 | ) 29 | assert_equal "list index", ti(:test_key) 30 | 31 | I18n.backend.store_translations( 32 | I18n.locale, 33 | crud: { global: { test_key: "crud global" } } 34 | ) 35 | assert_equal "crud global", ti(:test_key) 36 | 37 | I18n.backend.store_translations( 38 | I18n.locale, 39 | crud: { index: { test_key: "crud index" } } 40 | ) 41 | assert_equal "crud index", ti(:test_key) 42 | 43 | I18n.backend.store_translations( 44 | I18n.locale, 45 | crud_test_models: { global: { test_key: "test global" } } 46 | ) 47 | assert_equal "test global", ti(:test_key) 48 | 49 | I18n.backend.store_translations( 50 | I18n.locale, 51 | crud_test_models: { index: { test_key: "test index" } } 52 | ) 53 | assert_equal "test index", ti(:test_key) 54 | end 55 | 56 | test "translate association lookup" do 57 | assoc = CrudTestModel.reflect_on_association(:companion) 58 | 59 | I18n.backend.store_translations( 60 | I18n.locale, 61 | global: { associations: { test_key: "global" } } 62 | ) 63 | assert_equal "global", ta(:test_key, assoc) 64 | 65 | I18n.backend.store_translations( 66 | I18n.locale, 67 | activerecord: { 68 | associations: { 69 | crud_test_model: { 70 | test_key: "model" 71 | } 72 | } 73 | } 74 | ) 75 | assert_equal "model", ta(:test_key, assoc) 76 | 77 | I18n.backend.store_translations( 78 | I18n.locale, 79 | activerecord: { 80 | associations: { 81 | models: { 82 | crud_test_model: { 83 | companion: { 84 | test_key: "companion" 85 | } 86 | } 87 | } 88 | } 89 | } 90 | ) 91 | assert_equal "companion", ta(:test_key, assoc) 92 | 93 | assert_equal "global", ta(:test_key) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/table_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/custom_assertions" 3 | require "support/crud_test_model" 4 | 5 | # Test TableHelper 6 | class TableHelperTest < ActionView::TestCase 7 | include UtilityHelper 8 | include FormatHelper 9 | include I18nHelper 10 | include CustomAssertions 11 | include CrudTestHelper 12 | 13 | attr_accessor :params 14 | 15 | setup :reset_db, :setup_db, :create_test_data, :empty_params 16 | teardown :reset_db 17 | 18 | attr_reader :entries 19 | 20 | def format_size(obj) 21 | "#{f(obj.size)} items" 22 | end 23 | 24 | def format_string_size(obj) 25 | "#{f(obj.size)} chars" 26 | end 27 | 28 | def empty_params 29 | @params = {} 30 | end 31 | 32 | test "empty table renders message" do 33 | result = plain_table_or_message([]) 34 | assert result.html_safe? 35 | assert_match(/<div class=["']table["']>.*<\/div>/, result) 36 | end 37 | 38 | test "non empty table renders table" do 39 | result = plain_table_or_message(%w[foo bar]) do |t| 40 | t.attrs :size, :upcase 41 | end 42 | assert result.html_safe? 43 | assert_match(/^<table.*<\/table>$/, result) 44 | end 45 | 46 | test "table with attrs" do 47 | expected = DryCrud::Table::Builder.table( 48 | %w[foo bar], self, 49 | class: "table table-striped table-hover" 50 | ) do |t| 51 | t.attrs :size, :upcase 52 | end 53 | actual = plain_table(%w[foo bar], :size, :upcase) 54 | assert actual.html_safe? 55 | assert_equal expected, actual 56 | end 57 | 58 | test "standard list table" do 59 | @entries = CrudTestModel.all 60 | 61 | table = with_test_routing do 62 | list_table 63 | end 64 | 65 | assert_count 7, REGEXP_ROWS, table 66 | assert_count 14, REGEXP_SORT_HEADERS, table 67 | end 68 | 69 | test "custom list table with attributes" do 70 | @entries = CrudTestModel.all 71 | 72 | table = with_test_routing do 73 | list_table :name, :children, :companion_id 74 | end 75 | 76 | assert_count 7, REGEXP_ROWS, table 77 | assert_count 3, REGEXP_SORT_HEADERS, table 78 | end 79 | 80 | test "custom list table with block" do 81 | @entries = CrudTestModel.all 82 | 83 | table = with_test_routing do 84 | list_table do |t| 85 | t.attrs :name, :children, :companion_id 86 | t.col("head") { |e| tag.span(e.income.to_s) } 87 | end 88 | end 89 | 90 | assert_count 7, REGEXP_ROWS, table 91 | assert_count 4, REGEXP_HEADERS, table 92 | assert_count 0, REGEXP_SORT_HEADERS, table 93 | assert_count 6, /<span>.+?<\/span>/, table 94 | end 95 | 96 | test "custom list table with attributes and block" do 97 | @entries = CrudTestModel.all 98 | 99 | table = with_test_routing do 100 | list_table :name, :children, :companion_id do |t| 101 | t.col("head") { |e| tag.span(e.income.to_s) } 102 | end 103 | end 104 | 105 | assert_count 7, REGEXP_ROWS, table 106 | assert_count 3, REGEXP_SORT_HEADERS, table 107 | assert_count 4, REGEXP_HEADERS, table 108 | assert_count 6, /<span>.+?<\/span>/, table 109 | end 110 | 111 | test "standard list table with ascending sort params" do 112 | @params = { sort: "children", sort_dir: "asc" } 113 | @entries = CrudTestModel.all 114 | 115 | table = with_test_routing do 116 | list_table 117 | end 118 | 119 | sort_header_desc = %r{<th><a .*?sort_dir=desc.*?>Children</a> ↓</th>} 120 | assert_count 7, REGEXP_ROWS, table 121 | assert_count 13, REGEXP_SORT_HEADERS, table 122 | assert_count 1, sort_header_desc, table 123 | end 124 | 125 | test "standard list table with descending sort params" do 126 | @params = { sort: "children", sort_dir: "desc" } 127 | @entries = CrudTestModel.all 128 | 129 | table = with_test_routing do 130 | list_table 131 | end 132 | 133 | sort_header_asc = %r{<th><a .*?sort_dir=asc.*?>Children</a> ↑</th>} 134 | assert_count 7, REGEXP_ROWS, table 135 | assert_count 13, REGEXP_SORT_HEADERS, table 136 | assert_count 1, sort_header_asc, table 137 | end 138 | 139 | test "list table with custom column sort params" do 140 | @params = { sort: "chatty", sort_dir: "asc" } 141 | @entries = CrudTestModel.all 142 | 143 | table = with_test_routing do 144 | list_table :name, :children, :chatty 145 | end 146 | 147 | sort_header_desc = %r{<th><a .*?sort_dir=desc.*?>Chatty</a> ↓</th>} 148 | assert_count 7, REGEXP_ROWS, table 149 | assert_count 2, REGEXP_SORT_HEADERS, table 150 | assert_count 1, sort_header_desc, table 151 | end 152 | 153 | test "standard crud table" do 154 | @entries = CrudTestModel.all 155 | 156 | table = with_test_routing do 157 | crud_table 158 | end 159 | 160 | assert_count 7, REGEXP_ROWS, table 161 | assert_count 14, REGEXP_SORT_HEADERS, table 162 | assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links 163 | end 164 | 165 | test "custom crud table with attributes" do 166 | @entries = CrudTestModel.all 167 | 168 | table = with_test_routing do 169 | crud_table :name, :children, :companion_id 170 | end 171 | 172 | assert_count 7, REGEXP_ROWS, table 173 | assert_count 3, REGEXP_SORT_HEADERS, table 174 | assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links 175 | end 176 | 177 | test "custom crud table with block" do 178 | @entries = CrudTestModel.all 179 | 180 | table = with_test_routing do 181 | crud_table do |t| 182 | t.attrs :name, :children, :companion_id 183 | t.col("head") { |e| tag.span(e.income.to_s) } 184 | end 185 | end 186 | 187 | assert_count 7, REGEXP_ROWS, table 188 | assert_count 6, REGEXP_HEADERS, table 189 | assert_count 6, /<span>.+?<\/span>/m, table 190 | assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links 191 | end 192 | 193 | test "custom crud table with attributes and block" do 194 | @entries = CrudTestModel.all 195 | 196 | table = with_test_routing do 197 | crud_table :name, :children, :companion_id do |t| 198 | t.col("head") { |e| tag.span(e.income.to_s) } 199 | end 200 | end 201 | 202 | assert_count 7, REGEXP_ROWS, table 203 | assert_count 3, REGEXP_SORT_HEADERS, table 204 | assert_count 6, REGEXP_HEADERS, table 205 | assert_count 6, /<span>.+?<\/span>/m, table 206 | assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links 207 | end 208 | 209 | def entry 210 | @entry ||= CrudTestModel.first 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/helpers/utility_helper_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_test_model" 3 | 4 | # Test UtilityHelper 5 | class UtilityHelperTest < ActionView::TestCase 6 | include CrudTestHelper 7 | 8 | setup :reset_db, :setup_db, :create_test_data 9 | teardown :reset_db 10 | 11 | test "content_tag_nested escapes safe correctly" do 12 | html = content_tag_nested(:div, %w[a b]) { |e| tag.span(e) } 13 | assert_equal "<div><span>a</span><span>b</span></div>", html 14 | end 15 | 16 | test "content_tag_nested escapes unsafe correctly" do 17 | html = content_tag_nested(:div, %w[a b]) { |e| "<#{e}>" } 18 | assert_equal "<div><a><b></div>", html 19 | end 20 | 21 | test "content_tag_nested without block" do 22 | html = content_tag_nested(:div, %w[a b]) 23 | assert_equal "<div>ab</div>", html 24 | end 25 | 26 | test "safe_join without block" do 27 | html = safe_join([ "<a>", "<b>".html_safe ]) 28 | assert_equal "<a><b>", html 29 | end 30 | 31 | test "safe_join with block" do 32 | html = safe_join(%w[a b]) { |e| tag.span(e) } 33 | assert_equal "<span>a</span><span>b</span>", html 34 | end 35 | 36 | test "default attributes do not include id and password" do 37 | assert_equal %i[name email whatever children companion_id rating 38 | income birthdate gets_up_at last_seen human 39 | remarks created_at updated_at], 40 | default_crud_attrs 41 | end 42 | 43 | test "column types" do 44 | m = crud_test_models(:AAAAA) 45 | assert_equal :string, column_type(m, :name) 46 | assert_equal :integer, column_type(m, :children) 47 | assert_equal :integer, column_type(m, :companion_id) 48 | assert_nil column_type(m, :companion) 49 | assert_equal :float, column_type(m, :rating) 50 | assert_equal :decimal, column_type(m, :income) 51 | assert_equal :date, column_type(m, :birthdate) 52 | assert_equal :time, column_type(m, :gets_up_at) 53 | assert_equal :datetime, column_type(m, :last_seen) 54 | assert_equal :boolean, column_type(m, :human) 55 | assert_equal :text, column_type(m, :remarks) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/support/crud_controller_test_helper.rb: -------------------------------------------------------------------------------- 1 | # A module to include into your functional tests for your crud controller 2 | # subclasses. Simply implement the two methods #test_entry and 3 | # #test_entry_attrs to test the basic crud functionality. Override the test 4 | # methods if you changed the behaviour in your subclass controller. 5 | module CrudControllerTestHelper # rubocop:disable Metrics/ModuleLength 6 | def test_index # :nodoc: 7 | get :index, params: test_params 8 | assert_response :success 9 | assert entries.present? 10 | end 11 | 12 | def test_index_json # :nodoc: 13 | get :index, params: test_params(format: "json") 14 | assert_response :success 15 | assert entries.present? 16 | assert @response.body.starts_with?("[{"), @response.body 17 | end 18 | 19 | def test_index_search # rubocop:disable Metrics/AbcSize -- :nodoc: 20 | field = @controller.search_columns.first 21 | val = field && test_entry[field].to_s 22 | return if val.blank? # does not support search or no value in this field 23 | 24 | get :index, params: test_params(q: val[0..((val.size + 1) / 2)]) 25 | assert_response :success 26 | assert entries.present? 27 | assert entries.include?(test_entry) 28 | end 29 | 30 | def test_index_sort_asc # :nodoc: 31 | col = model_class.column_names.first 32 | get :index, params: test_params(sort: col, sort_dir: "asc") 33 | assert_response :success 34 | assert entries.present? 35 | sorted = entries.sort_by(&col.to_sym) 36 | assert_equal sorted, entries.to_a 37 | end 38 | 39 | def test_index_sort_desc # :nodoc: 40 | col = model_class.column_names.first 41 | get :index, params: test_params(sort: col, sort_dir: "desc") 42 | assert_response :success 43 | assert entries.present? 44 | sorted = entries.to_a.sort_by(&col.to_sym) 45 | assert_equal sorted.reverse, entries.to_a 46 | end 47 | 48 | def test_show # :nodoc: 49 | get :show, params: test_params(id: test_entry.id) 50 | assert_response :success 51 | assert_equal test_entry, entry 52 | end 53 | 54 | def test_show_json # :nodoc: 55 | get :show, params: test_params(id: test_entry.id, format: "json") 56 | assert_response :success 57 | assert_equal test_entry, entry 58 | assert @response.body.starts_with?("{") 59 | end 60 | 61 | def test_show_with_non_existing_id_raises_record_not_found # :nodoc: 62 | assert_raise(ActiveRecord::RecordNotFound) do 63 | get :show, params: test_params(id: 9999) 64 | end 65 | end 66 | 67 | def test_new # :nodoc: 68 | get :new, params: test_params 69 | assert_response :success 70 | assert entry.new_record? 71 | end 72 | 73 | def test_create # :nodoc: 74 | assert_difference("#{model_class.name}.count") do 75 | post :create, params: test_params(model_identifier => new_entry_attrs) 76 | end 77 | assert_redirected_to_show entry 78 | assert_not entry.new_record? 79 | assert_attrs_equal(new_entry_attrs) 80 | end 81 | 82 | def test_create_json # :nodoc: 83 | assert_difference("#{model_class.name}.count") do 84 | post :create, params: test_params(model_identifier => new_entry_attrs, 85 | format: "json") 86 | end 87 | assert_response :success 88 | assert @response.body.starts_with?('{"id":') 89 | end 90 | 91 | def test_edit # :nodoc: 92 | get :edit, params: test_params(id: test_entry.id) 93 | assert_response :success 94 | assert_equal test_entry, entry 95 | end 96 | 97 | def test_update # :nodoc: 98 | assert_no_difference("#{model_class.name}.count") do 99 | put :update, params: test_params(id: test_entry.id, 100 | model_identifier => edit_entry_attrs) 101 | end 102 | assert_attrs_equal(edit_entry_attrs) 103 | assert_redirected_to_show entry 104 | end 105 | 106 | def test_update_json # :nodoc: 107 | assert_no_difference("#{model_class.name}.count") do 108 | put :update, params: test_params(id: test_entry.id, 109 | model_identifier => edit_entry_attrs, 110 | format: "json") 111 | end 112 | assert_response :success 113 | assert @response.body.starts_with?('{"id":') 114 | end 115 | 116 | def test_destroy # :nodoc: 117 | assert_difference("#{model_class.name}.count", -1) do 118 | delete :destroy, params: test_params(id: test_entry.id) 119 | end 120 | assert_redirected_to_index 121 | end 122 | 123 | def test_destroy_json # :nodoc: 124 | assert_difference("#{model_class.name}.count", -1) do 125 | delete :destroy, params: test_params(id: test_entry.id, 126 | format: "json") 127 | end 128 | assert_response :success 129 | assert_equal "", @response.body.strip 130 | end 131 | 132 | private 133 | 134 | def assert_redirected_to_index # :nodoc: 135 | assert_redirected_to test_params(action: "index", 136 | id: nil, 137 | returning: true) 138 | end 139 | 140 | def assert_redirected_to_show(entry) # :nodoc: 141 | assert_redirected_to test_params(action: "show", 142 | id: entry.id) 143 | end 144 | 145 | def assert_attrs_equal(attrs) # :nodoc: 146 | attrs.each do |key, value| 147 | actual = entry.send(key) 148 | assert_equal value, 149 | actual, 150 | "#{key} is expected to be <#{value.inspect}>, " \ 151 | "got <#{actual.inspect}>" 152 | end 153 | end 154 | 155 | # The model class under test. 156 | def model_class 157 | @controller.model_class 158 | end 159 | 160 | # The param key for model attributes. 161 | def model_identifier 162 | @controller.model_identifier 163 | end 164 | 165 | # The entry as set by the controller. 166 | def entry 167 | @controller.send(:entry) 168 | end 169 | 170 | # The entries as set by the controller. 171 | def entries 172 | @controller.send(:entries) 173 | end 174 | 175 | # Test object used in several tests. 176 | def test_entry 177 | raise 'Implement the method "test_entry" in your test class' 178 | end 179 | 180 | # Attribute hash used in several tests. 181 | def test_entry_attrs 182 | raise 'Implement the method "test_entry_attrs" in your test class' 183 | end 184 | 185 | # Attribute hash used in edit/update tests. 186 | def edit_entry_attrs 187 | test_entry_attrs 188 | end 189 | 190 | # Attribute hash used in new/create tests. 191 | def new_entry_attrs 192 | test_entry_attrs 193 | end 194 | 195 | # The params to pass to an action, including required nesting params. 196 | def test_params(params = {}) 197 | nesting_params.merge(params) 198 | end 199 | 200 | # For nested controllers, collect hash with parent ids. 201 | def nesting_params 202 | params = {} 203 | Array(@controller.nesting).reverse.reduce(test_entry) do |parent, p| 204 | if p.is_a?(Class) && p < ActiveRecord::Base 205 | assoc = p.name.underscore 206 | params["#{assoc}_id"] = parent.send(:"#{assoc}_id") 207 | parent.send(assoc) 208 | else 209 | parent 210 | end 211 | end 212 | params 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/support/crud_test_helper.rb: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | REGEXP_ROWS = /<tr.+?<\/tr>/m.freeze 3 | REGEXP_HEADERS = /<th.+?<\/th>/m.freeze 4 | REGEXP_SORT_HEADERS = /<th.*?><a .*?sort_dir=asc.*?>.*?<\/a><\/th>/m.freeze 5 | REGEXP_ACTION_CELL = /<td class="action"><a .*?href.+?<\/a><\/td>/m.freeze 6 | 7 | # A simple test helper to prepare the test database with a CrudTestModel model. 8 | # This helper is used to test the CrudController and various helpers 9 | # without the need for an application based model. 10 | module CrudTestHelper 11 | # Controller helper methods for the tests 12 | 13 | def model_class 14 | CrudTestModel 15 | end 16 | 17 | def controller_name 18 | "crud_test_models" 19 | end 20 | 21 | def action_name 22 | "index" 23 | end 24 | 25 | def params 26 | {} 27 | end 28 | 29 | def path_args(entry) 30 | entry 31 | end 32 | 33 | def sortable?(_attr) 34 | true 35 | end 36 | 37 | delegate :h, to: :'ERB::Util' 38 | 39 | private 40 | 41 | # Sets up the test database with a crud_test_models table. 42 | # Look at the source to view the column definition. 43 | def setup_db 44 | without_transaction do 45 | c = ActiveRecord::Base.connection 46 | 47 | create_crud_test_models(c) 48 | create_other_crud_test_models(c) 49 | create_crud_test_models_other_crud_test_models(c) 50 | 51 | CrudTestModel.reset_column_information 52 | end 53 | end 54 | 55 | def create_crud_test_models(connection) 56 | connection.create_table :crud_test_models, force: true do |t| 57 | t.string :name, null: false, limit: 50 58 | t.string :email 59 | t.string :password 60 | t.string :whatever 61 | t.integer :children 62 | t.integer :companion_id 63 | t.float :rating 64 | t.decimal :income, precision: 14, scale: 4 65 | t.date :birthdate 66 | t.time :gets_up_at 67 | t.datetime :last_seen 68 | t.boolean :human, default: true 69 | t.text :remarks 70 | 71 | t.timestamps null: false 72 | end 73 | end 74 | 75 | def create_other_crud_test_models(connection) 76 | connection.create_table :other_crud_test_models, force: true do |t| 77 | t.string :name, null: false, limit: 50 78 | t.integer :more_id 79 | end 80 | end 81 | 82 | def create_crud_test_models_other_crud_test_models(connection) 83 | connection.create_table :crud_test_models_other_crud_test_models, 84 | force: true do |t| 85 | t.belongs_to :crud_test_model, index: { name: "parent" } 86 | t.belongs_to :other_crud_test_model, index: { name: "other" } 87 | end 88 | end 89 | 90 | # Removes the crud_test_models table from the database. 91 | def reset_db 92 | c = ActiveRecord::Base.connection 93 | %i[crud_test_models 94 | other_crud_test_models 95 | crud_test_models_other_crud_test_models].each do |table| 96 | c.drop_table(table) if c.data_source_exists?(table) 97 | end 98 | end 99 | 100 | # Creates 6 dummy entries for the crud_test_models table. 101 | def create_test_data 102 | (1..6).reduce(nil) { |a, e| create(e, a) } 103 | (1..6).each { |i| create_other(i) } 104 | end 105 | 106 | # Fixture-style accessor method to get CrudTestModel instances by name 107 | def crud_test_models(name) 108 | CrudTestModel.find_by(name: name.to_s) 109 | end 110 | 111 | def with_test_routing 112 | @routes ||= nil 113 | with_routing do |set| 114 | set.draw { resources :crud_test_models } 115 | # used to define a controller in these tests 116 | set.default_url_options = { controller: "crud_test_models" } 117 | yield 118 | end 119 | end 120 | 121 | def special_routing 122 | # test:unit uses instance variable, rspec the method 123 | controller = @controller || @_controller || controller 124 | @routes = ActionDispatch::Routing::RouteSet.new 125 | routes = @routes 126 | 127 | controller.singleton_class.send(:include, routes.url_helpers) 128 | 129 | if controller.respond_to?(:view_context_class) 130 | view_context_class = Class.new(controller.view_context_class) do 131 | include routes.url_helpers 132 | end 133 | custom_view_context = Module.new do 134 | define_method(:view_context_class) do 135 | view_context_class 136 | end 137 | end 138 | controller.extend(custom_view_context) 139 | end 140 | 141 | @routes.draw { resources :crud_test_models } 142 | end 143 | 144 | def create(index, companion) # rubocop:disable Metrics/AbcSize 145 | c = str(index) 146 | m = CrudTestModel.new( 147 | name: c, 148 | children: 10 - index, 149 | rating: "#{index}.#{index}".to_f, 150 | income: (10_000_000 * index) + (0.1111 * index), 151 | birthdate: "#{1900 + (10 * index)}-#{index}-#{index}", 152 | # store entire date to avoid time zone issues 153 | gets_up_at: Time.zone.local(2000, 1, 1, index, index), 154 | last_seen: "#{2000 + (10 * index)}-#{index}-#{index} " \ 155 | "1#{index}:2#{index}", 156 | human: index.even?, 157 | remarks: "#{c} #{str(index + 1)} #{str(index + 2)}\n" * 158 | ((index % 3) + 1) 159 | ) 160 | m.companion = companion 161 | m.save! 162 | m 163 | end 164 | 165 | def create_other(index) 166 | c = str(index) 167 | others = CrudTestModel.all[index..(index + 2)] 168 | OtherCrudTestModel.create!(name: c, 169 | other_ids: others.map(&:id), 170 | more_id: others.first.try(:id)) 171 | end 172 | 173 | def str(index) 174 | (index + 64).chr * 5 175 | end 176 | 177 | # A hack to avoid ddl in transaction issues with mysql. 178 | def without_transaction 179 | c = ActiveRecord::Base.connection 180 | start_transaction = false 181 | if c.adapter_name.downcase.include?("mysql") && 182 | c.open_transactions.positive? 183 | # in transactional tests, we may simply rollback 184 | c.execute("ROLLBACK") 185 | start_transaction = true 186 | end 187 | 188 | yield 189 | 190 | c.execute("BEGIN") if start_transaction 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/support/crud_test_model.rb: -------------------------------------------------------------------------------- 1 | # A dummy model used for general testing. 2 | class CrudTestModel < ApplicationRecord # :nodoc: 3 | belongs_to :companion, class_name: "CrudTestModel", optional: true 4 | has_and_belongs_to_many :others, class_name: "OtherCrudTestModel" 5 | has_many :mores, class_name: "OtherCrudTestModel", 6 | foreign_key: :more_id 7 | 8 | has_one :comrad, class_name: "CrudTestModel", foreign_key: :companion_id 9 | 10 | before_destroy :protect_if_companion 11 | 12 | validates :name, presence: true 13 | validates :rating, inclusion: { in: 1..10 } 14 | 15 | def to_s 16 | name 17 | end 18 | 19 | def chatty 20 | remarks.size 21 | end 22 | 23 | private 24 | 25 | def protect_if_companion 26 | if companion.present? 27 | errors.add(:base, "Cannot destroy model with companion") 28 | throw :abort 29 | end 30 | end 31 | end 32 | 33 | # Second dummy model to test associations. 34 | class OtherCrudTestModel < ApplicationRecord # :nodoc: 35 | has_and_belongs_to_many :others, class_name: "CrudTestModel" 36 | belongs_to :more, 37 | class_name: "CrudTestModel", 38 | optional: true 39 | 40 | def to_s 41 | name 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/support/crud_test_models_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for the dummy model. 2 | class CrudTestModelsController < CrudController # :nodoc: 3 | HANDLE_PREFIX = "handle_".freeze 4 | 5 | self.search_columns = %i[name whatever remarks] 6 | self.sort_mappings = { chatty: "length(remarks)" } 7 | self.default_sort = "name" 8 | self.permitted_attrs = [ :name, :email, :password, :whatever, :children, 9 | :companion_id, :rating, :income, :birthdate, 10 | :gets_up_at, :last_seen, :human, :remarks, 11 | { other_ids: [] } ] 12 | 13 | before_create :possibly_redirect 14 | before_create :handle_name 15 | before_destroy :handle_name 16 | 17 | before_render_new :possibly_redirect 18 | before_render_new :set_companions 19 | 20 | attr_reader :called_callbacks 21 | attr_accessor :should_redirect 22 | 23 | # don't use the standard layout as it may require different routes 24 | # than just the test route for this controller 25 | layout false 26 | 27 | def index 28 | entries 29 | render plain: "index js" if request.format.js? 30 | end 31 | 32 | def show 33 | render html: "custom html" if entry.name == "BBBBB" 34 | end 35 | 36 | def create 37 | super do |_format, success| 38 | flash[:notice] = "model got created" if success 39 | end 40 | end 41 | 42 | private 43 | 44 | def list_entries 45 | entries = super 46 | if params[:filter] 47 | entries = entries.where(rating: ...3) 48 | .except(:order) 49 | .order("children DESC") 50 | end 51 | entries 52 | end 53 | 54 | def build_entry 55 | entry = super 56 | entry.companion_id = model_params.delete(:companion_id) if params[model_identifier] 57 | entry 58 | end 59 | 60 | # custom callback 61 | def handle_name 62 | if entry.name == "illegal" 63 | flash[:alert] = "illegal name" 64 | throw :abort 65 | end 66 | end 67 | 68 | # callback to redirect if @should_redirect is set 69 | def possibly_redirect 70 | redirect_to action: "index" if should_redirect && !performed? 71 | end 72 | 73 | def set_companions 74 | @companions = CrudTestModel.where(human: true) 75 | end 76 | 77 | # create callback methods that record the before/after callbacks 78 | %i[create update save destroy].each do |a| 79 | callback = "before_#{a}" 80 | send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}") 81 | callback = "after_#{a}" 82 | send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}") 83 | end 84 | 85 | # create callback methods that record the before_render callbacks 86 | %i[index show new edit form].each do |a| 87 | callback = "before_render_#{a}" 88 | send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}") 89 | end 90 | 91 | # handle the called callbacks 92 | def method_missing(sym, *_args) 93 | if sym.to_s.starts_with?(HANDLE_PREFIX) 94 | called_callback(sym.to_s[HANDLE_PREFIX.size..].to_sym) 95 | else 96 | super 97 | end 98 | end 99 | 100 | def respond_to_missing?(sym, include_private = false) 101 | sym.to_s.starts_with?(HANDLE_PREFIX) || super 102 | end 103 | 104 | # records a callback 105 | def called_callback(callback) 106 | @called_callbacks ||= [] 107 | @called_callbacks << callback 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/generators/dry_crud/templates/test/support/custom_assertions.rb: -------------------------------------------------------------------------------- 1 | # A handful of convenient assertions. The aim of custom assertions is to 2 | # provide more specific error messages and to perform complex checks. 3 | # 4 | # Ideally, include this module into your test_helper.rb file: 5 | # # at the beginning of the file: 6 | # require 'support/custom_assertions' 7 | # 8 | # # inside the class definition: 9 | # include CustomAssertions 10 | module CustomAssertions 11 | # Asserts that regexp occurs exactly expected times in string. 12 | def assert_count(expected, regexp, string, msg = "") 13 | actual = string.scan(regexp).size 14 | msg = message(msg) do 15 | "Expected #{mu_pp(regexp)} to occur #{expected} time(s), " \ 16 | "but occured #{actual} time(s) in \n#{mu_pp(string)}" 17 | end 18 | assert expected == actual, msg 19 | end 20 | 21 | # Asserts that the given active model record is valid. 22 | # This method used to be part of Rails but was deprecated, no idea why. 23 | def assert_valid(record, msg = "") 24 | record.valid? 25 | msg = message(msg) do 26 | "Expected #{mu_pp(record)} to be valid, " \ 27 | "but has the following errors:\n" + 28 | mu_pp(record.errors.full_messages.join("\n")) 29 | end 30 | assert record.valid?, msg 31 | end 32 | 33 | # Asserts that the given active model record is not valid. 34 | # If you provide a set of invalid attribute symbols, all of and only these 35 | # attributes are expected to have errors. If no invalid attributes are 36 | # specified, only the invalidity of the record is asserted. 37 | def assert_not_valid(record, *invalid_attrs) 38 | msg = message do 39 | "Expected #{mu_pp(record)} to be invalid, but is valid." 40 | end 41 | assert_not record.valid?, msg 42 | 43 | if invalid_attrs.present? 44 | assert_invalid_attrs_have_errors(record, *invalid_attrs) 45 | assert_other_attrs_have_no_errors(record, *invalid_attrs) 46 | end 47 | end 48 | 49 | # The method used to by Test::Unit to format arguments. 50 | # Prints ActiveRecord objects in a simpler format. 51 | def mu_pp(obj) 52 | if obj.is_a?(ActiveRecord::Base) # :nodoc: 53 | obj.to_s 54 | else 55 | super 56 | end 57 | end 58 | 59 | private 60 | 61 | def assert_invalid_attrs_have_errors(record, *invalid_attrs) 62 | invalid_attrs.each do |a| 63 | msg = message do 64 | "Expected attribute #{mu_pp(a)} to be invalid, but is valid." 65 | end 66 | assert record.errors[a].present?, msg 67 | end 68 | end 69 | 70 | def assert_other_attrs_have_no_errors(record, *invalid_attrs) 71 | record.errors.each do |error| 72 | msg = message do 73 | "Attribute #{mu_pp(error.attribute)} not declared as invalid attribute, " \ 74 | "but has the following error(s):\n#{mu_pp(error.message)}" 75 | end 76 | assert invalid_attrs.include?(error.attribute), msg 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | def use_gem(name, options = {}) 2 | gem name, options 3 | @used_gems ||= [] 4 | @used_gems << name 5 | end 6 | 7 | # ask user for options 8 | templates = ask('Which template engine do you use? [ERB|haml]') 9 | tests = ask('Which testing framework do you use? [TESTUNIT|rspec]') 10 | options = '' 11 | 12 | if templates.present? && 'haml'.start_with?(templates.downcase) 13 | use_gem 'haml' 14 | options << ' --templates haml' 15 | end 16 | 17 | if tests.present? && 'rspec'.start_with?(tests.downcase) 18 | use_gem 'rspec-rails', group: %i[development test] 19 | options << ' --tests rspec' 20 | end 21 | 22 | use_gem 'dry_crud' 23 | 24 | # install missing gems 25 | installed = run('gem list', capture: true) 26 | news = @used_gems.any? do |g| 27 | installed !~ /#{g}/ 28 | end 29 | run 'bundle install' if news 30 | 31 | # setup rspec 32 | if tests.present? && 'rspec'.start_with?(tests.downcase) 33 | generate 'rspec:install' 34 | end 35 | 36 | # generate dry_crud with erb or haml 37 | generate 'dry_crud', options 38 | 39 | # remove gem from Gemfile 40 | gsub_file 'Gemfile', /gem .dry_crud./, '' 41 | -------------------------------------------------------------------------------- /test/templates/Gemfile.append: -------------------------------------------------------------------------------- 1 | gem "dry_crud", path: "../.." 2 | 3 | gem "simplecov", require: false, group: :test 4 | 5 | platforms :jruby do 6 | gem "jdbc-sqlite3" 7 | gem "activerecord-jdbcsqlite3-adapter" 8 | end 9 | 10 | gem "haml" 11 | 12 | gem "kaminari" 13 | 14 | gem "rspec-rails" 15 | -------------------------------------------------------------------------------- /test/templates/app/controllers/admin/cities_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | # Cities Controller nested under /admin and countries 3 | class CitiesController < TurboController 4 | self.nesting = :admin, Country 5 | 6 | self.search_columns = :name, "countries.name" 7 | 8 | self.default_sort = "countries.code, cities.name" 9 | 10 | self.permitted_attrs = %i[name person_ids] 11 | 12 | private 13 | 14 | def list_entries 15 | list = super.includes(:country) 16 | list = list.references(:countries) if list.respond_to?(:references) 17 | list 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/templates/app/controllers/admin/countries_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | # Countries Controller nested under /admin 3 | class CountriesController < TurboController 4 | self.nesting = :admin 5 | 6 | self.search_columns = :name, :code 7 | 8 | self.default_sort = "countries.name" 9 | 10 | self.permitted_attrs = %i[name code] 11 | 12 | def show 13 | redirect_to index_path if request.format.html? 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/templates/app/controllers/people_controller.rb: -------------------------------------------------------------------------------- 1 | # People Controller 2 | class PeopleController < TurboController 3 | self.search_columns = [ :name, :email, :remarks, "cities.name" ] 4 | 5 | self.default_sort = "people.name, countries.code, cities.name" 6 | 7 | self.sort_mappings = { city_id: "cities.name" } 8 | 9 | self.permitted_attrs = %i[name children city_id rating income 10 | birthdate gets_up_at last_seen remarks 11 | cool email password] 12 | 13 | private 14 | 15 | def list_entries 16 | list = super.includes(city: :country) 17 | if list.respond_to?(:references) 18 | list = list.references(:cities, :countries) 19 | end 20 | list 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/templates/app/controllers/turbo_controller.rb: -------------------------------------------------------------------------------- 1 | # Crud controller responding to js as well 2 | class TurboController < CrudController 3 | def turbo; end 4 | 5 | def update 6 | super do |format, _success| 7 | format.turbo_stream 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/templates/app/controllers/vips_controller.rb: -------------------------------------------------------------------------------- 1 | # List Controller for VIP people 2 | class VipsController < ListController 3 | self.search_columns = [ :name, :children, :rating, :remarks, "cities.name" ] 4 | 5 | self.sort_mappings = { city_id: "cities.name" } 6 | 7 | self.default_sort = "people.name, countries.code, cities.name" 8 | 9 | private 10 | 11 | class << self 12 | def model_class 13 | Person 14 | end 15 | end 16 | 17 | def list_entries 18 | list = super.where("rating > 5").includes(city: :country) 19 | if list.respond_to?(:references) 20 | list = list.references(:cities, :countries) 21 | end 22 | list 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/templates/app/helpers/cities_helper.rb: -------------------------------------------------------------------------------- 1 | # Cities Helper 2 | module CitiesHelper 3 | def format_city_id(entry) 4 | city = entry.city 5 | if city 6 | link_to(city, admin_country_city_path(city.country, city)) 7 | else 8 | ta(:no_entry) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/templates/app/helpers/people_helper.rb: -------------------------------------------------------------------------------- 1 | # People Helper 2 | module PeopleHelper 3 | def format_person_income(person) 4 | income = person.income 5 | income.present? ? "#{f(income)} $" : UtilityHelper::EMPTY_STRING 6 | end 7 | 8 | def f(value) 9 | case value 10 | when true then "iu" 11 | else super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/templates/app/models/city.rb: -------------------------------------------------------------------------------- 1 | # City model 2 | class City < ApplicationRecord 3 | belongs_to :country 4 | has_many :people 5 | 6 | validates :name, presence: true 7 | 8 | before_destroy :protect_with_inhabitants 9 | 10 | scope :options_list, -> { includes(:country).order("cities.name") } 11 | 12 | def to_s 13 | "#{name} (#{country.code})" 14 | end 15 | 16 | private 17 | 18 | def protect_with_inhabitants 19 | if people.exists? 20 | errors.add(:base, :protect_with_inhabitants) 21 | throw :abort 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/templates/app/models/country.rb: -------------------------------------------------------------------------------- 1 | # Country model 2 | class Country < ApplicationRecord 3 | has_many :cities, dependent: :destroy 4 | 5 | validates :name, :code, presence: true, uniqueness: true 6 | 7 | def to_s 8 | name 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/templates/app/models/person.rb: -------------------------------------------------------------------------------- 1 | # Person model 2 | class Person < ApplicationRecord 3 | belongs_to :city 4 | 5 | validates :name, presence: true 6 | 7 | scope :list, -> { order("people.name") } 8 | 9 | def to_s 10 | name 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_actions_index.html.erb: -------------------------------------------------------------------------------- 1 | <%= add_action_link %> 2 | <%= index_action_link admin_country_path(parent) %> -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_actions_index.html.haml: -------------------------------------------------------------------------------- 1 | = add_action_link 2 | = index_action_link admin_country_path(parent) -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_attrs.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_attrs @city, :people %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_attrs.html.haml: -------------------------------------------------------------------------------- 1 | = render_attrs entry, :people -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_form do |f| %> 2 | <%= f.labeled(:name, caption: 'Called') do %> 3 | <%= f.input_field :name %> 4 | <% end %> 5 | <%= f.labeled_input_field :person_ids, help: 'All people living in this city' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_form.html.haml: -------------------------------------------------------------------------------- 1 | = crud_form do |f| 2 | = f.labeled(:name, caption: 'Called') do 3 | = f.input_field :name 4 | = f.labeled_input_field :person_ids, help: 'All people living in this city' 5 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_hello.html.erb: -------------------------------------------------------------------------------- 1 | hello from cities -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_hello.html.haml: -------------------------------------------------------------------------------- 1 | hello from cities -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_list.html.erb: -------------------------------------------------------------------------------- 1 | <% @title = ti(:country_title, country: @parents.last) -%> 2 | 3 | <%= render 'crud/list' %> 4 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/cities/_list.html.haml: -------------------------------------------------------------------------------- 1 | - @title = ti(:country_title, country: @parents.last) 2 | 3 | = render 'crud/list' 4 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/countries/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_form do |f| %> 2 | <%= f.labeled_input_field :name %> 3 | <%= f.labeled_input_field :code, addon: 'CC', help: 'Two letter country code' %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/countries/_form.html.haml: -------------------------------------------------------------------------------- 1 | = crud_form do |f| 2 | = f.labeled_input_field :name 3 | = f.labeled_input_field :code, addon: 'CC', help: 'Two letter country code' 4 | -------------------------------------------------------------------------------- /test/templates/app/views/admin/countries/_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_table do |t| 2 | t.attr_with_show_link(:name) {|e| admin_country_cities_path(e) } 3 | t.sortable_attr(:code) 4 | end %> -------------------------------------------------------------------------------- /test/templates/app/views/admin/countries/_list.html.haml: -------------------------------------------------------------------------------- 1 | = crud_table do |t| 2 | - t.attr_with_show_link(:name) {|e| admin_country_cities_path(e) } 3 | - t.sortable_attr(:code) -------------------------------------------------------------------------------- /test/templates/app/views/layouts/_nav.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to 'MyApp', root_path, class: 'navbar-brand' %> 2 | <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> 3 | <span class="navbar-toggler-icon"></span> 4 | </button> 5 | <div class="collapse navbar-collapse" id="navbarNav"> 6 | <ul class="navbar-nav"> 7 | <li class="nav-item"><%= link_to t(:'global.menu.people'), people_path, class: 'nav-link' %></li> 8 | <li class="nav-item"><%= link_to t(:'global.menu.countries'), admin_countries_path, class: 'nav-link' %></li> 9 | <li class="nav-item"><%= link_to t(:'global.menu.vips'), vips_path, class: 'nav-link' %></li> 10 | </ul> 11 | </div> 12 | -------------------------------------------------------------------------------- /test/templates/app/views/layouts/_nav.html.haml: -------------------------------------------------------------------------------- 1 | = link_to 'MyApp', root_path, class: 'navbar-brand' 2 | 3 | %button.navbar-toggler{type: 'button', 'data-bs-toggle': 'collapse', 'data-bs-target': '#navbarNav', 'aria-controls': 'navbarNav', 'aria-expanded': 'false', 'aria-label': 'Toggle navigation'} 4 | %span.navbar-toggler-icon 5 | 6 | .collapse.navbar-collapse#navbarNav 7 | %ul.navbar-nav 8 | %li.nav-item= link_to t(:'global.menu.people'), people_path, class: 'nav-link' 9 | %li.nav-item= link_to t(:'global.menu.countries'), admin_countries_path, class: 'nav-link' 10 | %li.nav-item= link_to t(:'global.menu.vips'), vips_path, class: 'nav-link' 11 | -------------------------------------------------------------------------------- /test/templates/app/views/people/_attrs.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_attrs @person, *default_crud_attrs %> 2 | 3 | <dl class="dl-horizontal"> 4 | <%= labeled(ti(:i_think_its), ti(:nice)) %> 5 | <%= labeled(ti(:check_google)) do %> 6 | <%= link_to ti(:"link.maps"), "http://map.google.com/?q=#{@person.name}" %> 7 | <% end %> 8 | </dl> -------------------------------------------------------------------------------- /test/templates/app/views/people/_attrs.html.haml: -------------------------------------------------------------------------------- 1 | = render_attrs entry, *default_crud_attrs 2 | 3 | %dl.dl-horizontal 4 | = labeled(ti(:i_think_its), ti(:nice)) 5 | = labeled(ti(:check_google)) do 6 | = link_to ti(:"link.maps"), "http://map.google.com/?q=#{entry.name}" -------------------------------------------------------------------------------- /test/templates/app/views/people/_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= crud_table :name, :city_id, :birthdate, :children, :income %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/people/_list.html.haml: -------------------------------------------------------------------------------- 1 | = crud_table :name, :city_id, :birthdate, :children, :income -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_actions_index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'crud/actions_index' %> 2 | 3 | <%= action_link(ti(:'link.turbo'), 4 | nil, 5 | { action: 'turbo' }, 6 | data: { 'turbo-stream': true }) %> 7 | 8 | <div id="response"></div> 9 | 10 | <% content_for :javascripts do %> 11 | var sayHello = function() { alert('Hello'); }; 12 | <% end %> 13 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_actions_index.html.haml: -------------------------------------------------------------------------------- 1 | = render 'crud/actions_index' 2 | 3 | = action_link(ti(:'link.turbo'), 4 | nil, 5 | { action: 'turbo' }, 6 | data: { 'turbo-stream': true }) 7 | 8 | #response 9 | 10 | - content_for :javascripts do 11 | var sayHello = function() { alert('Hello'); }; 12 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_actions_show.html.erb: -------------------------------------------------------------------------------- 1 | <%= index_action_link %> 2 | <%= edit_action_link %> 3 | <%= action_link(ti(:"link.edit") + ' Turbo', 4 | 'pencil', 5 | edit_polymorphic_path(path_args(entry)), 6 | data: { 'turbo-stream': true }) %> 7 | <%= destroy_action_link %> 8 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_actions_show.html.haml: -------------------------------------------------------------------------------- 1 | = index_action_link 2 | = edit_action_link 3 | = action_link(ti(:"link.edit") + ' Turbo', 4 | 'pencil', 5 | edit_polymorphic_path(path_args(entry)), 6 | data: { 'turbo-stream': true }) 7 | = destroy_action_link 8 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_hello.html.erb: -------------------------------------------------------------------------------- 1 | hello from turbo 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/_hello.html.haml: -------------------------------------------------------------------------------- 1 | hello from turbo 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/edit.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.update(:content, partial: 'form') %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/edit.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.update(:content, partial: 'form') 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/show.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.update(:content, partial: 'attrs') %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/show.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.update(:content, partial: 'attrs') 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/turbo.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.update(:response, partial: 'hello') %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/turbo.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.update(:response, partial: 'hello') 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/update.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.update(:content, partial: entry.errors.present? ? 'form' : 'attrs') %> 2 | -------------------------------------------------------------------------------- /test/templates/app/views/turbo/update.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.update(:content, partial: entry.errors.present? ? 'form' : 'attrs') 2 | -------------------------------------------------------------------------------- /test/templates/config/database.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> 3 | pool: 5 4 | timeout: 5000 5 | 6 | # SQLite version 3.x 7 | # gem install sqlite3 8 | development: 9 | <<: *defaults 10 | database: db/development.sqlite3 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | <<: *defaults 17 | database: db/test.sqlite3 18 | 19 | production: 20 | <<: *defaults 21 | database: db/production.sqlite3 22 | -------------------------------------------------------------------------------- /test/templates/config/initializers/deprecations.rb: -------------------------------------------------------------------------------- 1 | # Time columns will become time zone aware in Rails 5.1. This 2 | # still causes `String`s to be parsed as if they were in `Time.zone`, 3 | # and `Time`s to be converted to `Time.zone`. 4 | 5 | # To keep the old behavior, you must add the following to your initializer: 6 | # config.active_record.time_zone_aware_types = [:datetime] 7 | 8 | # To silence this deprecation warning, add the following: 9 | Rails.application.config.active_record.time_zone_aware_types = [ :datetime, :time ] 10 | -------------------------------------------------------------------------------- /test/templates/config/locales/cities.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | global: 3 | menu: 4 | people: People 5 | countries: Countries 6 | vips: VIPs 7 | link: 8 | maps: Maps 9 | turbo: 10 | global: 11 | link: 12 | turbo: Turbo 13 | # cities controller 14 | admin/cities: 15 | global: 16 | confirm_delete: You kill city? 17 | button: 18 | save: Save City 19 | index: 20 | country_title: All cities of %{country} 21 | list: 22 | no_list_entries: Nada citta 23 | new: 24 | button: 25 | save: Create City 26 | update: 27 | flash: 28 | success: The %{model} got an update 29 | 30 | # people controller 31 | people: 32 | global: 33 | confirm_delete: You delete pipl? 34 | attrs: 35 | i_think_its: I think it's 36 | nice: Nice 37 | check_google: Check Google 38 | 39 | # vips controller 40 | vips: 41 | index: 42 | title: Listing VIPs 43 | no_list_entries: No VIPs found 44 | 45 | # model associations 46 | activerecord: 47 | errors: 48 | models: 49 | city: 50 | protect_with_inhabitants: You cannot destroy this city as long as it has any inhabitants. 51 | associations: 52 | person: 53 | no_entry: Nobody 54 | none_available: Nobody here 55 | city: 56 | none_available: No City 57 | -------------------------------------------------------------------------------- /test/templates/config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | date: 3 | abbr_day_names: 4 | - So 5 | - Mo 6 | - Di 7 | - Mi 8 | - Do 9 | - Fr 10 | - Sa 11 | abbr_month_names: 12 | - 13 | - Jan 14 | - Feb 15 | - Mär 16 | - Apr 17 | - Mai 18 | - Jun 19 | - Jul 20 | - Aug 21 | - Sep 22 | - Okt 23 | - Nov 24 | - Dez 25 | day_names: 26 | - Sonntag 27 | - Montag 28 | - Dienstag 29 | - Mittwoch 30 | - Donnerstag 31 | - Freitag 32 | - Samstag 33 | formats: 34 | default: ! '%d.%m.%Y' 35 | long: ! '%e. %B %Y' 36 | short: ! '%e. %b' 37 | month_names: 38 | - 39 | - Januar 40 | - Februar 41 | - März 42 | - April 43 | - Mai 44 | - Juni 45 | - Juli 46 | - August 47 | - September 48 | - Oktober 49 | - November 50 | - Dezember 51 | order: 52 | - :day 53 | - :month 54 | - :year 55 | datetime: 56 | distance_in_words: 57 | about_x_hours: 58 | one: etwa eine Stunde 59 | other: etwa %{count} Stunden 60 | about_x_months: 61 | one: etwa ein Monat 62 | other: etwa %{count} Monate 63 | about_x_years: 64 | one: etwa ein Jahr 65 | other: etwa %{count} Jahre 66 | almost_x_years: 67 | one: fast ein Jahr 68 | other: fast %{count} Jahre 69 | half_a_minute: eine halbe Minute 70 | less_than_x_minutes: 71 | one: weniger als eine Minute 72 | other: weniger als %{count} Minuten 73 | less_than_x_seconds: 74 | one: weniger als eine Sekunde 75 | other: weniger als %{count} Sekunden 76 | over_x_years: 77 | one: mehr als ein Jahr 78 | other: mehr als %{count} Jahre 79 | x_days: 80 | one: ein Tag 81 | other: ! '%{count} Tage' 82 | x_minutes: 83 | one: eine Minute 84 | other: ! '%{count} Minuten' 85 | x_months: 86 | one: ein Monat 87 | other: ! '%{count} Monate' 88 | x_seconds: 89 | one: eine Sekunde 90 | other: ! '%{count} Sekunden' 91 | prompts: 92 | day: Tag 93 | hour: Stunden 94 | minute: Minuten 95 | month: Monat 96 | second: Sekunden 97 | year: Jahr 98 | errors: &errors 99 | format: ! '%{attribute} %{message}' 100 | messages: 101 | accepted: muss akzeptiert werden 102 | blank: muss ausgefüllt werden 103 | confirmation: stimmt nicht mit der Bestätigung überein 104 | empty: muss ausgefüllt werden 105 | equal_to: muss genau %{count} sein 106 | even: muss gerade sein 107 | exclusion: ist nicht verfügbar 108 | greater_than: muss größer als %{count} sein 109 | greater_than_or_equal_to: muss größer oder gleich %{count} sein 110 | inclusion: ist kein gültiger Wert 111 | invalid: ist nicht gültig 112 | less_than: muss kleiner als %{count} sein 113 | less_than_or_equal_to: muss kleiner oder gleich %{count} sein 114 | not_a_number: ist keine Zahl 115 | not_an_integer: muss ganzzahlig sein 116 | odd: muss ungerade sein 117 | record_invalid: ! 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' 118 | taken: ist bereits vergeben 119 | too_long: ist zu lang (nicht mehr als %{count} Zeichen) 120 | too_short: ist zu kurz (nicht weniger als %{count} Zeichen) 121 | wrong_length: hat die falsche Länge (muss genau %{count} Zeichen haben) 122 | template: 123 | body: ! 'Bitte überprüfen Sie die folgenden Felder:' 124 | header: 125 | one: ! 'Konnte %{model} nicht speichern: ein Fehler.' 126 | other: ! 'Konnte %{model} nicht speichern: %{count} Fehler.' 127 | helpers: 128 | select: 129 | prompt: Bitte wählen 130 | submit: 131 | create: ! '%{model} erstellen' 132 | submit: ! '%{model} speichern' 133 | update: ! '%{model} aktualisieren' 134 | number: 135 | currency: 136 | format: 137 | delimiter: . 138 | format: ! '%n %u' 139 | precision: 2 140 | separator: ! ',' 141 | significant: false 142 | strip_insignificant_zeros: false 143 | unit: € 144 | format: 145 | delimiter: . 146 | precision: 2 147 | separator: ! ',' 148 | significant: false 149 | strip_insignificant_zeros: false 150 | human: 151 | decimal_units: 152 | format: ! '%n %u' 153 | units: 154 | billion: 155 | one: Milliarde 156 | other: Milliarden 157 | million: Millionen 158 | quadrillion: 159 | one: Billiarde 160 | other: Billiarden 161 | thousand: Tausend 162 | trillion: Billionen 163 | unit: '' 164 | format: 165 | delimiter: '' 166 | precision: 1 167 | significant: true 168 | strip_insignificant_zeros: true 169 | storage_units: 170 | format: ! '%n %u' 171 | units: 172 | byte: 173 | one: Byte 174 | other: Bytes 175 | gb: GB 176 | kb: KB 177 | mb: MB 178 | tb: TB 179 | percentage: 180 | format: 181 | delimiter: '' 182 | precision: 183 | format: 184 | delimiter: '' 185 | support: 186 | array: 187 | last_word_connector: ! ' und ' 188 | two_words_connector: ! ' und ' 189 | words_connector: ! ', ' 190 | time: 191 | am: vormittags 192 | formats: 193 | default: ! '%A, %d. %B %Y, %H:%M Uhr' 194 | long: ! '%A, %d. %B %Y, %H:%M Uhr' 195 | short: ! '%d. %B, %H:%M Uhr' 196 | pm: nachmittags 197 | # remove these aliases after 'activemodel' and 'activerecord' namespaces are removed from Rails repository 198 | activemodel: 199 | errors: 200 | <<: *errors 201 | activerecord: 202 | errors: 203 | <<: *errors -------------------------------------------------------------------------------- /test/templates/config/routes.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | TestApp::Application.routes.draw do 4 | resources :people do 5 | collection do 6 | get :turbo 7 | end 8 | end 9 | 10 | get "vips" => "vips#index", :as => :vips 11 | 12 | namespace :admin do 13 | resources :countries do 14 | collection do 15 | get :turbo 16 | end 17 | 18 | resources :cities do 19 | collection do 20 | get :turbo 21 | end 22 | end 23 | end 24 | end 25 | 26 | root to: "people#index" 27 | 28 | # Install the default routes as the lowest priority. 29 | # Note: These default routes make all actions in every controller accessible 30 | # via GET requests. You should consider removing or commenting them out if 31 | # you're using named routes and resources. 32 | # map.connect ':controller/:action/:id.:format' 33 | # map.connect ':controller/:action/:id' 34 | end 35 | -------------------------------------------------------------------------------- /test/templates/db/migrate/20100511174904_create_people_and_cities.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # initial migration 4 | class CreatePeopleAndCities < ActiveRecord::Migration[5.0] 5 | def change 6 | create_table :countries do |t| 7 | t.string :name, null: false 8 | t.string :code, null: :false, limit: 3 9 | end 10 | 11 | create_table :cities do |t| 12 | t.string :name, null: false 13 | t.integer :country_id, null: false 14 | end 15 | 16 | create_table :people do |t| 17 | t.string :name, null: false 18 | t.integer :children 19 | t.integer :city_id 20 | t.float :rating 21 | t.decimal :income, precision: 14, scale: 2 22 | t.date :birthdate 23 | t.time :gets_up_at 24 | t.datetime :last_seen 25 | t.text :remarks 26 | t.boolean :cool, null: false, default: false 27 | t.string :email 28 | t.string :password 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/templates/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | if City.count == 0 4 | 5 | ch = Country.create!(name: "Switzerland", code: "CH") 6 | de = Country.create!(name: "Germany", code: "DE") 7 | usa = Country.create!(name: "USA", code: "USA") 8 | gb = Country.create!(name: "England", code: "GB") 9 | jp = Country.create!(name: "Japan", code: "JP") 10 | 11 | be = City.create!(name: "Bern", country: ch) 12 | ny = City.create!(name: "New York", country: usa) 13 | sf = City.create!(name: "San Francisco", country: usa) 14 | lon = City.create!(name: "London", country: gb) 15 | br = City.create!(name: "Berlin", country: de) 16 | 17 | Person.create!(name: "Albert Einstein", 18 | city: be, 19 | children: 2, 20 | rating: 9.8, 21 | income: 84_000, 22 | birthdate: "1904-10-18", 23 | gets_up_at: "05:43", 24 | remarks: "Great physician\n Good cyclist") 25 | Person.create!(name: "Adolf Ogi", 26 | city: be, 27 | children: 3, 28 | rating: 4.2, 29 | income: 264_000, 30 | birthdate: "1938-01-22", 31 | gets_up_at: "04:30", 32 | remarks: "Freude herrscht!") 33 | Person.create!(name: "Jay Z", 34 | city: ny, 35 | children: 0, 36 | rating: 7.2, 37 | income: 868_345, 38 | birthdate: "1976-05-02", 39 | gets_up_at: "12:00", 40 | last_seen: "2011-03-10 17:29", 41 | cool: true, 42 | remarks: "I got 99 problems\nbut you *** ain't one\nTschie") 43 | Person.create!(name: "Queen Elisabeth", 44 | city: lon, 45 | children: 1, 46 | rating: 1.56, 47 | income: 345_622, 48 | birthdate: "1927-08-11", 49 | gets_up_at: "17:12", 50 | remarks: "") 51 | Person.create!(name: "Schopenhauer", 52 | city: br, 53 | children: 7, 54 | rating: 6.9, 55 | income: 14_000, 56 | birthdate: "1788-10-18", 57 | last_seen: "1854-09-01 11:01", 58 | remarks: "Neminem laede, immo omnes, quantum potes, iuva.") 59 | Person.create!(name: "ZZ Top", 60 | city: ny, 61 | children: 185, 62 | rating: 1.8, 63 | income: 84_000, 64 | birthdate: "1948-03-18", 65 | cool: true, 66 | remarks: "zzz..") 67 | Person.create!(name: "Andy Warhol", 68 | city: ny, 69 | children: 0, 70 | rating: 7.5, 71 | income: 123_000, 72 | birthdate: "1938-09-08", 73 | last_seen: "1984-10-10 23:39", 74 | remarks: "Tomato Soup") 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/templates/spec/controllers/admin/cities_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe Admin::CitiesController do 4 | fixtures :all 5 | 6 | include_examples "crud controller", {} 7 | 8 | let(:test_entry) { cities(:rj) } 9 | let(:test_entry_attrs) { { name: "Rejkiavik" } } 10 | alias_method :new_entry_attrs, :test_entry_attrs 11 | alias_method :edit_entry_attrs, :test_entry_attrs 12 | 13 | it "loads fixtures" do 14 | expect(City.count).to eq(3) 15 | end 16 | 17 | describe_action :get, :index do 18 | it "is ordered by default scope" do 19 | expected = test_entry.country.cities.includes(:country) 20 | .order("countries.code, cities.name") 21 | expected = expected.references(:countries) if expected.respond_to?(:references) 22 | entries == expected 23 | end 24 | 25 | it "sets parents" do 26 | expect(controller.send(:parents)).to eq([ :admin, test_entry.country ]) 27 | end 28 | 29 | it "sets parent variable" do 30 | expect(ivar(:country)).to eq(test_entry.country) 31 | end 32 | 33 | it "uses correct model_scope" do 34 | expect(controller.send(:model_scope)).to eq(test_entry.country.cities) 35 | end 36 | 37 | it "has correct path args" do 38 | expect(controller.send(:path_args, 2)).to eq( 39 | [ :admin, test_entry.country, 2 ] 40 | ) 41 | end 42 | end 43 | 44 | describe_action :get, :show, id: true do 45 | it "sets parents" do 46 | expect(controller.send(:parents)).to eq([ :admin, test_entry.country ]) 47 | end 48 | 49 | it "sets parent variable" do 50 | expect(ivar(:country)).to eq(test_entry.country) 51 | end 52 | end 53 | 54 | describe_action :post, :create do 55 | let(:params) { { model_identifier => new_entry_attrs } } 56 | 57 | it "sets parent" do 58 | expect(entry.country).to eq(test_entry.country) 59 | end 60 | end 61 | 62 | describe_action :delete, :destroy, id: true do 63 | context "with inhabitants" do 64 | let(:test_entry) { cities(:ny) } 65 | 66 | it "does not remove city from database", perform_request: false do 67 | expect { perform_request }.to change { City.count }.by(0) 68 | end 69 | 70 | it "redirects to referer", perform_request: false do 71 | ref = @request.env["HTTP_REFERER"] = 72 | admin_country_city_url(test_entry.country, test_entry) 73 | perform_request 74 | is_expected.to redirect_to(ref) 75 | end 76 | 77 | it_is_expected_to_have_flash(:alert) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/templates/spec/controllers/admin/countries_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe Admin::CountriesController do 4 | fixtures :all 5 | 6 | include_examples "crud controller", skip: %w[show html plain] 7 | 8 | let(:test_entry) { countries(:br) } 9 | let(:test_entry_attrs) do 10 | { name: "United States of America", code: "US" } 11 | end 12 | 13 | alias_method :new_entry_attrs, :test_entry_attrs 14 | alias_method :edit_entry_attrs, :test_entry_attrs 15 | 16 | it "loads fixtures" do 17 | expect(Country.count).to eq(3) 18 | end 19 | 20 | describe_action :get, :index do 21 | it "is ordered by default scope" do 22 | entries == Country.order(:name) 23 | end 24 | 25 | it "sets parents" do 26 | expect(controller.send(:parents)).to eq([ :admin ]) 27 | end 28 | 29 | it "sets nil parent" do 30 | expect(controller.send(:parent)).to be_nil 31 | end 32 | 33 | it "uses correct model_scope" do 34 | expect(controller.send(:model_scope)).to eq(Country.all) 35 | end 36 | 37 | it "has correct path args" do 38 | expect(controller.send(:path_args, 2)).to eq([ :admin, 2 ]) 39 | end 40 | end 41 | 42 | describe_action :get, :show do 43 | let(:params) { { id: test_entry.id } } 44 | it_is_expected_to_redirect_to_index 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/templates/spec/controllers/people_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe PeopleController do 4 | fixtures :all 5 | 6 | render_views 7 | 8 | include_examples "crud controller", {} 9 | 10 | let(:test_entry) { people(:john) } 11 | let(:test_entry_attrs) do 12 | { name: "Fischers Fritz", 13 | children: 2, 14 | income: 120, 15 | city_id: cities(:rj).id } 16 | end 17 | 18 | alias_method :new_entry_attrs, :test_entry_attrs 19 | alias_method :edit_entry_attrs, :test_entry_attrs 20 | 21 | it "loads fixtures" do 22 | expect(Person.count).to eq(2) 23 | end 24 | 25 | describe_action :get, :index do 26 | it "is ordered by default scope" do 27 | expected = Person.includes(city: :country) 28 | .order("people.name, countries.code, cities.name") 29 | expected = expected.references(:cities, :countries) if expected.respond_to?(:references) 30 | entries == expected 31 | end 32 | 33 | it "sets parents" do 34 | expect(controller.send(:parents)).to eq([]) 35 | end 36 | 37 | it "sets nil parent" do 38 | expect(controller.send(:parent)).to be_nil 39 | end 40 | 41 | it "uses correct model_scope" do 42 | expect(controller.send(:model_scope)).to eq(Person.all) 43 | end 44 | 45 | it "has correct path args" do 46 | expect(controller.send(:path_args, 2)).to eq([ 2 ]) 47 | end 48 | end 49 | 50 | describe_action :get, :show, id: true do 51 | context "turbo", format: :turbo_stream do 52 | it_is_expected_to_respond 53 | it { expect(response.body).to match(/<turbo-stream action="update" target="content">/) } 54 | end 55 | end 56 | 57 | describe_action :get, :edit, id: true do 58 | context "turbo", format: :turbo_stream do 59 | it_is_expected_to_respond 60 | it { expect(response.body).to match(/<turbo-stream action="update" target="content">/) } 61 | end 62 | end 63 | 64 | describe_action :put, :update, id: true do 65 | context "turbo", format: :turbo_stream do 66 | context "with valid params" do 67 | let(:params) { { person: { name: "New Name" } } } 68 | 69 | it_is_expected_to_respond 70 | it { expect(response.body).to match(/<turbo-stream action="update" target="content">/) } 71 | end 72 | 73 | context "with invalid params" do 74 | let(:params) { { person: { name: " " } } } 75 | 76 | it_is_expected_to_respond 77 | it { expect(response.body).to match(/<turbo-stream action="update" target="content">/) } 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/templates/spec/routing/cities_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe Admin::CitiesController do 4 | it "routes index" do 5 | expect(get: "admin/countries/1/cities").to route_to( 6 | controller: "admin/cities", action: "index", country_id: "1" 7 | ) 8 | end 9 | 10 | it "routes show" do 11 | expect(get: "admin/countries/2/cities/1").to route_to( 12 | controller: "admin/cities", action: "show", country_id: "2", id: "1" 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/templates/spec/routing/countries_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe Admin::CountriesController do 4 | it "routes index" do 5 | expect(get: "admin/countries").to route_to( 6 | controller: "admin/countries", action: "index" 7 | ) 8 | end 9 | 10 | it "routes show" do 11 | expect(get: "admin/countries/1").to route_to( 12 | controller: "admin/countries", action: "show", id: "1" 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/templates/test/controllers/admin/cities_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_controller_test_helper" 3 | 4 | module Admin 5 | # Cities Controller Test 6 | class CitiesControllerTest < ActionController::TestCase 7 | include CrudControllerTestHelper 8 | 9 | def test_setup 10 | assert_equal 3, City.count 11 | assert_recognizes({ controller: "admin/cities", 12 | action: "index", 13 | country_id: "1" }, 14 | "admin/countries/1/cities") 15 | assert_recognizes({ controller: "admin/cities", 16 | action: "show", 17 | country_id: "2", 18 | id: "1" }, 19 | "admin/countries/2/cities/1") 20 | end 21 | 22 | def test_index 23 | super 24 | expected = test_entry.country.cities.includes(:country) 25 | .order("countries.code, cities.name") 26 | expected = expected.references(:countries) if expected.respond_to?(:references) 27 | assert_equal expected.to_a, entries.to_a 28 | 29 | assert_equal [ :admin, test_entry.country ], @controller.send(:parents) 30 | assert_equal test_entry.country, @controller.send(:parent) 31 | assert_equal test_entry.country.cities.to_a, 32 | @controller.send(:model_scope).to_a 33 | assert_equal [ :admin, test_entry.country, 2 ], 34 | @controller.send(:path_args, 2) 35 | end 36 | 37 | def test_show 38 | super 39 | assert_equal [ :admin, test_entry.country ], @controller.send(:parents) 40 | assert_equal test_entry.country, @controller.send(:parent) 41 | end 42 | 43 | def test_create 44 | super 45 | assert_equal test_entry.country, entry.country 46 | end 47 | 48 | def test_destroy_with_inhabitants 49 | ny = cities(:ny) 50 | assert_no_difference("City.count") do 51 | @request.env["HTTP_REFERER"] = admin_country_city_url(ny.country, ny) 52 | delete :destroy, params: { country_id: ny.country_id, id: ny.id } 53 | end 54 | assert_redirected_to [ :admin, ny.country, ny ] 55 | assert flash[:alert].present? 56 | end 57 | 58 | private 59 | 60 | def test_entry 61 | cities(:rj) 62 | end 63 | 64 | def test_entry_attrs 65 | { name: "Rejkiavik" } 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/templates/test/controllers/admin/countries_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_controller_test_helper" 3 | 4 | module Admin 5 | # Countries Controller Test 6 | class CountriesControllerTest < ActionController::TestCase 7 | include CrudControllerTestHelper 8 | 9 | def test_setup 10 | assert_equal 3, Country.count 11 | assert_recognizes({ controller: "admin/countries", 12 | action: "index" }, 13 | "admin/countries") 14 | assert_recognizes({ controller: "admin/countries", 15 | action: "show", 16 | id: "1" }, 17 | "admin/countries/1") 18 | end 19 | 20 | def test_index 21 | super 22 | assert_equal Country.order("name").to_a, entries 23 | assert_equal [ :admin ], @controller.send(:parents) 24 | assert_nil @controller.send(:parent) 25 | assert_equal Country.all, @controller.send(:model_scope) 26 | assert_equal [ :admin, 2 ], @controller.send(:path_args, 2) 27 | end 28 | 29 | def test_show 30 | get :show, params: test_params(id: test_entry.id) 31 | assert_redirected_to_index 32 | end 33 | 34 | private 35 | 36 | def test_entry 37 | countries(:br) 38 | end 39 | 40 | def test_entry_attrs 41 | { name: "United States of America", code: "US" } 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/templates/test/controllers/people_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/crud_controller_test_helper" 3 | 4 | # People Controller Test 5 | class PeopleControllerTest < ActionController::TestCase 6 | include CrudControllerTestHelper 7 | 8 | def test_setup 9 | assert_equal 2, Person.count 10 | assert_recognizes({ controller: "people", 11 | action: "index" }, 12 | "/people") 13 | assert_recognizes({ controller: "people", 14 | action: "show", 15 | id: "1" }, 16 | "/people/1") 17 | end 18 | 19 | def test_index 20 | super 21 | assert_equal 2, entries.size 22 | expected = Person.includes(city: :country) 23 | .order("people.name, countries.code, cities.name") 24 | expected = expected.references(:cities, :countries) if expected.respond_to?(:references) 25 | assert_equal expected.to_a, entries 26 | 27 | assert_equal [], @controller.send(:parents) 28 | assert_nil @controller.send(:parent) 29 | assert_equal Person.all, @controller.send(:model_scope) 30 | assert_equal [ 2 ], @controller.send(:path_args, 2) 31 | end 32 | 33 | def test_index_search 34 | super 35 | assert_equal 1, entries.size 36 | end 37 | 38 | def test_show_turbo 39 | get :show, params: { id: test_entry.id }, as: :turbo_stream 40 | assert_response :success 41 | assert_match(/<turbo-stream action="update" target="content">/, response.body) 42 | end 43 | 44 | def test_edit_turbo 45 | get :edit, params: { id: test_entry.id }, as: :turbo_stream 46 | assert_response :success 47 | assert_match(/<turbo-stream action="update" target="content">/, response.body) 48 | end 49 | 50 | def test_update_turbo 51 | put :update, 52 | as: :turbo_stream, 53 | params: { id: test_entry.id, 54 | person: { name: "New Name" } } 55 | assert_response :success 56 | assert_match(/<turbo-stream action="update" target="content">/, response.body) 57 | end 58 | 59 | def test_update_fail_turbo 60 | put :update, 61 | as: :turbo_stream, 62 | params: { id: test_entry.id, 63 | person: { name: " " } } 64 | assert_response :success 65 | assert_match(/alert/, response.body) 66 | end 67 | 68 | private 69 | 70 | def test_entry 71 | people(:john) 72 | end 73 | 74 | def test_entry_attrs 75 | { name: "Fischers Fritz", 76 | children: 2, 77 | income: 120, 78 | city_id: cities(:rj).id } 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/templates/test/fixtures/cities.yml: -------------------------------------------------------------------------------- 1 | bern: 2 | name: Bern 3 | country: ch 4 | 5 | ny: 6 | name: New York 7 | country: usa 8 | 9 | rj: 10 | name: Rio de Janeiro 11 | country: br -------------------------------------------------------------------------------- /test/templates/test/fixtures/countries.yml: -------------------------------------------------------------------------------- 1 | ch: 2 | name: Switzerland 3 | code: CH 4 | 5 | usa: 6 | name: USA 7 | code: USA 8 | 9 | br: 10 | name: Brazil 11 | code: BR -------------------------------------------------------------------------------- /test/templates/test/fixtures/people.yml: -------------------------------------------------------------------------------- 1 | john: 2 | name: John Doe 3 | children: 0 4 | income: 300.20 5 | birthdate: 1972-04-04 6 | city: ny 7 | remarks: Bla Bla\nBla Bla 8 | 9 | jack: 10 | name: Jack Black 11 | children: 2 12 | income: 123.4567 13 | birthdate: 1980-09-12 14 | city: bern 15 | --------------------------------------------------------------------------------