Repository: railsadminteam/rails_admin Branch: master Commit: d8e0809ea4b3 Files: 722 Total size: 2.5 MB Directory structure: gitextract_n6znt0mu/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── code-ql.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .teatro.yml ├── .yardopts ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── Procfile.teatro ├── README.md ├── Rakefile ├── app/ │ ├── assets/ │ │ ├── javascripts/ │ │ │ └── rails_admin/ │ │ │ ├── application.js.erb │ │ │ └── custom/ │ │ │ └── ui.js │ │ └── stylesheets/ │ │ └── rails_admin/ │ │ ├── application.scss.erb │ │ └── custom/ │ │ ├── mixins.scss │ │ ├── theming.scss │ │ └── variables.scss │ ├── controllers/ │ │ └── rails_admin/ │ │ ├── application_controller.rb │ │ └── main_controller.rb │ ├── helpers/ │ │ └── rails_admin/ │ │ ├── application_helper.rb │ │ ├── form_builder.rb │ │ └── main_helper.rb │ └── views/ │ ├── kaminari/ │ │ └── ra-twitter-bootstrap/ │ │ ├── _gap.html.erb │ │ ├── _next_page.html.erb │ │ ├── _page.html.erb │ │ ├── _paginator.html.erb │ │ ├── _prev_page.html.erb │ │ └── without_count/ │ │ ├── _next_page.html.erb │ │ ├── _paginator.html.erb │ │ └── _prev_page.html.erb │ ├── layouts/ │ │ └── rails_admin/ │ │ ├── _head.html.erb │ │ ├── _navigation.html.erb │ │ ├── _secondary_navigation.html.erb │ │ ├── _sidebar_navigation.html.erb │ │ ├── application.html.erb │ │ ├── content.html.erb │ │ └── modal.js.erb │ └── rails_admin/ │ └── main/ │ ├── _dashboard_history.html.erb │ ├── _delete_notice.html.erb │ ├── _form_action_text.html.erb │ ├── _form_boolean.html.erb │ ├── _form_ck_editor.html.erb │ ├── _form_code_mirror.html.erb │ ├── _form_colorpicker.html.erb │ ├── _form_datetime.html.erb │ ├── _form_enumeration.html.erb │ ├── _form_field.html.erb │ ├── _form_file_upload.html.erb │ ├── _form_filtering_multiselect.html.erb │ ├── _form_filtering_select.html.erb │ ├── _form_froala.html.erb │ ├── _form_multiple_file_upload.html.erb │ ├── _form_nested_many.html.erb │ ├── _form_nested_one.html.erb │ ├── _form_polymorphic_association.html.erb │ ├── _form_simple_mde.html.erb │ ├── _form_text.html.erb │ ├── _form_wysihtml5.html.erb │ ├── _submit_buttons.html.erb │ ├── bulk_delete.html.erb │ ├── dashboard.html.erb │ ├── delete.html.erb │ ├── edit.html.erb │ ├── export.html.erb │ ├── history.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb ├── config/ │ ├── initializers/ │ │ ├── active_record_extensions.rb │ │ └── mongoid_extensions.rb │ ├── locales/ │ │ └── rails_admin.en.yml │ └── routes.rb ├── gemfiles/ │ ├── composite_primary_keys.gemfile │ ├── rails_6.0.gemfile │ ├── rails_6.1.gemfile │ ├── rails_7.0.gemfile │ ├── rails_7.1.gemfile │ ├── rails_7.2.gemfile │ └── rails_8.0.gemfile ├── lib/ │ ├── generators/ │ │ └── rails_admin/ │ │ ├── importmap_formatter.rb │ │ ├── install_generator.rb │ │ ├── templates/ │ │ │ ├── initializer.erb │ │ │ ├── rails_admin.js │ │ │ ├── rails_admin.scss.erb │ │ │ ├── rails_admin.vite.js │ │ │ └── rails_admin.webpacker.js │ │ └── utils.rb │ ├── rails_admin/ │ │ ├── abstract_model.rb │ │ ├── adapters/ │ │ │ ├── active_record/ │ │ │ │ ├── association.rb │ │ │ │ ├── object_extension.rb │ │ │ │ └── property.rb │ │ │ ├── active_record.rb │ │ │ ├── mongoid/ │ │ │ │ ├── association.rb │ │ │ │ ├── bson.rb │ │ │ │ ├── extension.rb │ │ │ │ ├── object_extension.rb │ │ │ │ └── property.rb │ │ │ └── mongoid.rb │ │ ├── config/ │ │ │ ├── actions/ │ │ │ │ ├── base.rb │ │ │ │ ├── bulk_delete.rb │ │ │ │ ├── dashboard.rb │ │ │ │ ├── delete.rb │ │ │ │ ├── edit.rb │ │ │ │ ├── export.rb │ │ │ │ ├── history_index.rb │ │ │ │ ├── history_show.rb │ │ │ │ ├── index.rb │ │ │ │ ├── new.rb │ │ │ │ ├── show.rb │ │ │ │ └── show_in_app.rb │ │ │ ├── actions.rb │ │ │ ├── configurable.rb │ │ │ ├── const_load_suppressor.rb │ │ │ ├── fields/ │ │ │ │ ├── association.rb │ │ │ │ ├── base.rb │ │ │ │ ├── collection_association.rb │ │ │ │ ├── factories/ │ │ │ │ │ ├── action_text.rb │ │ │ │ │ ├── active_storage.rb │ │ │ │ │ ├── association.rb │ │ │ │ │ ├── carrierwave.rb │ │ │ │ │ ├── devise.rb │ │ │ │ │ ├── dragonfly.rb │ │ │ │ │ ├── enum.rb │ │ │ │ │ ├── paperclip.rb │ │ │ │ │ ├── password.rb │ │ │ │ │ └── shrine.rb │ │ │ │ ├── group.rb │ │ │ │ ├── singular_association.rb │ │ │ │ ├── types/ │ │ │ │ │ ├── action_text.rb │ │ │ │ │ ├── active_record_enum.rb │ │ │ │ │ ├── active_storage.rb │ │ │ │ │ ├── all.rb │ │ │ │ │ ├── belongs_to_association.rb │ │ │ │ │ ├── boolean.rb │ │ │ │ │ ├── bson_object_id.rb │ │ │ │ │ ├── carrierwave.rb │ │ │ │ │ ├── citext.rb │ │ │ │ │ ├── ck_editor.rb │ │ │ │ │ ├── code_mirror.rb │ │ │ │ │ ├── color.rb │ │ │ │ │ ├── date.rb │ │ │ │ │ ├── datetime.rb │ │ │ │ │ ├── decimal.rb │ │ │ │ │ ├── dragonfly.rb │ │ │ │ │ ├── enum.rb │ │ │ │ │ ├── file_upload.rb │ │ │ │ │ ├── float.rb │ │ │ │ │ ├── froala.rb │ │ │ │ │ ├── has_and_belongs_to_many_association.rb │ │ │ │ │ ├── has_many_association.rb │ │ │ │ │ ├── has_one_association.rb │ │ │ │ │ ├── hidden.rb │ │ │ │ │ ├── inet.rb │ │ │ │ │ ├── integer.rb │ │ │ │ │ ├── json.rb │ │ │ │ │ ├── multiple_active_storage.rb │ │ │ │ │ ├── multiple_carrierwave.rb │ │ │ │ │ ├── multiple_file_upload.rb │ │ │ │ │ ├── numeric.rb │ │ │ │ │ ├── paperclip.rb │ │ │ │ │ ├── password.rb │ │ │ │ │ ├── polymorphic_association.rb │ │ │ │ │ ├── serialized.rb │ │ │ │ │ ├── shrine.rb │ │ │ │ │ ├── simple_mde.rb │ │ │ │ │ ├── string.rb │ │ │ │ │ ├── string_like.rb │ │ │ │ │ ├── text.rb │ │ │ │ │ ├── time.rb │ │ │ │ │ ├── timestamp.rb │ │ │ │ │ ├── uuid.rb │ │ │ │ │ └── wysihtml5.rb │ │ │ │ └── types.rb │ │ │ ├── fields.rb │ │ │ ├── groupable.rb │ │ │ ├── has_description.rb │ │ │ ├── has_fields.rb │ │ │ ├── has_groups.rb │ │ │ ├── hideable.rb │ │ │ ├── inspectable.rb │ │ │ ├── lazy_model.rb │ │ │ ├── model.rb │ │ │ ├── proxyable/ │ │ │ │ └── proxy.rb │ │ │ ├── proxyable.rb │ │ │ ├── sections/ │ │ │ │ ├── base.rb │ │ │ │ ├── create.rb │ │ │ │ ├── edit.rb │ │ │ │ ├── export.rb │ │ │ │ ├── list.rb │ │ │ │ ├── modal.rb │ │ │ │ ├── nested.rb │ │ │ │ ├── show.rb │ │ │ │ └── update.rb │ │ │ └── sections.rb │ │ ├── config.rb │ │ ├── engine.rb │ │ ├── extension.rb │ │ ├── extensions/ │ │ │ ├── cancancan/ │ │ │ │ └── authorization_adapter.rb │ │ │ ├── cancancan.rb │ │ │ ├── controller_extension.rb │ │ │ ├── paper_trail/ │ │ │ │ └── auditing_adapter.rb │ │ │ ├── paper_trail.rb │ │ │ ├── pundit/ │ │ │ │ └── authorization_adapter.rb │ │ │ ├── pundit.rb │ │ │ └── url_for_extension.rb │ │ ├── support/ │ │ │ ├── composite_keys_serializer.rb │ │ │ ├── csv_converter.rb │ │ │ ├── datetime.rb │ │ │ ├── es_module_processor.rb │ │ │ └── hash_helper.rb │ │ └── version.rb │ ├── rails_admin.rb │ └── tasks/ │ ├── .gitkeep │ └── rails_admin.rake ├── package.json ├── rails_admin.gemspec ├── spec/ │ ├── controllers/ │ │ └── rails_admin/ │ │ ├── application_controller_spec.rb │ │ └── main_controller_spec.rb │ ├── dummy_app/ │ │ ├── .browserslistrc │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── Gemfile │ │ ├── Procfile.dev │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── active_record/ │ │ │ │ ├── .gitkeep │ │ │ │ ├── abstract.rb │ │ │ │ ├── another_field_test.rb │ │ │ │ ├── ball.rb │ │ │ │ ├── carrierwave_uploader.rb │ │ │ │ ├── category.rb │ │ │ │ ├── cms/ │ │ │ │ │ └── basic_page.rb │ │ │ │ ├── cms.rb │ │ │ │ ├── comment/ │ │ │ │ │ └── confirmed.rb │ │ │ │ ├── comment.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── taggable.rb │ │ │ │ ├── deeply_nested_field_test.rb │ │ │ │ ├── division.rb │ │ │ │ ├── draft.rb │ │ │ │ ├── fan.rb │ │ │ │ ├── fanship.rb │ │ │ │ ├── favorite_player.rb │ │ │ │ ├── field_test.rb │ │ │ │ ├── hardball.rb │ │ │ │ ├── image.rb │ │ │ │ ├── league.rb │ │ │ │ ├── managed_team.rb │ │ │ │ ├── managing_user.rb │ │ │ │ ├── nested_fan.rb │ │ │ │ ├── nested_favorite_player.rb │ │ │ │ ├── nested_field_test.rb │ │ │ │ ├── paper_trail_test/ │ │ │ │ │ └── subclass_in_namespace.rb │ │ │ │ ├── paper_trail_test.rb │ │ │ │ ├── paper_trail_test_subclass.rb │ │ │ │ ├── paper_trail_test_with_custom_association.rb │ │ │ │ ├── player.rb │ │ │ │ ├── read_only_comment.rb │ │ │ │ ├── restricted_team.rb │ │ │ │ ├── shrine_uploader.rb │ │ │ │ ├── shrine_versioning_uploader.rb │ │ │ │ ├── team.rb │ │ │ │ ├── trail.rb │ │ │ │ ├── two_level/ │ │ │ │ │ ├── namespaced/ │ │ │ │ │ │ └── polymorphic_association_test.rb │ │ │ │ │ └── namespaced.rb │ │ │ │ ├── user/ │ │ │ │ │ └── confirmed.rb │ │ │ │ ├── user.rb │ │ │ │ └── without_table.rb │ │ │ ├── assets/ │ │ │ │ ├── builds/ │ │ │ │ │ └── .keep │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── javascripts/ │ │ │ │ │ ├── application.js │ │ │ │ │ ├── rails-ujs.esm.js.erb │ │ │ │ │ └── rails_admin/ │ │ │ │ │ └── custom/ │ │ │ │ │ └── ui.js │ │ │ │ └── stylesheets/ │ │ │ │ ├── application.css │ │ │ │ ├── rails_admin/ │ │ │ │ │ └── custom/ │ │ │ │ │ └── theming.scss │ │ │ │ └── rails_admin.scss │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ └── players_controller.rb │ │ │ ├── eager_loaded/ │ │ │ │ └── basketball.rb │ │ │ ├── frontend/ │ │ │ │ ├── entrypoints/ │ │ │ │ │ ├── application.js │ │ │ │ │ └── rails_admin.js │ │ │ │ └── stylesheets/ │ │ │ │ └── rails_admin.scss │ │ │ ├── javascript/ │ │ │ │ ├── application.js │ │ │ │ ├── rails_admin.js │ │ │ │ └── rails_admin.scss │ │ │ ├── jobs/ │ │ │ │ ├── application_job.rb │ │ │ │ └── null_job.rb │ │ │ ├── locales/ │ │ │ │ └── models.en.yml │ │ │ ├── mailers/ │ │ │ │ └── .gitkeep │ │ │ ├── mongoid/ │ │ │ │ ├── another_field_test.rb │ │ │ │ ├── ball.rb │ │ │ │ ├── carrierwave_uploader.rb │ │ │ │ ├── category.rb │ │ │ │ ├── cms/ │ │ │ │ │ └── basic_page.rb │ │ │ │ ├── cms.rb │ │ │ │ ├── comment/ │ │ │ │ │ └── confirmed.rb │ │ │ │ ├── comment.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── taggable.rb │ │ │ │ ├── deeply_nested_field_test.rb │ │ │ │ ├── division.rb │ │ │ │ ├── draft.rb │ │ │ │ ├── embed.rb │ │ │ │ ├── fan.rb │ │ │ │ ├── field_test.rb │ │ │ │ ├── hardball.rb │ │ │ │ ├── image.rb │ │ │ │ ├── league.rb │ │ │ │ ├── managed_team.rb │ │ │ │ ├── managing_user.rb │ │ │ │ ├── nested_field_test.rb │ │ │ │ ├── player.rb │ │ │ │ ├── read_only_comment.rb │ │ │ │ ├── restricted_team.rb │ │ │ │ ├── shrine_uploader.rb │ │ │ │ ├── shrine_versioning_uploader.rb │ │ │ │ ├── team.rb │ │ │ │ ├── two_level/ │ │ │ │ │ └── namespaced/ │ │ │ │ │ └── polymorphic_association_test.rb │ │ │ │ ├── user/ │ │ │ │ │ └── confirmed.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ └── application.html.erb │ │ │ └── players/ │ │ │ └── show.html.erb │ │ ├── babel.config.js │ │ ├── bin/ │ │ │ ├── bundle │ │ │ ├── dev │ │ │ ├── docker-entrypoint │ │ │ ├── importmap │ │ │ ├── rails │ │ │ ├── rake │ │ │ ├── setup │ │ │ ├── update │ │ │ ├── vite │ │ │ ├── webpack │ │ │ └── webpack-dev-server │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── database.yml │ │ │ ├── dockerfile.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── importmap.rails_admin.rb │ │ │ ├── importmap.rb │ │ │ ├── initializers/ │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── cors.rb │ │ │ │ ├── devise.rb │ │ │ │ ├── dragonfly.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── mongoid.rb │ │ │ │ ├── rails_admin.rb │ │ │ │ ├── secret_token.rb │ │ │ │ ├── session_patch.rb │ │ │ │ ├── session_store.rb │ │ │ │ ├── shrine.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ ├── devise.en.yml │ │ │ │ ├── en.yml │ │ │ │ └── fr.yml │ │ │ ├── mongoid5.yml │ │ │ ├── mongoid6.yml │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ ├── secrets.yml │ │ │ ├── storage.yml │ │ │ ├── vite.json │ │ │ ├── webpack/ │ │ │ │ ├── development.js │ │ │ │ ├── environment.js │ │ │ │ ├── production.js │ │ │ │ └── test.js │ │ │ └── webpacker.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ ├── 00000000000001_create_divisions_migration.rb │ │ │ │ ├── 00000000000002_create_drafts_migration.rb │ │ │ │ ├── 00000000000003_create_leagues_migration.rb │ │ │ │ ├── 00000000000004_create_players_migration.rb │ │ │ │ ├── 00000000000005_create_teams_migration.rb │ │ │ │ ├── 00000000000006_devise_create_users.rb │ │ │ │ ├── 00000000000007_create_histories_table.rb │ │ │ │ ├── 00000000000008_create_fans_migration.rb │ │ │ │ ├── 00000000000009_create_fans_teams_migration.rb │ │ │ │ ├── 00000000000010_add_revenue_to_team_migration.rb │ │ │ │ ├── 00000000000011_add_suspended_to_player_migration.rb │ │ │ │ ├── 00000000000012_add_avatar_columns_to_user.rb │ │ │ │ ├── 00000000000013_add_roles_to_user.rb │ │ │ │ ├── 00000000000014_add_color_to_team_migration.rb │ │ │ │ ├── 20101223222233_create_rel_tests.rb │ │ │ │ ├── 20110103205808_create_comments.rb │ │ │ │ ├── 20110123042530_rename_histories_to_rails_admin_histories.rb │ │ │ │ ├── 20110224184303_create_field_tests.rb │ │ │ │ ├── 20110328193014_create_cms_basic_pages.rb │ │ │ │ ├── 20110329183136_remove_league_id_from_teams.rb │ │ │ │ ├── 20110607152842_add_format_to_field_test.rb │ │ │ │ ├── 20110714095433_create_balls.rb │ │ │ │ ├── 20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb │ │ │ │ ├── 20110901131551_change_division_primary_key.rb │ │ │ │ ├── 20110901142530_rename_league_id_foreign_key_on_divisions.rb │ │ │ │ ├── 20110901150912_set_primary_key_not_null_for_divisions.rb │ │ │ │ ├── 20110901154834_change_length_for_rails_admin_histories.rb │ │ │ │ ├── 20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb │ │ │ │ ├── 20111115041025_add_type_to_balls.rb │ │ │ │ ├── 20111123092549_create_nested_field_tests.rb │ │ │ │ ├── 20111130075338_add_dragonfly_asset_name_to_field_tests.rb │ │ │ │ ├── 20111215083258_create_foo_bars.rb │ │ │ │ ├── 20120117151733_add_custom_field_to_teams.rb │ │ │ │ ├── 20120118122004_add_categories.rb │ │ │ │ ├── 20120319041705_drop_rel_tests.rb │ │ │ │ ├── 20120720075608_create_another_field_tests.rb │ │ │ │ ├── 20120928075608_create_images.rb │ │ │ │ ├── 20140412075608_create_deeply_nested_field_tests.rb │ │ │ │ ├── 20140826093220_create_paper_trail_tests.rb │ │ │ │ ├── 20140826093552_create_versions.rb │ │ │ │ ├── 20150815102450_add_refile_to_field_tests.rb │ │ │ │ ├── 20151027181550_change_field_test_id_to_nested_field_tests.rb │ │ │ │ ├── 20160728152942_add_main_sponsor_to_teams.rb │ │ │ │ ├── 20160728153058_add_formation_to_players.rb │ │ │ │ ├── 20171229220713_add_enum_fields_to_field_tests.rb │ │ │ │ ├── 20180701084251_create_active_storage_tables.active_storage.rb │ │ │ │ ├── 20180707101855_add_carrierwave_assets_to_field_tests.rb │ │ │ │ ├── 20181029101829_add_shrine_data_to_field_tests.rb │ │ │ │ ├── 20190531065324_create_action_text_tables.action_text.rb │ │ │ │ ├── 20201127111952_update_active_storage_tables.rb │ │ │ │ ├── 20210811121027_create_two_level_namespaced_polymorphic_association_tests.rb │ │ │ │ ├── 20210812115908_create_custom_versions.rb │ │ │ │ ├── 20211011235734_add_bool_field_open.rb │ │ │ │ ├── 20220416102741_create_composite_key_tables.rb │ │ │ │ └── 20240921171953_add_non_nullable_boolean_field.rb │ │ │ ├── schema.rb │ │ │ └── seeds.rb │ │ ├── doc/ │ │ │ └── README_FOR_APP │ │ ├── fly.toml │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── does_not_load_autoload_paths_not_in_eager_load.rb │ │ │ └── tasks/ │ │ │ └── .gitkeep │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ ├── robots.txt │ │ │ └── system/ │ │ │ └── dragonfly/ │ │ │ └── development/ │ │ │ └── 2011/ │ │ │ └── 11/ │ │ │ ├── 24/ │ │ │ │ └── 10_36_27_888_Pensive_Parakeet.jpg.meta │ │ │ └── 30/ │ │ │ └── 08_54_39_906_Costa_Rican_Frog.jpg.meta │ │ ├── vendor/ │ │ │ └── javascript/ │ │ │ └── .keep │ │ ├── vite.config.ts │ │ └── webpack.config.js │ ├── factories.rb │ ├── fixtures/ │ │ └── test.txt │ ├── helpers/ │ │ └── rails_admin/ │ │ ├── application_helper_spec.rb │ │ ├── form_builder_spec.rb │ │ └── main_helper_spec.rb │ ├── integration/ │ │ ├── actions/ │ │ │ ├── base_spec.rb │ │ │ ├── bulk_delete_spec.rb │ │ │ ├── dashboard_spec.rb │ │ │ ├── delete_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── export_spec.rb │ │ │ ├── history_index_spec.rb │ │ │ ├── history_show_spec.rb │ │ │ ├── index_spec.rb │ │ │ ├── new_spec.rb │ │ │ ├── show_in_app_spec.rb │ │ │ └── show_spec.rb │ │ ├── auditing/ │ │ │ └── paper_trail_spec.rb │ │ ├── authentication/ │ │ │ └── devise_spec.rb │ │ ├── authorization/ │ │ │ ├── cancancan_spec.rb │ │ │ └── pundit_spec.rb │ │ ├── fields/ │ │ │ ├── action_text_spec.rb │ │ │ ├── active_record_enum_spec.rb │ │ │ ├── active_storage_spec.rb │ │ │ ├── base_spec.rb │ │ │ ├── belongs_to_association_spec.rb │ │ │ ├── boolean_spec.rb │ │ │ ├── carrierwave_spec.rb │ │ │ ├── ck_editor_spec.rb │ │ │ ├── code_mirror_spec.rb │ │ │ ├── color_spec.rb │ │ │ ├── date_spec.rb │ │ │ ├── datetime_spec.rb │ │ │ ├── enum_spec.rb │ │ │ ├── file_upload_spec.rb │ │ │ ├── floara_spec.rb │ │ │ ├── has_and_belongs_to_many_association_spec.rb │ │ │ ├── has_many_association_spec.rb │ │ │ ├── has_one_association_spec.rb │ │ │ ├── hidden_spec.rb │ │ │ ├── multiple_active_storage_spec.rb │ │ │ ├── multiple_carrierwave_spec.rb │ │ │ ├── multiple_file_upload_spec.rb │ │ │ ├── paperclip_spec.rb │ │ │ ├── polymorphic_assosiation_spec.rb │ │ │ ├── serialized_spec.rb │ │ │ ├── shrine_spec.rb │ │ │ ├── simple_mde_spec.rb │ │ │ ├── time_spec.rb │ │ │ └── wysihtml5_spec.rb │ │ ├── rails_admin_spec.rb │ │ └── widgets/ │ │ ├── datetimepicker_spec.rb │ │ ├── filter_box_spec.rb │ │ ├── filtering_multi_select_spec.rb │ │ ├── filtering_select_spec.rb │ │ ├── nested_many_spec.rb │ │ ├── nested_one_spec.rb │ │ └── remote_form_spec.rb │ ├── orm/ │ │ ├── active_record.rb │ │ └── mongoid.rb │ ├── policies.rb │ ├── rails_admin/ │ │ ├── abstract_model_spec.rb │ │ ├── active_record_extension_spec.rb │ │ ├── adapters/ │ │ │ ├── active_record/ │ │ │ │ ├── association_spec.rb │ │ │ │ ├── object_extension_spec.rb │ │ │ │ └── property_spec.rb │ │ │ ├── active_record_spec.rb │ │ │ ├── mongoid/ │ │ │ │ ├── association_spec.rb │ │ │ │ ├── object_extension_spec.rb │ │ │ │ └── property_spec.rb │ │ │ └── mongoid_spec.rb │ │ ├── config/ │ │ │ ├── actions/ │ │ │ │ └── base_spec.rb │ │ │ ├── actions_spec.rb │ │ │ ├── configurable_spec.rb │ │ │ ├── const_load_suppressor_spec.rb │ │ │ ├── fields/ │ │ │ │ ├── association_spec.rb │ │ │ │ ├── base_spec.rb │ │ │ │ └── types/ │ │ │ │ ├── action_text_spec.rb │ │ │ │ ├── active_record_enum_spec.rb │ │ │ │ ├── active_storage_spec.rb │ │ │ │ ├── belongs_to_association_spec.rb │ │ │ │ ├── boolean_spec.rb │ │ │ │ ├── bson_object_id_spec.rb │ │ │ │ ├── carrierwave_spec.rb │ │ │ │ ├── citext_spec.rb │ │ │ │ ├── ck_editor_spec.rb │ │ │ │ ├── code_mirror_spec.rb │ │ │ │ ├── color_spec.rb │ │ │ │ ├── date_spec.rb │ │ │ │ ├── datetime_spec.rb │ │ │ │ ├── decimal_spec.rb │ │ │ │ ├── drangonfly_spec.rb │ │ │ │ ├── enum_spec.rb │ │ │ │ ├── file_upload_spec.rb │ │ │ │ ├── float_spec.rb │ │ │ │ ├── froala_spec.rb │ │ │ │ ├── has_and_belongs_to_many_association_spec.rb │ │ │ │ ├── has_many_association_spec.rb │ │ │ │ ├── has_one_association_spec.rb │ │ │ │ ├── hidden_spec.rb │ │ │ │ ├── inet_spec.rb │ │ │ │ ├── integer_spec.rb │ │ │ │ ├── json_spec.rb │ │ │ │ ├── multiple_active_storage_spec.rb │ │ │ │ ├── multiple_carrierwave_spec.rb │ │ │ │ ├── multiple_file_upload_spec.rb │ │ │ │ ├── numeric_spec.rb │ │ │ │ ├── paperclip_spec.rb │ │ │ │ ├── password_spec.rb │ │ │ │ ├── serialized_spec.rb │ │ │ │ ├── shrine_spec.rb │ │ │ │ ├── simple_mde_spec.rb │ │ │ │ ├── string_like_spec.rb │ │ │ │ ├── string_spec.rb │ │ │ │ ├── text_spec.rb │ │ │ │ ├── time_spec.rb │ │ │ │ ├── timestamp_spec.rb │ │ │ │ ├── uuid_spec.rb │ │ │ │ └── wysihtml5_spec.rb │ │ │ ├── fields_spec.rb │ │ │ ├── has_description_spec.rb │ │ │ ├── has_fields_spec.rb │ │ │ ├── lazy_model_spec.rb │ │ │ ├── model_spec.rb │ │ │ ├── proxyable_spec.rb │ │ │ ├── sections/ │ │ │ │ └── list_spec.rb │ │ │ └── sections_spec.rb │ │ ├── config_spec.rb │ │ ├── engine_spec.rb │ │ ├── extentions/ │ │ │ ├── cancancan/ │ │ │ │ └── authorization_adapter_spec.rb │ │ │ └── paper_trail/ │ │ │ ├── auditing_adapter_spec.rb │ │ │ └── version_proxy_spec.rb │ │ ├── install_generator_spec.rb │ │ ├── support/ │ │ │ ├── csv_converter_spec.rb │ │ │ ├── datetime_spec.rb │ │ │ └── hash_helper_spec.rb │ │ └── version_spec.rb │ ├── shared_examples/ │ │ └── shared_examples_for_field_types.rb │ ├── spec_helper.rb │ └── support/ │ ├── cuprite_logger.rb │ ├── fakeio.rb │ ├── fixtures.rb │ └── jquery.simulate.drag-sortable.js ├── src/ │ └── rails_admin/ │ ├── abstract-select.js │ ├── base.js │ ├── filter-box.js │ ├── filtering-multiselect.js │ ├── filtering-select.js │ ├── i18n.js │ ├── jquery.js │ ├── nested-form-hooks.js │ ├── remote-form.js │ ├── sidescroll.js │ ├── styles/ │ │ ├── base/ │ │ │ ├── README.txt │ │ │ ├── mixins.scss │ │ │ ├── theming.scss │ │ │ └── variables.scss │ │ ├── base.scss │ │ ├── filtering-multiselect.scss │ │ ├── filtering-select.scss │ │ └── widgets.scss │ ├── ui.js │ ├── vendor/ │ │ └── jquery_nested_form.js │ └── widgets.js └── vendor/ └── assets/ ├── javascripts/ │ └── rails_admin/ │ ├── bootstrap.js │ ├── flatpickr-with-locales.js │ ├── jquery-ui/ │ │ ├── data.js │ │ ├── effect.js │ │ ├── ie.js │ │ ├── keycode.js │ │ ├── position.js │ │ ├── safe-active-element.js │ │ ├── scroll-parent.js │ │ ├── unique-id.js │ │ ├── version.js │ │ ├── widget.js │ │ └── widgets/ │ │ ├── autocomplete.js │ │ ├── menu.js │ │ ├── mouse.js │ │ └── sortable.js │ ├── jquery3.js │ └── popper.js └── stylesheets/ └── rails_admin/ ├── bootstrap/ │ ├── _accordion.scss │ ├── _alert.scss │ ├── _badge.scss │ ├── _breadcrumb.scss │ ├── _button-group.scss │ ├── _buttons.scss │ ├── _card.scss │ ├── _carousel.scss │ ├── _close.scss │ ├── _containers.scss │ ├── _dropdown.scss │ ├── _forms.scss │ ├── _functions.scss │ ├── _grid.scss │ ├── _helpers.scss │ ├── _images.scss │ ├── _list-group.scss │ ├── _mixins.scss │ ├── _modal.scss │ ├── _nav.scss │ ├── _navbar.scss │ ├── _offcanvas.scss │ ├── _pagination.scss │ ├── _placeholders.scss │ ├── _popover.scss │ ├── _progress.scss │ ├── _reboot.scss │ ├── _root.scss │ ├── _spinners.scss │ ├── _tables.scss │ ├── _toasts.scss │ ├── _tooltip.scss │ ├── _transitions.scss │ ├── _type.scss │ ├── _utilities.scss │ ├── _variables.scss │ ├── bootstrap.scss │ ├── forms/ │ │ ├── _floating-labels.scss │ │ ├── _form-check.scss │ │ ├── _form-control.scss │ │ ├── _form-range.scss │ │ ├── _form-select.scss │ │ ├── _form-text.scss │ │ ├── _input-group.scss │ │ ├── _labels.scss │ │ └── _validation.scss │ ├── helpers/ │ │ ├── _clearfix.scss │ │ ├── _colored-links.scss │ │ ├── _position.scss │ │ ├── _ratio.scss │ │ ├── _stacks.scss │ │ ├── _stretched-link.scss │ │ ├── _text-truncation.scss │ │ ├── _visually-hidden.scss │ │ └── _vr.scss │ ├── mixins/ │ │ ├── _alert.scss │ │ ├── _backdrop.scss │ │ ├── _border-radius.scss │ │ ├── _box-shadow.scss │ │ ├── _breakpoints.scss │ │ ├── _buttons.scss │ │ ├── _caret.scss │ │ ├── _clearfix.scss │ │ ├── _color-scheme.scss │ │ ├── _container.scss │ │ ├── _deprecate.scss │ │ ├── _forms.scss │ │ ├── _gradients.scss │ │ ├── _grid.scss │ │ ├── _image.scss │ │ ├── _list-group.scss │ │ ├── _lists.scss │ │ ├── _pagination.scss │ │ ├── _reset-text.scss │ │ ├── _resize.scss │ │ ├── _table-variants.scss │ │ ├── _text-truncate.scss │ │ ├── _transition.scss │ │ ├── _utilities.scss │ │ └── _visually-hidden.scss │ ├── utilities/ │ │ └── _api.scss │ └── vendor/ │ └── _rfs.scss ├── flatpickr.css └── font-awesome.scss ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: "" assignees: "" --- **Describe the bug** A clear and concise description of what the bug is. **Reproduction steps** Steps to reproduce the issue seen. **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** - `rails` version: - `rails_admin` version: - `rails_admin` npm package version: - full stack trace (if there's an exception) Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "" labels: enhancement assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe proposed solution(s)** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/code-ql.yml ================================================ name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: "12 00 * * 5" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["ruby"] steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: [push, pull_request] jobs: rspec: name: RSpec strategy: fail-fast: false matrix: ruby: - "3.2" - "3.3" - "3.4" gemfile: [gemfiles/rails_8.0.gemfile] orm: [active_record] adapter: [sqlite3] asset: [webpack] include: - ruby: 2.6 gemfile: gemfiles/rails_6.0.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: 2.7 gemfile: gemfiles/rails_6.1.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: 2.7 gemfile: gemfiles/rails_6.1.gemfile orm: active_record adapter: sqlite3 asset: webpacker - ruby: "3.0" gemfile: gemfiles/rails_7.0.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: "3.1" gemfile: gemfiles/rails_7.1.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: "3.2" gemfile: gemfiles/rails_7.2.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: "3.4" gemfile: gemfiles/rails_8.0.gemfile orm: active_record adapter: mysql2 asset: importmap - ruby: "3.4" gemfile: gemfiles/rails_8.0.gemfile orm: active_record adapter: postgresql asset: sprockets - ruby: "3.4" gemfile: gemfiles/rails_8.0.gemfile orm: active_record adapter: sqlite3 asset: vite - ruby: "3.4" gemfile: gemfiles/rails_8.0.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: "3.2" gemfile: gemfiles/composite_primary_keys.gemfile orm: active_record adapter: sqlite3 asset: sprockets - ruby: 2.7 gemfile: gemfiles/rails_6.0.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: "3.0" gemfile: gemfiles/rails_6.1.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: "3.1" gemfile: gemfiles/rails_7.0.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: "3.2" gemfile: gemfiles/rails_7.1.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: "3.3" gemfile: gemfiles/rails_7.2.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: "3.4" gemfile: gemfiles/rails_8.0.gemfile orm: mongoid adapter: sqlite3 asset: sprockets - ruby: jruby-9.4 gemfile: gemfiles/rails_7.1.gemfile orm: active_record adapter: mysql2 asset: sprockets - ruby: jruby-9.4 gemfile: gemfiles/rails_7.1.gemfile orm: mongoid adapter: sqlite3 asset: sprockets runs-on: ubuntu-latest services: mysql: image: mysql:8.0 ports: - 3306:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes postgres: image: postgres:11 ports: - 5432:5432 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mongo: image: mongo:4.4 ports: - 27017:27017 env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} CI_ORM: ${{ matrix.orm }} CI_ASSET: ${{ matrix.asset }} JRUBY_OPTS: --debug steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true cache-version: gems-${{ hashFiles('Gemfile', 'gemfiles/*.gemfile') }} env: MAKEFLAGS: make --jobs 4 BUNDLE_WITHOUT: development - name: Set up Node uses: actions/setup-node@v4 with: node-version: "20" - name: Install ImageMagick run: sudo apt-get install imagemagick - name: Setup application env: BUNDLE_GEMFILE: ../../${{ matrix.gemfile }} CI_ASSET: ${{ matrix.asset }} CI_DB_ADAPTER: ${{ matrix.adapter }} RAILS_ENV: test NODE_OPTIONS: --openssl-legacy-provider run: | yarn install cd spec/dummy_app bundle exec rake rails_admin:prepare_ci_env db:create db:schema:load yarn install case "$CI_ASSET" in "webpack" ) yarn build && yarn build:css ;; "importmap" ) yarn build:css ;; esac cd ../../ - name: Run tests run: bundle exec rspec - name: Coveralls Parallel if: ${{ github.repository_owner == 'railsadminteam' }} uses: coverallsapp/github-action@v2 continue-on-error: true with: github-token: ${{ secrets.github_token }} flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }}-${{ matrix.orm }}-${{ matrix.adapter }} parallel: true coveralls: name: Coveralls if: ${{ github.repository_owner == 'railsadminteam' }} needs: rspec runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true prettier: name: Prettier runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 - name: Install dependencies run: yarn install - name: Run check run: yarn run prettier --check . rubocop: name: RuboCop runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3.2" bundler-cache: true cache-version: gems-${{ hashFiles('Gemfile') }} - name: Install dependencies run: bundle install --without development --jobs=3 --retry=3 --path=vendor/bundle - name: Run check run: bundle exec rake rubocop ================================================ FILE: .gitignore ================================================ *.gem *.log *.rbc *.swp .bundle .idea/ .vscode/ .rvmrc .sass-cache .yardoc /.emacs.desktop /gemfiles/*.lock /node_modules/* /rails_admin.gems /spec/generators/tmp /spec/lib/tmp /yarn.lock Gemfile.lock coverage/* db/*.sqlite3 db/*.sqlite3-journal doc/* log/*.log pkg/* spec/dummy_app/db/*.sqlite3 spec/dummy_app/db/*.sqlite3-journal spec/dummy_app/log/*.log spec/dummy_app/public/uploads spec/dummy_app/Gemfile*.lock tmp/**/* nbproject ================================================ FILE: .prettierignore ================================================ coverage lib/generators/rails_admin/templates spec/dummy_app/app/assets/builds spec/dummy_app/public spec/dummy_app/tmp spec/support/jquery.simulate.drag-sortable.js vendor ================================================ FILE: .rspec ================================================ --color --order=random --profile --exclude-pattern 'dummy_app/node_modules/rails_admin/**/*_spec.rb' ================================================ FILE: .rubocop.yml ================================================ inherit_from: .rubocop_todo.yml require: - rubocop-performance AllCops: Exclude: - "gemfiles/*" - "node_modules/**/*" - "spec/dummy_app/bin/**/*" - "spec/dummy_app/db/schema.rb" - "spec/dummy_app/node_modules/**/*" - "spec/dummy_app/tmp/**/*" - "vendor/bundle/**/*" NewCops: disable SuggestExtensions: false TargetRubyVersion: 2.6 Gemspec/DeprecatedAttributeAssignment: Enabled: true Layout/AccessModifierIndentation: EnforcedStyle: outdent Layout/DotPosition: EnforcedStyle: trailing Layout/LineEndStringConcatenationIndentation: Enabled: true Layout/LineLength: AllowURI: true Enabled: false Layout/MultilineAssignmentLayout: Enabled: true SupportedTypes: [case, if] Layout/SpaceBeforeBrackets: Enabled: true Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Lint/AmbiguousAssignment: Enabled: true Lint/AmbiguousRange: Enabled: true Lint/DeprecatedConstants: Enabled: true Lint/DuplicateBranch: Enabled: true IgnoreLiteralBranches: true Lint/DuplicateRegexpCharacterClassElement: Enabled: true Lint/EmptyBlock: Enabled: true Exclude: - "spec/**/*" Lint/EmptyClass: Enabled: true Exclude: - "spec/**/*" Lint/EmptyInPattern: Enabled: true Lint/LambdaWithoutLiteralBlock: Enabled: true Lint/NoReturnInBeginEndBlocks: Enabled: true Lint/NumberedParameterAssignment: Enabled: true Lint/OrAssignmentToConstant: Enabled: true Lint/RedundantDirGlobSort: Enabled: true Lint/SymbolConversion: Enabled: true Lint/ToEnumArguments: Enabled: true Lint/TripleQuotes: Enabled: true Lint/UnexpectedBlockArity: Enabled: true Lint/UnmodifiedReduceAccumulator: Enabled: true Metrics/AbcSize: Max: 69.98 # TODO: Lower to 15 Metrics/BlockNesting: Max: 3 Metrics/ClassLength: CountComments: false Max: 201 # TODO: Lower to 100 Metrics/CyclomaticComplexity: Max: 15 # TODO: Lower to 6 Metrics/MethodLength: CountComments: false Max: 29 # TODO: Lower to 15 Metrics/ModuleLength: Max: 219 # TODO: Lower to 100 Metrics/ParameterLists: Max: 8 # TODO: Lower to 4 CountKeywordArgs: true Metrics/PerceivedComplexity: Max: 17 # TODO: Lower to 7 Naming/FileName: Exclude: - "lib/rails_admin/bootstrap-sass.rb" Naming/InclusiveLanguage: Enabled: true Style/Alias: Enabled: false Style/ArgumentsForwarding: Enabled: true Style/CollectionCompact: Enabled: true Style/CollectionMethods: PreferredMethods: map: "collect" reduce: "inject" find: "detect" find_all: "select" Style/Documentation: Enabled: false Style/DocumentDynamicEvalDefinition: Enabled: false Style/DoubleNegation: Enabled: false Style/EachWithObject: Enabled: false Style/EndlessMethod: Enabled: true Style/HashConversion: Enabled: true Style/HashExcept: Enabled: true Style/IfWithBooleanLiteralBranches: Enabled: true Style/InPatternThen: Enabled: true Style/Lambda: Enabled: false Style/MultilineInPatternThen: Enabled: true Style/NegatedIfElseCondition: Enabled: true Style/NumericPredicate: Enabled: false Style/NilLambda: Enabled: true Style/QuotedSymbols: Enabled: true Style/RaiseArgs: EnforcedStyle: compact Style/RedundantArgument: Enabled: true Style/RedundantParentheses: Enabled: false Style/RedundantSelfAssignmentBranch: Enabled: true Style/StringChars: Enabled: true Style/SwapValues: Enabled: true Style/TrailingCommaInArguments: EnforcedStyleForMultiline: "comma" Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: "comma" Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: "comma" ================================================ FILE: .rubocop_todo.yml ================================================ # This configuration was generated by # `rubocop --auto-gen-config` # on 2021-11-20 13:57:54 UTC using RuboCop version 1.23.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 51 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: Enabled: false # Offense count: 1 Lint/ReturnInVoidContext: Exclude: - "lib/rails_admin/support/csv_converter.rb" # Offense count: 226 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. # IgnoredMethods: refine Metrics/BlockLength: Max: 1135 # Offense count: 1 # Configuration parameters: Max, CountKeywordArgs. Metrics/ParameterLists: MaxOptionalParameters: 4 # Offense count: 6 Naming/AccessorMethodName: Exclude: - "app/controllers/rails_admin/application_controller.rb" - "app/controllers/rails_admin/main_controller.rb" - "lib/rails_admin/abstract_model.rb" # Offense count: 2 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - "app/controllers/rails_admin/application_controller.rb" - "lib/rails_admin/config/has_description.rb" # Offense count: 2 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - "lib/rails_admin/abstract_model.rb" - "spec/rails_admin/adapters/mongoid/property_spec.rb" # Offense count: 1 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 Naming/VariableNumber: Exclude: - "spec/helpers/rails_admin/application_helper_spec.rb" # Offense count: 11 Style/ClassVars: Exclude: - "lib/rails_admin/abstract_model.rb" - "lib/rails_admin/config.rb" - "lib/rails_admin/config/actions.rb" - "lib/rails_admin/config/fields.rb" - "lib/rails_admin/config/fields/types.rb" # Offense count: 2 # Configuration parameters: MaxUnannotatedPlaceholdersAllowed, IgnoredMethods. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template # Offense count: 8 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - "app/helpers/rails_admin/main_helper.rb" - "lib/rails_admin.rb" - "lib/rails_admin/bootstrap-sass.rb" - "lib/rails_admin/config.rb" - "lib/rails_admin/config/actions.rb" - "lib/rails_admin/config/fields/types/polymorphic_association.rb" - "lib/rails_admin/config/sections/list.rb" # Offense count: 2 Style/MissingRespondToMissing: Exclude: - "lib/rails_admin/config/model.rb" - "lib/rails_admin/config/proxyable/proxy.rb" # Offense count: 17 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - "app/helpers/rails_admin/application_helper.rb" - "lib/rails_admin/config/fields/types/active_storage.rb" - "lib/rails_admin/config/fields/types/carrierwave.rb" - "lib/rails_admin/config/fields/types/dragonfly.rb" - "lib/rails_admin/config/fields/types/multiple_active_storage.rb" - "lib/rails_admin/config/fields/types/multiple_carrierwave.rb" - "lib/rails_admin/config/fields/types/multiple_file_upload.rb" - "lib/rails_admin/config/fields/types/paperclip.rb" - "lib/rails_admin/config/has_fields.rb" - "spec/integration/fields/file_upload_spec.rb" - "spec/integration/fields/multiple_file_upload_spec.rb" - "spec/orm/active_record.rb" - "spec/orm/mongoid.rb" - "spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb" # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - "app/helpers/rails_admin/application_helper.rb" - "lib/rails_admin/extensions/paper_trail/auditing_adapter.rb" # Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: Exclude: - "app/helpers/rails_admin/application_helper.rb" ================================================ FILE: .teatro.yml ================================================ project: platform: rails stage: before: - export CI_DB_ADAPTER=postgresql - export CI_ORM=active_record - cp Procfile.teatro Procfile - cd spec/dummy_app - cp ../../config/database.yml config/ - bundle install --without development --jobs=3 --retry=3 - ln -sf $PWD/public ../../public database: - bundle exec rake db:create db:migrate db:seed config: database: postgresql services: - postgresql ================================================ FILE: .yardopts ================================================ --no-private --protected --markup markdown - LICENSE.md ================================================ FILE: Appraisals ================================================ # frozen_string_literal: true appraise 'rails-6.0' do gem 'rails', '~> 6.0.0' gem 'concurrent-ruby', '1.3.4' # Workaround for https://github.com/rails/rails/issues/54260 gem 'psych', '~> 3.3' gem 'turbo-rails', '< 2.0.8' group :test do gem 'cancancan', ['~> 3.0', '< 3.6'] gem 'pundit', '~> 2.1.0' end group :active_record do platforms :jruby do gem 'activerecord-jdbcmysql-adapter', '~> 60.0' gem 'activerecord-jdbcpostgresql-adapter', '~> 60.0' gem 'activerecord-jdbcsqlite3-adapter', '~> 60.0' end end group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 7.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'rails-6.1' do gem 'rails', '~> 6.1.0' gem 'concurrent-ruby', '1.3.4' # Workaround for https://github.com/rails/rails/issues/54260 group :active_record do platforms :jruby do gem 'activerecord-jdbcmysql-adapter', '~> 61.0' gem 'activerecord-jdbcpostgresql-adapter', '~> 61.0' gem 'activerecord-jdbcsqlite3-adapter', '~> 61.0' end end group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 7.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'rails-7.0' do gem 'rails', '~> 7.0.0', '7.0.8.6' # Pinning until the fix for https://github.com/basecamp/trix/issues/1209 become available in actiontext gem 'concurrent-ruby', '1.3.4' # Workaround for https://github.com/rails/rails/issues/54260 gem 'importmap-rails', require: false gem 'nokogiri', '~> 1.16.0', platform: :jruby group :active_record do platforms :ruby, :mswin, :mingw, :x64_mingw do gem 'sqlite3', '~> 1.3' end platforms :jruby do gem 'activerecord-jdbcmysql-adapter', '~> 70.0' gem 'activerecord-jdbcpostgresql-adapter', '~> 70.0' gem 'activerecord-jdbcsqlite3-adapter', '~> 70.0' end end group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 8.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'rails-7.1' do gem 'rails', '~> 7.1.0' gem 'importmap-rails', require: false group :active_record do platforms :ruby, :mswin, :mingw, :x64_mingw do gem 'sqlite3', '~> 1.3' end platforms :jruby do gem 'activerecord-jdbcmysql-adapter', '~> 71.0' gem 'activerecord-jdbcpostgresql-adapter', '~> 71.0' gem 'activerecord-jdbcsqlite3-adapter', '~> 71.0' end end group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 8.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'rails-7.2' do gem 'rails', '~> 7.2.0' gem 'importmap-rails', require: false group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 8.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'rails-8.0' do gem 'rails', '~> 8.0.0' gem 'importmap-rails', require: false group :mongoid do gem 'cancancan-mongoid' gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' gem 'mongoid', '~> 9.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' gem 'shrine-mongoid', '~> 1.0' end end appraise 'composite_primary_keys' do gem 'rails', '~> 7.0.0', '7.0.8.6' # Pinning until the fix for https://github.com/basecamp/trix/issues/1209 become available in actiontext gem 'concurrent-ruby', '1.3.4' # Workaround for https://github.com/rails/rails/issues/54260 group :active_record do gem 'composite_primary_keys' platforms :ruby, :mswin, :mingw, :x64_mingw do gem 'sqlite3', '~> 1.3' end end end ================================================ FILE: CHANGELOG.md ================================================ # RailsAdmin Changelog ## [Unreleased](https://github.com/railsadminteam/rails_admin/tree/HEAD) [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.3.0...HEAD) ## [3.3.0](https://github.com/railsadminteam/rails_admin/tree/v3.3.0) - 2024-12-08 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.1...v3.3.0) ### Added - Rails 8.0 support ([#3702](https://github.com/railsadminteam/rails_admin/pull/3702)) ## [3.2.1](https://github.com/railsadminteam/rails_admin/tree/v3.2.0) - 2024-10-10 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0...v3.2.1) ### Fixed - Disable Turbo's prefetch behavior globally, to prevent custom actions unintentionally triggered ([f54a102](https://github.com/railsadminteam/rails_admin/commit/f54a102c6b0a420244ef044503944574ef1dfbd2), [#3701](https://github.com/railsadminteam/rails_admin/issues/3701)) ## [3.2.0](https://github.com/railsadminteam/rails_admin/tree/v3.2.0) - 2024-09-08 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0.rc...v3.2.0) ### Fixed - Fix polymorphic id doesn't reset properly in an edge case ([7b2ffb1](https://github.com/railsadminteam/rails_admin/commit/7b2ffb12386e06a0e6e0bace6d331fc5af989e38), [#3630](https://github.com/railsadminteam/rails_admin/pull/3630)) ## [3.2.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.2.0.rc) - 2024-08-25 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0.beta...v3.2.0.rc) ### Added - ActiveRecord 7.1 composite primary keys support ([53b89c9](https://github.com/railsadminteam/rails_admin/commit/53b89c9161e48c0f9b4ecd5f544398c9360ea50f)) ### Changed - Introduce SingularAssociation and CollectionAssociation to tidy up the view files ([876be11](https://github.com/railsadminteam/rails_admin/commit/876be11ec01237596b2f27e15239e86418ce7610)) ### Fixed - Fix to show a thumbnail of all representable ActiveStorage attachments ([#3656](https://github.com/railsadminteam/rails_admin/pull/3656), [7754ac3](https://github.com/railsadminteam/rails_admin/commit/7754ac34eb8e0af7605b2e52ae0646b17e9bb0c6)) - Fix to detect images properly in FileUpload and MultipleFileUpload field ([35c8702](https://github.com/railsadminteam/rails_admin/commit/35c8702351aa300bddcc950d36d68b80742f5011), [#3633](https://github.com/railsadminteam/rails_admin/pull/3633)) - Fix to reset polymorphic id selection upon type change ([#3630](https://github.com/railsadminteam/rails_admin/pull/3630), [13114e5](https://github.com/railsadminteam/rails_admin/commit/13114e5629d49eab14d58df1319eb068dacedba7)) - Lock jQuery UI version due to incompatibility with 1.14 ([5245d5b](https://github.com/railsadminteam/rails_admin/commit/5245d5bb91691d646219b5243f3f881a0144a3fd), [#3692](https://github.com/railsadminteam/rails_admin/issues/3692)) ## [3.2.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.2.0.beta) - 2024-07-13 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.4...v3.2.0.beta) ### Added - Allow turbo-rails 2 in gemspec ([#3671](https://github.com/railsadminteam/rails_admin/pull/3671)) - ViteRuby integration ([#3643](https://github.com/railsadminteam/rails_admin/pull/3643), [0e12e5b](https://github.com/railsadminteam/rails_admin/commit/0e12e5b465997c14d5b7a4d500a0d4cebed21aa9)) - Support client-side dynamic scoping ([12715f2](https://github.com/railsadminteam/rails_admin/commit/12715f2dd12d97f0676e548e4271906df424b89d), [#2934](https://github.com/railsadminteam/rails_admin/issues/2934)) - Add support for `%-l` option to Flatpickr ([#3616](https://github.com/railsadminteam/rails_admin/pull/3616)) ### Changed - Handle has_one assignment on the field level, making patched has_one getters/setters unnecessary ([91737ab](https://github.com/railsadminteam/rails_admin/commit/91737ab3c2fa22cbe08aedd28770a12704fde6c7)) ### Fixed - Use require_relative to avoid modifying $LOAD_PATH in gemspec ([#3690](https://github.com/railsadminteam/rails_admin/pull/3690)) - Tidy up trailing whitespace in gem post_install_message ([#3689](https://github.com/railsadminteam/rails_admin/pull/3689)) - Fix enum filter breaking when pre-populated ([d62f604](https://github.com/railsadminteam/rails_admin/commit/d62f604cc8d7d1434f7dfe0c5aca3aaf3dc2547b), [#3651](https://github.com/railsadminteam/rails_admin/issues/3651)) - Fix error on searching or sorting by ActiveStorage field ([dba6c4b](https://github.com/railsadminteam/rails_admin/commit/dba6c4b815fbe4aa4f62a13b660e865a89151838), [#3678](https://github.com/railsadminteam/rails_admin/issues/3678)) - Fix to remove trailing slash from meta tags ([#3672](https://github.com/railsadminteam/rails_admin/pull/3672)) - Fix default config path for ImportmapFormatter being misspelled ([#3676](https://github.com/railsadminteam/rails_admin/pull/3676)) - Fix table names not quoted properly on sorting ([#3652](https://github.com/railsadminteam/rails_admin/pull/3652), [#1631](https://github.com/railsadminteam/rails_admin/issues/1631)) - Fix ActiveStorage/ActionText detection less likely to cause false positives ([073b809](https://github.com/railsadminteam/rails_admin/commit/073b809853b6bc231841e3f8dd9d35875220c616), [#3659](https://github.com/railsadminteam/rails_admin/issues/3659)) - Fix boolean fields in a Mongoid embedded document fails to be updated ([#3555](https://github.com/railsadminteam/rails_admin/pull/3555), [#3554](https://github.com/railsadminteam/rails_admin/issues/3554)) - Fix wrongly referring to `:update_only` in nested fields ([#3649](https://github.com/railsadminteam/rails_admin/pull/3649)) - Fix to use HTML `q` element for better localization support ([#3636](https://github.com/railsadminteam/rails_admin/pull/3636)) - Fix `is_blank` and `is_present` filters breaking for uuid columns ([#3629](https://github.com/railsadminteam/rails_admin/pull/3629), [#3669](https://github.com/railsadminteam/rails_admin/issues/3669)) - Fix polymorphic association target classes not set correctly in subclasses ([2a89ebc](https://github.com/railsadminteam/rails_admin/commit/2a89ebcfa96243697988f6570b9c9be19a7a01b5), [#3631](https://github.com/railsadminteam/rails_admin/issues/3631)) ### Security - Validate `return_to` param using `request.base_url` to prevent arbitrary redirection ([#3627](https://github.com/railsadminteam/rails_admin/pull/3627)) ## [3.1.4](https://github.com/railsadminteam/rails_admin/tree/v3.1.4) - 2024-07-09 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.3...v3.1.4) ### Fixed - Fix [32f91e4](https://github.com/railsadminteam/rails_admin/commit/32f91e4b49205e44d3931c2e36d9f7273384a250) broke fields with HTML tags ([758d249](https://github.com/railsadminteam/rails_admin/commit/758d249d950062be6840f9c96e2a286e02b92a1e), [#3686](https://github.com/railsadminteam/rails_admin/issues/3686#issuecomment-2215491140)) ## [3.1.3](https://github.com/railsadminteam/rails_admin/tree/v3.1.3) - 2024-07-06 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.2...v3.1.3) ### Fixed - Fix bson 5.0 compatibility ([13da4f0](https://github.com/railsadminteam/rails_admin/commit/13da4f0191f5185dadecdfe9e6fc9ca808f7f73d)) - Fix Importmap 2.0 compatibility ([bd0cf97](https://github.com/railsadminteam/rails_admin/commit/bd0cf97530d93dc66577e5d1ea0da2ebf3b57737)) ### Security - Fix XSS vulnerability in the list view ([b5a287d](https://github.com/railsadminteam/rails_admin/commit/b5a287d82e2cbd1737a1a01e11ede2911cce7fef), [GHSA-8qgm-g2vv-vwvc](https://github.com/railsadminteam/rails_admin/security/advisories/GHSA-8qgm-g2vv-vwvc)) ## [3.1.2](https://github.com/railsadminteam/rails_admin/tree/v3.1.2) - 2023-03-23 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.1...v3.1.2) ### Fixed - Fix install failing with importmap setup ([aca22b6](https://github.com/railsadminteam/rails_admin/commit/aca22b6ba1eca1ac618525334cf14fc946e1c99e), [#3609](https://github.com/railsadminteam/rails_admin/issues/3609)) - Fix to show non-eager-loaded models which are explicitly configured ([87c9d5b](https://github.com/railsadminteam/rails_admin/commit/87c9d5bc5b6ffb423e72054b3cfe8f949c12c178), [#3604](https://github.com/railsadminteam/rails_admin/issues/3604)) - Fix `rails_admin.dom_ready` event not triggered with jQuery `on` ([2ee43de](https://github.com/railsadminteam/rails_admin/commit/2ee43deb1fa8d3a9e3ea0e589c1687d684e19ad6), [33773d7](https://github.com/railsadminteam/rails_admin/commit/33773d7f8dd43eeb0f6a7c125c4bee170132e5d2), [#3600](https://github.com/railsadminteam/rails_admin/discussions/3600)) - Restore caching in RailsAdmin::Config::Model#excluded? ([#3587](https://github.com/railsadminteam/rails_admin/pull/3587)) - Optimize/simplify viable_models file path to class name logic ([#3589](https://github.com/railsadminteam/rails_admin/pull/3589)) ## [3.1.1](https://github.com/railsadminteam/rails_admin/tree/v3.1.1) - 2022-12-18 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0...v3.1.1) ### Changed - Relax Font-Awesome dependency to allow Webpacker users to stay on 5.x ([3a7f348](https://github.com/railsadminteam/rails_admin/commit/3a7f34875248e446b48fd76870f0c337fee6dcf9), [#3565](https://github.com/railsadminteam/rails_admin/issues/3565)) ### Removed - Remove unused glphyicon assets ([#3578](https://github.com/railsadminteam/rails_admin/pull/3578)) ### Fixed - Simplify uses of defined? ([#3561](https://github.com/railsadminteam/rails_admin/pull/3561)) - Define jQuery object in separate file to support esbuild ([#3571](https://github.com/railsadminteam/rails_admin/pull/3571)) - Fix filter box being duplicated on browser back ([c6b1893](https://github.com/railsadminteam/rails_admin/commit/c6b18934cff3db0768836d799ee1bea54045709c), [#3570](https://github.com/railsadminteam/rails_admin/issues/3570)) - Fix sidebar menu expanding horizontally, preventing vertical scroll ([9997c10](https://github.com/railsadminteam/rails_admin/commit/9997c1095066aaac39afb27bf8de705cf6ccb1ef), [#3564](https://github.com/railsadminteam/rails_admin/issues/3564)) ## [3.1.0](https://github.com/railsadminteam/rails_admin/tree/v3.1.0) - 2022-11-06 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.rc2...v3.1.0) ### Fixed - Fix to use defer instead of async to ensure script loading order ([2a40976](https://github.com/railsadminteam/rails_admin/commit/2a409764b13a4e23fc848725c604d318e3375484), [#3513](https://github.com/railsadminteam/rails_admin/issues/3513)) - Improve filter method select box appearance ([#3559](https://github.com/railsadminteam/rails_admin/pull/3559)) ## [3.1.0.rc2](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.rc2) - 2022-10-02 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.rc...v3.1.0.rc2) ### Fixed - Fix sidebar style broken in attempt to support Bootstrap 5.2 ([d7abba4](https://github.com/railsadminteam/rails_admin/commit/d7abba4e8a2e380b4d7f8fb3b37302300af63de5), [#3553](https://github.com/railsadminteam/rails_admin/issues/3553)) ## [3.1.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.rc) - 2022-09-22 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.beta...v3.1.0.rc) ### Added - Add ability to limit filter operators ([be9a75e](https://github.com/railsadminteam/rails_admin/commit/be9a75e504edd9a754157a4ddba590e8a5d14149)) - Support filtering has_one associations ([9657774](https://github.com/railsadminteam/rails_admin/commit/9657774b423912357d8ad8a476b644bb4e36dc30)) - Add ability to set default order of PaperTrail versions ([a1c4c67](https://github.com/railsadminteam/rails_admin/commit/a1c4c673041642ae2a2aa07e3f0555e17be9d940), [#3095](https://github.com/railsadminteam/rails_admin/pull/3095)) - Add block-style DSL support for extension adapters ([951b708](https://github.com/railsadminteam/rails_admin/commit/951b70878cb007d54b4a7aeadd708d4c7668727b)) - Make sidebar navigation collapsible ([e8cb8ed](https://github.com/railsadminteam/rails_admin/commit/e8cb8edd39246bf75ed72295b7832faf5056367c), [#3198](https://github.com/railsadminteam/rails_admin/pull/3198)) - Add ability to show a help text under the search box ([94f16fb](https://github.com/railsadminteam/rails_admin/commit/94f16fb5fdfdaa867173e95820583a68e5a306c5)) - Support for ActiveStorage direct uploads ([e13c7e2](https://github.com/railsadminteam/rails_admin/commit/e13c7e23f789ed2a575eff01b75e52a41cc930b7), [#3296](https://github.com/railsadminteam/rails_admin/issues/3296)) ### Changed - Load ActionText assets statically to enable full-featured setup ([458d0fb](https://github.com/railsadminteam/rails_admin/commit/458d0fb56d79d4fa0a252ad1ca715fd0a3b4b900), [#3251](https://github.com/railsadminteam/rails_admin/issues/3251), [#3386](https://github.com/railsadminteam/rails_admin/issues/3386)) - Change ES Module processing not to affect non-RailsAdmin assets ([f8219bf](https://github.com/railsadminteam/rails_admin/commit/f8219bf3bc508c5e42f5d044cf6355a56726f8b2)) - Change RailsAdmin initialization process to evaluate the config block immediately but load constants lazily ([#3541](https://github.com/railsadminteam/rails_admin/pull/3541), [33e9214](https://github.com/railsadminteam/rails_admin/commit/33e92147214975cddace61737cbbde3117663efe)) - Restore the loading indicator back ([32e6b14](https://github.com/railsadminteam/rails_admin/commit/32e6b1463ecf66b38e6fdc7924426d9753d71eab), [b9ee7f0](https://github.com/railsadminteam/rails_admin/commit/b9ee7f0c202abc7c09726bdaa28536e7ec25127e), [#3500](https://github.com/railsadminteam/rails_admin/issues/3500)) ### Fixed - Fix Bootstrap 5.2 compatibility ([ef76fce](https://github.com/railsadminteam/rails_admin/commit/ef76fcea0b23aed04f6568448cd157ccc9ba30a0)) - Fix filtering select widget options being empty on browser back ([3cffc00](https://github.com/railsadminteam/rails_admin/commit/3cffc0002079bc9d515dc0f3b31513c7166aa2b9)) - Fix RailsAdmin widgets not activated after a validation error ([a604da5](https://github.com/railsadminteam/rails_admin/commit/a604da5670378e57da7b5492afad40e4f4ad083d)) - Fix export action didn't use export section for eager loading ([4cc3f30](https://github.com/railsadminteam/rails_admin/commit/4cc3f304cc0bc4a7dbaf41a6e4e12ecbdbba6e22), [#1954](https://github.com/railsadminteam/rails_admin/issues/1954)) - Fix Dart Sass 2.0 division deprecations ([#3544](https://github.com/railsadminteam/rails_admin/pull/3544), [3a177c2](https://github.com/railsadminteam/rails_admin/commit/3a177c2e8c1b1d782186ee5bbb3c0d44cc4c0060)) - Fix unable to focus elements on modals opened from remote forms ([#3539](https://github.com/railsadminteam/rails_admin/pull/3539), [#3538](https://github.com/railsadminteam/rails_admin/issues/3538)) - Improve pagination appearance on smaller screens ([a2e366e](https://github.com/railsadminteam/rails_admin/commit/a2e366e8839c9046b9f9fde219875a05bc1a66bb), [#3516](https://github.com/railsadminteam/rails_admin/issues/3516)) - Fix the value of submit buttons being lost on submission ([60e1150](https://github.com/railsadminteam/rails_admin/commit/60e115053ea34fa42c3099464106cf6e58dbfa03), [#3513](https://github.com/railsadminteam/rails_admin/issues/3513)) - Fix breaking with a has_and_belongs_to_many association with scope given ([c2bf6db](https://github.com/railsadminteam/rails_admin/commit/c2bf6db41a97b46741bc41b56fd9700c8bdfc9e4), [#2067](https://github.com/railsadminteam/rails_admin/issues/2067)) - Fix nested fields don't toggle properly after pushing 'Add a new ...' button ([d1f1154](https://github.com/railsadminteam/rails_admin/commit/d1f115425c4a8d119fdeb37802fe472ae278918d), [#3528](https://github.com/railsadminteam/rails_admin/issues/3528)) ## [3.1.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.beta) - 2022-06-20 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0...v3.1.0.beta) ### Added - Support Importmap and Webpack, via [importmap-rails](https://github.com/rails/importmap-rails) / [jsbundling-rails](https://github.com/rails/jsbundling-rails) and [cssbundling-rails](https://github.com/rails/cssbundling-rails) ([#3488](https://github.com/railsadminteam/rails_admin/pull/3488)) - Support [Composite Primary Keys](https://github.com/composite-primary-keys/composite_primary_keys) gem ([#3527](https://github.com/railsadminteam/rails_admin/pull/3527)) - Allow configuration of Navbar css class ([126f7ac](https://github.com/railsadminteam/rails_admin/commit/126f7ac79b2ce7846cbbed7bbca1cc26dfe46fb6), [#3507](https://github.com/railsadminteam/rails_admin/issues/3507)) ### Changed - Update vendorized jQuery to 3.6.0 ([#3524](https://github.com/railsadminteam/rails_admin/pull/3524)) - Enable frozen string literals across the project ([#3483](https://github.com/railsadminteam/rails_admin/pull/3483)) ### Fixed - Fix edit user link in the top navigation pointing to wrong URL ([#3531](https://github.com/railsadminteam/rails_admin/pull/3531)) - Fix MultipleActiveStorage field deleting previous attachments when updating a record in Rails 7.0 ([974c54a](https://github.com/railsadminteam/rails_admin/commit/974c54a2a3372690ca189f6e7e90ce365bcd4ff5), [#3520](https://github.com/railsadminteam/rails_admin/issues/3520)) - Fix remote form submission breaking when used with HTTP/2 ([#3515](https://github.com/railsadminteam/rails_admin/pull/3515)) - Fix to maintain 2.x hover / active behavior for side navigation links ([#3511](https://github.com/railsadminteam/rails_admin/pull/3511)) - Fix default sort by behavior when `list.sort_by` points to a field with a table reference for `:sortable` ([#3509](https://github.com/railsadminteam/rails_admin/pull/3509), [9959925](https://github.com/railsadminteam/rails_admin/commit/9959925e49812d6fdaf7341ede4b1d66d926e8d8)) - Fix to insert whitespace after sidebar navigation icon to maintain visual consistency ([#3504](https://github.com/railsadminteam/rails_admin/pull/3504)) - Fix orderable multiselect buttons not rendered correctly ([#3506](https://github.com/railsadminteam/rails_admin/pull/3506)) - Fix to use badges instead of labels, which are removed in Bootstrap 5 ([#3503](https://github.com/railsadminteam/rails_admin/pull/3503)) ## [3.0.0](https://github.com/railsadminteam/rails_admin/tree/v3.0.0) - 2022-03-21 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc4...v3.0.0) ### Fixed - Fix table sorting not working ([83a0c88](https://github.com/railsadminteam/rails_admin/commit/83a0c889f1de38a0b24c412b0f0f7484add86b29), [#3497](https://github.com/railsadminteam/rails_admin/issues/3497)) - Fix reset button by the query box not working ([4a583e9](https://github.com/railsadminteam/rails_admin/commit/4a583e924c5bef6bfc46d6a49ab751c2323ba23e)) ## [3.0.0.rc4](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc4) - 2022-03-13 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc3...v3.0.0.rc4) ### Added - Instruct users on potential issues with npm package installation ([2b0594c](https://github.com/railsadminteam/rails_admin/commit/2b0594c6824eb4eed217cbab9477639b61eb99db), [c7a74f1](https://github.com/railsadminteam/rails_admin/commit/c7a74f1ce7a040b8c87b24fe1907ddd9088bf1e5)) ### Changed - Upgrade vendorized Flatpickr to 4.6.11 ([7f8c831](https://github.com/railsadminteam/rails_admin/commit/7f8c831b9aa407987ba89191c21040e2e72e366e)) ### Fixed - Fix not utilizing full browser width after Bootstrap 5 upgrade ([#3493](https://github.com/railsadminteam/rails_admin/pull/3493)) - Fix the style for show views broken on Bootstrap 5 upgrade ([#3491](https://github.com/railsadminteam/rails_admin/pull/3491)) - Fix Pundit 2.2 deprecation for not using Pundit::Authorization ([e38eb46](https://github.com/railsadminteam/rails_admin/commit/e38eb46ffe79cd866c34b837dd4cfbb65361558f)) - Fix JS issues when navigating across the main app and RailsAdmin ([eb4a185](https://github.com/railsadminteam/rails_admin/commit/eb4a18558e9d8c69aeea3fd733f5dc251f3f79f9), [#3484](https://github.com/railsadminteam/rails_admin/pull/3484)) ## [3.0.0.rc3](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc3) - 2022-02-27 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc2...v3.0.0.rc3) ### Fixed - Fix the style of list scope tabs ([#3477](https://github.com/railsadminteam/rails_admin/pull/3477)) - Fix rake tasks executed twice ([7d56cd6](https://github.com/railsadminteam/rails_admin/commit/7d56cd6af2d468cca44af5211559af0e744f5cb4)) - Fix the style of the header user link when show_gravatar is false ([#3475](https://github.com/railsadminteam/rails_admin/pull/3475)) - Fix 'Cancel' button for delete/bulk_delete action also didn't work ([1fa8486](https://github.com/railsadminteam/rails_admin/commit/1fa8486ead39596e9e0ac62a945fb0aed63929a8), [#3468](https://github.com/railsadminteam/rails_admin/issues/3468)) - Fix failing to export after introducing Turbo Drive ([c749d93](https://github.com/railsadminteam/rails_admin/commit/c749d939e29434937e8558bcb1f3e219fe98c69d), [#3461 (comment)](https://github.com/railsadminteam/rails_admin/pull/3461#issuecomment-1048588801)) ## [3.0.0.rc2](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc2) - 2022-02-20 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc...v3.0.0.rc2) ### Added - Some improvements to make the upgrade process more friendlier ([#3471](https://github.com/railsadminteam/rails_admin/pull/3471), [3771542](https://github.com/railsadminteam/rails_admin/commit/377154268257d3753093dcd3dd2fd79b7438b9aa), [3b04a96](https://github.com/railsadminteam/rails_admin/commit/3b04a9616169d568b1a6cee26becf9bfea0ee386)) ### Fixed - Fix 'Save and add another', 'Save and edit', 'Cancel' buttons didn't work right ([ac0a563](https://github.com/railsadminteam/rails_admin/commit/ac0a563a0bf91bf33c693e19717148ed77f843fd), [#3468](https://github.com/railsadminteam/rails_admin/issues/3468)) - Fix failing to precompile assets when the database connection is unavailable ([#3470](https://github.com/railsadminteam/rails_admin/pull/3470), [#3469](https://github.com/railsadminteam/rails_admin/issues/3469)) - Fix custom theme overrides not working ([3d7f3b3](https://github.com/railsadminteam/rails_admin/commit/3d7f3b33a1ca6fabbd8606bb178babae930cce25), [#3466](https://github.com/railsadminteam/rails_admin/issues/3466)) ## [3.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc) - 2022-02-06 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.beta2...v3.0.0.rc) ### Added - Support Mongoid's Storage Field Names ([cefa23c](https://github.com/railsadminteam/rails_admin/commit/cefa23c9d23d06dc1134228e142e6f0aa4655c54), [#1745](https://github.com/railsadminteam/rails_admin/issues/1745)) - Allow save/delete operations to be disabled based on an object's `#read_only?` status ([9cd7541](https://github.com/railsadminteam/rails_admin/commit/9cd7541a2e6af4ae4941b200840a1474baeb2f06), [#1684](https://github.com/railsadminteam/rails_admin/issues/1684)) - Allow customizing model's last created time ([d6d380a](https://github.com/railsadminteam/rails_admin/commit/d6d380a02e955c3b14ff7dc30f1809a2a6cd0f47), [#3010](https://github.com/railsadminteam/rails_admin/issues/3010)) - Add ability to hide the dashboard history section ([#3189](https://github.com/railsadminteam/rails_admin/pull/3189)) - Add model scope configuration option, which enables 'unscoped' mode ([8d905f9](https://github.com/railsadminteam/rails_admin/commit/8d905f9e2f1102e8addf324b496720e3fd47bf1d), [#1348](https://github.com/railsadminteam/rails_admin/issues/1348)) ### Changed - Switch from pjax to Turbo Drive, due to pjax's low maintenance activity ([#3461](https://github.com/railsadminteam/rails_admin/pull/3461), [#3435](https://github.com/railsadminteam/rails_admin/pull/3435)) - Upgrade Bootstrap to 5.1.3 ([#3455](https://github.com/railsadminteam/rails_admin/pull/3455), [#3083](https://github.com/railsadminteam/rails_admin/issues/3083)) - Switch datetime picker library to Flatpickr ([#3455](https://github.com/railsadminteam/rails_admin/pull/3455)) ### Removed - Drop support for Ruby 2.5 ([#3430](https://github.com/railsadminteam/rails_admin/pull/3430)) - Remove Sections::List#sort_reverse because of having very limited usecase ([0c7bc61](https://github.com/railsadminteam/rails_admin/commit/0c7bc6124a189e5307f2bd1f960dd06495932d10), [#1181](https://github.com/railsadminteam/rails_admin/issues/1181)) ### Fixed - Fix failing to detect encoding with JDBC MySQL adapter ([0dfe2e4](https://github.com/railsadminteam/rails_admin/commit/0dfe2e4d1301cc353f972b28ab19554a53cad482)) - Fix unable to start app when using redis-session-store ([#3462](https://github.com/railsadminteam/rails_admin/pull/3462)) - Fix ActiveRecord ObjectExtension has_one setter breaks with custom primary key class ([0e2e0e4](https://github.com/railsadminteam/rails_admin/commit/0e2e0e4e24328a063813a3a5266a15faee7960c8), [#3460](https://github.com/railsadminteam/rails_admin/issues/3460)) - Fix inheritance of parent_controller not updated properly when controllers were eagerly loaded ([#3458](https://github.com/railsadminteam/rails_admin/pull/3458)) - Fix to retrieve actions correctly in the action `#bulk_action` ([#3407](https://github.com/railsadminteam/rails_admin/pull/3407)) - Fix issue when RailsAdmin::MainController needs to dispatch a method call using `#respond_to_missing?` ([da51b91](https://github.com/railsadminteam/rails_admin/commit/da51b91f712b235c0c6e2a120724fcb31ff28168), [#3454](https://github.com/railsadminteam/rails_admin/issues/3454)) - Fix modal foreign keys are not prepopulated unless the association inverse_of is configured ([75504d0](https://github.com/railsadminteam/rails_admin/commit/75504d08ee6197a3a2c72a56ea30f01272b1d28b), [#2585](https://github.com/railsadminteam/rails_admin/issues/2585)) ## [3.0.0.beta2](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.beta2) - 2021-12-25 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.beta...v3.0.0.beta2) ### Fixed - Fix NameError 'uninitialized constant RailsAdmin::Version' on install ([d2ad520](https://github.com/railsadminteam/rails_admin/commit/d2ad5209f98d8678c6317b49f13727b8605048b3), [#3452](https://github.com/railsadminteam/rails_admin/issues/3452)) ## [3.0.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.beta) - 2021-12-20 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.2.1...v3.0.0.beta) ### Added - Rails 7.0.0 support ([011b9ae](https://github.com/railsadminteam/rails_admin/commit/011b9aea12d2c3a5e8654786e49071254baacf18), [670d803](https://github.com/railsadminteam/rails_admin/commit/670d803c262429600c6eb7f59f8b36aa145081e5)) - Webpacker support ([#3414](https://github.com/railsadminteam/rails_admin/pull/3414)) - Add #link_target to action configuration ([#3419](https://github.com/railsadminteam/rails_admin/pull/3419)) - Add not like (=does not contain) operator ([#3410](https://github.com/railsadminteam/rails_admin/pull/3410)) - Support for PostgreSQL citext data type ([#3413](https://github.com/railsadminteam/rails_admin/pull/3413), [#2177](https://github.com/railsadminteam/rails_admin/issues/2177)) - Allow #configure to handle multiple fields for a section at once ([#3406](https://github.com/railsadminteam/rails_admin/pull/3406), [#2667](https://github.com/railsadminteam/rails_admin/issues/2667)) - Add has_one id setters/getters, eliminating the need for explicitly defining them ([42f0a5f](https://github.com/railsadminteam/rails_admin/commit/42f0a5f5aac4b7481dcd4950803d12077b598ce5), [#2625](https://github.com/railsadminteam/rails_admin/issues/2625)) - Support for Mongoid's has_and_belongs_to_many custom primary_key ([3f67637](https://github.com/railsadminteam/rails_admin/commit/3f676371ce7fc088220095afa12d3e20c2c6123a), [#3097](https://github.com/railsadminteam/rails_admin/pull/3097)) - Support for eager-loading arbitrary associations ([4404758](https://github.com/railsadminteam/rails_admin/commit/44047580b7d536a84a92c173a3bd00dc9d211399), [#2928](https://github.com/railsadminteam/rails_admin/issues/2928)) - Support for nullable boolean field ([7583369](https://github.com/railsadminteam/rails_admin/commit/7583369a1d1763e542d36930a968726425cacb6c), [#3145](https://github.com/railsadminteam/rails_admin/issues/3145)) - Support for configuration reload in development mode ([e4ae669](https://github.com/railsadminteam/rails_admin/commit/e4ae6698e52e56a1cefdf5c4097b29b8306f21e2), [#2726](https://github.com/railsadminteam/rails_admin/issues/2726), [08f50aa](https://github.com/railsadminteam/rails_admin/commit/08f50aabc1922aeb289ced4ccbe9f983bc1aaa89), [#3420](https://github.com/railsadminteam/rails_admin/issues/3420)) - Add 'No objects found' placeholder in filtering-select as well ([7e3a1a6](https://github.com/railsadminteam/rails_admin/commit/7e3a1a632811a917f2cbd8dbe471861bc0a1ac85), [#3332](https://github.com/railsadminteam/rails_admin/issues/3332)) - Add inline_edit to HasManyAssociation as well ([798ab1b](https://github.com/railsadminteam/rails_admin/commit/798ab1b6ae400f2b853441a9fa48f8e7ad24208d), [#1911](https://github.com/railsadminteam/rails_admin/issues/1911)) - Add hover highlight to the list table for better visibility ([#3221](https://github.com/railsadminteam/rails_admin/pull/3221)) - Add ability to show disabled actions, as well as completely hiding ([6c877ea](https://github.com/railsadminteam/rails_admin/commit/6c877eac1a052881b2d4950a44d41f7fd77b044f), [#1765](https://github.com/railsadminteam/rails_admin/issues/1765)) - Add the message 'no records found' when a list is empty ([#3365](https://github.com/railsadminteam/rails_admin/pull/3365), [a5fe6f8](https://github.com/railsadminteam/rails_admin/commit/a5fe6f806498975a91ae11544ce21310592774a2), [#3329](https://github.com/railsadminteam/rails_admin/issues/3329)) - Add a way to clear belongs_to selection using mouse ([ac3fe35](https://github.com/railsadminteam/rails_admin/commit/ac3fe35b65e858a3b528a523e1b6fbeafe1d359b), [#2090](https://github.com/railsadminteam/rails_admin/issues/2090)) - Add HTML5 validation for float-like field types ([#3378](https://github.com/railsadminteam/rails_admin/pull/3378), [#3289](https://github.com/railsadminteam/rails_admin/issues/3289)) ### Changed - Remove horizontal pagination and always use sidescroll view for list action table ([d51e943](https://github.com/railsadminteam/rails_admin/commit/d51e94314de4b510df0767a695d5953bb7b3fad5)) - Replace image assets with Font Awesome icons ([a0a568b](https://github.com/railsadminteam/rails_admin/commit/a0a568bc866158382b52ec24639699695146794c)) - Switch templates from HAML to ERB ([#3425](https://github.com/railsadminteam/rails_admin/pull/3425), [#3439](https://github.com/railsadminteam/rails_admin/pull/3439), [#3173](https://github.com/railsadminteam/rails_admin/issues/3173)) - Rewrite some JavaScript code not to use jQuery ([#3416](https://github.com/railsadminteam/rails_admin/pull/3416), [#3417](https://github.com/railsadminteam/rails_admin/pull/3417)) - Upgrade FontAwesome to 5.15.4 ([cb1ac73](https://github.com/railsadminteam/rails_admin/commit/cb1ac732c4df85cc082c4e40f2c8a7d144ceb9f2)) - Stop using AbstractObject and use raw model instances with extension ([af88091](https://github.com/railsadminteam/rails_admin/commit/af88091d11590dc95df4866c62fba0c49a7bb9a8), [#2847](https://github.com/railsadminteam/rails_admin/issues/2847)) - Switch from jquery_ujs to rails-ujs ([#3390](https://github.com/railsadminteam/rails_admin/pull/3390), [dea63f4](https://github.com/railsadminteam/rails_admin/commit/dea63f49ee18644ee5e8dee1fb57e0c742d27a97)) - Make colorpicker field use HTML5 native color picker ([#3387](https://github.com/railsadminteam/rails_admin/pull/3387)) - Change to use ISO 8601 time format for browser-server communication, instead of localized value ([01e8d5f](https://github.com/railsadminteam/rails_admin/commit/01e8d5fc8ec94e68af6fdbd80759a751cd83f74a), [#3344](https://github.com/railsadminteam/rails_admin/issues/3344)) ### Removed - Remove dependency for builder and remotipart ([#3427](https://github.com/railsadminteam/rails_admin/pull/3427), [58b76d1](https://github.com/railsadminteam/rails_admin/commit/58b76d1e66ec06b752567da9c118f611105ff39f)) - Remove capitalization helper, letting I18n to perform necessary transformation ([#3396](https://github.com/railsadminteam/rails_admin/pull/3396)) - Remove jQuery Migrate ([#3389](https://github.com/railsadminteam/rails_admin/pull/3389), [b385d4d](https://github.com/railsadminteam/rails_admin/commit/b385d4d50f372de65d8c6c33a4b8256403894483)) - Remove the legacy history adapter([#3374](https://github.com/railsadminteam/rails_admin/issues/3374), [b627580](https://github.com/railsadminteam/rails_admin/commit/b62758039308872e7abe91a51715e1608bfd0915)) - Drop support for Ruby < 2.5 and Rails 5.x([decf428](https://github.com/railsadminteam/rails_admin/commit/decf4280183b8b6a453aa802942fc825524c2f13), [17e20b6](https://github.com/railsadminteam/rails_admin/commit/17e20b6daef1708598ab8cff5678501f8bac4709)) ### Fixed - Reduce object allocations when rendering main navigation menu ([#3412](https://github.com/railsadminteam/rails_admin/pull/3412)) - Fix N+1 queries for ActiveStorage attachments ([e4d5b2f](https://github.com/railsadminteam/rails_admin/commit/e4d5b2f3bece0f5f3e5f588e20e52031ad33e124), [#3282](https://github.com/railsadminteam/rails_admin/issues/3282)) - Fix to convert DateTime format for Moment.js as much as possible ([6d5c049](https://github.com/railsadminteam/rails_admin/commit/6d5c049124d0b390f4612e8e6524f00500afe52e), [#2736](https://github.com/railsadminteam/rails_admin/issues/2736), [#3009](https://github.com/railsadminteam/rails_admin/issues/3009)) - Fix config.parent_controller to work after the class loading ([5bd9805](https://github.com/railsadminteam/rails_admin/commit/5bd980564a565906a6fda87d2ef31590d3e3b0a5), [#2790](https://github.com/railsadminteam/rails_admin/issues/2790)) - Fix NoMethodError when Mongoid's raise_not_found_error is false ([973bd8e](https://github.com/railsadminteam/rails_admin/commit/973bd8e50591a538841575c33b6221706481dae3), [#2623](https://github.com/railsadminteam/rails_admin/issues/2623)) - Fix NoMethodError "undefined method 'has_one_attached'" ([e4ae669](https://github.com/railsadminteam/rails_admin/commit/e4ae6698e52e56a1cefdf5c4097b29b8306f21e2), [#3025](https://github.com/railsadminteam/rails_admin/issues/3025)) - Fix NoMethodError "undefined method `label' for nil:NilClass" on export ([f2104b5](https://github.com/railsadminteam/rails_admin/commit/f2104b595930491a18ede44c9e1a8c881a0316b8), [#1685](https://github.com/railsadminteam/rails_admin/issues/1685)) - Fix Kaminari's custom param_name was not used in history_index and history_show ([#3227](https://github.com/railsadminteam/rails_admin/pull/3227), [#3400](https://github.com/railsadminteam/rails_admin/pull/3400)) - Fix Gravater and email were not shown when the current user is not editable ([bd44929](https://github.com/railsadminteam/rails_admin/commit/bd449292088cb68aad22b282b6344778862187e5), [#3237](https://github.com/railsadminteam/rails_admin/issues/3237)) - Fix RailsAdmin::Config.reset didn't clear the effect of previous included_models/excluded_models ([1190d51](https://github.com/railsadminteam/rails_admin/commit/1190d510ee73949af618bd279e4712f1be4550b6), [#3305](https://github.com/railsadminteam/rails_admin/issues/3305)) - Fix duplication of filtering-multiselect on browser back ([3c10b09](https://github.com/railsadminteam/rails_admin/commit/3c10b0918b65e64624f06ebd5e402181f478cc64), [#3211](https://github.com/railsadminteam/rails_admin/issues/3211)) - Fix no error message is shown on failure with dependent: :restrict_with_error ([bf353cc](https://github.com/railsadminteam/rails_admin/commit/bf353cc76d6e56d511e9c5a87d0ffbd83669e3f2), [#3323](https://github.com/railsadminteam/rails_admin/issues/3223)) - Fix read-only associations are shown empty if it has no value ([7580f33](https://github.com/railsadminteam/rails_admin/commit/7580f3366a6e4a16b439de7fd64860fe26628ad7), [#2681](https://github.com/railsadminteam/rails_admin/issues/2681)) - Fix hidden fields taking up some space ([5aaee51](https://github.com/railsadminteam/rails_admin/commit/5aaee5153441fd82854a998ac02ebbe303b82bf2), [#3380](https://github.com/railsadminteam/rails_admin/issues/3380)) - Fix to show validation errors in modals ([f67defb](https://github.com/railsadminteam/rails_admin/commit/f67defb55ad9d1a1fe8428029c947bcd00b5ce8a), [#1735](https://github.com/railsadminteam/rails_admin/issues/1735)) - Fix image file detection by using Mime::Type ([#3398](https://github.com/railsadminteam/rails_admin/pull/3398), [#3239](https://github.com/railsadminteam/rails_admin/issues/3239)) - Fix 'no objects' message not showing up in filtering-multiselect widget ([aa5545c](https://github.com/railsadminteam/rails_admin/commit/aa5545c928ce0de80142966c19b64be76d88286f)) - Fix 'Delete Image' translation does not work well in some languages ([#3382](https://github.com/railsadminteam/rails_admin/pull/3382), [#3260](https://github.com/railsadminteam/rails_admin/issues/3260)) - Fix polymorphic associations don't work with namespaced classes ([#3377](https://github.com/railsadminteam/rails_admin/pull/3377), [#3376](https://github.com/railsadminteam/rails_admin/issues/3376)) - Fix Boolean pretty_value to include default fallback ([#3379](https://github.com/railsadminteam/rails_admin/pull/3379)) - Fix history#index not supporting models with custom version classes ([ed19f9e](https://github.com/railsadminteam/rails_admin/commit/ed19f9e793b91de1c2bc9133e026b0396e6ec777)) - Fix models stored in eager_load_paths are not picked up by #viable_models ([#3373](https://github.com/railsadminteam/rails_admin/issues/3373), [238f18e](https://github.com/railsadminteam/rails_admin/commit/238f18ee2386f9858670b7995dcb628b8fe6bde9)) - Fix polymorphic associations don't work with namespaced classes([#3376](https://github.com/railsadminteam/rails_admin/issues/3376)) ## [2.2.1](https://github.com/railsadminteam/rails_admin/tree/v2.2.0) - 2021-08-08 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.2.0...v2.2.1) ### Fixed - Fix missing select options for single-select enum filters([#3372](https://github.com/railsadminteam/rails_admin/pull/3372)) ## [2.2.0](https://github.com/railsadminteam/rails_admin/tree/v2.2.0) - 2021-07-24 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.1.1...v2.2.0) ### Added - Support for PaperTrail's alternative versions association name([#3354](https://github.com/railsadminteam/rails_admin/pull/3354)) ### Changed - Update jQuery to 3.x with introducing jQuery.migrate([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421), [#3370](https://github.com/railsadminteam/rails_admin/pull/3370)) - Update Moment.js to 2.29.1([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421), [7962a194](https://github.com/railsadminteam/rails_admin/commit/7962a19469a709c00f481a50a6d1e7ddd1e37cc6)) - Update Bootstrap to 3.4.1([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421)) - Update Bootstrap Datetime Picker to 4.17.49([7962a194](https://github.com/railsadminteam/rails_admin/commit/7962a19469a709c00f481a50a6d1e7ddd1e37cc6)) ### Removed - Remove unnecessary devise patch([#3352](https://github.com/railsadminteam/rails_admin/pull/3352)) ### Fixed - Zeitwerk incompatibility([#3190](https://github.com/railsadminteam/rails_admin/issues/3328), [97ccc289](https://github.com/railsadminteam/rails_admin/commit/97ccc28940d65fee53b30c409c49032fbb0885db)) ## [2.1.1](https://github.com/railsadminteam/rails_admin/tree/v2.1.1) - 2021-03-14 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.1.0...v2.1.1) ### Fixed - Fix AbstractObject's proxying was incompatible with keyword arguments in Ruby 3.0 ([#3342](https://github.com/railsadminteam/rails_admin/issues/3342)) ## [2.1.0](https://github.com/railsadminteam/rails_admin/tree/v2.1.0) - 2021-02-28 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.2...v2.1.0) ### Added - Ability to set default filter operator for fields ([#3318](https://github.com/railsadminteam/rails_admin/pull/3318)) - Shrine 3.x support ([#3257](https://github.com/railsadminteam/rails_admin/pull/3257)) - Rails 6.1 compatibility ([f0c46f1e](https://github.com/railsadminteam/rails_admin/commit/f0c46f1e128b5d31d812ff3a80d15db8692c848b)) ### Fixed - Some translation entries of filtering-multiselect weren't localizable ([#3315](https://github.com/railsadminteam/rails_admin/pull/3315)) - Thumbnail generation breaks when used with ActiveStorage 6.x and ruby-vips ([#3255](https://github.com/railsadminteam/rails_admin/pull/3255), [2dba791c](https://github.com/railsadminteam/rails_admin/commit/2dba791c9135b3202d662f90fac443d282869bd6)) - Hide present/blank filter options for required fields ([#3340](https://github.com/railsadminteam/rails_admin/pull/3340)) - Fix to show correct filename for multiple attachments ([#3295](https://github.com/railsadminteam/rails_admin/pull/3295)) - Fix encoding detection was incompatible with DB connection proxies like active_record_host_pool gem ([#3313](https://github.com/railsadminteam/rails_admin/pull/3313)) - Fix hidden fields breaking indentation ([#3278](https://github.com/railsadminteam/rails_admin/pull/3278), [#2487](https://github.com/railsadminteam/rails_admin/issues/2487)) ### Removed - Remove `yell_for_non_accessible_fields` option since it has no effect since 0.5.0 ([#3249](https://github.com/railsadminteam/rails_admin/pull/3249)) ## [2.0.2](https://github.com/railsadminteam/rails_admin/tree/v2.0.2) - 2020-03-17 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.1...v2.0.2) ### Fixed - Fix to use I18n to translate the button 'Reset filters'([#3248](https://github.com/railsadminteam/rails_admin/pull/3248)) ### Security - Fix XSS vulnerability in nested forms([d72090ec](https://github.com/railsadminteam/rails_admin/commit/d72090ec6a07c3b9b7b48ab50f3d405f91ff4375)) ## [2.0.1](https://github.com/railsadminteam/rails_admin/tree/v2.0.1) - 2019-12-31 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0...v2.0.1) ### Fixed - Fix Zeitwerk incompatible behavior of autoloading constants during initialization([#3190](https://github.com/railsadminteam/rails_admin/issues/3190), [e275012b](https://github.com/railsadminteam/rails_admin/commit/e275012b630453cb1187e71a938382a3c5d3ef39)) - Fix empty fields being hidden regardless of `compact_show_view`([#3213](https://github.com/railsadminteam/rails_admin/pull/3213)) - Fix `filter_scope` not using `default_search_operator` as default([#3212](https://github.com/railsadminteam/rails_admin/pull/3212)) - Fix PaperTrail integration returning `nil` as username instead of `whodunnit`([#3210](https://github.com/railsadminteam/rails_admin/pull/3210)) - Fix Sprockets 4 incompatibility of vendorized Fontawesome([#3204](https://github.com/railsadminteam/rails_admin/issues/3204), [#3207](https://github.com/railsadminteam/rails_admin/pull/3207)) ### Security - Update moment.js to 2.24.0 to address security vulnerability([#3182](https://github.com/railsadminteam/rails_admin/issues/3182), [#3201](https://github.com/railsadminteam/rails_admin/pull/3201)) ## [2.0.0](https://github.com/railsadminteam/rails_admin/tree/v2.0.0) - 2019-08-18 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0.rc...v2.0.0) ### Fixed - Fix support for belongs_to with custom primary key was broken in 2.0.0.rc([#3184](https://github.com/railsadminteam/rails_admin/issues/3184), [0e92ca43](https://github.com/railsadminteam/rails_admin/commit/0e92ca43fe7782a8d62ae9285a8d3de7857c9853)) - Fix missing translation `en.admin.misc.ago`([#3180](https://github.com/railsadminteam/rails_admin/pull/3180)) ## [2.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v2.0.0.rc) - 2019-08-04 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0.beta...v2.0.0.rc) ### Added - Add Support for CarrierWave 2.0 multiple file upload's keep, append and reorder feature([fb093e04](https://github.com/railsadminteam/rails_admin/commit/fb093e04502e7bff30594f5baf1227abb7199384)) - Add ability to configure way how custom actions show up in root/top/sidebar navigation([#2844](https://github.com/railsadminteam/rails_admin/pull/2844)) ### Changed - [BREAKING CHANGE] Stop authorization adapters assigning attributes on create and update, just check for permission instead([#3120](https://github.com/railsadminteam/rails_admin/pull/3120), [c84d1703](https://github.com/railsadminteam/rails_admin/commit/c84d1703b47b396382d152471dac8fc8dc41aefd)) - [BREAKING CHANGE] Do not show tableless models by default([#3157](https://github.com/railsadminteam/rails_admin/issues/3157), [87b38b33](https://github.com/railsadminteam/rails_admin/commit/87b38b336cc668a74803dec4628215e2e2941248)) - [BREAKING CHANGE] Convert empty string into nil for nullable string-like fields to achieve uniqueness-index friendliness([#2099](https://github.com/railsadminteam/rails_admin/issues/2099), [#3172](https://github.com/railsadminteam/rails_admin/issues/3172), [3f9ab1cc](https://github.com/railsadminteam/rails_admin/commit/3f9ab1cc009caa8b466f34da692c3561da2235e4)) - Extract head from application template for ease of customization([#3114](https://github.com/railsadminteam/rails_admin/pull/3114)) - Rename `delete_key` to `delete_value`, used to identify which file to delete in multiple file upload([8b8c3a44](https://github.com/railsadminteam/rails_admin/commit/8b8c3a44177465823128e9b48b11467ccf7db001)) - Get rid of CoffeeScript, use plain JavaScript instead([#3111](https://github.com/railsadminteam/rails_admin/issues/3111), [#3168](https://github.com/railsadminteam/rails_admin/pull/3168)) - Replace sass-rails with sassc-rails([#3156](https://github.com/railsadminteam/rails_admin/pull/3156)) ### Removed - Drop support for CanCan, please use its successor CanCanCan([6b7495f1](https://github.com/railsadminteam/rails_admin/commit/6b7495f1454e30027a9d77b911206cc7703170a3)) - Drop support for CanCanCan legacy `can :dashboard` style dashboard ability notation([5bebac24](https://github.com/railsadminteam/rails_admin/commit/5bebac2488906f3739717108efadedaa091ccaf5)) - Drop Refile support due to maintenance inactivity([25ae06a9](https://github.com/railsadminteam/rails_admin/commit/25ae06a9a6eb534afa4a5f17e64ca346086bb3b8)) ### Fixed - Fix PaperTrail pagination breaks when Kaminari's `page_method_name` is set([#3170](https://github.com/railsadminteam/rails_admin/issues/3170), [136b943c](https://github.com/railsadminteam/rails_admin/commit/136b943ce842eba6b0a13dc2956ddc9ce20d006c)) - Fix failing to pass config location to CKEditor([#3162](https://github.com/railsadminteam/rails_admin/issues/3162), [c38b76d7](https://github.com/railsadminteam/rails_admin/commit/c38b76d707f198be0ac8baa1ff02dde7bd02344f)) - Fix CarrierWave multiple file uploader breaking when used with Fog([#3070](https://github.com/railsadminteam/rails_admin/issues/3070)) - Fix placeholder being picked up as a selection in filtering-multiselect([#2807](https://github.com/railsadminteam/rails_admin/issues/2807), [15502601](https://github.com/railsadminteam/rails_admin/commit/15502601ccd0bbbaeb364a0ee605f360d65c5cbb)) - Fix breaking with has_many and custom primary key([#1878](https://github.com/railsadminteam/rails_admin/issues/1878), [be7d2f4a](https://github.com/railsadminteam/rails_admin/commit/be7d2f4a3ad1fda788f92a0e0dd4a83b98f141f4)) - Fix to choose right LIKE statement in per-model basis([#1676](https://github.com/railsadminteam/rails_admin/issues/1676), [4ea4575e](https://github.com/railsadminteam/rails_admin/commit/4ea4575e934e26cec4a5b214b9fe68cb96b40247)) - Fix polymorphic associations not using STI base classes for polymorphic type([#2136](https://github.com/railsadminteam/rails_admin/pull/2136)) ### Security - Add `rel="noopener"` to all `target="_blank"` links to prevent Reverse tabnabbing([#2960](https://github.com/railsadminteam/rails_admin/issues/2960), [#3169](https://github.com/railsadminteam/rails_admin/pull/3169)) ## [2.0.0.beta](https://github.com/railsadminteam/rails_admin/tree/v2.0.0.beta) - 2019-06-08 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.2...v2.0.0.beta) ### Added - Rails 6 support([#3122](https://github.com/railsadminteam/rails_admin/pull/3122)) - ActionText support([#3144](https://github.com/railsadminteam/rails_admin/issues/3144), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/ActionText)) - sass-rails 6 support([#3129](https://github.com/railsadminteam/rails_admin/issues/3129)) - Sidescroll feature([#3017](https://github.com/railsadminteam/rails_admin/pull/3017), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/Horizontally-scrolling-table-with-frozen-columns-in-list-view)) - Custom search feature([#343](https://github.com/railsadminteam/rails_admin/issues/343), [#3019](https://github.com/railsadminteam/rails_admin/pull/3019), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/Custom-Search)) - Filtering-select feature for polymorphic association([#2886](https://github.com/railsadminteam/rails_admin/pull/2886)) - Shrine support([#3081](https://github.com/railsadminteam/rails_admin/pull/3081)) - Flexibility for localication of _time_ ago([#3135](https://github.com/railsadminteam/rails_admin/pull/3135), [49add741](https://github.com/railsadminteam/rails_admin/commit/49add7413794e2a1423b86399ef476414d22970f)) ### Changed - Vendorize font-awesome to allow using different version in app([#3039](https://github.com/railsadminteam/rails_admin/issues/3039)) - Stop inlining JavaScripts for CSP friendliness([#3087](https://github.com/railsadminteam/rails_admin/issues/3087)) - Richtext editors now uses CDN-hosted assets([#3126](https://github.com/railsadminteam/rails_admin/issues/3126)) ### Removed - Remove deprecated DSL syntax for richtext editors([e0b390d9](https://github.com/railsadminteam/rails_admin/commit/e0b390d99eab64c99f1f3cccae2029649e90e11c)) - Drop support for Ruby 2.1 and Rails 4.x([dd247804](https://github.com/railsadminteam/rails_admin/commit/dd24780445f4dd676ae033c69a5b64347b80c3bc)) ### Fixed - Fix Mongoid query and filter parsing value twice([#2755](https://github.com/railsadminteam/rails_admin/issues/2755)) - Fix thread-safety issues([#2897](https://github.com/railsadminteam/rails_admin/issues/2897), [#2942](https://github.com/railsadminteam/rails_admin/issues/2942), [1d22bc66](https://github.com/railsadminteam/rails_admin/commit/1d22bc66168ac9ea478ea95b4b3b79f41263c0bd)) - Fix compact_show_view not showing Boolean falses([#2416](https://github.com/railsadminteam/rails_admin/issues/2416)) - Fix PaperTrail fail to fetch versions for STI subclasses([#2865](https://github.com/railsadminteam/rails_admin/pull/2865)) - Fix Dragonfly factory breaks if a model not extending Dragonfly::Model is passed([#2720](https://github.com/railsadminteam/rails_admin/pull/2720)) - Fix PaperTrail adapter not using Kaminari's `page_method_name` for pagination([#2712](https://github.com/railsadminteam/rails_admin/pull/2712)) - Fix #bulk_menu was not using passed `abstract_model` ([#2782](https://github.com/railsadminteam/rails_admin/pull/2782)) - Fix wrong styles when using multiple instances of CodeMirror([#3107](https://github.com/railsadminteam/rails_admin/pull/3107)) - Fix password being cleared when used with Devise 4.6([72bc0373](https://github.com/railsadminteam/rails_admin/commit/72bc03736162ffef8e5b99f42ca605d17fe7e7d0)) - ActiveStorage factory caused const missing for Mongoid([#3088](https://github.com/railsadminteam/rails_admin/pull/3088), [db927687](https://github.com/railsadminteam/rails_admin/commit/db9276879c8e8c5e8772261725ef0e0cdadd9cf1)) - Fix exact matches were using LIKE, which was not index-friendly([#3000](https://github.com/railsadminteam/rails_admin/pull/3000)) - Middleware check failed when using RedisStore([#3076](https://github.com/railsadminteam/rails_admin/issues/3076)) - Fix field being reset to default after an error([#3066](https://github.com/railsadminteam/rails_admin/pull/3066)) ## [1.4.2](https://github.com/railsadminteam/rails_admin/tree/v1.4.2) - 2018-09-23 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.1...v1.4.2) ### Fixed - Fix `can't modify frozen Array` error on startup([#3060](https://github.com/railsadminteam/rails_admin/issues/3060)) - Fix deprecation warning with PaperTrail.whodunnit([#3059](https://github.com/railsadminteam/rails_admin/pull/3059)) ## [1.4.1](https://github.com/railsadminteam/rails_admin/tree/v1.4.1) - 2018-08-19 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.0...v1.4.1) ### Fixed - Export crashes for models with JSON field([#3056](https://github.com/railsadminteam/rails_admin/pull/3056)) - Middlewares being mangled by engine initializer, causing app's session store configuration to be overwritten([#3048](https://github.com/railsadminteam/rails_admin/issues/3048), [59478af9](https://github.com/railsadminteam/rails_admin/commit/59478af9a05c76bdfe35e94e63c60ba89c27a483)) ## [1.4.0](https://github.com/railsadminteam/rails_admin/tree/v1.4.0) - 2018-07-22 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.3.0...v1.4.0) ### Added - Support for ActiveStorage([#2990](https://github.com/railsadminteam/rails_admin/issues/2990), [#3037](https://github.com/railsadminteam/rails_admin/pull/3037)) - Support for multiple file upload for ActiveStorage and CarrierWave ([5bb2d375](https://github.com/railsadminteam/rails_admin/commit/5bb2d375a236268e51c7e8682c2d110d9e52970f)) - Support for Mongoid 7.0([9ef623f6](https://github.com/railsadminteam/rails_admin/commit/9ef623f6cba73adbf86833d9eb07f1be3924a133), [#3013](https://github.com/railsadminteam/rails_admin/issues/3013)) - Support for CanCanCan 2.0([a32d49e4](https://github.com/railsadminteam/rails_admin/commit/a32d49e4b96944905443588a1216b3362ee64c1a), [#2901](https://github.com/railsadminteam/rails_admin/issues/2901)) - Support for Pundit 2.0([bc60c978](https://github.com/railsadminteam/rails_admin/commit/bc60c978adfebe09cdad2c199878d8ff966374f1)) - Support for jquery-ui-rails 6.0([#2951](https://github.com/railsadminteam/rails_admin/issues/2951), [#3003](https://github.com/railsadminteam/rails_admin/issues/3003)) ### Fixed - Make code reloading work([#3041](https://github.com/railsadminteam/rails_admin/pull/3041)) - Improved support for Rails API mode, requiring needed middlewares in engine's initializer([#2919](https://github.com/railsadminteam/rails_admin/issues/2919), [#3006](https://github.com/railsadminteam/rails_admin/pull/3006)) - Make the link text to uploaded file shorter, instead of showing full url([#2983](https://github.com/railsadminteam/rails_admin/pull/2983)) - Fix duplication of filters on browser back([#2998](https://github.com/railsadminteam/rails_admin/pull/2998)) - Fix "can't modify frozen array" exception on code reload([#2999](https://github.com/railsadminteam/rails_admin/pull/2999)) - Fix incorrectly comparing numeric columns with empty string when handling blank operator([#3007](https://github.com/railsadminteam/rails_admin/pull/3007)) ## [1.3.0](https://github.com/railsadminteam/rails_admin/tree/v1.3.0) - 2018-02-18 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.2.0...v1.3.0) ### Added - Configurability for forgery protection setting([#2989](https://github.com/railsadminteam/rails_admin/pull/2989)) - Configurability for the number of audit records displayed into dashboard([#2982](https://github.com/railsadminteam/rails_admin/pull/2982)) - Add limited pagination mode, which doesn't require count query([#2968](https://github.com/railsadminteam/rails_admin/pull/2968)) - Prettier output of JSON field value([#2937](https://github.com/railsadminteam/rails_admin/pull/2937), [#2973](https://github.com/railsadminteam/rails_admin/pull/2973), [#2980](https://github.com/railsadminteam/rails_admin/pull/2980)) - Add markdown field support through SimpleMDE([#2949](https://github.com/railsadminteam/rails_admin/pull/2949)) - Checkboxes for bulk actions in index page can be turned off now([#2917](https://github.com/railsadminteam/rails_admin/pull/2917)) ### Fixed - Parse JS translations as JSON([#2925](https://github.com/railsadminteam/rails_admin/pull/2925)) - Re-selecting an item after unselecting has no effect in filtering-multiselect([#2912](https://github.com/railsadminteam/rails_admin/issues/2912)) - Stop memoization of datetime parser to handle locale changes([#2824](https://github.com/railsadminteam/rails_admin/pull/2824)) - Filters for ActiveRecord Enum field behaved incorrectly for enums whose labels are different from values([#2971](https://github.com/railsadminteam/rails_admin/pull/2971)) - Client-side required validation was not enforced in filtering-select widget([#2905](https://github.com/railsadminteam/rails_admin/pull/2905)) - Filter refresh button was broken([#2890](https://github.com/railsadminteam/rails_admin/pull/2890)) ### Security - Fix XSS vulnerability in filter and multi-select widget([#2985](https://github.com/railsadminteam/rails_admin/issues/2985), [44f09ed7](https://github.com/railsadminteam/rails_admin/commit/44f09ed72b5e0e917a5d61bd89c48d97c494b41c)) ## [1.2.0](https://github.com/railsadminteam/rails_admin/tree/v1.2.0) - 2017-05-31 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.1.1...v1.2.0) ### Added - Add ILIKE support for PostgreSQL/PostGIS adapter, multibyte downcase for other adapters([#2766](https://github.com/railsadminteam/rails_admin/pull/2766)) - Support for UUID query([#2766](https://github.com/railsadminteam/rails_admin/pull/2766)) - Support for Haml 5([#2840](https://github.com/railsadminteam/rails_admin/pull/2840), [#2870](https://github.com/railsadminteam/rails_admin/pull/2870), [#2877](https://github.com/railsadminteam/rails_admin/pull/2877)) - Add instance option to append a CSS class for rows([#2860](https://github.com/railsadminteam/rails_admin/pull/2860)) ### Fixed - Remove usage of alias_method_chain, deprecated in Rails 5.0([#2864](https://github.com/railsadminteam/rails_admin/pull/2864)) - Load models from eager_load, not autoload_paths([#2771](https://github.com/railsadminteam/rails_admin/pull/2771)) - jQuery 3.0 doesn't have size(), use length instead([#2841](https://github.com/railsadminteam/rails_admin/pull/2841)) - Prepopulation of the new form didn't work with namespaced models([#2701](https://github.com/railsadminteam/rails_admin/pull/2701)) ## [1.1.1](https://github.com/railsadminteam/rails_admin/tree/v1.1.1) - 2016-12-25 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.1.0...v1.1.1) ### Fixed - CSV export broke with empty tables([#2796](https://github.com/railsadminteam/rails_admin/issues/2796), [#2797](https://github.com/railsadminteam/rails_admin/pull/2797)) - ActiveRecord adapter's #encoding did not work with Oracle enhanced adapter([#2789](https://github.com/railsadminteam/rails_admin/pull/2789)) - ActiveRecord 5 belongs_to presence validators were unintentionally disabled due to initialization mishandling([#2785](https://github.com/railsadminteam/rails_admin/issues/2785), [#2786](https://github.com/railsadminteam/rails_admin/issues/2786)) - Destroy failure caused subsequent index action to return 404, instead of 200([#2775](https://github.com/railsadminteam/rails_admin/issues/2775), [#2776](https://github.com/railsadminteam/rails_admin/pull/2776)) - CSVConverter#to_csv now accepts string-keyed hashes([#2740](https://github.com/railsadminteam/rails_admin/issues/2740), [#2741](https://github.com/railsadminteam/rails_admin/pull/2741)) ### Security - Fix CSRF vulnerability([b13e879e](https://github.com/railsadminteam/rails_admin/commit/b13e879eb93b661204e9fb5e55f7afa4f397537a)) ## [1.1.0](https://github.com/railsadminteam/rails_admin/tree/v1.1.0) - 2016-10-30 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.0.0...v1.1.0) ### Added - DSL for association eager-loading([#1325](https://github.com/railsadminteam/rails_admin/issues/1325), [#1342](https://github.com/railsadminteam/rails_admin/issues/1342)) ### Fixed - Fix nested has_many form failing to add items([#2737](https://github.com/railsadminteam/rails_admin/pull/2737)) ## [1.0.0](https://github.com/railsadminteam/rails_admin/tree/v1.0.0) - 2016-09-19 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.0.0.rc...v1.0.0) ### Added - Introduce setup hook for authorization/auditing adapters([ba2088c6](https://github.com/railsadminteam/rails_admin/commit/ba2088c6ecabd354b4b67c50bb00fccdbd1e6240)) - Add viewport meta tag for mobile layout adjustment([#2664](https://github.com/railsadminteam/rails_admin/pull/2664)) - Support for ActiveRecord::Enum using string columns([#2680](https://github.com/railsadminteam/rails_admin/pull/2680)) ### Changed - Limit children for deletion notice([#2491](https://github.com/railsadminteam/rails_admin/pull/2491)) - [BREAKING CHANGE] Change parent controller to ActionController::Base for out-of-box support of Rails 5 API mode([#2688](https://github.com/railsadminteam/rails_admin/issues/2688)) - To keep old behavior, add `config.parent_controller = '::ApplicationController'` in your RailsAdmin initializer. ### Fixed - ActiveRecord Enum fields could not be updated correctly([#2659](https://github.com/railsadminteam/rails_admin/pull/2659), [#2713](https://github.com/railsadminteam/rails_admin/issues/2713)) - Fix performance issue with filtering-multiselect widget([#2715](https://github.com/railsadminteam/rails_admin/pull/2715)) - Restore back rails_admin_controller?([#2268](https://github.com/railsadminteam/rails_admin/issues/2268)) - Duplication of autocomplete fields when using browser back/forward buttons([#2677](https://github.com/railsadminteam/rails_admin/issues/2677), [#2678](https://github.com/railsadminteam/rails_admin/pull/2678)) - Filter refresh button was broken([#2705](https://github.com/railsadminteam/rails_admin/issues/2705), [#2706](https://github.com/railsadminteam/rails_admin/pull/2706)) - Fix presence filtering on boolean columns([#1099](https://github.com/railsadminteam/rails_admin/issues/1099), [#2675](https://github.com/railsadminteam/rails_admin/pull/2675)) - Pundit::AuthorizationNotPerformedError was raised when used with Pundit([#2683](https://github.com/railsadminteam/rails_admin/pull/2683)) ## [1.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v1.0.0.rc) - 2016-07-18 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.8.1...v1.0.0.rc) ### Added - Rails 5 support - PaperTrail 5 support([9c42783a](https://github.com/railsadminteam/rails_admin/commit/9c42783aa65b704f4a5d467608c49b746c47b81b)) - Support for multiple configuration blocks([#1781](https://github.com/railsadminteam/rails_admin/pull/1781), [#2670](https://github.com/railsadminteam/rails_admin/pull/2670)) - Default association limit is now configurable([#2508](https://github.com/railsadminteam/rails_admin/pull/2058)) ### Changed - Prefix kaminari bootstrap views with `ra-` to avoid name conflict([#2283](https://github.com/railsadminteam/rails_admin/issues/2283), [#2651](https://github.com/railsadminteam/rails_admin/pull/2651)) - Gravatar icon is now optional([#2570](https://github.com/railsadminteam/rails_admin/pull/2570)) - Improve bootstrap-wysihtml5-rails support([#2650](https://github.com/railsadminteam/rails_admin/pull/2650)) - Explicitly call the #t method on I18n([#2564](https://github.com/railsadminteam/rails_admin/pull/2564)) - Improve dashboard performance by querying with id instead of updated_at([#2514](https://github.com/railsadminteam/rails_admin/issues/2514), [#2551](https://github.com/railsadminteam/rails_admin/pull/2551)) - Improve encoding support in CSV converter([#2508](https://github.com/railsadminteam/rails_admin/pull/2508), [dca8911f](https://github.com/railsadminteam/rails_admin/commit/dca8911f240ea11ebb186c33573188aa9e1b031d)) - Add SVG file extension to the image detection method([#2533](https://github.com/railsadminteam/rails_admin/pull/2533)) - Update linear gradient syntax to make autoprefixer happy([#2531](https://github.com/railsadminteam/rails_admin/pull/2531)) - Improve export layout ([#2505](https://github.com/railsadminteam/rails_admin/pull/2505)) ### Removed - Remove safe_yaml dependency([#2397](https://github.com/railsadminteam/rails_admin/pull/2397)) - Drop support for Ruby < 2.1.0 ### Fixed - Pagination did not work when showing all history([#2553](https://github.com/railsadminteam/rails_admin/pull/2553)) - Make filter-box label clickable([#2573](https://github.com/railsadminteam/rails_admin/pull/2573)) - Colorpicker form did not have the default css class `form-control`([#2571](https://github.com/railsadminteam/rails_admin/pull/2571)) - Stop assuming locale en is available([#2155](https://github.com/railsadminteam/rails_admin/issues/2155)) - Fix undefined method error with nested polymorphics([#1338](https://github.com/railsadminteam/rails_admin/issues/1338), [#2110](https://github.com/railsadminteam/rails_admin/pull/2110)) - Fix issue with nav does not check pjax config from an action([#2309](https://github.com/railsadminteam/rails_admin/pull/2309)) - Model label should be pluralized with locale([#1983](https://github.com/railsadminteam/rails_admin/pull/1983)) - Fix delocalize strftime_format for DateTime.strptime to support minus([#2547](https://github.com/railsadminteam/rails_admin/pull/2547)) - Fix Syntax Error in removal of new nested entity([#2539](https://github.com/railsadminteam/rails_admin/pull/2539)) - Fix momentjs translations for '%-d' format day of the month([#2540](https://github.com/railsadminteam/rails_admin/pull/2540)) - Fix Mongoid BSON object field ([#2495](https://github.com/railsadminteam/rails_admin/issues/2495)) - Make browser ignore validations of removed nested child models([#2443](https://github.com/railsadminteam/rails_admin/issues/2443), [#2490](https://github.com/railsadminteam/rails_admin/pull/2490)) ## [0.8.1](https://github.com/railsadminteam/rails_admin/tree/v0.8.1) - 2015-11-24 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.8.0...v0.8.1) ### Fixed - `vendor/` directory was missing from gemspec([#2481](https://github.com/railsadminteam/rails_admin/issues/2481), [#2482](https://github.com/railsadminteam/rails_admin/pull/2482)) ## [0.8.0](https://github.com/railsadminteam/rails_admin/tree/v0.8.0) - 2015-11-23 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.7.0...v0.8.0) ### Added - Feature to deactivate filtering-multiselect widget's remove buttons through `removable?` field option([#2446](https://github.com/railsadminteam/rails_admin/issues/2446)) - Pundit integration([#2399](https://github.com/railsadminteam/rails_admin/pull/2399) by Team CodeBenders, RGSoC'15) - Refile support([#2385](https://github.com/railsadminteam/rails_admin/pull/2385)) ### Changed - Some UI improvements in export view([#2394](https://github.com/railsadminteam/rails_admin/pull/2394)) - `rails_admin/custom/variables.scss` is now imported first to take advantage of Sass's `default!`([#2404](https://github.com/railsadminteam/rails_admin/pull/2404)) - Proxy classes now inherit from BasicObject([#2434](https://github.com/railsadminteam/rails_admin/issues/2434)) - Show sidebar scrollbar only on demand([#2419](https://github.com/railsadminteam/rails_admin/pull/2419)) - RailsAdmin no longer gets excluded from NewRelic instrumentation by default([#2402](https://github.com/railsadminteam/rails_admin/pull/2402)) - Improve efficiency of filter query in Postgres([#2401](https://github.com/railsadminteam/rails_admin/pull/2401)) - Replace old jQueryUI datepicker with jQuery Bootstrap datetimepicker ([#2391](https://github.com/railsadminteam/rails_admin/pull/2391)) - Turn Hash#symbolize into a helper to prevent namespace conflict([#2388](https://github.com/railsadminteam/rails_admin/pull/2388)) ### Removed - The L10n translation `admin.misc.filter_date_format` datepicker search filters, has been dropped in favor of field oriented configuration ([#2391](https://github.com/railsadminteam/rails_admin/pull/2391)) ### Fixed - AR#count broke when default-scoped with select([#2129](https://github.com/railsadminteam/rails_admin/pull/2129), [#2447](https://github.com/railsadminteam/rails_admin/issues/2447)) - Datepicker could not handle Spanish date properly([#982](https://github.com/railsadminteam/rails_admin/issues/982), [#2451](https://github.com/railsadminteam/rails_admin/pull/2451)) - Paperclip's `attachment_definitions` does not exist unless `has_attached_file`-ed([#1674](https://github.com/railsadminteam/rails_admin/issues/1674)) - `.btn` class was used without a modifier([#2417](https://github.com/railsadminteam/rails_admin/pull/2417)) - Filtering-multiselect widget ignored order([#2231](https://github.com/railsadminteam/rails_admin/issues/2231), [#2412](https://github.com/railsadminteam/rails_admin/pull/2412)) - Add missing .alert-dismissible class to flash([#2411](https://github.com/railsadminteam/rails_admin/pull/2411)) - Keep field order on changing the existing field's type([#2409](https://github.com/railsadminteam/rails_admin/pull/2409)) - Add button for nested-many form in modal disappeared on click([#2372](https://github.com/railsadminteam/rails_admin/issues/2372), [#2383](https://github.com/railsadminteam/rails_admin/pull/2383)) ### Security - Fix XSS vulnerability in polymorphic select([#2479](https://github.com/railsadminteam/rails_admin/pull/2479)) ## [0.7.0](https://github.com/railsadminteam/rails_admin/tree/v0.7.0) - 2015-08-16 [Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.6.8...v0.7.0) ### Added - Support for ActiveRecord::Enum ([#1993](https://github.com/railsadminteam/rails_admin/issues/1993)) - Multiselect-widget shows user friendly message, instead of just being blank ([#1369](https://github.com/railsadminteam/rails_admin/issues/1369), [#2360](https://github.com/railsadminteam/rails_admin/pull/2360)) - Configuration option to turn browser validation off ([#2339](https://github.com/railsadminteam/rails_admin/pull/2339), [#2373](https://github.com/railsadminteam/rails_admin/pull/2373)) ### Changed - Multiselect-widget inserts a new item to the bottom, instead of top ([#2167](https://github.com/railsadminteam/rails_admin/pull/2167)) - Migrated Cerulean theme to Bootstrap3 ([#2352](https://github.com/railsadminteam/rails_admin/pull/2352)) - Better html markup for input fields ([#2336](https://github.com/railsadminteam/rails_admin/pull/2336)) - Update filter dropdown button to Bootstrap3 ([#2277](https://github.com/railsadminteam/rails_admin/pull/2277)) - Improve navbar appearance ([#2310](https://github.com/railsadminteam/rails_admin/pull/2310)) - Do not monkey patch the app's YAML ([#2331](https://github.com/railsadminteam/rails_admin/pull/2331)) ### Fixed - Browser validation prevented saving of persisted upload fields ([#2376](https://github.com/railsadminteam/rails_admin/issues/2376)) - Fix inconsistent styling in static_navigation ([#2378](https://github.com/railsadminteam/rails_admin/pull/2378)) - Fix css regression for has_one and has_many nested form ([#2337](https://github.com/railsadminteam/rails_admin/pull/2337)) - HTML string inputs should not have a size attribute valorized with 0 ([#2335](https://github.com/railsadminteam/rails_admin/pull/2335)) ### Security - Fix XSS vulnerability in filtering-select widget - Fix XSS vulnerability in association fields ([#2343](https://github.com/railsadminteam/rails_admin/issues/2343)) ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing In the spirit of [free software][free-sw], **everyone** is encouraged to help improve this project. [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html Here are some ways _you_ can contribute: - by using alpha, beta, and prerelease versions - by triaging bug reports - by writing or editing documentation - by writing specifications - by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) - by refactoring code - by fixing [issues][] - by reviewing patches - by suggesting new features - [financially][gittip] [issues]: https://github.com/railsadminteam/rails_admin/issues [gittip]: https://www.gittip.com/sferik/ ## Development ### Running tests locally To run the test suite, you need Google Chrome and ImageMagick (or GraphicsMagick). Example installation on macOS using Homebrew: ```bash # install imagemagick: brew install imagemagick # install google chrome with cask: brew install brew-cask brew install google-chrome --cask # install mysql brew install mysql # install openssl brew install openssl@3 ``` Example installation on Ubuntu: ```bash # install imagemagick: sudo apt update -y && sudo apt install imagemagick -y # install google chrome: sudo apt update -y && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo apt install ./google-chrome-stable_current_amd64.deb -y ``` Then you need to do this one-time setup: ```bash # On Mac, you may run into problems with the mysql2 gem that require flags to be set in bundle config # See: https://github.com/brianmario/mysql2/issues/1345 bundle config build.mysql2 '-- --with-cflags="-Wno-error=implicit-function-declaration" --with-ldflags=-L/opt/homebrew/opt/zstd/lib' bundle install yarn install # install dependencies for each appraisal: bundle exec appraisal install # precompile assets in the dummy app: cd spec/dummy_app && yarn install && yarn build && yarn build:css && cd - ``` Then you will be able to run the specs: ```bash bundle exec appraisal rails-7.0 rspec ``` ### Tests run against multiple versions of Rails In CI, we use [appraisal] to run tests against multiple versions of Rails. The [gemfiles/ directory] contains the Gemfiles used to create the CI build matrix. See the [GitHub Actions configuration] for more details on what Ruby versions run what Rails versions. [appraisal]: https://github.com/thoughtbot/appraisal [gemfiles/ directory]: ./gemfiles [github actions configuration]: ./.github/workflows/test.yml ## Getting Help We use a [mailing list][list] for user support. If you've got a "how do I do this?" or "this doesn't seem to work the way I expect" type of issue or just want some help, please post a message to the [mailing list][list]. ## Submitting an Issue If you're confident that you've found a bug in rails_admin, please [open an issue][issues], but check to make sure it hasn't already been submitted. When submitting a bug report, please include a [Gist][] that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby version, and operating system. Ideally, a bug report should include a pull request with failing specs. [gist]: https://gist.github.com/ [list]: http://groups.google.com/group/rails_admin ## Submitting a Pull Request 1. [Fork the repository.][fork] 2. Create a branch. 3. Add specs for your unimplemented feature or bug fix. 4. Run `bundle exec rake spec`. If your specs pass, return to step 3. 5. Implement your feature or bug fix. 6. Run `bundle exec rake default`. If your specs fail, return to step 5. 7. Run `open coverage/index.html`. If your changes are not completely covered by your tests, return to step 3. 8. Add, commit, and push your changes. 9. [Submit a pull request.][pr] [fork]: http://help.github.com/fork-a-repo/ [pr]: http://help.github.com/send-pull-requests/ ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' gem 'appraisal', '>= 2.0' gem 'devise', '~> 4.7' gem 'net-smtp', require: false gem 'rails' gem 'sassc-rails', '~> 2.1' gem 'turbo-rails' gem 'vite_rails', require: false gem 'webpacker', require: false gem 'webrick' group :development, :test do gem 'pry', '>= 0.9' end group :test do gem 'cancancan', '~> 3.0' gem 'carrierwave', ['>= 2.0.0.rc', '< 3'] gem 'cuprite', '!= 0.15.1' gem 'database_cleaner-active_record', '>= 2.0', require: false gem 'dragonfly', '~> 1.0' gem 'factory_bot', '>= 4.2', '!= 6.4.5' gem 'generator_spec', '>= 0.8' gem 'kt-paperclip' gem 'launchy', '>= 2.2' gem 'mini_magick', '>= 3.4' gem 'pundit' gem 'rack-cache', require: 'rack/cache' gem 'rspec-expectations', '!= 3.8.3' gem 'rspec-rails', '>= 4.0.0.beta2' gem 'rspec-retry' gem 'rubocop', ['~> 1.20', '!= 1.22.2'], require: false gem 'rubocop-performance', require: false gem 'shrine', '~> 3.0' gem 'simplecov', '>= 0.9', require: false gem 'simplecov-lcov', require: false gem 'timecop', '>= 0.5' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] end group :active_record do gem 'paper_trail', '>= 12.0' platforms :ruby, :mswin, :mingw, :x64_mingw do gem 'mysql2', '>= 0.3.14' gem 'pg', '>= 1.0.0' gem 'sqlite3', '>= 1.3.0' end end gemspec ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2010-2016 Erik Michaels-Ober Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Procfile.teatro ================================================ web: cd spec/dummy_app && bundle exec rails server ================================================ FILE: README.md ================================================ # RailsAdmin [![Gem Version](https://img.shields.io/gem/v/rails_admin.svg)][gem] [![Build Status](https://github.com/railsadminteam/rails_admin/actions/workflows/test.yml/badge.svg)][ghactions] [![Coverage Status](https://img.shields.io/coveralls/railsadminteam/rails_admin.svg)][coveralls] [![Code Climate](https://codeclimate.com/github/railsadminteam/rails_admin.svg)][codeclimate] [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=rails_admin&package-manager=bundler&version-scheme=semver)][semver] [gem]: https://rubygems.org/gems/rails_admin [ghactions]: https://github.com/railsadminteam/rails_admin/actions/workflows/test.yml [coveralls]: https://coveralls.io/r/railsadminteam/rails_admin [codeclimate]: https://codeclimate.com/github/railsadminteam/rails_admin [semver]: https://dependabot.com/compatibility-score.html?dependency-name=rails_admin&package-manager=bundler&version-scheme=semver RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data. ## Getting started - Check out [the docs][docs]. - Try the [live demo][demo]. ([Source code][dummy_app]) [demo]: https://rails-admin.fly.dev/admin/ [dummy_app]: https://github.com/railsadminteam/rails_admin/tree/master/spec/dummy_app [docs]: https://github.com/railsadminteam/rails_admin/wiki ## Features - CRUD any data with ease - Custom actions - Automatic form validation - Search and filtering - Export data to CSV/JSON/XML - Authentication (via [Devise](https://github.com/plataformatec/devise) or other) - Authorization (via [CanCanCan](https://github.com/CanCanCommunity/cancancan) or [Pundit](https://github.com/elabs/pundit)) - User action history (via [PaperTrail](https://github.com/airblade/paper_trail)) - Supported ORMs - ActiveRecord - Mongoid ## Installation 1. On your gemfile: `gem 'rails_admin', '~> 3.0'` 2. Run `bundle install` 3. Run `rails g rails_admin:install` 4. Provide a namespace for the routes when asked 5. Start a server `rails s` and administer your data at [/admin](http://localhost:3000/admin). (if you chose default namespace: /admin) ## Upgrading from 2.x Due to introduction of Webpack/Webpacker support, some additional dependency and configuration will be needed. Running `rails g rails_admin:install` will suggest you some required changes, based on current setup of your app. ## Configuration ### Global In `config/initializers/rails_admin.rb`: [Details](https://github.com/railsadminteam/rails_admin/wiki/Base-configuration) To begin with, you may be interested in setting up [Devise](https://github.com/railsadminteam/rails_admin/wiki/Devise), [CanCanCan](https://github.com/railsadminteam/rails_admin/wiki/Cancancan) or [Papertrail](https://github.com/railsadminteam/rails_admin/wiki/Papertrail)! ### Per model ```ruby class Ball < ActiveRecord::Base validates :name, presence: true belongs_to :player rails_admin do configure :player do label 'Owner of this ball: ' end end end ``` Details: [Models](https://github.com/railsadminteam/rails_admin/wiki/Models), [Groups](https://github.com/railsadminteam/rails_admin/wiki/Groups), [Fields](https://github.com/railsadminteam/rails_admin/wiki/Fields) ## Support If you have a question, please check this README, the wiki, and the [list of known issues][troubleshoot]. [troubleshoot]: https://github.com/railsadminteam/rails_admin/wiki/Troubleshoot If you still have a question, you can ask the [official RailsAdmin mailing list][list]. [list]: http://groups.google.com/group/rails_admin If you think you found a bug in RailsAdmin, you can [submit an issue](https://github.com/railsadminteam/rails_admin/issues/new). ## Supported Ruby Versions This library aims to support and is [tested against][ghactions] the following Ruby implementations: - Ruby 2.6 - Ruby 2.7 - Ruby 3.0 - Ruby 3.1 - Ruby 3.2 - [JRuby][] [jruby]: http://jruby.org/ ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. Dir['lib/tasks/*.rake'].each { |rake| load rake } require 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) task test: :spec begin require 'rubocop/rake_task' RuboCop::RakeTask.new rescue LoadError desc 'Run RuboCop' task :rubocop do warn 'Rubocop is disabled' end end task default: %i[spec rubocop] namespace :vendorize do desc 'Tasks for vendorizing assets' task :flatpickr do Dir.chdir(__dir__) flatpickr = File.read('node_modules/flatpickr/dist/flatpickr.js') locales = Dir.glob('node_modules/flatpickr/dist/l10n/*.js').map { |f| File.read(f) } File.write('vendor/assets/javascripts/rails_admin/flatpickr-with-locales.js', ([flatpickr] + locales).join("\n")) end end ================================================ FILE: app/assets/javascripts/rails_admin/application.js.erb ================================================ //= require 'rails-ujs' //= require 'turbo' //= require 'rails_admin/jquery3' //= require 'jquery_nested_form' //= require 'rails_admin/jquery-ui/effect' //= require 'rails_admin/jquery-ui/widgets/sortable' //= require 'rails_admin/jquery-ui/widgets/autocomplete' //= require 'rails_admin/flatpickr-with-locales' //= require 'rails_admin/popper' //= require 'rails_admin/bootstrap' //= require 'rails_admin/abstract-select' //= require 'rails_admin/filter-box' //= require 'rails_admin/filtering-multiselect' //= require 'rails_admin/filtering-select' //= require 'rails_admin/remote-form' //= require 'rails_admin/nested-form-hooks' //= require 'rails_admin/i18n' //= require 'rails_admin/widgets' //= require 'rails_admin/sidescroll' //= require 'rails_admin/ui' //= require 'rails_admin/custom/ui' <% if defined?(ActiveStorage::Engine) %> //= require activestorage <% end %> <% if defined?(ActionText::Engine) && Rails.gem_version >= Gem::Version.new('7.0') %> //= require trix //= require actiontext <% end %> ================================================ FILE: app/assets/javascripts/rails_admin/custom/ui.js ================================================ // override this file in your application to add custom behaviour ================================================ FILE: app/assets/stylesheets/rails_admin/application.scss.erb ================================================ @charset "UTF-8"; /*** Variables ***/ @import "rails_admin/custom/variables"; @import "rails_admin/styles/base/variables"; /*** Mixins ***/ @import "rails_admin/styles/base/mixins"; @import "rails_admin/custom/mixins"; /*** Bootstrap ***/ @import "rails_admin/bootstrap/bootstrap"; /*** Libraries ***/ @import "rails_admin/flatpickr"; @import "rails_admin/styles/filtering-select"; @import "rails_admin/styles/filtering-multiselect"; @import "rails_admin/styles/widgets"; /*** Font-awesome ***/ @import "rails_admin/font-awesome"; /*** RailsAdmin Theming ***/ @import "rails_admin/styles/base/theming"; @import "rails_admin/custom/theming"; <% if defined?(ActionText::Engine) && Rails.gem_version >= Gem::Version.new('7.0') %> @import "trix"; <% end %> ================================================ FILE: app/assets/stylesheets/rails_admin/custom/mixins.scss ================================================ /* Customize Sass mixins from Twitter-Bootstrap/RailsAdmin theme or add new ones for your own use. Copy this file to your app/assets/rails_admin/custom/mixins.scss, leave this one untouched Don't require it in your application.rb Available mixins to use/override: https://github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap/mixins https://github.com/railsadminteam/rails_admin/blob/master/src/rails_admin/styles/base/mixins.scss Plus the ones from your theme. */ ================================================ FILE: app/assets/stylesheets/rails_admin/custom/theming.scss ================================================ /* Customize RailsAdmin theme here. Copy this file to your app/assets/stylesheets/rails_admin/custom/theming.scss, leave this one untouched Don't require it in your application.rb Look at the markup in RailsAdmin and go there to get inspiration from: http://getbootstrap.com Test me: (actual color should be the one defined in variables.scss if you did) body { background-color: $link-color; } */ ================================================ FILE: app/assets/stylesheets/rails_admin/custom/variables.scss ================================================ /* Customize Sass variables from Twitter-Bootstrap/RailsAdmin theme or add new ones for your own use. Copy this file to your app/assets/rails_admin/custom/variables.scss, leave this one untouched Don't require it in your application.rb Available variables to use/override: https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss https://github.com/railsadminteam/rails_admin/blob/master/src/rails_admin/styles/base/variables.scss Plus the ones from your themes. Test me: pink links $link-color: #F0F; */ ================================================ FILE: app/controllers/rails_admin/application_controller.rb ================================================ # frozen_string_literal: true require 'rails_admin/abstract_model' module RailsAdmin class ModelNotFound < ::StandardError end class ObjectNotFound < ::StandardError end class ActionNotAllowed < ::StandardError end class ApplicationController < Config.parent_controller.constantize include RailsAdmin::Extensions::ControllerExtension protect_from_forgery(Config.forgery_protection_settings) before_action :_authenticate! before_action :_authorize! before_action :_audit! helper_method :_current_user, :_get_plugin_name attr_reader :object, :model_config, :abstract_model, :authorization_adapter def get_model @model_name = to_model_name(params[:model_name]) raise RailsAdmin::ModelNotFound unless (@abstract_model = RailsAdmin::AbstractModel.new(@model_name)) raise RailsAdmin::ModelNotFound if (@model_config = @abstract_model.config).excluded? @properties = @abstract_model.properties end def get_object raise RailsAdmin::ObjectNotFound unless (@object = @abstract_model.get(params[:id], @model_config.scope)) end def to_model_name(param) param.split('~').collect(&:camelize).join('::') end def _current_user instance_eval(&RailsAdmin::Config.current_user_method) end private def _get_plugin_name @plugin_name_array ||= [RailsAdmin.config.main_app_name.is_a?(Proc) ? instance_eval(&RailsAdmin.config.main_app_name) : RailsAdmin.config.main_app_name].flatten end def _authenticate! instance_eval(&RailsAdmin::Config.authenticate_with) end def _authorize! instance_eval(&RailsAdmin::Config.authorize_with) end def _audit! instance_eval(&RailsAdmin::Config.audit_with) end def rails_admin_controller? true end rescue_from RailsAdmin::ObjectNotFound do flash[:error] = I18n.t('admin.flash.object_not_found', model: @model_name, id: params[:id]) params[:action] = 'index' @status_code = :not_found index end rescue_from RailsAdmin::ModelNotFound do flash[:error] = I18n.t('admin.flash.model_not_found', model: @model_name) params[:action] = 'dashboard' @status_code = :not_found dashboard end end end ================================================ FILE: app/controllers/rails_admin/main_controller.rb ================================================ # frozen_string_literal: true module RailsAdmin class MainController < RailsAdmin::ApplicationController include ActionView::Helpers::TextHelper include RailsAdmin::MainHelper include RailsAdmin::ApplicationHelper before_action :check_for_cancel def bulk_action get_model process(params[:bulk_action]) if params[:bulk_action].in?(RailsAdmin::Config::Actions.all(:bulkable, controller: self, abstract_model: @abstract_model).collect(&:route_fragment)) end def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = get_association_scope_from_params, pagination = !(params[:associated_collection] || params[:all] || params[:bulk_ids])) scope = model_config.scope auth_scope = @authorization_adapter&.query(auth_scope_key, model_config.abstract_model) scope = scope.merge(auth_scope) if auth_scope scope = scope.instance_eval(&additional_scope) if additional_scope get_collection(model_config, scope, pagination) end private def action_missing(name, *_args) action = RailsAdmin::Config::Actions.find(name.to_sym) raise AbstractController::ActionNotFound.new("The action '#{name}' could not be found for #{self.class.name}") unless action get_model unless action.root? get_object if action.member? @authorization_adapter.try(:authorize, action.authorization_key, @abstract_model, @object) @action = action.with({controller: self, abstract_model: @abstract_model, object: @object}) raise(ActionNotAllowed) unless @action.enabled? @page_name = wording_for(:title) instance_eval(&@action.controller) end def method_missing(name, *args, &block) action = RailsAdmin::Config::Actions.find(name.to_sym) if action action_missing name, *args, &block else super end end def respond_to_missing?(sym, include_private) if RailsAdmin::Config::Actions.find(sym) true else super end end def back_or_index allowed_return_to?(params[:return_to].to_s) ? params[:return_to] : index_path end def allowed_return_to?(url) url != request.fullpath && url.start_with?(request.base_url, '/') && !url.start_with?('//') end def get_sort_hash(model_config) field = model_config.list.fields.detect { |f| f.name.to_s == params[:sort] } # If no sort param, default to the `sort_by` specified in the list config field ||= model_config.list.possible_fields.detect { |f| f.name == model_config.list.sort_by.try(:to_sym) } column = if field.nil? || field.sortable == false # use default sort, asked field does not exist or is not sortable model_config.list.sort_by else field.sort_column end params[:sort_reverse] ||= 'false' {sort: column, sort_reverse: (params[:sort_reverse] == (field&.sort_reverse&.to_s || 'true'))} end def redirect_to_on_success notice = I18n.t('admin.flash.successful', name: @model_config.label, action: I18n.t("admin.actions.#{@action.key}.done")) if params[:_add_another] redirect_to new_path(return_to: params[:return_to]), flash: {success: notice} elsif params[:_add_edit] redirect_to edit_path(id: @object.id, return_to: params[:return_to]), flash: {success: notice} else redirect_to back_or_index, flash: {success: notice} end end def visible_fields(action, model_config = @model_config) model_config.send(action).with(controller: self, view: view_context, object: @object).visible_fields end def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key]) return unless target_params.present? fields = visible_fields(action, model_config) allowed_methods = fields.collect(&:allowed_methods).flatten.uniq.collect(&:to_s) << 'id' << '_destroy' fields.each { |field| field.parse_input(target_params) } target_params.slice!(*allowed_methods) target_params.permit! if target_params.respond_to?(:permit!) fields.select(&:nested_form).each do |association| children_params = association.multiple? ? target_params[association.method_name].try(:values) : [target_params[association.method_name]].compact (children_params || []).each do |children_param| sanitize_params_for!(:nested, association.associated_model_config, children_param) end end end def handle_save_error(whereto = :new) flash.now[:error] = I18n.t('admin.flash.error', name: @model_config.label, action: I18n.t("admin.actions.#{@action.key}.done").html_safe).html_safe flash.now[:error] += %(
- #{@object.errors.full_messages.join('
- ')}).html_safe respond_to do |format| format.html { render whereto, status: :not_acceptable } format.js { render whereto, layout: 'rails_admin/modal', status: :not_acceptable, content_type: Mime[:html].to_s } end end def check_for_cancel return unless params[:_continue] || (params[:bulk_action] && !params[:bulk_ids]) redirect_to(back_or_index, notice: I18n.t('admin.flash.noaction')) end def get_collection(model_config, scope, pagination) section = @action.key == :export ? model_config.export : model_config.list eager_loads = section.fields.flat_map(&:eager_load_values) options = {} options = options.merge(page: (params[Kaminari.config.param_name] || 1).to_i, per: (params[:per] || model_config.list.items_per_page)) if pagination options = options.merge(include: eager_loads) unless eager_loads.blank? options = options.merge(get_sort_hash(model_config)) options = options.merge(query: params[:query]) if params[:query].present? options = options.merge(filters: params[:f]) if params[:f].present? options = options.merge(bulk_ids: params[:bulk_ids]) if params[:bulk_ids] model_config.abstract_model.all(options, scope) end def get_association_scope_from_params return nil unless params[:associated_collection].present? source_abstract_model = RailsAdmin::AbstractModel.new(to_model_name(params[:source_abstract_model])) source_model_config = source_abstract_model.config source_object = source_abstract_model.get(params[:source_object_id]) action = params[:current_action].in?(%w[create update]) ? params[:current_action] : 'edit' @association = source_model_config.send(action).fields.detect { |f| f.name == params[:associated_collection].to_sym }.with(controller: self, object: source_object) @association.associated_collection_scope end end end ================================================ FILE: app/helpers/rails_admin/application_helper.rb ================================================ # frozen_string_literal: true module RailsAdmin module ApplicationHelper def authorized?(action_name, abstract_model = nil, object = nil) object = nil if object.try :new_record? action(action_name, abstract_model, object).try(:authorized?) end def current_action params[:action].in?(%w[create new]) ? 'create' : 'update' end def current_action?(action, abstract_model = @abstract_model, object = @object) @action.custom_key == action.custom_key && abstract_model.try(:to_param) == @abstract_model.try(:to_param) && (@object.try(:persisted?) ? @object.id == object.try(:id) : !object.try(:persisted?)) end def action(key, abstract_model = nil, object = nil) RailsAdmin::Config::Actions.find(key, controller: controller, abstract_model: abstract_model, object: object) end def actions(scope = :all, abstract_model = nil, object = nil) RailsAdmin::Config::Actions.all(scope, controller: controller, abstract_model: abstract_model, object: object) end def edit_user_link return nil unless _current_user.try(:email).present? return nil unless (abstract_model = RailsAdmin.config(_current_user.class).abstract_model) edit_action = action(:edit, abstract_model, _current_user) authorized = edit_action.try(:authorized?) content = edit_user_link_label if authorized edit_url = rails_admin.url_for( action: edit_action.action_name, model_name: abstract_model.to_param, controller: 'rails_admin/main', id: _current_user.id, ) link_to content, edit_url, class: 'nav-link' else content_tag :span, content, class: 'nav-link' end end def logout_path if defined?(Devise) scope = Devise::Mapping.find_scope!(_current_user) begin main_app.send("destroy_#{scope}_session_path") rescue StandardError false end elsif main_app.respond_to?(:logout_path) main_app.logout_path end end def logout_method return [Devise.sign_out_via].flatten.first if defined?(Devise) :delete end def wording_for(label, action = @action, abstract_model = @abstract_model, object = @object) model_config = abstract_model.try(:config) object = nil unless abstract_model && object.is_a?(abstract_model.model) action = RailsAdmin::Config::Actions.find(action.to_sym) if action.is_a?(Symbol) || action.is_a?(String) I18n.t( "admin.actions.#{action.i18n_key}.#{label}", model_label: model_config&.label, model_label_plural: model_config&.label_plural, object_label: model_config && object.try(model_config.object_label_method), ) end def main_navigation nodes_stack = RailsAdmin::Config.visible_models(controller: controller) node_model_names = nodes_stack.collect { |c| c.abstract_model.model_name } parent_groups = nodes_stack.group_by { |n| n.parent&.to_s } nodes_stack.group_by(&:navigation_label).collect do |navigation_label, nodes| nodes = nodes.select { |n| n.parent.nil? || !n.parent.to_s.in?(node_model_names) } li_stack = navigation parent_groups, nodes label = navigation_label || t('admin.misc.navigation') collapsible_stack(label, 'main', li_stack) end.join.html_safe end def root_navigation actions(:root).select(&:show_in_sidebar).group_by(&:sidebar_label).collect do |label, nodes| li_stack = nodes.map do |node| url = rails_admin.url_for(action: node.action_name, controller: 'rails_admin/main') nav_icon = node.link_icon ? %().html_safe : '' content_tag :li do link_to nav_icon + " " + wording_for(:menu, node), url, class: "nav-link" end end.join.html_safe label ||= t('admin.misc.root_navigation') collapsible_stack(label, 'action', li_stack) end.join.html_safe end def static_navigation li_stack = RailsAdmin::Config.navigation_static_links.collect do |title, url| content_tag(:li, link_to(title.to_s, url, target: '_blank', rel: 'noopener noreferrer', class: 'nav-link')) end.join.html_safe label = RailsAdmin::Config.navigation_static_label || t('admin.misc.navigation_static_label') collapsible_stack(label, 'static', li_stack) || '' end def navigation(parent_groups, nodes, level = 0) nodes.collect do |node| abstract_model = node.abstract_model model_param = abstract_model.to_param url = rails_admin.url_for(action: :index, controller: 'rails_admin/main', model_name: model_param) nav_icon = node.navigation_icon ? %().html_safe : '' css_classes = ['nav-link'] css_classes.push("nav-level-#{level}") if level > 0 css_classes.push('active') if @action && current_action?(@action, model_param) li = content_tag :li, data: {model: model_param} do link_to nav_icon + " " + node.label_plural, url, class: css_classes.join(' ') end child_nodes = parent_groups[abstract_model.model_name] child_nodes ? li + navigation(parent_groups, child_nodes, level + 1) : li end.join.html_safe end def breadcrumb(action = @action, _acc = []) begin (parent_actions ||= []) << action end while action.breadcrumb_parent && (action = action(*action.breadcrumb_parent)) # rubocop:disable Lint/Loop content_tag(:ol, class: 'breadcrumb') do parent_actions.collect do |a| am = a.send(:eval, 'bindings[:abstract_model]') o = a.send(:eval, 'bindings[:object]') content_tag(:li, class: ['breadcrumb-item', current_action?(a, am, o) && 'active']) do if current_action?(a, am, o) wording_for(:breadcrumb, a, am, o) elsif a.http_methods.include?(:get) link_to rails_admin.url_for(action: a.action_name, controller: 'rails_admin/main', model_name: am.try(:to_param), id: (o.try(:persisted?) && o.try(:id) || nil)) do wording_for(:breadcrumb, a, am, o) end else content_tag(:span, wording_for(:breadcrumb, a, am, o)) end end end.reverse.join.html_safe end end # parent => :root, :collection, :member # perf matters here (no action view trickery) def menu_for(parent, abstract_model = nil, object = nil, only_icon = false) actions = actions(parent, abstract_model, object).select { |a| a.http_methods.include?(:get) && a.show_in_menu } actions.collect do |action| wording = wording_for(:menu, action) li_class = ['nav-item', 'icon', "#{action.key}_#{parent}_link"]. concat(action.enabled? ? [] : ['disabled']) content_tag(:li, {class: li_class}.merge(only_icon ? {title: wording, rel: 'tooltip'} : {})) do label = content_tag(:i, '', {class: action.link_icon}) + ' ' + content_tag(:span, wording, (only_icon ? {style: 'display:none'} : {})) if action.enabled? || !only_icon href = if action.enabled? rails_admin.url_for(action: action.action_name, controller: 'rails_admin/main', model_name: abstract_model.try(:to_param), id: (object.try(:persisted?) && object.try(:id) || nil)) else 'javascript:void(0)' end content_tag(:a, label, {href: href, target: action.link_target, class: ['nav-link', current_action?(action) && 'active', !action.enabled? && 'disabled'].compact}.merge(action.turbo? ? {} : {data: {turbo: 'false'}})) else content_tag(:span, label) end end end.join(' ').html_safe end def bulk_menu(abstract_model = @abstract_model) actions = actions(:bulkable, abstract_model) return '' if actions.empty? content_tag :li, class: 'nav-item dropdown dropdown-menu-end' do content_tag(:a, class: 'nav-link dropdown-toggle', data: {'bs-toggle': 'dropdown'}, href: '#') { t('admin.misc.bulk_menu_title').html_safe + ' ' + ''.html_safe } + content_tag(:ul, class: 'dropdown-menu', style: 'left:auto; right:0;') do actions.collect do |action| content_tag :li do link_to wording_for(:bulk_link, action, abstract_model), '#', class: 'dropdown-item bulk-link', data: {action: action.action_name} end end.join.html_safe end end.html_safe end def flash_alert_class(flash_key) case flash_key.to_s when 'error' then 'alert-danger' when 'alert' then 'alert-warning' when 'notice' then 'alert-info' else "alert-#{flash_key}" end end def handle_asset_dependency_error yield rescue LoadError => e if /sassc/.match?(e.message) e = e.exception <<~MSG #{e.message} RailsAdmin requires the gem sassc-rails, make sure to put `gem 'sassc-rails'` to Gemfile. MSG end raise e end # Workaround for https://github.com/rails/rails/issues/31325 def image_tag(source, options = {}) if %w[ActiveStorage::Variant ActiveStorage::VariantWithRecord ActiveStorage::Preview].include? source.class.to_s super main_app.route_for(ActiveStorage.resolve_model_to_route, source), options else super end end private def edit_user_link_label [ RailsAdmin::Config.show_gravatar && image_tag(gravatar_url(_current_user.email), alt: ''), content_tag(:span, _current_user.email), ].filter(&:present?).join.html_safe end def gravatar_url(email) "https://secure.gravatar.com/avatar/#{Digest::MD5.hexdigest email}?s=30" end def collapsible_stack(label, class_prefix, li_stack) return nil unless li_stack.present? collapse_classname = "#{class_prefix}-#{Digest::MD5.hexdigest(label)[0..7]}" content_tag(:li, class: 'mb-1') do content_tag(:button, 'aria-expanded': true, class: 'btn btn-toggle align-items-center rounded', data: {bs_toggle: "collapse", bs_target: ".sidebar .#{collapse_classname}"}) do content_tag(:i, '', class: 'fas fa-chevron-down') + html_escape(' ' + label) end + content_tag(:div, class: "collapse show #{collapse_classname}") do content_tag(:ul, class: 'btn-toggle-nav list-unstyled fw-normal pb-1') do li_stack end end end end end end ================================================ FILE: app/helpers/rails_admin/form_builder.rb ================================================ # frozen_string_literal: true require 'nested_form/builder_mixin' module RailsAdmin class FormBuilder < ::ActionView::Helpers::FormBuilder include ::NestedForm::BuilderMixin include ::RailsAdmin::ApplicationHelper def generate(options = {}) without_field_error_proc_added_div do options.reverse_merge!( action: @template.controller.params[:action], model_config: @template.instance_variable_get(:@model_config), nested_in: false, ) object_infos + visible_groups(options[:model_config], generator_action(options[:action], options[:nested_in])).collect do |fieldset| fieldset_for fieldset, options[:nested_in] end.join.html_safe + (options[:nested_in] ? '' : @template.render(partial: 'rails_admin/main/submit_buttons')) end end def fieldset_for(fieldset, nested_in) fields = fieldset.with( form: self, object: @object, view: @template, controller: @template.controller, ).visible_fields return if fields.empty? @template.content_tag :fieldset do contents = [] contents << @template.content_tag(:legend, %( #{fieldset.label}).html_safe, style: fieldset.name == :default ? 'display:none' : '') contents << @template.content_tag(:p, fieldset.help) if fieldset.help.present? contents << fields.collect { |field| field_wrapper_for(field, nested_in) }.join contents.join.html_safe end end def field_wrapper_for(field, nested_in) # do not show nested field if the target is the origin return if nested_field_association?(field, nested_in) @template.content_tag(:div, class: "control-group row mb-3 #{field.type_css_class} #{field.css_class} #{'error' if field.errors.present?}", id: "#{dom_id(field)}_field") do if field.label label(field.method_name, field.label, class: 'col-sm-2 col-form-label text-md-end') + (field.nested_form ? field_for(field) : input_for(field)) else field.nested_form ? field_for(field) : input_for(field) end end end def input_for(field) css = 'col-sm-10 controls' css += ' has-error' if field.errors.present? @template.content_tag(:div, class: css) do field_for(field) + errors_for(field) + help_for(field) end end def errors_for(field) field.errors.present? ? @template.content_tag(:span, field.errors.to_sentence, class: 'help-inline text-danger') : ''.html_safe end def help_for(field) field.help.present? ? @template.content_tag(:div, field.help, class: 'form-text') : ''.html_safe end def field_for(field) field.read_only? ? @template.content_tag(:div, field.pretty_value, class: 'form-control-static') : field.render end def object_infos model_config = RailsAdmin.config(object) model_label = model_config.label object_label = if object.new_record? I18n.t('admin.form.new_model', name: model_label) else object.send(model_config.object_label_method).presence || "#{model_config.label} ##{object.id}" end %().html_safe end def jquery_namespace(field) %(#{'#modal ' if @template.controller.params[:modal]}##{dom_id(field)}_field) end def dom_id(field) (@dom_id ||= {})[field.name] ||= [ @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, '_').sub(/_$/, ''), options[:index], field.method_name, ].reject(&:blank?).join('_') end def dom_name(field) (@dom_name ||= {})[field.name] ||= %(#{@object_name}#{options[:index] && "[#{options[:index]}]"}[#{field.method_name}]#{field.is_a?(Config::Fields::Association) && field.multiple? ? '[]' : ''}) end def hidden_field(method, options = {}) if method == :id && object.id.is_a?(Array) super method, {value: RailsAdmin.config.composite_keys_serializer.serialize(object.id)} else super end end protected def generator_action(action, nested) if nested action = :nested elsif @template.request.format == 'text/javascript' action = :modal end action end def visible_groups(model_config, action) model_config.send(action).with( form: self, object: @object, view: @template, controller: @template.controller, ).visible_groups end def without_field_error_proc_added_div default_field_error_proc = ::ActionView::Base.field_error_proc begin ::ActionView::Base.field_error_proc = proc { |html_tag, _instance| html_tag } yield ensure ::ActionView::Base.field_error_proc = default_field_error_proc end end private def nested_field_association?(field, nested_in) field.inverse_of.presence && nested_in.presence && field.inverse_of == nested_in.name && (@template.instance_variable_get(:@model_config).abstract_model == field.abstract_model || field.name == nested_in.inverse_of) end end end ================================================ FILE: app/helpers/rails_admin/main_helper.rb ================================================ # frozen_string_literal: true module RailsAdmin module MainHelper def rails_admin_form_for(*args, &block) options = args.extract_options!.reverse_merge(builder: RailsAdmin::FormBuilder) (options[:html] ||= {})[:novalidate] ||= !RailsAdmin::Config.browser_validations form_for(*(args << options), &block) << after_nested_form_callbacks end def get_indicator(percent) return '' if percent < 0 # none return 'info' if percent < 34 # < 1/100 of max return 'success' if percent < 67 # < 1/10 of max return 'warning' if percent < 84 # < 1/3 of max 'danger' # > 1/3 of max end def filterable_fields @filterable_fields ||= @model_config.list.fields.select(&:filterable?) end def ordered_filters return @ordered_filters if @ordered_filters.present? @index = 0 @ordered_filters = (params[:f].try(:permit!).try(:to_h) || @model_config.list.filters).inject({}) do |memo, filter| field_name = filter.is_a?(Array) ? filter.first : filter (filter.is_a?(Array) ? filter.last : {(@index += 1) => {'v' => ''}}).each do |index, filter_hash| if filter_hash['disabled'].blank? memo[index] = {field_name => filter_hash} else params[:f].delete(field_name) end end memo end.to_a.sort_by(&:first) end def ordered_filter_options if ordered_filters @ordered_filter_options ||= ordered_filters.map do |duplet| filter_for_field = duplet[1] filter_name = filter_for_field.keys.first filter_hash = filter_for_field.values.first unless (field = filterable_fields.find { |f| f.name == filter_name.to_sym }&.with({view: self})) raise "#{filter_name} is not currently filterable; filterable fields are #{filterable_fields.map(&:name).join(', ')}" end field.filter_options.merge( index: duplet[0], operator: filter_hash['o'] || field.default_filter_operator, value: filter_hash['v'], ) end end end end end ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/_gap.html.erb ================================================
  • <%= raw(t 'admin.pagination.truncate') %>
  • ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/_next_page.html.erb ================================================ <% if current_page.last? %> <% else %> <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/_page.html.erb ================================================ <% if page.current? %>
  • <%= link_to page, url, class: 'page-link' %>
  • <% else %>
  • <%= link_to page, url, class: 'page-link' %>
  • <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/_paginator.html.erb ================================================ <%= paginator.render do %> <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.erb ================================================ <% if current_page.first? %> <% else %> <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.erb ================================================ <% if current_page.last? %> <% else %> <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.erb ================================================ <%= paginator.render do %> <% end %> ================================================ FILE: app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.erb ================================================ <% if current_page.first? %> <% else %> <% end %> ================================================ FILE: app/views/layouts/rails_admin/_head.html.erb ================================================ <%= csrf_meta_tag %> <% case RailsAdmin::config.asset_source when :webpacker %> <%= stylesheet_pack_tag "rails_admin", data: {'turbo-track': 'reload'} %> <%= javascript_pack_tag "rails_admin", defer: true, data: {'turbo-track': 'reload'} %> <% when :sprockets %> <% handle_asset_dependency_error do %> <%= stylesheet_link_tag "rails_admin/application.css", media: :all, data: {'turbo-track': 'reload'} %> <%= javascript_include_tag "rails_admin/application.js", defer: true, data: {'turbo-track': 'reload'} %> <% end %> <% when :vite %> <%= vite_javascript_tag "rails_admin", defer: true, data: {'turbo-track': 'reload'} %> <% when :webpack %> <%= stylesheet_link_tag "rails_admin.css", media: :all, data: {'turbo-track': 'reload'} %> <%= javascript_include_tag "rails_admin.js", defer: true, data: {'turbo-track': 'reload'} %> <% when :importmap %> <%= stylesheet_link_tag "rails_admin.css", media: :all, data: {'turbo-track': 'reload'} %> <%= javascript_inline_importmap_tag(RailsAdmin::Engine.importmap.to_json(resolver: self)) %> <%= javascript_importmap_module_preload_tags(RailsAdmin::Engine.importmap) %> <%= javascript_importmap_shim_nonce_configuration_tag if respond_to? :javascript_importmap_shim_nonce_configuration_tag %> <%= javascript_importmap_shim_tag if respond_to? :javascript_importmap_shim_tag %> <%= # Preload jQuery and make it global, unless jQuery UI fails to initialize tag.script "import jQuery from 'jquery'; window.jQuery = jQuery;".html_safe, type: "module" %> <%= javascript_import_module_tag 'rails_admin' %> <% else raise "Unknown asset_source: #{RailsAdmin::config.asset_source}" end %> ================================================ FILE: app/views/layouts/rails_admin/_navigation.html.erb ================================================
    <%= _get_plugin_name[0] || 'Rails' %> <%= _get_plugin_name[1] || 'Admin' %>
    ================================================ FILE: app/views/layouts/rails_admin/_secondary_navigation.html.erb ================================================ ================================================ FILE: app/views/layouts/rails_admin/_sidebar_navigation.html.erb ================================================ ================================================ FILE: app/views/layouts/rails_admin/application.html.erb ================================================ <%= render "layouts/rails_admin/head" %>
    " id="admin-js">
    <%= render "layouts/rails_admin/sidebar_navigation" %>
    <%= render template: 'layouts/rails_admin/content' %>
    ================================================ FILE: app/views/layouts/rails_admin/content.html.erb ================================================ <%= "#{@abstract_model.try(:pretty_name) || @page_name} | #{[_get_plugin_name[0] || 'Rails', _get_plugin_name[1] || 'Admin'].join(' ')}" %>

    <%= @page_name %>

    <% flash && flash.each do |key, value| %>
    <%= value %>
    <% end %> <%= yield %> ================================================ FILE: app/views/layouts/rails_admin/modal.js.erb ================================================ <% flash && flash.each do |key, value| %>
    <%= value %>
    <% end %> <%= yield %> ================================================ FILE: app/views/rails_admin/main/_dashboard_history.html.erb ================================================ <% @history.each do |t| %> <% abstract_model = RailsAdmin.config(t.table).abstract_model %> <% if o = abstract_model.try(:get, t.item) %> <% label = o.send(abstract_model.config.object_label_method) %> <% if show_action = action(:show, abstract_model, o) %> <% else %> <% end %> <% else %> <% label = Object.const_defined?(t.table) ? t.table.constantize.model_name.human : t.table %> <% end %> <% end %>
    <%= t("admin.table_headers.username") %> <%= t("admin.table_headers.item") %> <%= t("admin.table_headers.changes") %>
    <%= t.try :username %> <%= link_to(label, url_for(action: show_action.action_name, model_name: abstract_model.to_param, id: o.id)) %> <%= label %> <%= "#{label} ##{t.item}" %> <%= t.message %>
    ================================================ FILE: app/views/rails_admin/main/_delete_notice.html.erb ================================================ <% object = delete_notice %>
  • <%= @abstract_model.pretty_name %> <% wording = object.send(@model_config.object_label_method) %> <% if show_action = action(:show, @abstract_model, object) %> <%= link_to(wording, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: object.id)) %> <% else %> <%= wording %> <% end %>
  • ================================================ FILE: app/views/rails_admin/main/_form_action_text.html.erb ================================================ <% js_data = { csspath: field.css_location, jspath: field.js_location, warn_dynamic_load: field.warn_dynamic_load } %> <%= form.rich_text_area field.method_name, field.html_attributes.reverse_merge(data: { options: js_data.to_json }) %> ================================================ FILE: app/views/rails_admin/main/_form_boolean.html.erb ================================================ <% if field.nullable? %>
    <% {'1': [true, 'btn-outline-success'], '0': [false, 'btn-outline-danger'], '': [nil, 'btn-outline-secondary']}.each do |text, (value, btn_class)| %> <%= form.radio_button field.method_name, text, field.html_attributes.reverse_merge({ checked: field.form_value == value, required: field.required, class: 'btn-check' }) %> <%= form.label "#{field.method_name}_#{text}", class: "#{field.css_classes[value]} btn #{btn_class}" do %> <%= field.labels[value].html_safe %> <% end %> <% end %>
    <% else %>
    <%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, checked: field.form_value.in?([true, '1']), required: field.required, class: 'form-check-input' }) %>
    <% end %> ================================================ FILE: app/views/rails_admin/main/_form_ck_editor.html.erb ================================================ <% js_data = { jspath: field.location ? field.location : field.base_location + "ckeditor.js", base_location: field.base_location, options: { customConfig: field.config_js ? field.config_js : field.base_location + "config.js" } } %> <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'ckeditor', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> ================================================ FILE: app/views/rails_admin/main/_form_code_mirror.html.erb ================================================ <% js_data = { csspath: field.css_location, jspath: field.js_location, options: field.config, locations: field.assets } %> <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'codemirror', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> ================================================ FILE: app/views/rails_admin/main/_form_colorpicker.html.erb ================================================
    <%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({class: 'form-control', value: field.form_value}) %>
    ================================================ FILE: app/views/rails_admin/main/_form_datetime.html.erb ================================================
    <%= form.text_field field.method_name, field.html_attributes.reverse_merge({autocomplete: 'off', class: 'form-control', data: {datetimepicker: true, options: field.datepicker_options.to_json}, value: field.form_value}) %> <%= form.label(field.method_name, class: 'input-group-text') do %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/_form_enumeration.html.erb ================================================ <% unless field.multiple? %>
    <%= form.select field.method_name, field.enum, { include_blank: true }.reverse_merge({ selected: field.form_value }), field.html_attributes.reverse_merge({ data: { enumeration: true }, placeholder: t('admin.misc.search') }) %>
    <% else %> <% js_data = { xhr: false, sortable: false, cacheAll: true, regional: { add: t("admin.misc.add_new"), chooseAll: t("admin.misc.chose_all"), clearAll: t("admin.misc.clear_all"), down: t("admin.misc.down"), remove: t("admin.misc.remove"), search: t("admin.misc.search"), up: t("admin.misc.up") } } %> <%= form.select field.method_name, field.enum, { selected: field.form_value, object: form.object }, field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: js_data.to_json }, multiple: true}) %> <% end %> ================================================ FILE: app/views/rails_admin/main/_form_field.html.erb ================================================ <%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, class: 'form-control', required: field.required}) %> ================================================ FILE: app/views/rails_admin/main/_form_file_upload.html.erb ================================================ <% file = field.value %> <% if field.cache_method %> <%= form.hidden_field(field.cache_method, value: field.cache_value) %> <% end %>
    <% if value = field.pretty_value %> <%= value %> <% end %> <%= form.file_field(field.name, {data: {fileupload: true}}.deep_merge(field.html_attributes)) %>
    <% if field.optional? && field.errors.blank? && file && field.delete_method %> <%= I18n.t('admin.actions.delete.link', object_label: field.label) %> <%= form.check_box(field.delete_method, style: 'display:none;') %> <% end %> ================================================ FILE: app/views/rails_admin/main/_form_filtering_multiselect.html.erb ================================================ <% config = field.associated_model_config %>
    <%= form.select field.method_name, field.collection, { selected: field.form_value, object: form.object }, field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: field.widget_options.to_json }, multiple: true}) %>
    <% if authorized?(:new, config.abstract_model) && field.inline_add %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/_form_filtering_select.html.erb ================================================ <% config = field.associated_model_config %>
    <%= form.select field.method_name, field.collection, { selected: field.form_value, include_blank: true }, field.html_attributes.reverse_merge({ data: { filteringselect: true, options: field.widget_options.to_json }, placeholder: t('admin.misc.search') }) %>
    ================================================ FILE: app/views/rails_admin/main/_form_froala.html.erb ================================================ <% js_data = { csspath: field.css_location, jspath: field.js_location, config_options: field.config_options.to_json } %> <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'froala-wysiwyg', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> ================================================ FILE: app/views/rails_admin/main/_form_multiple_file_upload.html.erb ================================================ <% field.attachments.each_with_index do |attachment, i| %>
    <%= attachment.pretty_value %> <% if field.delete_method || field.keep_method %> <%= I18n.t('admin.form.delete_file', field_label: field.label, number: i + 1) %> <% if field.keep_method %> <%= form.check_box(field.keep_method, {multiple:true, checked: true, style: 'display:none;'}, attachment.keep_value, nil) %> <% elsif field.delete_method %> <%= form.check_box(field.delete_method, {multiple:true, style: 'display:none;'}, attachment.delete_value, nil) %> <% end %> <% end %>
    <% end %> <%= form.file_field(field.name, { data: { :"multiple-fileupload" => true }, multiple: true }.deep_merge(field.html_attributes)) %> <% if field.cache_method %> <%= form.hidden_field(field.cache_method) %> <% end %> ================================================ FILE: app/views/rails_admin/main/_form_nested_many.html.erb ================================================
    <% if field.inline_add %> <%= form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { class: 'btn btn-info' } %> <% end %>
    <%= form.errors_for(field) %> <%= form.help_for(field) %>
    <%= form.fields_for field.name do |nested_form| %> <% if field.nested_form[:allow_destroy] || nested_form.options[:child_index] == "new_#{field.name}" %> <%= nested_form.link_to_remove ''.html_safe %> <% end %> <%= nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/_form_nested_one.html.erb ================================================
    <% if field.inline_add %> <%= form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { class: 'btn btn-info', :'data-add-label' => " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".gsub("\n", "") } %> <% end %>
    <%= form.errors_for(field) %> <%= form.help_for(field) %>
    <%= form.fields_for field.name do |nested_form| %> <% if field.nested_form[:allow_destroy] %> <%= nested_form.link_to_remove ''.html_safe %> <% end %> <%= nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/_form_polymorphic_association.html.erb ================================================ <% column_type_dom_id = form.dom_id(field).sub(field.method_name.to_s, field.type_column) %>
    <% field.widget_options_for_types.each do |model, value| %>
    <% end %> <%= form.select field.type_column, field.type_collection, {include_blank: true, selected: field.selected_type}, class: "form-select", id: column_type_dom_id, data: { polymorphic: true, urls: field.type_urls.to_json }, style: "float: left; margin-right: 10px;" %>
    <%= form.select field.method_name, field.collection, {include_blank: true, selected: field.selected_id}, class: "form-control", data: { filteringselect: true, options: field.widget_options }, placeholder: 'Search' %>
    ================================================ FILE: app/views/rails_admin/main/_form_simple_mde.html.erb ================================================ <% js_data = { js_location: field.js_location, css_location: field.css_location, instance_config: field.instance_config } %> <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'simplemde', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> ================================================ FILE: app/views/rails_admin/main/_form_text.html.erb ================================================ <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: false, options: {}.to_json }).reverse_merge({ value: field.form_value, class: 'form-control', required: field.required }) %> ================================================ FILE: app/views/rails_admin/main/_form_wysihtml5.html.erb ================================================ <% js_data = { csspath: field.css_location, jspath: field.js_location, config_options: field.config_options.to_json } %> <%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'bootstrap-wysihtml5', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> ================================================ FILE: app/views/rails_admin/main/_submit_buttons.html.erb ================================================
    <% if @action.enabled? && authorized?(:new, @abstract_model) %> <% end %> <% if @action.enabled? && authorized?(:edit, @abstract_model) %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/bulk_delete.html.erb ================================================

    <%= I18n.t('admin.form.bulk_delete') %>

    <%= form_tag bulk_delete_path(model_name: @abstract_model.to_param, bulk_ids: @objects.map(&:id)), method: :delete do %>
    <% end %> ================================================ FILE: app/views/rails_admin/main/dashboard.html.erb ================================================ <% if @abstract_models %> <% @abstract_models.each do |abstract_model| %> <% if authorized? :index, abstract_model %> <% index_path = index_path(model_name: abstract_model.to_param) %> <% row_class = "#{cycle("odd", "even")}#{" link" if index_path} #{abstract_model.param_key}_links" %> <% last_created = @most_recent_created[abstract_model.model.name] %> <% active = last_created.try(:today?) %> <% end %> <% end %>
    <%= t "admin.table_headers.model_name" %> <%= t "admin.table_headers.last_created" %> <%= t "admin.table_headers.records" %>
    <%= link_to abstract_model.config.label_plural, index_path %> <% if last_created %> <%= t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t("admin.misc.ago")}" %> <% end %> <% count = @count[abstract_model.model.name] %> <% percent = count > 0 ? (@max <= 1 ? count : ((Math.log(count+1) * 100.0) / Math.log(@max+1)).to_i) : -1 %>
    <%= @count[abstract_model.model.name] %>
    <% end %> <% if @history && authorized?(:history_index) %>

    <%= t("admin.actions.history_index.menu") %>

    <%= render partial: 'rails_admin/main/dashboard_history' %>
    <% end %> ================================================ FILE: app/views/rails_admin/main/delete.html.erb ================================================

    <%= t("admin.form.are_you_sure_you_want_to_delete_the_object", model_name: @abstract_model.pretty_name.downcase) %> <%= @model_config.with(object: @object).object_label %> <%= t("admin.form.all_of_the_following_related_items_will_be_deleted") %>

    <%= form_for(@object, url: delete_path(model_name: @abstract_model.to_param, id: @object.id), html: {method: "delete"}) do %>
    <% end %> ================================================ FILE: app/views/rails_admin/main/edit.html.erb ================================================ <%= rails_admin_form_for @object, url: edit_path(@abstract_model, @object.id), as: @abstract_model.param_key, html: { method: "put", multipart: true, class: "main", data: { title: @page_name } } do |form| %> <%= form.generate action: :update %> <% end %> ================================================ FILE: app/views/rails_admin/main/export.html.erb ================================================ <% params = request.params.except(:action, :controller, :utf8, :page, :per_page, :format, :authenticity_token) %> <% visible_fields = @model_config.export.with(view: self, object: @abstract_model.model.new, controller: self.controller).visible_fields %> <%= form_tag export_path(params.merge(all: true)), method: 'post', class: "main", data: {turbo: false} do %>
    <%= t('admin.export.select') %>
    <%= t('admin.export.fields_from', name: @model_config.label_plural.downcase) %>
    <% visible_fields.select{ |f| !f.association? || f.association.polymorphic? }.each do |field| %> <% list = field.virtual? ? 'methods' : 'only' %>
    <% if field.association? && field.association.polymorphic? %> <% polymorphic_type_column_name = @abstract_model.properties.detect {|p| field.association.foreign_type == p.name }.name %> <% else %> <% end %>
    <% end %>
    <% visible_fields.select{ |f| f.association? && !f.association.polymorphic? }.each do |field| %> <% fields = field.associated_model_config.export.with(controller: self.controller, view: self, object: (associated_model = field.associated_model_config.abstract_model.model).new).visible_fields.select{ |f| !f.association? } %>
    <%= t('admin.export.fields_from_associated', name: field.label.downcase) %>
    <% fields.each do |associated_model_field| %> <% list = associated_model_field.virtual? ? 'methods' : 'only' %>
    <% end %>
    <% end %>
    <%= t('admin.export.options_for', name: 'csv') %>
    <% guessed_encoding = @abstract_model.encoding %>
    <%= select_tag 'csv_options[encoding_to]', options_for_select(Encoding.name_list.sort), include_blank: true, placeholder: t('admin.misc.search'), :'data-enumeration' => true %>

    <%= t('admin.export.csv.encoding_to_help', name: guessed_encoding) %>

    <%= t('admin.export.csv.skip_header_help') %>

    <%= select_tag 'csv_options[generator][col_sep]', options_for_select({ '' => t('admin.export.csv.default_col_sep'), " ','" => ',', " ';'" => ';', '' => "'\t'" }), placeholder: t('admin.misc.search'), :'data-enumeration' => true %>

    <%= t('admin.export.csv.col_sep_help', value: t('admin.export.csv.default_col_sep')) %>

    <% end %> ================================================ FILE: app/views/rails_admin/main/history.html.erb ================================================ <% params = request.params.except(:action, :controller, :model_name) %> <% query = params[:query] %> <% filter = params[:filter] %> <% sort = params[:sort] %> <% sort_reverse = params[:sort_reverse] %> <% path_method = params[:id] ? "history_show_path" : "history_index_path" %> <%= form_tag("", method: "get", class: "search form-inline") do %>
    " type="search" value="<%= query %>" />
    <% end %> <% columns = [] %> <% columns << { property_name: "created_at", css_class: "created_at",link_text: t('admin.table_headers.created_at') } %> <% columns << { property_name: "username", css_class: "username", link_text: t('admin.table_headers.username') } %> <% columns << { property_name: "item", css_class: "item", link_text: t('admin.table_headers.item') } if @general %> <% columns << { property_name: "message", css_class: "message", link_text: t('admin.table_headers.message') } %> <% columns.each do |column| %> <% property_name = column[:property_name] %> <% selected = (sort == property_name) %> <% sort_direction = (sort_reverse ? "headerSortUp" : "headerSortDown" if selected) %> <% sort_location = send(path_method, params.except("sort_reverse").merge(model_name: @abstract_model.to_param, sort: property_name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {})) %> <% end %> <% @history.each_with_index do |object, index| %> <% unless object.created_at.nil? %> <% end %> <% if @general %> <% if o = @abstract_model.get(object.item) %> <% label = o.send(@abstract_model.config.object_label_method) %> <% if show_action = action(:show, @abstract_model, o) %> <% else %> <% end %> <% else %> <% end %> <% end %> <% end %>
    <%= column[:link_text] %>
    <%= l(object.created_at, format: :long, default: l(object.created_at, format: :long)) %> <%= object.username %> <%= link_to(label, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: o.id)) %> <%= label %> <%= "#{@abstract_model.config.label} ##{object.item}" %> <%= object.message.in?(['delete', 'new']) ? t("admin.actions.#{object.message}.done").capitalize : object.message %>
    <% unless params[:all] || !@history.respond_to?(:current_page) %> <%= paginate(@history, theme: 'ra-twitter-bootstrap') %> <%= link_to(t("admin.misc.show_all"), send(path_method, params.merge(all: true)), class: "show-all btn btn-light") unless (tc = @history.total_count) <= @history.size || tc > 100 %> <% end %> ================================================ FILE: app/views/rails_admin/main/index.html.erb ================================================ <% query = params[:query] params = request.params.except(:authenticity_token, :action, :controller, :utf8, :bulk_export) params.delete(:query) if params[:query].blank? params.delete(:sort_reverse) unless params[:sort_reverse] == 'true' sort_reverse = params[:sort_reverse] sort = params[:sort] params.delete(:sort) if params[:sort] == @model_config.list.sort_by.to_s export_action = RailsAdmin::Config::Actions.find(:export, { controller: self.controller, abstract_model: @abstract_model }) export_action = nil unless export_action && authorized?(export_action.authorization_key, @abstract_model) description = RailsAdmin.config(@abstract_model.model_name).description properties = @model_config.list.with(controller: self.controller, view: self, object: @abstract_model.model.new).fields_for_table checkboxes = @model_config.list.checkboxes? table_table_header_count = begin count = checkboxes ? 1 : 0 count = count + properties.count end %> <% content_for :contextual_tabs do %> <% if filterable_fields.present? %> <% end %> <% if checkboxes %> <%= bulk_menu %> <% end %> <% end %>
    <%= form_tag(index_path(params.except(*%w[page f query])), method: :get) do %>

    " type="search" value="<%= query %>" />
    <% if @model_config.list.search_help.present? %>
    <%= @model_config.list.search_help %>
    <% end %>
    <% if export_action %> <%= link_to wording_for(:link, export_action), export_path(params.except('page')), class: 'btn btn-info' %> <% end %>
    <% end %> <% unless @model_config.list.scopes.empty? %> <% end %> <%= form_tag bulk_action_path(model_name: @abstract_model.to_param), method: :post, id: "bulk_form", class: ["form", "mb-3"] do %> <%= hidden_field_tag :bulk_action %> <% if description.present? %>

    <%= description %>

    <% end %>
    <% if checkboxes %> <% end %> <% properties.each do |property| %> <% selected = (sort == property.name.to_s) %> <% if property.sortable %> <% sort_location = index_path params.except('sort_reverse').except('page').merge(sort: property.name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {}) %> <% sort_direction = (sort_reverse == 'true' ? "headerSortUp" : "headerSortDown" if selected) %> <% end %> <% end %> <% @objects.each do |object| %> <% if checkboxes %> <% end %> <% properties.map{ |property| property.bind(:object, object) }.each do |property| %> <% value = property.pretty_value %> <%= content_tag(:td, class: [property.sticky? && 'sticky', property.css_class, property.type_css_class].select(&:present?), title: strip_tags(value.to_s)) do %> <%= value %> <% end %> <% end %> <% end %> <% if @objects.empty? %> <% end %>
    " data-href="<%= property.sortable && sort_location %>" rel="tooltip" title="<%= property.hint %>"> <%= property.label %>
    <%= check_box_tag "bulk_ids[]", object.id.to_s, false %>
    <%= I18n.t('admin.actions.index.no_records') %>
    <% if @model_config.list.limited_pagination %>
    <%= paginate(@objects, theme: 'ra-twitter-bootstrap/without_count', total_pages: Float::INFINITY) %>
    <% elsif @objects.respond_to?(:total_count) %> <% total_count = @objects.total_count.to_i %>
    <%= paginate(@objects, theme: 'ra-twitter-bootstrap') %>
    <%= link_to(t("admin.misc.show_all"), index_path(params.merge(all: true)), class: "show-all btn btn-light clearfix") unless total_count > 100 || total_count <= @objects.to_a.size %>
    <%= "#{total_count} #{@model_config.pluralize(total_count).downcase}" %>
    <% else %>
    <%= "#{@objects.size} #{@model_config.pluralize(@objects.size).downcase}" %>
    <% end %> <% end %>
    ================================================ FILE: app/views/rails_admin/main/new.html.erb ================================================ <%= rails_admin_form_for @object, url: new_path(model_name: @abstract_model.to_param), as: @abstract_model.param_key, html: { multipart: true, class: "main", data: { title: @page_name } } do |form| %> <%= form.generate action: :create %> <% end %> ================================================ FILE: app/views/rails_admin/main/show.html.erb ================================================ <% @model_config.show.with(object: @object, view: self, controller: self.controller).visible_groups.each do |fieldset| %> <% unless (fields = fieldset.with(object: @object, view: self, controller: self.controller).visible_fields).empty? %> <% unless (fields = fields.reject{ |f| RailsAdmin::config.compact_show_view && (f.formatted_value.nil? || f.formatted_value == '') }).empty? %>

    <%= fieldset.label %>

    <% if fieldset.help %>

    <%= fieldset.help %>

    <% end %>
    <% fields.each_with_index do |field, index| %>
    <%= field.label %>
    <%= field.pretty_value %>
    <% end %>
    <% end %> <% end %> <% end %> ================================================ FILE: config/initializers/active_record_extensions.rb ================================================ # frozen_string_literal: true ActiveSupport.on_load(:active_record) do module ActiveRecord class Base def self.rails_admin(&block) RailsAdmin.config(self, &block) end def rails_admin_default_object_label_method new_record? ? "new #{self.class}" : "#{self.class} ##{id}" end def safe_send(value) if has_attribute?(value) read_attribute(value) else send(value) end end end end end ================================================ FILE: config/initializers/mongoid_extensions.rb ================================================ # frozen_string_literal: true if defined?(::Mongoid::Document) require 'rails_admin/adapters/mongoid/extension' Mongoid::Document.include RailsAdmin::Adapters::Mongoid::Extension end ================================================ FILE: config/locales/rails_admin.en.yml ================================================ en: admin: js: true: "True" false: "False" is_present: Is present is_blank: Is blank date: Date ... between_and_: Between ... and ... today: Today yesterday: Yesterday this_week: This week last_week: Last week time: Time ... number: Number ... contains: Contains does_not_contain: Does not contain is_exactly: Is exactly starts_with: Starts with ends_with: Ends with too_many_objects: "Too many objects, use search box above" no_objects: "No objects found" clear: Clear loading: "Loading..." toggle_navigation: Toggle navigation home: name: "Home" pagination: previous: "« Prev" next: "Next »" truncate: "…" misc: search: "Search" filter: "Filter" reset_filters: "Reset filters" refresh: "Refresh" show_all: "Show all" add_filter: "Add filter" bulk_menu_title: "Selected items" remove: "Remove" add_new: "Add new" chose_all: "Choose all" clear_all: "Clear all" up: "Up" down: "Down" navigation: "Navigation" root_navigation: "Actions" navigation_static_label: "Links" log_out: "Log out" time_ago: "%{time} ago" ago: "ago" more: "Plus %{count} more %{models_name}" flash: successful: "%{name} successfully %{action}" error: "%{name} failed to be %{action}" noaction: "No actions were taken" model_not_found: "Model '%{model}' could not be found" object_not_found: "%{model} with id '%{id}' could not be found" table_headers: model_name: "Model name" last_created: "Last created" records: "Records" username: "User" changes: "Changes" created_at: "Date/Time" item: "Item" message: "Message" actions: dashboard: title: "Site Administration" menu: "Dashboard" breadcrumb: "Dashboard" index: title: "List of %{model_label_plural}" menu: "List" breadcrumb: "%{model_label_plural}" no_records: "No records found" show: title: "Details for %{model_label} '%{object_label}'" menu: "Show" breadcrumb: "%{object_label}" show_in_app: menu: "Show in app" new: title: "New %{model_label}" menu: "Add new" breadcrumb: "New" link: "Add a new %{model_label}" done: "created" edit: title: "Edit %{model_label} '%{object_label}'" menu: "Edit" breadcrumb: "Edit" link: "Edit this %{model_label}" done: "updated" delete: title: "Delete %{model_label} '%{object_label}'" menu: "Delete" breadcrumb: "Delete" link: "Delete '%{object_label}'" done: "deleted" bulk_delete: title: "Delete %{model_label_plural}" menu: "Multiple delete" breadcrumb: "Multiple delete" bulk_link: "Delete selected %{model_label_plural}" export: title: "Export %{model_label_plural}" menu: "Export" breadcrumb: "Export" link: "Export found %{model_label_plural}" bulk_link: "Export selected %{model_label_plural}" done: "exported" history_index: title: "History for %{model_label_plural}" menu: "History" breadcrumb: "History" history_show: title: "History for %{model_label} '%{object_label}'" menu: "History" breadcrumb: "History" form: cancel: "Cancel" basic_info: "Basic info" required: "Required" optional: "Optional" one_char: "character" char_length_up_to: "length up to" char_length_of: "length of" save: "Save" save_and_add_another: "Save and add another" save_and_edit: "Save and edit" delete_file: "Delete '%{field_label}' #%{number}" all_of_the_following_related_items_will_be_deleted: "? The following related items may be deleted or orphaned:" are_you_sure_you_want_to_delete_the_object: "Are you sure you want to delete this %{model_name}" confirmation: "Yes, I'm sure" bulk_delete: "The following objects will be deleted, which may delete or orphan some of their related dependencies:" new_model: "%{name} (new)" export: confirmation: "Export to %{name}" select: "Select fields to export" select_all_fields: "Select All Fields" fields_from: "Fields from %{name}" fields_from_associated: "Fields from associated %{name}" display: "Display %{name}: %{type}" options_for: "Options for %{name}" empty_value_for_associated_objects: "" click_to_reverse_selection: "Click to reverse selection" csv: header_for_root_methods: "%{name}" # 'model' is available header_for_association_methods: "%{name} [%{association}]" encoding_to: "Encode to" encoding_to_help: "Choose output encoding. Leave empty to let current input encoding untouched: (%{name})" skip_header: "No header" skip_header_help: "Do not output a header (no fields description)" default_col_sep: "," col_sep: "Column separator" col_sep_help: "Leave blank for default ('%{value}')" # value is default_col_sep ================================================ FILE: config/routes.rb ================================================ # frozen_string_literal: true RailsAdmin::Engine.routes.draw do controller 'main' do RailsAdmin::Config::Actions.all(:root).each { |action| match "/#{action.route_fragment}", action: action.action_name, as: action.action_name, via: action.http_methods } scope ':model_name' do RailsAdmin::Config::Actions.all(:collection).each { |action| match "/#{action.route_fragment}", action: action.action_name, as: action.action_name, via: action.http_methods } post '/bulk_action', action: :bulk_action, as: 'bulk_action' scope ':id' do RailsAdmin::Config::Actions.all(:member).each { |action| match "/#{action.route_fragment}", action: action.action_name, as: action.action_name, via: action.http_methods } end end end end ================================================ FILE: gemfiles/composite_primary_keys.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 7.0.0", "7.0.8.6" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "concurrent-ruby", "1.3.4" group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" gem "composite_primary_keys" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", "~> 1.3" end end gemspec path: "../" ================================================ FILE: gemfiles/rails_6.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 6.0.0" gem "sassc-rails", "~> 2.1" gem "turbo-rails", "< 2.0.8" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "concurrent-ruby", "1.3.4" gem "psych", "~> 3.3" group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", ["~> 3.0", "< 3.6"] gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit", "~> 2.1.0" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", ">= 1.3.0" end platforms :jruby do gem "activerecord-jdbcmysql-adapter", "~> 60.0" gem "activerecord-jdbcpostgresql-adapter", "~> 60.0" gem "activerecord-jdbcsqlite3-adapter", "~> 60.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 7.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: gemfiles/rails_6.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 6.1.0" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "concurrent-ruby", "1.3.4" group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", ">= 1.3.0" end platforms :jruby do gem "activerecord-jdbcmysql-adapter", "~> 61.0" gem "activerecord-jdbcpostgresql-adapter", "~> 61.0" gem "activerecord-jdbcsqlite3-adapter", "~> 61.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 7.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: gemfiles/rails_7.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 7.0.0", "7.0.8.6" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "concurrent-ruby", "1.3.4" gem "importmap-rails", require: false gem "nokogiri", "~> 1.16.0", platform: :jruby group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", "~> 1.3" end platforms :jruby do gem "activerecord-jdbcmysql-adapter", "~> 70.0" gem "activerecord-jdbcpostgresql-adapter", "~> 70.0" gem "activerecord-jdbcsqlite3-adapter", "~> 70.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 8.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: gemfiles/rails_7.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 7.1.0" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "importmap-rails", require: false group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", "~> 1.3" end platforms :jruby do gem "activerecord-jdbcmysql-adapter", "~> 71.0" gem "activerecord-jdbcpostgresql-adapter", "~> 71.0" gem "activerecord-jdbcsqlite3-adapter", "~> 71.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 8.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: gemfiles/rails_7.2.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 7.2.0" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "importmap-rails", require: false group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", ">= 1.3.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 8.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: gemfiles/rails_8.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "appraisal", ">= 2.0" gem "devise", "~> 4.7" gem "net-smtp", require: false gem "rails", "~> 8.0.0" gem "sassc-rails", "~> 2.1" gem "turbo-rails" gem "vite_rails", require: false gem "webpacker", require: false gem "webrick" gem "importmap-rails", require: false group :development, :test do gem "pry", ">= 0.9" end group :test do gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] gem "cuprite", "!= 0.15.1" gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" gem "pundit" gem "rack-cache", require: "rack/cache" gem "rspec-expectations", "!= 3.8.3" gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] end group :active_record do gem "paper_trail", ">= 12.0" platforms :ruby, :mswin, :mingw, :x64_mingw do gem "mysql2", ">= 0.3.14" gem "pg", ">= 1.0.0" gem "sqlite3", ">= 1.3.0" end end group :mongoid do gem "cancancan-mongoid" gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" gem "mongoid", "~> 9.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" gem "shrine-mongoid", "~> 1.0" end gemspec path: "../" ================================================ FILE: lib/generators/rails_admin/importmap_formatter.rb ================================================ # frozen_string_literal: true require 'importmap/packager' module RailsAdmin class ImportmapFormatter attr_reader :packager def initialize(path = 'config/importmap.rails_admin.rb') @packager = Importmap::Packager.new(path) end def format imports = packager.import("rails_admin@#{RailsAdmin::Version.js}", from: 'jspm.io') imports = imports[:imports] if imports.key?(:imports) # Use ESM compatible version to work around https://github.com/cljsjs/packages/issues/1579 imports['@popperjs/core'].gsub!('lib/index.js', 'dist/esm/popper.js') # Tidy up jQuery UI dependencies jquery_uis = imports.keys.filter { |key, _| key =~ /jquery-ui/ } imports['jquery-ui/'] = imports[jquery_uis.first].gsub(%r{(@[^/@]+)/[^@]+$}, '\1/') imports.reject! { |key, _| jquery_uis.include? key } pins = ['pin "rails_admin", preload: true', packager.pin_for('rails_admin/src/rails_admin/base', imports.delete('rails_admin'))] (pins + imports.map { |package, url| packager.pin_for(package, url) }).join("\n") end end end ================================================ FILE: lib/generators/rails_admin/install_generator.rb ================================================ # frozen_string_literal: true require 'rails/generators' require 'rails_admin/version' require File.expand_path('utils', __dir__) module RailsAdmin class InstallGenerator < Rails::Generators::Base source_root File.expand_path('templates', __dir__) include Generators::Utils::InstanceMethods argument :_namespace, type: :string, required: false, desc: 'RailsAdmin url namespace' class_option :asset, type: :string, required: false, default: nil, desc: 'Asset delivery method [options: webpacker, webpack, sprockets, importmap, vite]' desc 'RailsAdmin installation generator' def install if File.read(File.join(destination_root, 'config/routes.rb')).include?('mount RailsAdmin::Engine') display "Skipped route addition, since it's already there" else namespace = ask_for('Where do you want to mount rails_admin?', 'admin', _namespace) route("mount RailsAdmin::Engine => '/#{namespace}', as: 'rails_admin'") end if File.exist? File.join(destination_root, 'config/initializers/rails_admin.rb') insert_into_file 'config/initializers/rails_admin.rb', " config.asset_source = :#{asset}\n", after: "RailsAdmin.config do |config|\n" else template 'initializer.erb', 'config/initializers/rails_admin.rb' end display "Using [#{asset}] for asset delivery method" case asset when 'webpack' configure_for_webpack when 'importmap' configure_for_importmap when 'webpacker' configure_for_webpacker5 when 'vite' configure_for_vite when 'sprockets' configure_for_sprockets else raise "Unknown asset source: #{asset}" end end private def asset return options['asset'] if options['asset'] if defined?(Webpacker) 'webpacker' elsif Rails.root.join('webpack.config.js').exist? 'webpack' elsif Rails.root.join('config/importmap.rb').exist? 'importmap' elsif defined?(ViteRuby) 'vite' else 'sprockets' end end def configure_for_sprockets gem 'sassc-rails' end def configure_for_webpacker5 run "yarn add rails_admin@#{RailsAdmin::Version.js}" template 'rails_admin.webpacker.js', 'app/javascript/packs/rails_admin.js' template 'rails_admin.scss.erb', 'app/javascript/stylesheets/rails_admin.scss' # To work around https://github.com/railsadminteam/rails_admin/issues/3565 add_package_json_field('resolutions', {'rails_admin/@fortawesome/fontawesome-free' => '^5.15.0'}) end def configure_for_vite vite_source_code_dir = ViteRuby.config.source_code_dir run "yarn add rails_admin@#{RailsAdmin::Version.js} sass" template('rails_admin.vite.js', File.join(vite_source_code_dir, 'entrypoints', 'rails_admin.js')) @fa_font_path = '@fortawesome/fontawesome-free/webfonts' template('rails_admin.scss.erb', File.join(vite_source_code_dir, 'stylesheets', 'rails_admin.scss')) end def configure_for_webpack run "yarn add rails_admin@#{RailsAdmin::Version.js}" template 'rails_admin.js', 'app/javascript/rails_admin.js' webpack_config = File.join(destination_root, 'webpack.config.js') marker = %r{application: ["']./app/javascript/application.js["']} if File.exist?(webpack_config) && File.read(webpack_config) =~ marker insert_into_file 'webpack.config.js', %(,\n rails_admin: "./app/javascript/rails_admin.js"), after: marker else say 'Add `rails_admin: "./app/javascript/rails_admin.js"` to the entry section in your webpack.config.js.', :red end setup_css({'build' => 'webpack --config webpack.config.js'}) end def configure_for_importmap run "yarn add rails_admin@#{RailsAdmin::Version.js}" template 'rails_admin.js', 'app/javascript/rails_admin.js' require_relative 'importmap_formatter' add_file 'config/importmap.rails_admin.rb', ImportmapFormatter.new.format setup_css end def setup_css(additional_script_entries = {}) gem 'cssbundling-rails' rake 'css:install:sass' @fa_font_path = '.' template 'rails_admin.scss.erb', 'app/assets/stylesheets/rails_admin.scss' asset_config = %{Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")\n} if File.exist? File.join(destination_root, 'config/initializers/assets.rb') append_to_file 'config/initializers/assets.rb', asset_config else add_file 'config/initializers/assets.rb', asset_config end add_package_json_field('scripts', additional_script_entries.merge({'build:css' => 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules'}), <<~INSTRUCTION) Taking 'build:css' as an example, if you're already have application.sass.css for the sass build, the resulting script would look like: sass ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules INSTRUCTION end def add_package_json_field(name, entries, instruction = nil) display "Add #{name} to package.json" package = begin JSON.parse(File.read(File.join(destination_root, 'package.json'))) rescue Errno::ENOENT, JSON::ParserError {} end if package[name] && (package[name].keys & entries.keys).any? say <<~MESSAGE, :red You need to merge "#{name}": #{JSON.pretty_generate(entries)} into the existing #{name} in your package.json.#{instruction && "\n#{instruction}"} MESSAGE else package[name] ||= {} entries.each do |entry, value| package[name][entry] = value end add_file 'package.json', "#{JSON.pretty_generate(package)}\n" end end end end ================================================ FILE: lib/generators/rails_admin/templates/initializer.erb ================================================ RailsAdmin.config do |config| config.asset_source = :<%= asset %> ### Popular gems integration ## == Devise == # config.authenticate_with do # warden.authenticate! scope: :user # end # config.current_user_method(&:current_user) ## == CancanCan == # config.authorize_with :cancancan ## == Pundit == # config.authorize_with :pundit ## == PaperTrail == # config.audit_with :paper_trail, 'User', 'PaperTrail::Version' # PaperTrail >= 3.0.0 ### More at https://github.com/railsadminteam/rails_admin/wiki/Base-configuration ## == Gravatar integration == ## To disable Gravatar integration in Navigation Bar set to false # config.show_gravatar = true config.actions do dashboard # mandatory index # mandatory new export bulk_delete show edit delete show_in_app ## With an audit adapter, you can add: # history_index # history_show end end ================================================ FILE: lib/generators/rails_admin/templates/rails_admin.js ================================================ import "rails_admin/src/rails_admin/base"; ================================================ FILE: lib/generators/rails_admin/templates/rails_admin.scss.erb ================================================ <%= @fa_font_path ? %{$fa-font-path: "#{@fa_font_path}";\n} : '' %>@import "rails_admin/src/rails_admin/styles/base"; ================================================ FILE: lib/generators/rails_admin/templates/rails_admin.vite.js ================================================ import "~/stylesheets/rails_admin.scss"; import "rails_admin/src/rails_admin/base"; ================================================ FILE: lib/generators/rails_admin/templates/rails_admin.webpacker.js ================================================ import "rails_admin/src/rails_admin/base"; import "../stylesheets/rails_admin.scss"; ================================================ FILE: lib/generators/rails_admin/utils.rb ================================================ # frozen_string_literal: true module RailsAdmin module Generators module Utils module InstanceMethods def display(output, color = :green) say(" - #{output}", color) end def ask_for(wording, default_value = nil, override_if_present_value = nil) if override_if_present_value.present? display("Using [#{override_if_present_value}] for question '#{wording}'") && override_if_present_value else ask(" ? #{wording} Press for [#{default_value}] >", :yellow).presence || default_value end end end end end end ================================================ FILE: lib/rails_admin/abstract_model.rb ================================================ # frozen_string_literal: true require 'rails_admin/support/datetime' module RailsAdmin class AbstractModel cattr_accessor :all attr_reader :adapter, :model_name class << self def reset @@all = nil end def all(adapter = nil) @@all ||= Config.models_pool.collect { |m| new(m) }.compact adapter ? @@all.select { |m| m.adapter == adapter } : @@all end alias_method :old_new, :new def new(m) m = m.constantize unless m.is_a?(Class) (am = old_new(m)).model && am.adapter ? am : nil rescue *([LoadError, NameError] + (defined?(ActiveRecord) ? ['ActiveRecord::NoDatabaseError'.constantize, 'ActiveRecord::ConnectionNotEstablished'.constantize] : [])) puts "[RailsAdmin] Could not load model #{m}, assuming model is non existing. (#{$ERROR_INFO})" unless Rails.env.test? nil end @@polymorphic_parents = {} def polymorphic_parents(adapter, model_name, name) @@polymorphic_parents[adapter.to_sym] ||= {}.tap do |hash| all(adapter).each do |am| am.associations.select(&:as).each do |association| (hash[[association.klass.to_s.underscore, association.as].join('_').to_sym] ||= []) << am.model end end end @@polymorphic_parents[adapter.to_sym][[model_name.to_s.underscore, name].join('_').to_sym] end # For testing def reset_polymorphic_parents @@polymorphic_parents = {} end end def initialize(model_or_model_name) @model_name = model_or_model_name.to_s ancestors = model.ancestors.collect(&:to_s) if ancestors.include?('ActiveRecord::Base') && !model.abstract_class? && model.table_exists? initialize_active_record elsif ancestors.include?('Mongoid::Document') initialize_mongoid end end # do not store a reference to the model, does not play well with ActiveReload/Rails3.2 def model @model_name.constantize end def quoted_table_name table_name end def quote_column_name(name) name end def to_s model.to_s end def config Config.model self end def to_param @model_name.split('::').collect(&:underscore).join('~') end def param_key @model_name.split('::').collect(&:underscore).join('_') end def pretty_name model.model_name.human end def where(conditions) model.where(conditions) end def each_associated_children(object) associations.each do |association| case association.type when :has_one child = object.send(association.name) yield(association, [child]) if child when :has_many children = object.send(association.name) yield(association, Array.new(children)) end end end def format_id(id) id end def parse_id(id) id end private def initialize_active_record @adapter = :active_record require 'rails_admin/adapters/active_record' extend Adapters::ActiveRecord end def initialize_mongoid @adapter = :mongoid require 'rails_admin/adapters/mongoid' extend Adapters::Mongoid end def parse_field_value(field, value) value.is_a?(Array) ? value.map { |v| field.parse_value(v) } : field.parse_value(value) end class StatementBuilder def initialize(column, type, value, operator) @column = column @type = type @value = value @operator = operator end def to_statement return if [@operator, @value].any? { |v| v == '_discard' } unary_operators[@operator] || unary_operators[@value] || build_statement_for_type_generic end protected def get_filtering_duration FilteringDuration.new(@operator, @value).get_duration end def build_statement_for_type_generic build_statement_for_type || begin case @type when :date build_statement_for_date when :datetime, :timestamp, :time build_statement_for_datetime_or_timestamp end end end def build_statement_for_type raise 'You must override build_statement_for_type in your StatementBuilder' end def build_statement_for_integer_decimal_or_float case @value when Array val, range_begin, range_end = *@value.collect do |v| next unless v.to_i.to_s == v || v.to_f.to_s == v @type == :integer ? v.to_i : v.to_f end case @operator when 'between' range_filter(range_begin, range_end) else column_for_value(val) if val end else if @value.to_i.to_s == @value || @value.to_f.to_s == @value @type == :integer ? column_for_value(@value.to_i) : column_for_value(@value.to_f) end end end def build_statement_for_date start_date, end_date = get_filtering_duration if start_date start_date = begin start_date.to_date rescue StandardError nil end end if end_date end_date = begin end_date.to_date rescue StandardError nil end end range_filter(start_date, end_date) end def build_statement_for_datetime_or_timestamp start_date, end_date = get_filtering_duration start_date = start_date.beginning_of_day if start_date.is_a?(Date) end_date = end_date.end_of_day if end_date.is_a?(Date) range_filter(start_date, end_date) end def unary_operators raise 'You must override unary_operators in your StatementBuilder' end def range_filter(_min, _max) raise 'You must override range_filter in your StatementBuilder' end class FilteringDuration def initialize(operator, value) @value = value @operator = operator end def get_duration case @operator when 'between' then between when 'today' then today when 'yesterday' then yesterday when 'this_week' then this_week when 'last_week' then last_week else default end end def today [Date.today, Date.today] end def yesterday [Date.yesterday, Date.yesterday] end def this_week [Date.today.beginning_of_week, Date.today.end_of_week] end def last_week [1.week.ago.to_date.beginning_of_week, 1.week.ago.to_date.end_of_week] end def between [@value[1], @value[2]] end def default [default_date, default_date] end private def default_date Array.wrap(@value).first end end end end end ================================================ FILE: lib/rails_admin/adapters/active_record/association.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module ActiveRecord class Association attr_reader :association, :model def initialize(association, model) @association = association @model = model end def name association.name.to_sym end def pretty_name name.to_s.tr('_', ' ').capitalize end def type association.macro end def field_type if polymorphic? :polymorphic_association else :"#{association.macro}_association" end end def klass if options[:polymorphic] polymorphic_parents(:active_record, association.active_record.name.to_s, name) || [] else association.klass end end def primary_key return nil if polymorphic? value = case type when :has_one association.klass.primary_key else association.association_primary_key end if value.is_a? Array :id else value.to_sym end end def foreign_key if association.options[:query_constraints].present? association.options[:query_constraints].map(&:to_sym) elsif association.foreign_key.is_a?(Array) association.foreign_key.map(&:to_sym) else association.foreign_key.to_sym end end def foreign_key_nullable? return true if foreign_key.nil? || type != :has_many (column = klass.columns_hash[foreign_key.to_s]).nil? || column.null end def foreign_type options[:foreign_type].try(:to_sym) || :"#{name}_type" if options[:polymorphic] end def foreign_inverse_of nil end def key_accessor case type when :has_many, :has_and_belongs_to_many :"#{name.to_s.singularize}_ids" when :has_one :"#{name}_id" else if foreign_key.is_a?(Array) :"#{name}_id" else foreign_key end end end def as options[:as].try :to_sym end def polymorphic? options[:polymorphic] || false end def inverse_of options[:inverse_of].try :to_sym end def read_only? (klass.all.instance_exec(&scope).readonly_value if scope.is_a?(Proc) && scope.arity == 0) || association.nested? || false end def nested_options model.nested_attributes_options.try { |o| o[name.to_sym] } end def association? true end delegate :options, :scope, to: :association, prefix: false delegate :polymorphic_parents, to: RailsAdmin::AbstractModel end end end end ================================================ FILE: lib/rails_admin/adapters/active_record/object_extension.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module ActiveRecord module ObjectExtension def assign_attributes(attributes) super if attributes end end end end end ================================================ FILE: lib/rails_admin/adapters/active_record/property.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module ActiveRecord class Property attr_reader :property, :model def initialize(property, model) @property = property @model = model end def name property.name.to_sym end def pretty_name property.name.to_s.tr('_', ' ').capitalize end def type if serialized? :serialized else property.type end end def length property.limit end def nullable? property.null end def serial? model.primary_key == property.name end def association? false end def read_only? model.readonly_attributes.include? property.name.to_s end private def serialized? model.type_for_attribute(property.name).instance_of?(::ActiveRecord::Type::Serialized) end end end end end ================================================ FILE: lib/rails_admin/adapters/active_record.rb ================================================ # frozen_string_literal: true require 'active_record' require 'rails_admin/adapters/active_record/association' require 'rails_admin/adapters/active_record/object_extension' require 'rails_admin/adapters/active_record/property' module RailsAdmin module Adapters module ActiveRecord DISABLED_COLUMN_TYPES = %i[tsvector blob binary spatial hstore geometry].freeze def new(params = {}) model.new(params).extend(ObjectExtension) end def get(id, scope = scoped) object = primary_key_scope(scope, id).first return unless object object.extend(ObjectExtension) end def scoped model.all end def first(options = {}, scope = nil) all(options, scope).first end def all(options = {}, scope = nil) scope ||= scoped scope = scope.includes(options[:include]) if options[:include] scope = scope.limit(options[:limit]) if options[:limit] scope = bulk_scope(scope, options) if options[:bulk_ids] scope = query_scope(scope, options[:query]) if options[:query] scope = filter_scope(scope, options[:filters]) if options[:filters] scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) if options[:page] && options[:per] scope = sort_scope(scope, options) if options[:sort] scope end def count(options = {}, scope = nil) all(options.merge(limit: false, page: false), scope).count(:all) end def destroy(objects) Array.wrap(objects).each(&:destroy) end def associations model.reflect_on_all_associations.collect do |association| Association.new(association, model) end end def properties columns = model.columns.reject do |c| c.type.blank? || DISABLED_COLUMN_TYPES.include?(c.type.to_sym) || c.try(:array) end columns.collect do |property| Property.new(property, model) end end def base_class model.base_class end delegate :primary_key, :table_name, to: :model, prefix: false def quoted_table_name model.quoted_table_name end def quote_column_name(name) model.connection.quote_column_name(name) end def encoding adapter = if ::ActiveRecord::Base.respond_to?(:connection_db_config) ::ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] else ::ActiveRecord::Base.connection_config[:adapter] end case adapter when 'postgresql' ::ActiveRecord::Base.connection.select_one("SELECT ''::text AS str;").values.first.encoding when 'mysql2' if RUBY_ENGINE == 'jruby' ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding else ::ActiveRecord::Base.connection.raw_connection.encoding end when 'oracle_enhanced' ::ActiveRecord::Base.connection.select_one('SELECT dummy FROM DUAL').values.first.encoding else ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding end end def embedded? false end def cyclic? false end def adapter_supports_joins? true end def format_id(id) if primary_key.is_a? Array RailsAdmin.config.composite_keys_serializer.serialize(id) else id end end def parse_id(id) if primary_key.is_a?(Array) ids = RailsAdmin.config.composite_keys_serializer.deserialize(id) primary_key.each_with_index do |key, i| ids[i] = model.type_for_attribute(key).cast(ids[i]) end ids else id end end private def primary_key_scope(scope, id) if primary_key.is_a? Array scope.where(primary_key.zip(parse_id(id)).to_h) else scope.where(primary_key => id) end end def bulk_scope(scope, options) if primary_key.is_a? Array options[:bulk_ids].map { |id| primary_key_scope(scope, id) }.reduce(&:or) else scope.where(primary_key => options[:bulk_ids]) end end def sort_scope(scope, options) direction = options[:sort_reverse] ? :asc : :desc case options[:sort] when String, Symbol scope.reorder("#{options[:sort]} #{direction}") when Array scope.reorder(options[:sort].zip(Array.new(options[:sort].size) { direction }).to_h) when Hash scope.reorder(options[:sort].map { |table_name, column| "#{table_name}.#{column}" }. zip(Array.new(options[:sort].size) { direction }).to_h) else raise ArgumentError.new("Unsupported sort value: #{options[:sort]}") end end class WhereBuilder def initialize(scope) @statements = [] @values = [] @tables = [] @scope = scope end def add(field, value, operator) field.searchable_columns.flatten.each do |column_infos| statement, value1, value2 = StatementBuilder.new(column_infos[:column], column_infos[:type], value, operator, @scope.connection.adapter_name).to_statement @statements << statement if statement.present? @values << value1 unless value1.nil? @values << value2 unless value2.nil? table, column = column_infos[:column].split('.') @tables.push(table) if column end end def build scope = @scope.where(@statements.join(' OR '), *@values) scope = scope.references(*@tables.uniq) if @tables.any? scope end end def query_scope(scope, query, fields = config.list.fields.select(&:queryable?)) if config.list.search_by scope.send(config.list.search_by, query) else wb = WhereBuilder.new(scope) fields.each do |field| value = parse_field_value(field, query) wb.add(field, value, field.search_operator) end # OR all query statements wb.build end end # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...} # "0055" is the filter index, no use here. o is the operator, v the value def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?)) filters.each_pair do |field_name, filters_dump| filters_dump.each_value do |filter_dump| wb = WhereBuilder.new(scope) field = fields.detect { |f| f.name.to_s == field_name } value = parse_field_value(field, filter_dump[:v]) wb.add(field, value, (filter_dump[:o] || RailsAdmin::Config.default_search_operator)) # AND current filter statements to other filter statements scope = wb.build end end scope end def build_statement(column, type, value, operator) StatementBuilder.new(column, type, value, operator, model.connection.adapter_name).to_statement end class StatementBuilder < RailsAdmin::AbstractModel::StatementBuilder def initialize(column, type, value, operator, adapter_name) super column, type, value, operator @adapter_name = adapter_name end protected def unary_operators case @type when :boolean boolean_unary_operators when :uuid uuid_unary_operators when :integer, :decimal, :float numeric_unary_operators else generic_unary_operators end end private def generic_unary_operators { '_blank' => ["(#{@column} IS NULL OR #{@column} = '')"], '_present' => ["(#{@column} IS NOT NULL AND #{@column} != '')"], '_null' => ["(#{@column} IS NULL)"], '_not_null' => ["(#{@column} IS NOT NULL)"], '_empty' => ["(#{@column} = '')"], '_not_empty' => ["(#{@column} != '')"], } end def boolean_unary_operators generic_unary_operators.merge( '_blank' => ["(#{@column} IS NULL)"], '_empty' => ["(#{@column} IS NULL)"], '_present' => ["(#{@column} IS NOT NULL)"], '_not_empty' => ["(#{@column} IS NOT NULL)"], ) end alias_method :numeric_unary_operators, :boolean_unary_operators alias_method :uuid_unary_operators, :boolean_unary_operators def range_filter(min, max) if min && max && min == max ["(#{@column} = ?)", min] elsif min && max ["(#{@column} BETWEEN ? AND ?)", min, max] elsif min ["(#{@column} >= ?)", min] elsif max ["(#{@column} <= ?)", max] end end def build_statement_for_type case @type when :boolean then build_statement_for_boolean when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float when :string, :text, :citext then build_statement_for_string_or_text when :enum then build_statement_for_enum when :belongs_to_association then build_statement_for_belongs_to_association when :uuid then build_statement_for_uuid end end def build_statement_for_boolean case @value when 'false', 'f', '0' ["(#{@column} IS NULL OR #{@column} = ?)", false] when 'true', 't', '1' ["(#{@column} = ?)", true] end end def column_for_value(value) ["(#{@column} = ?)", value] end def build_statement_for_belongs_to_association return if @value.blank? ["(#{@column} = ?)", @value.to_i] if @value.to_i.to_s == @value end def build_statement_for_string_or_text return if @value.blank? return ["(#{@column} = ?)", @value] if ['is', '='].include?(@operator) @value = @value.mb_chars.downcase unless %w[postgresql postgis].include? ar_adapter @value = case @operator when 'default', 'like', 'not_like' "%#{@value}%" when 'starts_with' "#{@value}%" when 'ends_with' "%#{@value}" else return end if %w[postgresql postgis].include? ar_adapter if @operator == 'not_like' ["(#{@column} NOT ILIKE ?)", @value] else ["(#{@column} ILIKE ?)", @value] end elsif @operator == 'not_like' ["(LOWER(#{@column}) NOT LIKE ?)", @value] else ["(LOWER(#{@column}) LIKE ?)", @value] end end def build_statement_for_enum return if @value.blank? ["(#{@column} IN (?))", Array.wrap(@value)] end def build_statement_for_uuid column_for_value(@value) if /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/.match?(@value.to_s) end def ar_adapter @adapter_name.downcase end end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid/association.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module Mongoid class Association attr_reader :association, :model delegate :autosave?, to: :association def initialize(association, model) @association = association @model = model end def name association.name.to_sym end def pretty_name name.to_s.tr('_', ' ').capitalize end def type case macro.to_sym when :belongs_to, :referenced_in, :embedded_in :belongs_to when :has_one, :references_one, :embeds_one :has_one when :has_many, :references_many, :embeds_many :has_many when :has_and_belongs_to_many, :references_and_referenced_in_many :has_and_belongs_to_many else raise "Unknown association type: #{macro.inspect}" end end def field_type if polymorphic? :polymorphic_association else :"#{type}_association" end end def klass if polymorphic? && %i[referenced_in belongs_to].include?(macro) polymorphic_parents(:mongoid, association.inverse_class_name, name) || [] else association.klass end end def primary_key case type when :belongs_to, :has_and_belongs_to_many association.primary_key.to_sym else :_id end end def foreign_key return if embeds? begin association.foreign_key.to_sym rescue StandardError nil end end def foreign_key_nullable? return if foreign_key.nil? true end def foreign_type return unless polymorphic? && %i[referenced_in belongs_to].include?(macro) association.inverse_type.try(:to_sym) || :"#{name}_type" end def foreign_inverse_of return unless polymorphic? && %i[referenced_in belongs_to].include?(macro) inverse_of_field.try(:to_sym) end def key_accessor case macro.to_sym when :has_many :"#{name.to_s.singularize}_ids" when :has_one :"#{name}_id" when :embedded_in, :embeds_one, :embeds_many nil else foreign_key end end def as association.as.try :to_sym end def polymorphic? association.polymorphic? && %i[referenced_in belongs_to].include?(macro) end def inverse_of association.inverse_of.try :to_sym end def read_only? false end def nested_options nested = nested_attributes_options.try { |o| o[name] } if !nested && %i[embeds_one embeds_many].include?(macro.to_sym) && !cyclic? raise <<~MSG Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin, because embedded model doesn't have top-level access. Please add `accepts_nested_attributes_for :#{association.name}' line to `#{model}' model. MSG end nested end def association? true end def macro association.try(:macro) || association.class.name.split('::').last.underscore.to_sym end def embeds? %i[embeds_one embeds_many].include?(macro) end private def inverse_of_field association.respond_to?(:inverse_of_field) && association.inverse_of_field end def cyclic? association.respond_to?(:cyclic?) ? association.cyclic? : association.cyclic end delegate :nested_attributes_options, to: :model, prefix: false delegate :polymorphic_parents, to: RailsAdmin::AbstractModel end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid/bson.rb ================================================ # frozen_string_literal: true require 'mongoid' module RailsAdmin module Adapters module Mongoid class Bson OBJECT_ID = if defined?(Moped::BSON) Moped::BSON::ObjectId elsif defined?(BSON::ObjectId) BSON::ObjectId end class << self def parse_object_id(value) OBJECT_ID.from_string(value) rescue StandardError => e raise e if %w[ Moped::Errors::InvalidObjectId BSON::ObjectId::Invalid BSON::InvalidObjectId BSON::Error::InvalidObjectId ].exclude?(e.class.to_s) end end end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid/extension.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module Mongoid module Extension extend ActiveSupport::Concern included do class_attribute :nested_attributes_options self.nested_attributes_options = {} class << self def rails_admin(&block) RailsAdmin.config(self, &block) end alias_method :accepts_nested_attributes_for_without_rails_admin, :accepts_nested_attributes_for alias_method :accepts_nested_attributes_for, :accepts_nested_attributes_for_with_rails_admin end end def rails_admin_default_object_label_method new_record? ? "new #{self.class}" : "#{self.class} ##{id}" end def safe_send(value) if attributes.detect { |k, _v| k.to_s == value.to_s } read_attribute(value) else send(value) end end module ClassMethods # Mongoid accepts_nested_attributes_for does not store options in accessible scope, # so we intercept the call and store it in instance variable which can be accessed from outside def accepts_nested_attributes_for_with_rails_admin(*args) options = args.extract_options! args.each do |arg| nested_attributes_options[arg.to_sym] = options.reverse_merge(allow_destroy: false, update_only: false) end args << options accepts_nested_attributes_for_without_rails_admin(*args) end end end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid/object_extension.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module Mongoid module ObjectExtension def self.extended(object) object.associations.each do |name, association| association = Association.new(association, object.class) case association.macro when :has_many unless association.autosave? object.singleton_class.after_create do send(name).each(&:save) end end when :has_one unless association.autosave? object.singleton_class.after_create do send(name)&.save end end end end end end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid/property.rb ================================================ # frozen_string_literal: true module RailsAdmin module Adapters module Mongoid class Property STRING_TYPE_COLUMN_NAMES = %i[name title subject].freeze attr_reader :property, :model def initialize(property, model) @property = property @model = model end def name (property.options[:as] || property.name).to_sym end def pretty_name (property.options[:as] || property.name).to_s.tr('_', ' ').capitalize end def type case property.type.to_s when 'Array', 'Hash', 'Money' :serialized when 'BigDecimal' :decimal when 'Boolean', 'Mongoid::Boolean' :boolean when 'BSON::ObjectId', 'Moped::BSON::ObjectId' :bson_object_id when 'Date' :date when 'ActiveSupport::TimeWithZone', 'DateTime', 'Time' :datetime when 'Float' :float when 'Integer' :integer when 'Object' object_field_type when 'String' string_field_type when 'Symbol' :string else :string end end def length (length_validation_lookup || 255) if type == :string end def nullable? true end def serial? name == :_id end def association? false end def read_only? model.readonly_attributes.include? property.name.to_s end private def object_field_type association = Association.new model.relations.values.detect { |r| r.try(:foreign_key).try(:to_sym) == name }, model if %i[belongs_to referenced_in embedded_in].include?(association.macro) :bson_object_id else :string end end def string_field_type if ((length = length_validation_lookup) && length < 256) || STRING_TYPE_COLUMN_NAMES.include?(name) :string else :text end end def length_validation_lookup shortest = model.validators.select { |validator| validator.respond_to?(:attributes) && validator.attributes.include?(name.to_sym) && validator.kind == :length && validator.options[:maximum] }.min do |a, b| a.options[:maximum] <=> b.options[:maximum] end shortest && shortest.options[:maximum] end end end end end ================================================ FILE: lib/rails_admin/adapters/mongoid.rb ================================================ # frozen_string_literal: true require 'mongoid' require 'rails_admin/config/sections/list' require 'rails_admin/adapters/mongoid/association' require 'rails_admin/adapters/mongoid/object_extension' require 'rails_admin/adapters/mongoid/property' require 'rails_admin/adapters/mongoid/bson' module RailsAdmin module Adapters module Mongoid DISABLED_COLUMN_TYPES = %w[Range Moped::BSON::Binary BSON::Binary Mongoid::Geospatial::Point].freeze def parse_object_id(value) Bson.parse_object_id(value) end def new(params = {}) model.new(params).extend(ObjectExtension) end def get(id, scope = scoped) object = scope.find(id) return nil unless object object.extend(ObjectExtension) rescue StandardError => e raise e if %w[ Mongoid::Errors::DocumentNotFound Mongoid::Errors::InvalidFind Moped::Errors::InvalidObjectId BSON::InvalidObjectId BSON::Error::InvalidObjectId ].exclude?(e.class.to_s) end def scoped model.scoped end def first(options = {}, scope = nil) all(options, scope).first end def all(options = {}, scope = nil) scope ||= scoped scope = scope.includes(*options[:include]) if options[:include] scope = scope.limit(options[:limit]) if options[:limit] scope = scope.any_in(_id: options[:bulk_ids]) if options[:bulk_ids] scope = query_scope(scope, options[:query]) if options[:query] scope = filter_scope(scope, options[:filters]) if options[:filters] scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) if options[:page] && options[:per] scope = sort_by(options, scope) if options[:sort] scope rescue NoMethodError => e if /page/.match?(e.message) e = e.exception <<~ERROR #{e.message} If you don't have kaminari-mongoid installed, add `gem 'kaminari-mongoid'` to your Gemfile. ERROR end raise e end def count(options = {}, scope = nil) all(options.merge(limit: false, page: false), scope).count end def destroy(objects) Array.wrap(objects).each(&:destroy) end def primary_key '_id' end def associations model.relations.values.collect do |association| Association.new(association, model) end end def properties fields = model.fields.reject { |_name, field| DISABLED_COLUMN_TYPES.include?(field.type.to_s) } fields.collect { |_name, field| Property.new(field, model) } end def base_class klass = model klass = klass.superclass while klass.hereditary? klass end def table_name model.collection_name.to_s end def encoding Encoding::UTF_8 end def embedded? associations.detect { |a| a.macro == :embedded_in } end def cyclic? model.cyclic? end def adapter_supports_joins? false end private def build_statement(column, type, value, operator) StatementBuilder.new(column, type, value, operator).to_statement end def make_field_conditions(field, value, operator) conditions_per_collection = {} field.searchable_columns.each do |column_infos| collection_name, column_name = parse_collection_name(column_infos[:column]) statement = build_statement(column_name, column_infos[:type], value, operator) next unless statement conditions_per_collection[collection_name] ||= [] conditions_per_collection[collection_name] << statement end conditions_per_collection end def query_scope(scope, query, fields = config.list.fields.select(&:queryable?)) if config.list.search_by scope.send(config.list.search_by, query) else statements = [] fields.each do |field| value = parse_field_value(field, query) conditions_per_collection = make_field_conditions(field, value, field.search_operator) statements.concat make_condition_for_current_collection(field, conditions_per_collection) end scope.where(statements.any? ? {'$or' => statements} : {}) end end # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...} # "0055" is the filter index, no use here. o is the operator, v the value def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?)) statements = [] filters.each_pair do |field_name, filters_dump| filters_dump.each_value do |filter_dump| field = fields.detect { |f| f.name.to_s == field_name } next unless field value = parse_field_value(field, filter_dump[:v]) conditions_per_collection = make_field_conditions(field, value, (filter_dump[:o] || 'default')) field_statements = make_condition_for_current_collection(field, conditions_per_collection) if field_statements.many? statements << {'$or' => field_statements} elsif field_statements.any? statements << field_statements.first end end end scope.where(statements.any? ? {'$and' => statements} : {}) end def parse_collection_name(column) collection_name, column_name = column.split('.') if associations.detect { |a| a.name == collection_name.to_sym }.try(:embeds?) [table_name, column] else [collection_name, column_name] end end def make_condition_for_current_collection(target_field, conditions_per_collection) result = [] conditions_per_collection.each do |collection_name, conditions| if collection_name == table_name # conditions referring current model column are passed directly result.concat conditions else # otherwise, collect ids of documents that satisfy search condition result.concat perform_search_on_associated_collection(target_field.name, conditions) end end result end def perform_search_on_associated_collection(field_name, conditions) target_association = associations.detect { |a| a.name == field_name } return [] unless target_association model = target_association.klass case target_association.type when :belongs_to, :has_and_belongs_to_many [{target_association.foreign_key.to_s => {'$in' => model.where('$or' => conditions).all.collect { |r| r.send(target_association.primary_key) }}}] when :has_many, :has_one [{target_association.primary_key.to_s => {'$in' => model.where('$or' => conditions).all.collect { |r| r.send(target_association.foreign_key) }}}] end end def sort_by(options, scope) return scope unless options[:sort] case options[:sort] when String field_name, collection_name = options[:sort].split('.').reverse raise 'sorting by associated model column is not supported in Non-Relational databases' if collection_name && collection_name != table_name when Symbol field_name = options[:sort].to_s end if options[:sort_reverse] scope.asc field_name else scope.desc field_name end end class StatementBuilder < RailsAdmin::AbstractModel::StatementBuilder protected def unary_operators { '_blank' => {@column => {'$in' => [nil, '']}}, '_present' => {@column => {'$nin' => [nil, '']}}, '_null' => {@column => nil}, '_not_null' => {@column => {'$ne' => nil}}, '_empty' => {@column => ''}, '_not_empty' => {@column => {'$ne' => ''}}, } end private def build_statement_for_type case @type when :boolean then build_statement_for_boolean when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float when :string, :text then build_statement_for_string_or_text when :enum then build_statement_for_enum when :belongs_to_association, :bson_object_id then build_statement_for_belongs_to_association_or_bson_object_id end end def build_statement_for_boolean case @value when 'false', 'f', '0' {@column => false} when 'true', 't', '1' {@column => true} end end def column_for_value(value) {@column => value} end def build_statement_for_string_or_text return if @value.blank? @value = case @operator when 'not_like' Regexp.compile("^((?!#{Regexp.escape(@value)}).)*$", Regexp::IGNORECASE) when 'default', 'like' Regexp.compile(Regexp.escape(@value), Regexp::IGNORECASE) when 'starts_with' Regexp.compile("^#{Regexp.escape(@value)}", Regexp::IGNORECASE) when 'ends_with' Regexp.compile("#{Regexp.escape(@value)}$", Regexp::IGNORECASE) when 'is', '=' @value.to_s else return end {@column => @value} end def build_statement_for_enum return if @value.blank? {@column => {'$in' => Array.wrap(@value)}} end def build_statement_for_belongs_to_association_or_bson_object_id {@column => @value} if @value end def range_filter(min, max) if min && max && min == max {@column => min} elsif min && max {@column => {'$gte' => min, '$lte' => max}} elsif min {@column => {'$gte' => min}} elsif max {@column => {'$lte' => max}} end end end end end end ================================================ FILE: lib/rails_admin/config/actions/base.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' module RailsAdmin module Config module Actions class Base include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable include RailsAdmin::Config::Hideable register_instance_option :only do nil end register_instance_option :except do [] end register_instance_option :show_in_navigation do root? end register_instance_option :show_in_sidebar do !show_in_navigation end register_instance_option :show_in_menu do true end register_instance_option :sidebar_label do nil end # http://getbootstrap.com/2.3.2/base-css.html#icons register_instance_option :link_icon do 'fas fa-question' end # Should the action be visible register_instance_option :visible? do authorized? end register_instance_option :enabled? do bindings[:abstract_model].nil? || ( (only.nil? || [only].flatten.collect(&:to_s).include?(bindings[:abstract_model].to_s)) && ![except].flatten.collect(&:to_s).include?(bindings[:abstract_model].to_s) && !bindings[:abstract_model].config.excluded? ) && (!respond_to?(:writable?) || writable?) end register_instance_option :authorized? do enabled? && ( bindings[:controller].try(:authorization_adapter).nil? || bindings[:controller].authorization_adapter.authorized?(authorization_key, bindings[:abstract_model], bindings[:object]) ) end # Is the action acting on the root level (Example: /admin/contact) register_instance_option :root? do false end # Is the action on a model scope (Example: /admin/team/export) register_instance_option :collection? do false end # Is the action on an object scope (Example: /admin/team/1/edit) register_instance_option :member? do false end # Target window [_self, _blank] register_instance_option :link_target do nil end # Determines whether to navigate via Turbo Drive or not register_instance_option :turbo? do true end # This block is evaluated in the context of the controller when action is called # You can access: # - @objects if you're on a model scope # - @abstract_model & @model_config if you're on a model or object scope # - @object if you're on an object scope register_instance_option :controller do proc do render action: @action.template_name end end # Model scoped actions only. You will need to handle params[:bulk_ids] in controller register_instance_option :bulkable? do false end # View partial name (called in default :controller block) register_instance_option :template_name do key.to_sym end # For CanCanCan and the like register_instance_option :authorization_key do key.to_sym end # List of methods allowed. Note that you are responsible for correctly handling them in :controller block register_instance_option :http_methods do [:get] end # Url fragment register_instance_option :route_fragment do custom_key.to_s end # Controller action name register_instance_option :action_name do custom_key.to_sym end # I18n key register_instance_option :i18n_key do key end # User should override only custom_key (action name and route fragment change, allows for duplicate actions) register_instance_option :custom_key do key end # Breadcrumb parent register_instance_option :breadcrumb_parent do if root? [:dashboard] elsif collection? [:index, bindings[:abstract_model]] elsif member? [:show, bindings[:abstract_model], bindings[:object]] end end # Off API. def key self.class.key end def self.key name.to_s.demodulize.underscore.to_sym end end end end end ================================================ FILE: lib/rails_admin/config/actions/bulk_delete.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class BulkDelete < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :collection do true end register_instance_option :http_methods do %i[post delete] end register_instance_option :controller do proc do if request.post? # BULK DELETE @objects = list_entries(@model_config, :destroy) if @objects.blank? flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label), action: t('admin.actions.delete.done')) redirect_to index_path else render @action.template_name end elsif request.delete? # BULK DESTROY destroyed = nil not_destroyed = nil unless params[:bulk_ids].blank? @objects = list_entries(@model_config, :destroy) unless @objects.blank? processed_objects = @abstract_model.destroy(@objects) destroyed = processed_objects.select(&:destroyed?) not_destroyed = processed_objects - destroyed destroyed.each do |object| @auditing_adapter&.delete_object(object, @abstract_model, _current_user) end end end if destroyed.nil? flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label), action: t('admin.actions.delete.done')) else flash[:success] = t('admin.flash.successful', name: pluralize(destroyed.count, @model_config.label), action: t('admin.actions.delete.done')) unless destroyed.empty? flash[:error] = t('admin.flash.error', name: pluralize(not_destroyed.count, @model_config.label), action: t('admin.actions.delete.done')) unless not_destroyed.empty? end redirect_to back_or_index end end end register_instance_option :authorization_key do :destroy end register_instance_option :bulkable? do true end end end end end ================================================ FILE: lib/rails_admin/config/actions/dashboard.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class Dashboard < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :root? do true end register_instance_option :breadcrumb_parent do nil end register_instance_option :auditing_versions_limit do 100 end register_instance_option :controller do proc do @history = @auditing_adapter&.latest(@action.auditing_versions_limit) if @action.history? if @action.statistics? model_configs = RailsAdmin::Config.visible_models(controller: self) @abstract_models = model_configs.map(&:abstract_model) @most_recent_created = {} @count = {} @max = 0 model_configs.each do |config| scope = @authorization_adapter&.query(:index, config.abstract_model) current_count = config.abstract_model.count({}, scope) @max = current_count > @max ? current_count : @max name = config.abstract_model.model.name @count[name] = current_count @most_recent_created[name] = config.last_created_at end end render @action.template_name, status: @status_code || :ok end end register_instance_option :route_fragment do '' end register_instance_option :link_icon do 'fas fa-home' end register_instance_option :statistics? do true end register_instance_option :history? do true end end end end end ================================================ FILE: lib/rails_admin/config/actions/delete.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class Delete < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :member do true end register_instance_option :route_fragment do 'delete' end register_instance_option :http_methods do %i[get delete] end register_instance_option :authorization_key do :destroy end register_instance_option :controller do proc do if request.get? # DELETE respond_to do |format| format.html { render @action.template_name } format.js { render @action.template_name, layout: false } end elsif request.delete? # DESTROY @auditing_adapter&.delete_object(@object, @abstract_model, _current_user) if @object.destroy flash[:success] = t('admin.flash.successful', name: @model_config.label, action: t('admin.actions.delete.done')) redirect_to index_path else handle_save_error :delete end end end end register_instance_option :link_icon do 'fas fa-times' end register_instance_option :writable? do !(bindings[:object] && bindings[:object].readonly?) end end end end end ================================================ FILE: lib/rails_admin/config/actions/edit.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class Edit < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :member do true end register_instance_option :http_methods do %i[get put] end register_instance_option :controller do proc do if request.get? # EDIT respond_to do |format| format.html { render @action.template_name } format.js { render @action.template_name, layout: 'rails_admin/modal', content_type: Mime[:html].to_s } end elsif request.put? # UPDATE sanitize_params_for!(request.xhr? ? :modal : :update) @object.assign_attributes(params[@abstract_model.param_key]) @authorization_adapter&.authorize(:update, @abstract_model, @object) changes = @object.changes if @object.save @auditing_adapter&.update_object(@object, @abstract_model, _current_user, changes) respond_to do |format| format.html { redirect_to_on_success } format.json { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } end else handle_save_error :edit end end end end register_instance_option :link_icon do 'fas fa-pencil-alt' end register_instance_option :writable? do !(bindings[:object] && bindings[:object].readonly?) end end end end end ================================================ FILE: lib/rails_admin/config/actions/export.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class Export < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :collection do true end register_instance_option :http_methods do %i[get post] end register_instance_option :controller do proc do format = params[:json] && :json || params[:csv] && :csv || params[:xml] && :xml if format request.format = format @schema = HashHelper.symbolize(params[:schema].slice(:except, :include, :methods, :only).permit!.to_h) if params[:schema] # to_json and to_xml expect symbols for keys AND values. @objects = list_entries(@model_config, :export) index else render @action.template_name end end end register_instance_option :bulkable? do true end register_instance_option :link_icon do 'fas fa-file-export' end end end end end ================================================ FILE: lib/rails_admin/config/actions/history_index.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class HistoryIndex < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :authorization_key do :history end register_instance_option :collection do true end register_instance_option :route_fragment do 'history' end register_instance_option :controller do proc do @general = true @history = @auditing_adapter&.listing_for_model(@abstract_model, params[:query], params[:sort], params[:sort_reverse], params[:all], params[Kaminari.config.param_name]) || [] render @action.template_name end end register_instance_option :template_name do :history end register_instance_option :link_icon do 'fas fa-book' end end end end end ================================================ FILE: lib/rails_admin/config/actions/history_show.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class HistoryShow < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :authorization_key do :history end register_instance_option :member do true end register_instance_option :route_fragment do 'history' end register_instance_option :controller do proc do @general = false @history = @auditing_adapter&.listing_for_object(@abstract_model, @object, params[:query], params[:sort], params[:sort_reverse], params[:all], params[Kaminari.config.param_name]) || [] render @action.template_name end end register_instance_option :template_name do :history end register_instance_option :link_icon do 'fas fa-book' end end end end end ================================================ FILE: lib/rails_admin/config/actions/index.rb ================================================ # frozen_string_literal: true require 'activemodel-serializers-xml' module RailsAdmin module Config module Actions class Index < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :collection do true end register_instance_option :http_methods do %i[get post] end register_instance_option :route_fragment do '' end register_instance_option :breadcrumb_parent do parent_model = bindings[:abstract_model].try(:config).try(:parent) am = parent_model && RailsAdmin.config(parent_model).try(:abstract_model) if am [:index, am] else [:dashboard] end end register_instance_option :controller do proc do @objects ||= list_entries unless @model_config.list.scopes.empty? if params[:scope].blank? @objects = @objects.send(@model_config.list.scopes.first) unless @model_config.list.scopes.first.nil? elsif @model_config.list.scopes.collect(&:to_s).include?(params[:scope]) @objects = @objects.send(params[:scope].to_sym) end end respond_to do |format| format.html do render @action.template_name, status: @status_code || :ok end format.json do output = if params[:compact] if @association @association.collection(@objects).collect { |(label, id)| {id: id, label: label} } else @objects.collect { |object| {id: object.id.to_s, label: object.send(@model_config.object_label_method).to_s} } end else @objects.to_json(@schema) end if params[:send_data] send_data output, filename: "#{params[:model_name]}_#{DateTime.now.strftime('%Y-%m-%d_%Hh%Mm%S')}.json" else render json: output, root: false end end format.xml do output = @objects.to_xml(@schema) if params[:send_data] send_data output, filename: "#{params[:model_name]}_#{DateTime.now.strftime('%Y-%m-%d_%Hh%Mm%S')}.xml" else render xml: output end end format.csv do header, encoding, output = CSVConverter.new(@objects, @schema).to_csv(params[:csv_options].permit!.to_h) if params[:send_data] send_data output, type: "text/csv; charset=#{encoding}; #{'header=present' if header}", disposition: "attachment; filename=#{params[:model_name]}_#{DateTime.now.strftime('%Y-%m-%d_%Hh%Mm%S')}.csv" else render plain: output end end end end end register_instance_option :link_icon do 'fas fa-th-list' end end end end end ================================================ FILE: lib/rails_admin/config/actions/new.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class New < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :collection do true end register_instance_option :http_methods do %i[get post] # NEW / CREATE end register_instance_option :controller do proc do if request.get? # NEW @object = @abstract_model.new @action = @action.with(@action.bindings.merge(object: @object)) @authorization_adapter&.attributes_for(:new, @abstract_model)&.each do |name, value| @object.send("#{name}=", value) end object_params = params[@abstract_model.param_key] if object_params sanitize_params_for!(request.xhr? ? :modal : :create) @object.assign_attributes(@object.attributes.merge(object_params.to_h)) end respond_to do |format| format.html { render @action.template_name } format.js { render @action.template_name, layout: 'rails_admin/modal', content_type: Mime[:html].to_s } end elsif request.post? # CREATE @modified_assoc = [] @object = @abstract_model.new sanitize_params_for!(request.xhr? ? :modal : :create) @object.assign_attributes(params[@abstract_model.param_key]) @authorization_adapter&.authorize(:create, @abstract_model, @object) if @object.save @auditing_adapter&.create_object(@object, @abstract_model, _current_user) respond_to do |format| format.html { redirect_to_on_success } format.json { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } end else handle_save_error end end end end register_instance_option :link_icon do 'fas fa-plus' end register_instance_option :writable? do !(bindings[:object] && bindings[:object].readonly?) end end end end end ================================================ FILE: lib/rails_admin/config/actions/show.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class Show < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :member do true end register_instance_option :route_fragment do '' end register_instance_option :breadcrumb_parent do [:index, bindings[:abstract_model]] end register_instance_option :controller do proc do respond_to do |format| format.json { render json: @object } format.html { render @action.template_name } end end end register_instance_option :link_icon do 'fas fa-info-circle' end end end end end ================================================ FILE: lib/rails_admin/config/actions/show_in_app.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class ShowInApp < RailsAdmin::Config::Actions::Base RailsAdmin::Config::Actions.register(self) register_instance_option :member do true end register_instance_option :visible? do authorized? && begin bindings[:controller].main_app.url_for(bindings[:object]) rescue StandardError false end end register_instance_option :controller do proc do redirect_to main_app.url_for(@object) end end register_instance_option :link_icon do 'fas fa-eye' end register_instance_option :turbo? do false end end end end end ================================================ FILE: lib/rails_admin/config/actions.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Actions class << self def all(scope = nil, bindings = {}) if scope.is_a?(Hash) bindings = scope scope = :all end scope ||= :all init_actions! actions = case scope when :all @@actions when :root @@actions.select(&:root?) when :collection @@actions.select(&:collection?) when :bulkable @@actions.select(&:bulkable?) when :member @@actions.select(&:member?) end actions = actions.collect { |action| action.with(bindings) } bindings[:controller] ? actions.select(&:visible?) : actions end def find(custom_key, bindings = {}) init_actions! action = @@actions.detect { |a| a.custom_key == custom_key }.try(:with, bindings) bindings[:controller] ? (action.try(:visible?) && action || nil) : action end def collection(key, parent_class = :base, &block) add_action key, parent_class, :collection, &block end def member(key, parent_class = :base, &block) add_action key, parent_class, :member, &block end def root(key, parent_class = :base, &block) add_action key, parent_class, :root, &block end def add_action(key, parent_class, parent, &block) a = "RailsAdmin::Config::Actions::#{parent_class.to_s.camelize}".constantize.new a.instance_eval(%( #{parent} true def key :#{key} end ), __FILE__, __LINE__ - 5) add_action_custom_key(a, &block) end def reset @@actions = nil end def register(name, klass = nil) if klass.nil? && name.is_a?(Class) klass = name name = klass.to_s.demodulize.underscore.to_sym end instance_eval %{ def #{name}(&block) action = #{klass}.new add_action_custom_key(action, &block) end }, __FILE__, __LINE__ - 5 end private def init_actions! @@actions ||= [ Dashboard.new, Index.new, Show.new, New.new, Edit.new, Export.new, Delete.new, BulkDelete.new, HistoryShow.new, HistoryIndex.new, ShowInApp.new, ] end def add_action_custom_key(action, &block) action.instance_eval(&block) if block @@actions ||= [] if action.custom_key.in?(@@actions.collect(&:custom_key)) raise "Action #{action.custom_key} already exists. Please change its custom key." else @@actions << action end end end end end end require 'rails_admin/config/actions/base' require 'rails_admin/config/actions/dashboard' require 'rails_admin/config/actions/index' require 'rails_admin/config/actions/show' require 'rails_admin/config/actions/show_in_app' require 'rails_admin/config/actions/history_show' require 'rails_admin/config/actions/history_index' require 'rails_admin/config/actions/new' require 'rails_admin/config/actions/edit' require 'rails_admin/config/actions/export' require 'rails_admin/config/actions/delete' require 'rails_admin/config/actions/bulk_delete' ================================================ FILE: lib/rails_admin/config/configurable.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config # A module for all configurables. module Configurable def self.included(base) base.send :extend, ClassMethods end def has_option?(name) # rubocop:disable Naming/PredicatePrefix? options = self.class.instance_variable_get('@config_options') options&.key?(name) end # Register an instance option for this object only def register_instance_option(option_name, &default) scope = class << self; self; end self.class.register_instance_option(option_name, scope, &default) end def register_deprecated_instance_option(option_name, replacement_option_name = nil, &custom_error) scope = class << self; self; end self.class.register_deprecated_instance_option(option_name, replacement_option_name, scope, &custom_error) end private def with_recurring(option_name, value_proc, default_proc) # Track recursive invocation with an instance variable. This prevents run-away recursion # and allows configurations such as # label { "#{label}".upcase } # This will use the default definition when called recursively. Thread.current[:rails_admin_recurring] ||= {} Thread.current[:rails_admin_recurring][self] ||= {} if Thread.current[:rails_admin_recurring][self][option_name] instance_eval(&default_proc) else Thread.current[:rails_admin_recurring][self][option_name] = true instance_eval(&value_proc) end ensure Thread.current[:rails_admin_recurring].delete(self) end module ClassMethods # Register an instance option. Instance option is a configuration # option that stores its value within an instance variable and is # accessed by an instance method. Both go by the name of the option. def register_instance_option(option_name, scope = self, &default) options = scope.instance_variable_get('@config_options') || scope.instance_variable_set('@config_options', {}) option_name = option_name.to_s options[option_name] = nil # If it's a boolean create an alias for it and remove question mark if option_name.end_with?('?') scope.send(:define_method, "#{option_name.chop!}?") do send(option_name) end end # Define getter/setter by the option name scope.send(:define_method, option_name) do |*args, &block| if !args[0].nil? || block # Invocation with args --> This is the declaration of the option, i.e. setter instance_variable_set("@#{option_name}_registered", args[0].nil? ? block : args[0]) else # Invocation without args nor block --> It's the use of the option, i.e. getter value = instance_variable_get("@#{option_name}_registered") case value when Proc value = with_recurring(option_name, value, default) when nil value = instance_eval(&default) end value end end end def register_deprecated_instance_option(option_name, replacement_option_name = nil, scope = self) scope.send(:define_method, option_name) do |*args, &block| if replacement_option_name ActiveSupport::Deprecation.warn("The #{option_name} configuration option is deprecated, please use #{replacement_option_name}.") send(replacement_option_name, *args, &block) elsif block_given? yield else raise "The #{option_name} configuration option is removed without replacement." end end end # Register a class option. Class option is a configuration # option that stores it's value within a class object's instance variable # and is accessed by a class method. Both go by the name of the option. def register_class_option(option_name, &default) scope = class << self; self; end register_instance_option(option_name, scope, &default) end end end end end ================================================ FILE: lib/rails_admin/config/const_load_suppressor.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module ConstLoadSuppressor class << self @original_const_missing = nil def suppressing raise 'Constant Loading is already suppressed' if @original_const_missing begin @original_const_missing = Object.method(:const_missing) intercept_const_missing yield ensure Object.define_singleton_method(:const_missing, @original_const_missing) @original_const_missing = nil end end def allowing if @original_const_missing begin Object.define_singleton_method(:const_missing, @original_const_missing) yield ensure intercept_const_missing end else yield end end private def intercept_const_missing Object.define_singleton_method(:const_missing) do |name| ConstProxy.new(name.to_s) end end end class ConstProxy < BasicObject attr_reader :name def initialize(name) @name = name end def klass @klass ||= begin unless ::Object.const_defined?(name) ::Kernel.raise <<~MESSAGE The constant #{name} is not loaded yet upon the execution of the RailsAdmin initializer. We don't recommend to do this and may lead to issues, but if you really have to do so you can explicitly require it by adding: require '#{name.underscore}' on top of config/initializers/rails_admin.rb. MESSAGE end name.constantize end end def method_missing(method_name, *args, &block) klass.send(method_name, *args, &block) end def respond_to_missing?(method_name, include_private = false) super || klass.respond_to?(method_name, include_private) end end end end end ================================================ FILE: lib/rails_admin/config/fields/association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config' require 'rails_admin/config/fields/base' module RailsAdmin module Config module Fields class Association < RailsAdmin::Config::Fields::Base # Reader for the association information hash def association @properties end def method_name nested_form ? :"#{name}_attributes" : association.key_accessor end register_instance_option :pretty_value do v = bindings[:view] [value].flatten.select(&:present?).collect do |associated| amc = polymorphic? ? RailsAdmin.config(associated) : associated_model_config # perf optimization for non-polymorphic associations am = amc.abstract_model wording = associated.send(amc.object_label_method) can_see = !am.embedded? && (show_action = v.action(:show, am, associated)) can_see ? v.link_to(wording, v.url_for(action: show_action.action_name, model_name: am.to_param, id: associated.id)) : ERB::Util.html_escape(wording) end.to_sentence.html_safe.presence || '-' end # Accessor whether association is visible or not. By default # association checks whether the child model is excluded in # configuration or not. register_instance_option :visible? do @visible ||= !associated_model_config.excluded? end # use the association name as a key, not the association key anymore! register_instance_option :label do (@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name association.name end # scope for possible associable records register_instance_option :associated_collection_scope do # bindings[:object] & bindings[:controller] available associated_collection_scope_limit = (associated_collection_cache_all ? nil : 30) proc do |scope| scope.limit(associated_collection_scope_limit) end end # inverse relationship register_instance_option :inverse_of do association.inverse_of end # preload entire associated collection (per associated_collection_scope) on load # Be sure to set limit in associated_collection_scope if set is large register_instance_option :associated_collection_cache_all do @associated_collection_cache_all ||= dynamically_scope_by.blank? && (associated_model_config.abstract_model.count < associated_model_limit) end # client-side dynamic scoping register_instance_option :dynamically_scope_by do nil end # parses #dynamically_scope_by and returns a Hash in the form of # {[form field name in this model]: [field name in the associated model]} def dynamic_scope_relationships @dynamic_scope_relationships ||= Array.wrap(dynamically_scope_by).flat_map do |field| field.is_a?(Hash) ? field.to_a : [[field, field]] end.map do |field_name, target_name| # rubocop:disable Style/MultilineBlockChain field = section.fields.detect { |f| f.name == field_name } raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{abstract_model.model_name}'" unless field target_field = associated_model_config.list.fields.detect { |f| f.name == target_name } raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{associated_model_config.abstract_model.model_name}'" unless target_field raise "Field '#{field_name}' in '#{associated_model_config.abstract_model.model_name}' can't be used for dynamic scoping because it's not filterable" unless target_field.filterable [field.method_name, target_name] end.to_h end # determines whether association's elements can be removed register_instance_option :removable? do association.foreign_key_nullable? end register_instance_option :eager_load do !!searchable end register_instance_option :inline_add do true end register_instance_option :inline_edit do true end # Reader for the association's child model's configuration def associated_model_config @associated_model_config ||= RailsAdmin.config(association.klass) end # Reader for the association's child model object's label method def associated_object_label_method @associated_object_label_method ||= associated_model_config.object_label_method end # Reader for associated primary key def associated_primary_key association.primary_key end # Returns params which are to be set in modals def associated_prepopulate_params {} end # Reader whether this is a polymorphic association def polymorphic? association.polymorphic? end # Reader for nested attributes register_instance_option :nested_form do association.nested_options end # Reader for the association's value unformatted def value bindings[:object].send(association.name) end # Returns collection of all selectable records def collection(scope = nil) (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)). map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key)).to_s] } end # has many? def multiple? true end def virtual? true end def associated_model_limit RailsAdmin.config.default_associated_collection_limit end private def format_key(key) if key.is_a?(Array) RailsAdmin.config.composite_keys_serializer.serialize(key) else key end end end end end end ================================================ FILE: lib/rails_admin/config/fields/base.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' require 'rails_admin/config/groupable' require 'rails_admin/config/inspectable' module RailsAdmin module Config module Fields class Base # rubocop:disable Metrics/ClassLength include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable include RailsAdmin::Config::Hideable include RailsAdmin::Config::Groupable include RailsAdmin::Config::Inspectable attr_reader :name, :properties, :abstract_model, :parent, :root attr_accessor :defined, :order, :section NAMED_INSTANCE_VARIABLES = %i[ @parent @root @section @children_fields_registered @associated_model_config @group ].freeze def initialize(parent, name, properties) @parent = parent @root = parent.root @abstract_model = parent.abstract_model @defined = false @name = name.to_sym @order = 0 @properties = properties @section = parent end register_instance_option :css_class do "#{name}_field" end def type_css_class "#{type}_type" end def virtual? properties.blank? end register_instance_option :column_width do nil end register_instance_option :sticky? do false end register_instance_option :sortable do !virtual? || children_fields.first || false end def sort_column if sortable == true "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(name)}" elsif (sortable.is_a?(String) || sortable.is_a?(Symbol)) && sortable.to_s.include?('.') # just provide sortable, don't do anything smart sortable elsif sortable.is_a?(Hash) # just join sortable hash, don't do anything smart "#{sortable.keys.first}.#{sortable.values.first}" elsif association? # use column on target table "#{associated_model_config.abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}" else # use described column in the field conf. "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}" end end register_instance_option :searchable do !virtual? || children_fields.first || false end register_instance_option :search_operator do RailsAdmin::Config.default_search_operator end register_instance_option :queryable? do !virtual? end register_instance_option :filterable? do !!searchable end register_instance_option :filter_operators do [] end register_instance_option :default_filter_operator do nil end def filter_options { label: label, name: name, operator: default_filter_operator, operators: filter_operators, type: type, } end # serials and dates are reversed in list, which is more natural (last modified items first). register_instance_option :sort_reverse? do false end # list of columns I should search for that field [{ column: 'table_name.column', type: field.type }, {..}] register_instance_option :searchable_columns do @searchable_columns ||= case searchable when true [{column: "#{abstract_model.table_name}.#{name}", type: type}] when false [] when :all # valid only for associations table_name = associated_model_config.abstract_model.table_name associated_model_config.list.fields.collect { |f| {column: "#{table_name}.#{f.name}", type: f.type} } else [searchable].flatten.collect do |f| if f.is_a?(String) && f.include?('.') # table_name.column table_name, column = f.split '.' type = nil elsif f.is_a?(Hash) # => am = AbstractModel.new(f.keys.first) if f.keys.first.is_a?(Class) table_name = am&.table_name || f.keys.first column = f.values.first property = am&.properties&.detect { |p| p.name == f.values.first.to_sym } type = property&.type else # am = (association? ? associated_model_config.abstract_model : abstract_model) table_name = am.table_name column = f property = am.properties.detect { |p| p.name == f.to_sym } type = property&.type end {column: "#{table_name}.#{column}", type: (type || :string)} end end end register_instance_option :formatted_value do value end # output for pretty printing (show, list) register_instance_option :pretty_value do formatted_value.presence || ' - ' end # output for printing in export view (developers beware: no bindings[:view] and no data!) register_instance_option :export_value do pretty_value end # Accessor for field's help text displayed below input field. register_instance_option :help do (@help ||= {})[::I18n.locale] ||= generic_field_help end register_instance_option :html_attributes do { required: required?, } end register_instance_option :default_value do nil end # Accessor for field's label. # # @see RailsAdmin::AbstractModel.properties register_instance_option :label do (@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name name end register_instance_option :hint do (@hint ||= '') end # Accessor for field's maximum length per database. # # @see RailsAdmin::AbstractModel.properties register_instance_option :length do @length ||= properties&.length end # Accessor for field's length restrictions per validations # register_instance_option :valid_length do @valid_length ||= abstract_model.model.validators_on(name).detect { |v| v.kind == :length }.try(&:options) || {} end register_instance_option :partial do :form_field end # Accessor for whether this is field is mandatory. # # @see RailsAdmin::AbstractModel.properties register_instance_option :required? do context = if bindings && bindings[:object] bindings[:object].persisted? ? :update : :create else :nil end (@required ||= {})[context] ||= !!([name] + children_fields).uniq.detect do |column_name| abstract_model.model.validators_on(column_name).detect do |v| !(v.options[:allow_nil] || v.options[:allow_blank]) && %i[presence numericality attachment_presence].include?(v.kind) && (v.options[:on] == context || v.options[:on].blank?) && (v.options[:if].blank? && v.options[:unless].blank?) end end end # Accessor for whether this is a serial field (aka. primary key, identifier). # # @see RailsAdmin::AbstractModel.properties register_instance_option :serial? do properties&.serial? end register_instance_option :view_helper do :text_field end register_instance_option :read_only? do !editable? end # init status in the view register_instance_option :active? do false end register_instance_option :visible? do returned = true (RailsAdmin.config.default_hidden_fields || {}).each do |section, fields| next unless self.section.is_a?("RailsAdmin::Config::Sections::#{section.to_s.camelize}".constantize) returned = false if fields.include?(name) end returned end # columns mapped (belongs_to, paperclip, etc.). First one is used for searching/sorting by default register_instance_option :children_fields do [] end register_instance_option :eager_load do false end register_deprecated_instance_option :eager_load?, :eager_load def eager_load_values case eager_load when true [name] when false, nil [] else Array.wrap(eager_load) end end register_instance_option :render do bindings[:view].render partial: "rails_admin/main/#{partial}", locals: {field: self, form: bindings[:form]} end def editable? !((@properties && @properties.read_only?) || (bindings[:object] && bindings[:object].readonly?)) end # Is this an association def association? is_a?(RailsAdmin::Config::Fields::Association) end # Reader for validation errors of the bound object def errors ([name] + children_fields).uniq.collect do |column_name| bindings[:object].errors[column_name] end.uniq.flatten end # Reader whether field is optional. # # @see RailsAdmin::Config::Fields::Base.register_instance_option :required? def optional? !required? end # Inverse accessor whether this field is required. # # @see RailsAdmin::Config::Fields::Base.register_instance_option :required? def optional(state = nil, &block) if !state.nil? || block required state.nil? ? proc { instance_eval(&block) == false } : state == false else optional? end end # Writer to make field optional. # # @see RailsAdmin::Config::Fields::Base.optional def optional=(state) optional(state) end # Reader for field's type def type @type ||= self.class.name.to_s.demodulize.underscore.to_sym end # Reader for field's value def value bindings[:object].safe_send(name) rescue NoMethodError => e raise e.exception <<~ERROR #{e.message} If you want to use a RailsAdmin virtual field(= a field without corresponding instance method), you should declare 'formatted_value' in the field definition. field :#{name} do formatted_value{ bindings[:object].call_some_method } end ERROR end # Reader for nested attributes register_instance_option :nested_form do false end # Allowed methods for the field in forms register_instance_option :allowed_methods do [method_name] end def generic_help "#{required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')}. " end def generic_field_help model = abstract_model.model_name.underscore model_lookup = :"admin.help.#{model}.#{name}" translated = I18n.translate(model_lookup, help: generic_help, default: [generic_help]) (translated.is_a?(Hash) ? translated.to_a.first[1] : translated).html_safe end def parse_value(value) value end def parse_input(_params) # overridden end def inverse_of nil end def method_name name end def form_default_value (default_value if bindings[:object].new_record? && value.nil?) end def form_value form_default_value.nil? ? formatted_value : form_default_value end end end end end ================================================ FILE: lib/rails_admin/config/fields/collection_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/association' module RailsAdmin module Config module Fields class CollectionAssociation < Association # orderable associated objects register_instance_option :orderable do false end register_instance_option :partial do nested_form ? :form_nested_many : :form_filtering_multiselect end def collection(scope = nil) if scope super elsif associated_collection_cache_all selected = selected_ids i = 0 super.sort_by { |a| [selected.index(a[1]) || selected.size, i += 1] } else value.map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key))] } end end def associated_prepopulate_params {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}} end def multiple? true end def selected_ids value.map { |s| format_key(s.send(associated_primary_key)).to_s } end def parse_input(params) return unless associated_model_config.abstract_model.primary_key.is_a?(Array) if nested_form params[method_name].each_value do |value| value[:id] = associated_model_config.abstract_model.parse_id(value[:id]) end elsif params[method_name].is_a?(Array) params[method_name] = params[method_name].map { |key| associated_model_config.abstract_model.parse_id(key) if key.present? }.compact if params[method_name].empty? # Workaround for Arel::Visitors::UnsupportedVisitError in #ids_writer, until https://github.com/rails/rails/pull/51116 is in place params.delete(method_name) params[name] = [] end end end def form_default_value (default_value if bindings[:object].new_record? && value.empty?) end def form_value form_default_value.nil? ? selected_ids : form_default_value end def widget_options { xhr: !associated_collection_cache_all, 'edit-url': (inline_edit && bindings[:view].authorized?(:edit, associated_model_config.abstract_model) ? bindings[:view].edit_path(model_name: associated_model_config.abstract_model.to_param, id: '__ID__') : ''), remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), scopeBy: dynamic_scope_relationships, sortable: !!orderable, removable: !!removable, cacheAll: !!associated_collection_cache_all, regional: { add: ::I18n.t('admin.misc.add_new'), chooseAll: ::I18n.t('admin.misc.chose_all'), clearAll: ::I18n.t('admin.misc.clear_all'), down: ::I18n.t('admin.misc.down'), remove: ::I18n.t('admin.misc.remove'), search: ::I18n.t('admin.misc.search'), up: ::I18n.t('admin.misc.up'), }, } end end end end end ================================================ FILE: lib/rails_admin/config/fields/factories/action_text.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| if defined?(::ActionText) && properties.try(:association?) && (match = /\Arich_text_(.+)\Z/.match properties.name) && properties.klass.to_s == 'ActionText::RichText' field = RailsAdmin::Config::Fields::Types.load(:action_text).new(parent, match[1], properties) fields << field true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/active_storage.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| if defined?(::ActiveStorage) && properties.try(:association?) && (match = /\A(.+)_attachments?\Z/.match properties.name) && properties.klass.to_s == 'ActiveStorage::Attachment' name = match[1] field = RailsAdmin::Config::Fields::Types.load( properties.type == :has_many ? :multiple_active_storage : :active_storage, ).new(parent, name, properties) fields << field associations = if properties.type == :has_many [:"#{name}_attachments", :"#{name}_blobs"] else [:"#{name}_attachment", :"#{name}_blob"] end children_fields = associations.map do |child_name| child_association = parent.abstract_model.associations.detect { |p| p.name.to_sym == child_name } next unless child_association child_field = fields.detect { |f| f.name == child_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_association, fields) child_field.hide unless field == child_field child_field.filterable(false) unless field == child_field child_field.name end.flatten.compact field.children_fields(children_fields) true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/belongs_to_association' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| parent.abstract_model.associations.filter { |a| Array(a.foreign_key).include?(properties.name) && %i[belongs_to has_and_belongs_to_many].include?(a.type) }.each do |association| field = RailsAdmin::Config::Fields::Types.load(association.field_type).new(parent, association.name, association) fields << field child_columns = [] possible_field_names = if association.polymorphic? %i[foreign_key foreign_type foreign_inverse_of] else [:foreign_key] end.flat_map { |k| Array(association.send(k)) }.compact parent.abstract_model.properties.select { |p| possible_field_names.include? p.name }.each do |column| child_field = fields.detect { |f| f.name.to_s == column.name.to_s } child_field ||= RailsAdmin::Config::Fields.default_factory.call(parent, column, fields) child_columns << child_field end child_columns.each do |child_column| child_column.hide child_column.filterable(false) end field.children_fields child_columns.collect(&:name) end.any? end ================================================ FILE: lib/rails_admin/config/fields/factories/carrierwave.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| model = parent.abstract_model.model if defined?(::CarrierWave) && model.is_a?(CarrierWave::Mount) && model.uploaders.include?(attachment_name = properties.name.to_s.chomp('_file_name').to_sym) columns = [model.uploader_options[attachment_name][:mount_on] || attachment_name, :"#{attachment_name}_content_type", :"#{attachment_name}_file_size"] field = RailsAdmin::Config::Fields::Types.load( %i[serialized json].include?(properties.type) ? :multiple_carrierwave : :carrierwave, ).new(parent, attachment_name, properties) fields << field children_fields = [] columns.each do |children_column_name| child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } next unless child_properties children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide unless field == children_field children_field.filterable(false) unless field == children_field children_fields << children_field.name end field.children_fields(children_fields) true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/devise.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/password' # Register a custom field factory for devise model RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| if properties.name == :encrypted_password extensions = %i[password_salt reset_password_token remember_token] fields << RailsAdmin::Config::Fields::Types.load(:password).new(parent, :password, properties) fields << RailsAdmin::Config::Fields::Types.load(:password).new(parent, :password_confirmation, properties) extensions.each do |ext| properties = parent.abstract_model.properties.detect { |p| ext == p.name } next unless properties field = fields.detect { |f| f.name == ext } unless field RailsAdmin::Config::Fields.default_factory.call(parent, properties, fields) field = fields.last end field.hide end true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/dragonfly.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| extensions = %i[name uid] if (properties.name.to_s =~ /^(.+)_uid$/) && defined?(::Dragonfly) && parent.abstract_model.model.respond_to?(:dragonfly_attachment_classes) && parent.abstract_model.model.dragonfly_attachment_classes.collect(&:attribute).include?(attachment_name = Regexp.last_match[1].to_sym) field = RailsAdmin::Config::Fields::Types.load(:dragonfly).new(parent, attachment_name, properties) children_fields = [] extensions.each do |ext| children_column_name = :"#{attachment_name}_#{ext}" child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } next unless child_properties children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide children_field.filterable(false) children_fields << children_field.name end field.children_fields(children_fields) fields << field true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/enum.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types/enum' require 'rails_admin/config/fields/types/active_record_enum' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| model = parent.abstract_model.model method_name = "#{properties.name}_enum" # NOTICE: _method_name could be `to_enum` and this method defined in Object. if !Object.respond_to?(method_name) && \ (model.respond_to?(method_name) || \ model.method_defined?(method_name)) fields << RailsAdmin::Config::Fields::Types::Enum.new(parent, properties.name, properties) true elsif model.respond_to?(:defined_enums) && model.defined_enums[properties.name.to_s] fields << RailsAdmin::Config::Fields::Types::ActiveRecordEnum.new(parent, properties.name, properties) true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/paperclip.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| extensions = %i[file_name content_type file_size updated_at fingerprint] model = parent.abstract_model.model if (properties.name.to_s =~ /^(.+)_file_name$/) && defined?(::Paperclip) && model.try(:attachment_definitions) && model.attachment_definitions.key?(attachment_name = Regexp.last_match[1].to_sym) field = RailsAdmin::Config::Fields::Types.load(:paperclip).new(parent, attachment_name, properties) children_fields = [] extensions.each do |ext| children_column_name = :"#{attachment_name}_#{ext}" child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } next unless child_properties children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide children_field.filterable(false) children_fields << children_field.name end field.children_fields(children_fields) fields << field true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/password.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types/password' # Register a custom field factory for properties named as password. More property # names can be registered in RailsAdmin::Config::Fields::Password.column_names # array. # # @see RailsAdmin::Config::Fields::Types::Password.column_names # @see RailsAdmin::Config::Fields.register_factory RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| if [:password].include?(properties.name) fields << RailsAdmin::Config::Fields::Types::Password.new(parent, properties.name, properties) true else false end end ================================================ FILE: lib/rails_admin/config/fields/factories/shrine.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| next false unless defined?(::Shrine) attachment_names = parent.abstract_model.model.ancestors.select { |m| m.is_a?(Shrine::Attachment) }.map { |a| a.instance_variable_get('@name') } next false if attachment_names.blank? attachment_name = attachment_names.detect { |a| a == properties.name.to_s.chomp('_data').to_sym } next false unless attachment_name field = RailsAdmin::Config::Fields::Types.load(:shrine).new(parent, attachment_name, properties) fields << field data_field_name = :"#{attachment_name}_data" child_properties = parent.abstract_model.properties.detect { |p| p.name == data_field_name } next true unless child_properties children_field = fields.detect { |f| f.name == data_field_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide unless field == children_field children_field.filterable(false) unless field == children_field field.children_fields([data_field_name]) true end ================================================ FILE: lib/rails_admin/config/fields/group.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/string/inflections' require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' module RailsAdmin module Config module Fields # A container for groups of fields in edit views class Group include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable include RailsAdmin::Config::Hideable attr_reader :name, :abstract_model, :parent, :root attr_accessor :section def initialize(parent, name) @parent = parent @root = parent.root @abstract_model = parent.abstract_model @section = parent @name = name.to_s.tr(' ', '_').downcase.to_sym end # Defines a configuration for a field by proxying parent's field method # and setting the field's group as self # # @see RailsAdmin::Config::Fields.field def field(name, type = nil, &block) field = section.field(name, type, &block) # Directly manipulate the variable instead of using the accessor # as group probably is not yet registered to the parent object. field.instance_variable_set('@group', self) field end # Reader for fields attached to this group def fields section.fields.select { |f| f.group == self } end # Defines configuration for fields by their type # # @see RailsAdmin::Config::Fields.fields_of_type def fields_of_type(type, &block) selected = section.fields.select { |f| type == f.type } selected.each { |f| f.instance_eval(&block) } if block selected end # Reader for fields that are marked as visible def visible_fields section.with(bindings).visible_fields.select { |f| f.group == self } end # Should it open by default register_instance_option :active? do true end # Configurable group label which by default is group's name humanized. register_instance_option :label do (@label ||= {})[::I18n.locale] ||= (parent.fields.detect { |f| f.name == name }.try(:label) || name.to_s.humanize) end # Configurable help text register_instance_option :help do nil end end end end end ================================================ FILE: lib/rails_admin/config/fields/singular_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/association' module RailsAdmin module Config module Fields class SingularAssociation < Association register_instance_option :filter_operators do %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank]) end register_instance_option :formatted_value do (o = value) && o.send(associated_model_config.object_label_method) end register_instance_option :partial do nested_form ? :form_nested_one : :form_filtering_select end def collection(scope = nil) if associated_collection_cache_all || scope super else [[formatted_value, selected_id]] end end def multiple? false end def selected_id raise NoMethodError # abstract end def parse_input(params) return unless nested_form && params[method_name].try(:[], :id).present? ids = associated_model_config.abstract_model.parse_id(params[method_name][:id]) ids = ids.to_composite_keys.to_s if ids.respond_to?(:to_composite_keys) params[method_name][:id] = ids end def form_value form_default_value.nil? ? selected_id : form_default_value end def widget_options { xhr: !associated_collection_cache_all, remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), scopeBy: dynamic_scope_relationships, } end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/action_text.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class ActionText < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :version do '1.3.1' end register_instance_option :css_location do "https://cdnjs.cloudflare.com/ajax/libs/trix/#{version}/trix.css" end register_instance_option :js_location do "https://cdnjs.cloudflare.com/ajax/libs/trix/#{version}/trix.js" end register_instance_option :warn_dynamic_load do true end register_instance_option :partial do :form_action_text end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/active_record_enum.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/enum' module RailsAdmin module Config module Fields module Types class ActiveRecordEnum < Enum RailsAdmin::Config::Fields::Types.register(self) def type :enum end register_instance_option :enum do abstract_model.model.defined_enums[name.to_s] end register_instance_option :pretty_value do bindings[:object].send(name).presence || ' - ' end register_instance_option :multiple? do false end register_instance_option :queryable do false end def parse_value(value) return unless value.present? abstract_model.model.attribute_types[name.to_s].serialize(value) end def parse_input(params) value = params[name] return unless value params[name] = parse_input_value(value) end def form_value enum[super] || super end private def parse_input_value(value) abstract_model.model.attribute_types[name.to_s].deserialize(value) end def type_cast_value(value) abstract_model.model.column_types[name.to_s].type_cast_from_user(value) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/active_storage.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/file_upload' module RailsAdmin module Config module Fields module Types class ActiveStorage < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :thumb_method do {resize_to_limit: [100, 100]} end register_instance_option :delete_method do "remove_#{name}" if bindings[:object].respond_to?("remove_#{name}") end register_instance_option :image? do value && (value.representable? || value.content_type.match?(/^image/)) end register_instance_option :eager_load do {"#{name}_attachment": :blob} end register_instance_option :direct? do false end register_instance_option :html_attributes do { required: required? && !value.present?, }.merge( direct? && {data: {direct_upload_url: bindings[:view].main_app.rails_direct_uploads_url}} || {}, ) end register_instance_option :searchable do false end register_instance_option :sortable do false end def resource_url(thumb = false) return nil unless value if thumb && value.representable? thumb = thumb_method if thumb == true representation = value.representation(thumb) Rails.application.routes.url_helpers.rails_blob_representation_path( representation.blob.signed_id, representation.variation.key, representation.blob.filename, only_path: true ) else Rails.application.routes.url_helpers.rails_blob_path(value, only_path: true) end end def value attachment = super attachment if attachment&.attached? end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/all.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/action_text' require 'rails_admin/config/fields/types/active_record_enum' require 'rails_admin/config/fields/types/active_storage' require 'rails_admin/config/fields/types/belongs_to_association' require 'rails_admin/config/fields/types/boolean' require 'rails_admin/config/fields/types/bson_object_id' require 'rails_admin/config/fields/types/date' require 'rails_admin/config/fields/types/datetime' require 'rails_admin/config/fields/types/decimal' require 'rails_admin/config/fields/types/dragonfly' require 'rails_admin/config/fields/types/enum' require 'rails_admin/config/fields/types/file_upload' require 'rails_admin/config/fields/types/paperclip' require 'rails_admin/config/fields/types/carrierwave' require 'rails_admin/config/fields/types/multiple_file_upload' require 'rails_admin/config/fields/types/multiple_active_storage' require 'rails_admin/config/fields/types/multiple_carrierwave' require 'rails_admin/config/fields/types/float' require 'rails_admin/config/fields/types/has_and_belongs_to_many_association' require 'rails_admin/config/fields/types/has_many_association' require 'rails_admin/config/fields/types/has_one_association' require 'rails_admin/config/fields/types/integer' require 'rails_admin/config/fields/types/password' require 'rails_admin/config/fields/types/polymorphic_association' require 'rails_admin/config/fields/types/string' require 'rails_admin/config/fields/types/hidden' require 'rails_admin/config/fields/types/text' require 'rails_admin/config/fields/types/serialized' require 'rails_admin/config/fields/types/shrine' require 'rails_admin/config/fields/types/time' require 'rails_admin/config/fields/types/timestamp' require 'rails_admin/config/fields/types/color' require 'rails_admin/config/fields/types/simple_mde' require 'rails_admin/config/fields/types/ck_editor' require 'rails_admin/config/fields/types/code_mirror' require 'rails_admin/config/fields/types/wysihtml5' require 'rails_admin/config/fields/types/froala' require 'rails_admin/config/fields/types/json' require 'rails_admin/config/fields/types/inet' require 'rails_admin/config/fields/types/uuid' require 'rails_admin/config/fields/types/citext' ================================================ FILE: lib/rails_admin/config/fields/types/belongs_to_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/singular_association' module RailsAdmin module Config module Fields module Types class BelongsToAssociation < RailsAdmin::Config::Fields::SingularAssociation RailsAdmin::Config::Fields::Types.register(self) register_instance_option :sortable do @sortable ||= abstract_model.adapter_supports_joins? && associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? associated_model_config.object_label_method : {abstract_model.table_name => method_name} end register_instance_option :searchable do @searchable ||= associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? [associated_model_config.object_label_method, {abstract_model.model => method_name}] : {abstract_model.model => method_name} end register_instance_option :eager_load do true end register_instance_option :allowed_methods do nested_form ? [method_name] : Array(association.foreign_key) end def selected_id if association.foreign_key.is_a?(Array) format_key(association.foreign_key.map { |attribute| bindings[:object].safe_send(attribute) }) else bindings[:object].safe_send(association.key_accessor) end end def parse_input(params) return super if nested_form return unless params[method_name].present? && association.foreign_key.is_a?(Array) association.foreign_key.zip(RailsAdmin.config.composite_keys_serializer.deserialize(params.delete(method_name))).each do |key, value| params[key] = value end end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/boolean.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Fields module Types class Boolean < RailsAdmin::Config::Fields::Base # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :labels do { true => %(), false => %(), nil => %(), } end register_instance_option :css_classes do { true => 'success', false => 'danger', nil => 'default', } end register_instance_option :filter_operators do %w[_discard true false] + (required? ? [] : %w[_separator _present _blank]) end register_instance_option :nullable? do properties&.nullable? end register_instance_option :view_helper do :check_box end register_instance_option :pretty_value do %(#{labels[form_value]}).html_safe end register_instance_option :export_value do value.inspect end register_instance_option :partial do :form_boolean end def form_value case value when true, false value end end # Accessor for field's help text displayed below input field. def generic_help '' end def parse_input(params) params[name] = params[name].presence if params.key?(name) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/bson_object_id.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string' module RailsAdmin module Config module Fields module Types class BsonObjectId < RailsAdmin::Config::Fields::Types::String # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :label do label = ((@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name name) label = 'Id' if label == '' label end def generic_help 'BSON::ObjectId' end register_instance_option :read_only do true end register_instance_option :sort_reverse? do serial? end def parse_value(value) value.present? ? abstract_model.parse_object_id(value) : nil end def parse_input(params) params[name] = parse_value(params[name]) if params[name].is_a?(::String) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/carrierwave.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' module RailsAdmin module Config module Fields module Types class Carrierwave < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :thumb_method do @thumb_method ||= ((versions = bindings[:object].send(name).versions.keys).detect { |k| k.in?([:thumb, :thumbnail, 'thumb', 'thumbnail']) } || versions.first.to_s) end register_instance_option :delete_method do "remove_#{name}" end register_instance_option :cache_method do "#{name}_cache" end def resource_url(thumb = false) return nil unless (uploader = bindings[:object].send(name)).present? thumb.present? ? uploader.send(thumb).url : uploader.url end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/citext.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class Citext < Text RailsAdmin::Config::Fields::Types.register(:citext, self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/ck_editor.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class CKEditor < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :version do '4.11.4' end # If you want to have a different toolbar configuration for CKEditor # create your own custom config.js and override this configuration register_instance_option :config_js do nil end # Use this if you want to point to a cloud instances of CKeditor register_instance_option :location do nil end # Use this if you want to point to a cloud instances of the base CKeditor register_instance_option :base_location do "https://cdnjs.cloudflare.com/ajax/libs/ckeditor/#{version}/" end register_instance_option :partial do :form_ck_editor end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/code_mirror.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class CodeMirror < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) # Pass the theme and mode for Codemirror register_instance_option :config do { mode: 'css', theme: 'night', } end register_instance_option :version do '5.46.0' end # Pass the location of the theme and mode for Codemirror register_instance_option :assets do { mode: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/#{version}/mode/css/css.min.js", theme: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/#{version}/theme/night.min.css", } end register_instance_option :js_location do "https://cdnjs.cloudflare.com/ajax/libs/codemirror/#{version}/codemirror.min.js" end register_instance_option :css_location do "https://cdnjs.cloudflare.com/ajax/libs/codemirror/#{version}/codemirror.min.css" end register_instance_option :partial do :form_code_mirror end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/color.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string_like' module RailsAdmin module Config module Fields module Types class Color < StringLike RailsAdmin::Config::Fields::Types.register(self) register_instance_option :pretty_value do bindings[:view].content_tag :strong, (value.presence || ' - '), style: "color: #{color}" end register_instance_option :partial do :form_colorpicker end register_instance_option :view_helper do :color_field end register_instance_option :color do if value.present? if /^[0-9a-fA-F]{3,6}$/.match?(value) "##{value}" else value end else 'white' end end register_instance_option :export_value do formatted_value end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/date.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/datetime' module RailsAdmin module Config module Fields module Types class Date < RailsAdmin::Config::Fields::Types::Datetime RailsAdmin::Config::Fields::Types.register(self) def parse_value(value) ::Date.parse(value) if value.present? end register_instance_option :date_format do :long end register_instance_option :datepicker_options do { allowInput: true, altFormat: flatpickr_format, } end register_instance_option :i18n_scope do %i[date formats] end register_instance_option :html_attributes do { required: required?, size: 18, } end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/datetime.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' require 'rails_admin/support/datetime' module RailsAdmin module Config module Fields module Types class Datetime < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) def parse_value(value) ::Time.zone.parse(value) end def parse_input(params) params[name] = parse_value(params[name]) if params[name] end register_instance_option :filter_operators do %w[default between today yesterday this_week last_week] + (required? ? [] : %w[_separator _not_null _null]) end def filter_options super.merge( datetimepicker_options: datepicker_options, ) end register_instance_option :date_format do :long end register_instance_option :i18n_scope do %i[time formats] end register_instance_option :strftime_format do ::I18n.t(date_format, scope: i18n_scope, raise: true) rescue ::I18n::ArgumentError '%B %d, %Y %H:%M' end register_instance_option :flatpickr_format do RailsAdmin::Support::Datetime.to_flatpickr_format(strftime_format) end register_instance_option :datepicker_options do { allowInput: true, enableTime: true, altFormat: flatpickr_format, } end register_instance_option :html_attributes do { required: required?, size: 22, } end register_instance_option :sort_reverse? do true end register_instance_option :queryable? do false end register_instance_option :formatted_value do time = (value || default_value) if time ::I18n.l(time, format: strftime_format) else ''.html_safe end end register_instance_option :partial do :form_datetime end register_deprecated_instance_option :momentjs_format do ActiveSupport::Deprecation.warn('The momentjs_format configuration option is deprecated, please use flatpickr_format with corresponding values here: https://flatpickr.js.org/formatting/') end def form_value value&.in_time_zone&.strftime('%FT%T') || form_default_value end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/decimal.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types class Decimal < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :html_attributes do { required: required?, step: 'any', } end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/dragonfly.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' module RailsAdmin module Config module Fields module Types # Field type that supports Paperclip file uploads class Dragonfly < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :image? do if abstract_model.model.new.respond_to?("#{name}_name") mime_type = Mime::Type.lookup_by_extension(bindings[:object].send("#{name}_name").to_s.split('.').last) mime_type.to_s.match?(/^image/) else true # Dragonfly really is image oriented end end register_instance_option :delete_method do "remove_#{name}" end register_instance_option :cache_method do "retained_#{name}" end register_instance_option :thumb_method do '100x100>' end def resource_url(thumb = false) return nil unless (v = value) thumb ? v.thumb(thumb).try(:url) : v.url end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/enum.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string' module RailsAdmin module Config module Fields module Types class Enum < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) register_instance_option :filter_operators do %w[_discard] + enum.map do |label, value| {label: label, value: value || label} end + (required? ? [] : %w[_separator _present _blank]) end register_instance_option :partial do :form_enumeration end register_instance_option :enum_method do @enum_method ||= bindings[:object].class.respond_to?("#{name}_enum") || (bindings[:object] || abstract_model.model.new).respond_to?("#{name}_enum") ? "#{name}_enum" : name end register_instance_option :enum do if abstract_model.model.respond_to?(enum_method) abstract_model.model.send(enum_method) else (bindings[:object] || abstract_model.model.new).send(enum_method) end end register_instance_option :pretty_value do if enum.is_a?(::Hash) enum.select { |_k, v| v.to_s == value.to_s }.keys.first.to_s.presence || value.presence || ' - ' elsif enum.is_a?(::Array) && enum.first.is_a?(::Array) enum.detect { |e| e[1].to_s == value.to_s }.try(:first).to_s.presence || value.presence || ' - ' else value.presence || ' - ' end end register_instance_option :multiple? do properties && [:serialized].include?(properties.type) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/file_upload.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string' module RailsAdmin module Config module Fields module Types class FileUpload < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) register_instance_option :partial do :form_file_upload end register_instance_option :thumb_method do nil end register_instance_option :delete_method do nil end register_instance_option :cache_method do nil end register_instance_option :cache_value do bindings[:object].public_send(cache_method) if cache_method end register_instance_option :export_value do resource_url.to_s end register_instance_option :link_name do value end register_instance_option :pretty_value do if value.presence v = bindings[:view] url = resource_url if image thumb_url = resource_url(thumb_method) image_html = v.image_tag(thumb_url, class: 'img-thumbnail') url == thumb_url ? image_html : v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') else v.link_to(link_name, url, target: '_blank', rel: 'noopener noreferrer') end end end register_instance_option :image? do mime_type = Mime::Type.lookup_by_extension(extension) mime_type.to_s.match?(/^image/) end register_instance_option :allowed_methods do [method_name, delete_method, cache_method].compact end register_instance_option :html_attributes do { required: required? && !value.present?, } end def extension URI.parse(resource_url).path.split('.').last rescue URI::InvalidURIError nil end # virtual class def resource_url raise 'not implemented' end def virtual? true end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/float.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types class Float < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :html_attributes do { required: required?, step: 'any', } end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/froala.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class Froala < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) # See https://www.froala.com/wysiwyg-editor/docs/options register_instance_option :config_options do nil end register_instance_option :version do '2.9.5' end register_instance_option :css_location do "https://cdnjs.cloudflare.com/ajax/libs/froala-editor/#{version}/css/froala_editor.min.css" end register_instance_option :js_location do "https://cdnjs.cloudflare.com/ajax/libs/froala-editor/#{version}/js/froala_editor.min.js" end register_instance_option :partial do :form_froala end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/collection_association' module RailsAdmin module Config module Fields module Types class HasAndBelongsToManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/has_many_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/collection_association' module RailsAdmin module Config module Fields module Types class HasManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/has_one_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/singular_association' module RailsAdmin module Config module Fields module Types class HasOneAssociation < RailsAdmin::Config::Fields::SingularAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :allowed_methods do nested_form ? [method_name] : [name] end def associated_prepopulate_params {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}} end def parse_input(params) return super if nested_form id = params.delete(method_name) params[name] = associated_model_config.abstract_model.get(id) if id end def selected_id format_key(value.try(:id)).try(:to_s) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/hidden.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string_like' module RailsAdmin module Config module Fields module Types class Hidden < StringLike RailsAdmin::Config::Fields::Types.register(self) register_instance_option :view_helper do :hidden_field end register_instance_option :label do false end register_instance_option :help do false end def generic_help false end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/inet.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' module RailsAdmin module Config module Fields module Types class Inet < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/integer.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types class Integer < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :sort_reverse? do serial? end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/json.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class Json < RailsAdmin::Config::Fields::Types::Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) RailsAdmin::Config::Fields::Types.register(:jsonb, self) register_instance_option :formatted_value do value ? JSON.pretty_generate(value) : nil end register_instance_option :pretty_value do bindings[:view].content_tag(:pre) { formatted_value }.html_safe end register_instance_option :export_value do formatted_value end def parse_value(value) value.present? ? JSON.parse(value) : nil end def parse_input(params) params[name] = parse_value(params[name]) if params[name].is_a?(::String) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/multiple_active_storage.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/multiple_file_upload' module RailsAdmin module Config module Fields module Types class MultipleActiveStorage < RailsAdmin::Config::Fields::Types::MultipleFileUpload RailsAdmin::Config::Fields::Types.register(self) class ActiveStorageAttachment < RailsAdmin::Config::Fields::Types::MultipleFileUpload::AbstractAttachment register_instance_option :thumb_method do {resize_to_limit: [100, 100]} end register_instance_option :keep_value do value.signed_id end register_instance_option :delete_value do value.id end register_instance_option :image? do value && (value.representable? || value.content_type.match?(/^image/)) end def resource_url(thumb = false) return nil unless value if thumb && value.representable? representation = value.representation(thumb_method) Rails.application.routes.url_helpers.rails_blob_representation_path( representation.blob.signed_id, representation.variation.key, representation.blob.filename, only_path: true ) else Rails.application.routes.url_helpers.rails_blob_path(value, only_path: true) end end end register_instance_option :attachment_class do ActiveStorageAttachment end register_instance_option :keep_method do method_name if ::ActiveStorage.gem_version >= Gem::Version.new('7.1') || ::ActiveStorage.replace_on_assign_to_many end register_instance_option :delete_method do "remove_#{name}" if bindings[:object].respond_to?("remove_#{name}") end register_instance_option :eager_load do {"#{name}_attachments": :blob} end register_instance_option :direct? do false end register_instance_option :html_attributes do { required: required? && !value.present?, }.merge( direct? && {data: {direct_upload_url: bindings[:view].main_app.rails_direct_uploads_url}} || {}, ) end register_instance_option :searchable do false end register_instance_option :sortable do false end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/multiple_carrierwave.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/multiple_file_upload' module RailsAdmin module Config module Fields module Types class MultipleCarrierwave < RailsAdmin::Config::Fields::Types::MultipleFileUpload RailsAdmin::Config::Fields::Types.register(self) class CarrierwaveAttachment < RailsAdmin::Config::Fields::Types::MultipleFileUpload::AbstractAttachment register_instance_option :thumb_method do @thumb_method ||= ((versions = value.versions.keys).detect { |k| k.in?([:thumb, :thumbnail, 'thumb', 'thumbnail']) } || versions.first.to_s) end register_instance_option :keep_value do value.cache_name || value.identifier end register_instance_option :delete_value do value.file.filename end def resource_url(thumb = false) return nil unless value thumb.present? ? value.send(thumb).url : value.url end end register_instance_option :attachment_class do CarrierwaveAttachment end register_instance_option :cache_method do "#{name}_cache" unless ::CarrierWave::VERSION >= '2' end register_instance_option :keep_method do name if ::CarrierWave::VERSION >= '2' end register_instance_option :reorderable? do ::CarrierWave::VERSION >= '2' end register_instance_option :delete_method do "delete_#{name}" if bindings[:object].respond_to?("delete_#{name}") end def value bindings[:object].send(name) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/multiple_file_upload.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Fields module Types class MultipleFileUpload < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) class AbstractAttachment include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable attr_reader :value def initialize(value) @value = value end register_instance_option :thumb_method do nil end register_instance_option :keep_value do nil end register_instance_option :delete_value do nil end register_deprecated_instance_option :delete_key, :delete_value register_instance_option :pretty_value do if value.presence v = bindings[:view] url = resource_url if image thumb_url = resource_url(thumb_method) image_html = v.image_tag(thumb_url, class: 'img-thumbnail') url == thumb_url ? image_html : v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') else display_value = value.respond_to?(:filename) ? value.filename : value v.link_to(display_value, url, target: '_blank', rel: 'noopener noreferrer') end end end register_instance_option :image? do mime_type = Mime::Type.lookup_by_extension(extension) mime_type.to_s.match?(/^image/) end def resource_url(_thumb = false) raise 'not implemented' end def extension URI.parse(resource_url).path.split('.').last rescue URI::InvalidURIError nil end end def initialize(*args) super @attachment_configurations = [] end register_instance_option :attachment_class do AbstractAttachment end register_instance_option :partial do :form_multiple_file_upload end register_instance_option :cache_method do nil end register_instance_option :delete_method do nil end register_instance_option :keep_method do nil end register_instance_option :reorderable? do false end register_instance_option :export_value do attachments.map(&:resource_url).map(&:to_s).join(',') end register_instance_option :pretty_value do bindings[:view].safe_join attachments.map(&:pretty_value), ' ' end register_instance_option :allowed_methods do [method_name, cache_method, delete_method].compact end register_instance_option :html_attributes do { required: required? && !value.present?, } end def attachment(&block) @attachment_configurations << block end def attachments Array(value).map do |attached| attachment = attachment_class.new(attached) @attachment_configurations.each do |config| attachment.instance_eval(&config) end attachment.with(bindings) end end # virtual class def virtual? true end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/numeric.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' module RailsAdmin module Config module Fields module Types class Numeric < RailsAdmin::Config::Fields::Base # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :filter_operators do %w[default between] + (required? ? [] : %w[_separator _not_null _null]) end register_instance_option :view_helper do :number_field end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/paperclip.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' module RailsAdmin module Config module Fields module Types # Field type that supports Paperclip file uploads class Paperclip < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :delete_method do "delete_#{name}" if bindings[:object].respond_to?("delete_#{name}") end register_instance_option :thumb_method do @styles ||= bindings[:object].send(name).styles.collect(&:first) @thumb_method ||= @styles.detect { |s| [:thumb, 'thumb', :thumbnail, 'thumbnail'].include?(s) } || @styles.first || :original end def resource_url(thumb = false) value.try(:url, (thumb || :original)) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/password.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string' module RailsAdmin module Config module Fields module Types class Password < RailsAdmin::Config::Fields::Types::String # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :view_helper do :password_field end def parse_input(params) if params[name].present? params[name] = params[name] else params.delete(name) end end register_instance_option :formatted_value do ''.html_safe end # Password field's value does not need to be read def value '' end register_instance_option :visible do section.is_a?(RailsAdmin::Config::Sections::Edit) end register_instance_option :pretty_value do '*****' end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/polymorphic_association.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/belongs_to_association' module RailsAdmin module Config module Fields module Types class PolymorphicAssociation < RailsAdmin::Config::Fields::Types::BelongsToAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :partial do :form_polymorphic_association end # Accessor whether association is visible or not. By default # association checks that any of the child models are included in # configuration. register_instance_option :visible? do associated_model_config.any? end register_instance_option :formatted_value do (o = value) && o.send(RailsAdmin.config(o).object_label_method) end register_instance_option :sortable do false end register_instance_option :searchable do false end # TODO: not supported yet register_instance_option :associated_collection_cache_all do false end # TODO: not supported yet register_instance_option :associated_collection_scope do nil end register_instance_option :allowed_methods do [children_fields] end register_instance_option :eager_load do false end def associated_model_config @associated_model_config ||= association.klass.collect { |type| RailsAdmin.config(type) }.reject(&:excluded?) end def collection(_scope = nil) if value [[formatted_value, selected_id]] else [[]] end end def type_column association.foreign_type.to_s end def type_collection associated_model_config.collect do |config| [config.label, config.abstract_model.model.name] end end def type_urls types = associated_model_config.collect do |config| [config.abstract_model.model.name, config.abstract_model.to_param] end ::Hash[*types.collect { |v| [v[0], bindings[:view].index_path(v[1])] }.flatten] end # Reader for field's value def value bindings[:object].send(association.name) end def widget_options_for_types type_collection.inject({}) do |options, model| options.merge( model.second.downcase.gsub('::', '-') => { xhr: true, remote_source: bindings[:view].index_path(model.second.underscore, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, current_action: bindings[:view].current_action, compact: true), float_left: false, }, ) end end def widget_options widget_options_for_types[selected_type.try(:downcase)] || {float_left: false} end def selected_type bindings[:object].send(type_column) end def parse_input(params) if (type_value = params[association.foreign_type.to_sym]).present? config = associated_model_config.find { |c| type_value == c.abstract_model.model.name } params[association.foreign_type.to_sym] = config.abstract_model.base_class.name if config end end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/serialized.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class Serialized < RailsAdmin::Config::Fields::Types::Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :formatted_value do RailsAdmin.yaml_dump(value) unless value.nil? end def parse_value(value) value.present? ? (RailsAdmin.yaml_load(value) || nil) : nil end def parse_input(params) params[name] = parse_value(params[name]) if params[name].is_a?(::String) end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/shrine.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/file_upload' module RailsAdmin module Config module Fields module Types class Shrine < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :thumb_method do unless defined? @thumb_method @thumb_method = begin next nil unless bindings[:object].respond_to?("#{name}_derivatives") derivatives = bindings[:object].public_send("#{name}_derivatives") if derivatives.key?(:thumb) :thumb elsif derivatives.key?(:thumbnail) :thumbnail else derivatives.keys.first end end end @thumb_method end register_instance_option :delete_method do "remove_#{name}" if bindings[:object].respond_to?("remove_#{name}") end register_instance_option :cache_method do name if bindings[:object].try("cached_#{name}_data") end register_instance_option :cache_value do bindings[:object].try("cached_#{name}_data") end register_instance_option :link_name do value.original_filename end def resource_url(thumb = nil) return nil unless value thumb && bindings[:object].public_send(:"#{name}", thumb).try(:url) || value.url end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/simple_mde.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class SimpleMDE < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) # If you want to have a different SimpleMDE config for each instance # you can override this option with these values: https://github.com/sparksuite/simplemde-markdown-editor#configuration register_instance_option :instance_config do nil end register_instance_option :version do '1.11.2' end register_instance_option :js_location do "https://cdnjs.cloudflare.com/ajax/libs/simplemde/#{version}/simplemde.min.js" end register_instance_option :css_location do "https://cdnjs.cloudflare.com/ajax/libs/simplemde/#{version}/simplemde.min.css" end register_instance_option :partial do :form_simple_mde end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/string.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string_like' module RailsAdmin module Config module Fields module Types class String < StringLike RailsAdmin::Config::Fields::Types.register(self) def input_size [50, length.to_i].reject(&:zero?).min end register_instance_option :html_attributes do { required: required?, maxlength: length, size: input_size, } end def generic_help text = "#{required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')}. " if valid_length.present? && valid_length[:is].present? text += "#{I18n.translate('admin.form.char_length_of').capitalize} #{valid_length[:is]}." else max_length = [length, valid_length[:maximum] || nil].compact.min min_length = [0, valid_length[:minimum] || nil].compact.max if max_length text += if min_length == 0 "#{I18n.translate('admin.form.char_length_up_to').capitalize} #{max_length}." else "#{I18n.translate('admin.form.char_length_of').capitalize} #{min_length}-#{max_length}." end end end text end register_instance_option :partial do :form_field end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/string_like.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/base' module RailsAdmin module Config module Fields module Types class StringLike < RailsAdmin::Config::Fields::Base register_instance_option :filter_operators do %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank]) end register_instance_option :treat_empty_as_nil? do properties.try(:nullable?) end def parse_input(params) params[name] = params[name].presence if params.key?(name) && treat_empty_as_nil? end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/text.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string_like' module RailsAdmin module Config module Fields module Types class Text < StringLike # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) register_instance_option :html_attributes do { required: required?, cols: '48', rows: '3', } end register_instance_option :partial do :form_text end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/time.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/datetime' module RailsAdmin module Config module Fields module Types class Time < RailsAdmin::Config::Fields::Types::Datetime RailsAdmin::Config::Fields::Types.register(self) def parse_value(value) abstract_model.model.type_for_attribute(name.to_s).serialize(super)&.change(year: 2000, month: 1, day: 1) end register_instance_option :filter_operators do %w[default between] + (required? ? [] : %w[_separator _not_null _null]) end register_instance_option :datepicker_options do { allowInput: true, altFormat: flatpickr_format, enableTime: true, noCalendar: true, } end register_instance_option :strftime_format do '%H:%M' end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/timestamp.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/datetime' module RailsAdmin module Config module Fields module Types class Timestamp < RailsAdmin::Config::Fields::Types::Datetime # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/uuid.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/string' module RailsAdmin module Config module Fields module Types class Uuid < RailsAdmin::Config::Fields::Types::String RailsAdmin::Config::Fields::Types.register(self) end end end end end ================================================ FILE: lib/rails_admin/config/fields/types/wysihtml5.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/types/text' module RailsAdmin module Config module Fields module Types class Wysihtml5 < Text # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) # If you want to have a different toolbar configuration for wysihtml5 # you can use a Ruby hash to configure these options: # https://github.com/bootstrap-wysiwyg/bootstrap3-wysiwyg register_instance_option :config_options do { toolbar: { fa: true, }, } end register_instance_option :version do '0.3.3' end register_instance_option :css_location do "https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-wysiwyg/#{version}/bootstrap3-wysihtml5.min.css" end register_instance_option :js_location do "https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-wysiwyg/#{version}/bootstrap3-wysihtml5.all.min.js" end register_instance_option :partial do :form_wysihtml5 end end end end end end ================================================ FILE: lib/rails_admin/config/fields/types.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/string/inflections' require 'rails_admin/config/fields' require 'rails_admin/config/fields/association' module RailsAdmin module Config module Fields module Types @@registry = {} def self.load(type) @@registry.fetch(type.to_sym) { raise "Unsupported field datatype: #{type}" } end def self.register(type, klass = nil) if klass.nil? && type.is_a?(Class) klass = type type = klass.name.to_s.demodulize.underscore end @@registry[type.to_sym] = klass end require 'rails_admin/config/fields/types/all' end end end end ================================================ FILE: lib/rails_admin/config/fields.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Fields # Default field factory loads fields based on their property type or # association type. # # @see RailsAdmin::Config::Fields.registry mattr_reader :default_factory @@default_factory = proc do |parent, properties, fields| # If it's an association if properties.association? association = parent.abstract_model.associations.detect { |a| a.name.to_s == properties.name.to_s } field = RailsAdmin::Config::Fields::Types.load("#{association.polymorphic? ? :polymorphic : properties.type}_association").new(parent, properties.name, association) else field = RailsAdmin::Config::Fields::Types.load(properties.type).new(parent, properties.name, properties) end fields << field field end # Registry of field factories. # # Field factory is an anonymous function that receives the parent object, # an array of field properties and an array of fields already instantiated. # # If the factory returns true then that property will not be run through # the rest of the registered factories. If it returns false then the # arguments will be passed to the next factory. # # By default a basic factory is registered which loads fields by their # database column type. Also a password factory is registered which # loads fields if their name is password. Third default factory is a # devise specific factory which loads fields for devise user models. # # @see RailsAdmin::Config::Fields.register_factory # @see rails_admin/config/fields/factories/password.rb # @see rails_admin/config/fields/factories/devise.rb @@registry = [@@default_factory] # Build an array of fields by the provided parent object's abstract_model's # property and association information. Each property and association is # passed to the registered field factories which will populate the fields # array that will be returned. # # @see RailsAdmin::Config::Fields.registry def self.factory(parent) fields = [] # Load fields for all properties (columns) parent.abstract_model.properties.each do |properties| # Unless a previous factory has already loaded current field as well next if fields.detect { |f| f.name == properties.name } # Loop through factories until one returns true @@registry.detect { |factory| factory.call(parent, properties, fields) } end # Load fields for all associations (relations) parent.abstract_model.associations.reject { |a| a.type == :belongs_to }.each do |association| # :belongs_to are created by factory for belongs_to fields # Unless a previous factory has already loaded current field as well next if fields.detect { |f| f.name == association.name } # Loop through factories until one returns true @@registry.detect { |factory| factory.call(parent, association, fields) } end fields end # Register a field factory to be included in the factory stack. # # Factories are invoked lifo (last in first out). # # @see RailsAdmin::Config::Fields.registry def self.register_factory(&block) @@registry.unshift(block) end end end end require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/factories/password' require 'rails_admin/config/fields/factories/enum' require 'rails_admin/config/fields/factories/devise' require 'rails_admin/config/fields/factories/paperclip' require 'rails_admin/config/fields/factories/dragonfly' require 'rails_admin/config/fields/factories/carrierwave' require 'rails_admin/config/fields/factories/active_storage' require 'rails_admin/config/fields/factories/shrine' require 'rails_admin/config/fields/factories/action_text' require 'rails_admin/config/fields/factories/association' ================================================ FILE: lib/rails_admin/config/groupable.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/group' module RailsAdmin module Config module Groupable # Register a group instance variable and accessor methods for objects # extending the has groups mixin. The extended objects must implement # reader for a parent object which includes this module. # # @see RailsAdmin::Config::HasGroups.group # @see RailsAdmin::Config::Fields::Group def group(name = nil) @group = parent.group(name) unless name.nil? # setter @group ||= parent.group(:default) # getter end end end end ================================================ FILE: lib/rails_admin/config/has_description.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config # Provides accessor and autoregistering of model's description. module HasDescription attr_reader :description def desc(description, &_block) @description ||= description end end end end ================================================ FILE: lib/rails_admin/config/has_fields.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config # Provides accessors and autoregistering of model's fields. module HasFields # Defines a configuration for a field. def field(name, type = nil, add_to_section = true, &block) field = _fields.detect { |f| name == f.name } # some fields are hidden by default (belongs_to keys, has_many associations in list views.) # unhide them if config specifically defines them field.show if field && !field.instance_variable_get("@#{field.name}_registered").is_a?(Proc) # Specify field as virtual if type is not specifically set and field was not # found in default stack if field.nil? && type.nil? field = (_fields << RailsAdmin::Config::Fields::Types.load(:string).new(self, name, nil)).last # Register a custom field type if one is provided and it is different from # one found in default stack elsif type && type != (field.nil? ? nil : field.type) if field properties = field.properties field = _fields[_fields.index(field)] = RailsAdmin::Config::Fields::Types.load(type).new(self, name, properties) else properties = abstract_model.properties.detect { |p| name == p.name } field = (_fields << RailsAdmin::Config::Fields::Types.load(type).new(self, name, properties)).last end end # If field has not been yet defined add some default properties if add_to_section && !field.defined field.defined = true field.order = _fields.count(&:defined) end # If a block has been given evaluate it and sort fields after that field.instance_eval(&block) if block field end # configure field(s) from the default group in a section without changing the original order. def configure(name, type = nil, &block) [*name].each { |field_name| field(field_name, type, false, &block) } end # include fields by name and apply an optional block to each (through a call to fields), # or include fields by conditions if no field names def include_fields(*field_names, &block) if field_names.empty? _fields.select { |f| f.instance_eval(&block) }.each do |f| next if f.defined f.defined = true f.order = _fields.count(&:defined) end else fields(*field_names, &block) end end # exclude fields by name or by condition (block) def exclude_fields(*field_names, &block) block ||= proc { |f| field_names.include?(f.name) } _fields.each { |f| f.defined = true } if _fields.select(&:defined).empty? _fields.select { |f| f.instance_eval(&block) }.each { |f| f.defined = false } end # API candy alias_method :exclude_fields_if, :exclude_fields alias_method :include_fields_if, :include_fields def include_all_fields include_fields_if { true } end # Returns all field configurations for the model configuration instance. If no fields # have been defined returns all fields. Defined fields are sorted to match their # order property. If order was not specified it will match the order in which fields # were defined. # # If a block is passed it will be evaluated in the context of each field def fields(*field_names, &block) return all_fields if field_names.empty? && !block if field_names.empty? defined = _fields.select(&:defined) defined = _fields if defined.empty? else defined = field_names.collect { |field_name| _fields.detect { |f| f.name == field_name } } end defined.collect do |f| unless f.defined f.defined = true f.order = _fields.count(&:defined) end f.instance_eval(&block) if block f end end # Defines configuration for fields by their type. def fields_of_type(type, &block) _fields.select { |f| type == f.type }.map! { |f| f.instance_eval(&block) } if block end # Accessor for all fields def all_fields ((ro_fields = _fields(true)).select(&:defined).presence || ro_fields).collect do |f| f.section = self f end end # Get all fields defined as visible, in the correct order. def visible_fields i = 0 all_fields.collect { |f| f.with(bindings) }.select(&:visible?).sort_by { |f| [f.order, i += 1] } # stable sort, damn end def possible_fields _fields(true) end protected # Raw fields. # Recursively returns parent section's raw fields # Duping it if accessed for modification. def _fields(readonly = false) return @_fields if @_fields return @_ro_fields if readonly && @_ro_fields if instance_of?(RailsAdmin::Config::Sections::Base) @_ro_fields = @_fields = RailsAdmin::Config::Fields.factory(self) else # parent is RailsAdmin::Config::Model, recursion is on Section's classes @_ro_fields ||= parent.send(self.class.superclass.to_s.underscore.split('/').last)._fields(true).clone.freeze end readonly ? @_ro_fields : (@_fields ||= @_ro_fields.collect(&:clone)) end end end end ================================================ FILE: lib/rails_admin/config/has_groups.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/fields/group' module RailsAdmin module Config module HasGroups # Accessor for a group # # If group with given name does not yet exist it will be created. If a # block is passed it will be evaluated in the context of the group def group(name, &block) group = parent.groups.detect { |g| name == g.name } group ||= (parent.groups << RailsAdmin::Config::Fields::Group.new(self, name)).last group.tap { |g| g.section = self }.instance_eval(&block) if block group end # Reader for groups that are marked as visible def visible_groups parent.groups.collect { |f| f.section = self; f.with(bindings) }.select(&:visible?).select do |g| # rubocop:disable Style/Semicolon g.visible_fields.present? end end end end end ================================================ FILE: lib/rails_admin/config/hideable.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config # Defines a visibility configuration module Hideable # Visibility defaults to true. def self.included(klass) klass.register_instance_option :visible? do !root.try :excluded? end end # Reader whether object is hidden. def hidden? !visible end # Writer to hide object. def hide(&block) visible block ? proc { instance_eval(&block) == false } : false end # Writer to show field. def show(&block) visible block || true end end end end ================================================ FILE: lib/rails_admin/config/inspectable.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Inspectable def inspect set_named_instance_variables instance_name = try(:name) || try(:abstract_model).try(:model).try(:name) instance_name = instance_name ? "[#{instance_name}]" : '' instance_vars = instance_variables.collect do |v| instance_variable_name(v) end.join(', ') "#<#{self.class.name}#{instance_name} #{instance_vars}>" end private def instance_variable_name(variable) value = instance_variable_get(variable) if self.class::NAMED_INSTANCE_VARIABLES.include?(variable) if value.respond_to?(:name) "#{variable}=#{value.name.inspect}" else "#{variable}=#{value.class.name}" end else "#{variable}=#{value.inspect}" end end def set_named_instance_variables self.class.const_set('NAMED_INSTANCE_VARIABLES', []) unless defined?(self.class::NAMED_INSTANCE_VARIABLES) end end end end ================================================ FILE: lib/rails_admin/config/lazy_model.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/model' module RailsAdmin module Config class LazyModel < BasicObject def initialize(entity, &block) @entity = entity @deferred_blocks = [*block] @initialized = false end def add_deferred_block(&block) if @initialized @model.instance_eval(&block) else @deferred_blocks << block end end def target @model ||= ::RailsAdmin::Config::Model.new(@entity) # When evaluating multiple configuration blocks, the order of # execution is important. As one would expect (in my opinion), # options defined within a resource should take precedence over # more general options defined in an initializer. This way, # general settings for a number of resources could be specified # in the initializer, while models could override these settings # later, if required. # # CAVEAT: It cannot be guaranteed that blocks defined in an initializer # will be loaded (and adde to @deferred_blocks) first. For instance, if # the initializer references a model class before defining # a RailsAdmin configuration block, the configuration from the # resource will get added to @deferred_blocks first: # # # app/models/some_model.rb # class SomeModel # rails_admin do # : # end # end # # # config/initializers/rails_admin.rb # model = 'SomeModel'.constantize # blocks from SomeModel get loaded # model.config model do # blocks from initializer gets loaded # : # end # # Thus, sort all blocks to execute for a resource by Proc.source_path, # to guarantee that blocks from 'config/initializers' evaluate before # blocks defined within a model class. unless @deferred_blocks.empty? @deferred_blocks. partition { |block| block.source_location.first =~ %r{config/initializers} }. flatten. each { |block| @model.instance_eval(&block) } @deferred_blocks = [] end @initialized = true @model end def method_missing(method_name, *args, &block) target.send(method_name, *args, &block) end def respond_to_missing?(method_name, include_private = false) super || target.respond_to?(method_name, include_private) end end end end ================================================ FILE: lib/rails_admin/config/model.rb ================================================ # frozen_string_literal: true require 'rails_admin/config' require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' require 'rails_admin/config/has_groups' require 'rails_admin/config/fields/group' require 'rails_admin/config/fields' require 'rails_admin/config/has_fields' require 'rails_admin/config/has_description' require 'rails_admin/config/sections' require 'rails_admin/config/actions' require 'rails_admin/config/inspectable' module RailsAdmin module Config # Model specific configuration object. class Model include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable include RailsAdmin::Config::Hideable include RailsAdmin::Config::Sections include RailsAdmin::Config::Inspectable attr_reader :abstract_model, :parent, :root attr_accessor :groups NAMED_INSTANCE_VARIABLES = %i[@parent @root].freeze def initialize(entity) @parent = nil @root = self @abstract_model = case entity when RailsAdmin::AbstractModel entity when Class, String RailsAdmin::AbstractModel.new(entity) when Symbol RailsAdmin::AbstractModel.new(entity.to_s) else RailsAdmin::AbstractModel.new(entity.class) end @groups = [RailsAdmin::Config::Fields::Group.new(self, :default).tap { |g| g.label { I18n.translate('admin.form.basic_info') } }] end def excluded? return @excluded if defined?(@excluded) @excluded = !RailsAdmin::AbstractModel.all.collect(&:model_name).include?(abstract_model.try(:model_name)) end def object_label bindings[:object].send(object_label_method).presence || bindings[:object].send(:rails_admin_default_object_label_method) end # The display for a model instance (i.e. a single database record). # Unless configured in a model config block, it'll try to use :name followed by :title methods, then # any methods that may have been added to the label_methods array via Configuration. # Failing all of these, it'll return the class name followed by the model's id. register_instance_option :object_label_method do @object_label_method ||= Config.label_methods.detect { |method| (@dummy_object ||= abstract_model.model.new).respond_to? method } || :rails_admin_default_object_label_method end register_instance_option :label do (@label ||= {})[::I18n.locale] ||= abstract_model.model.model_name.human end register_instance_option :label_plural do (@label_plural ||= {})[::I18n.locale] ||= abstract_model.model.model_name.human(count: Float::INFINITY, default: label.pluralize(::I18n.locale)) end def pluralize(count) count == 1 ? label : label_plural end register_instance_option :weight do 0 end # parent node in navigation/breadcrumb register_instance_option :parent do @parent_model ||= begin klass = abstract_model.model.superclass klass = nil if klass.to_s.in?(%w[Object BasicObject ActiveRecord::Base]) klass end end register_instance_option :navigation_label do @navigation_label ||= if (parent_module = abstract_model.model.try(:module_parent) || abstract_model.model.try!(:parent)) != Object parent_module.to_s end end register_instance_option :navigation_icon do nil end register_instance_option :scope do abstract_model.scoped end register_instance_option :last_created_at do abstract_model.model.last.try(:created_at) if abstract_model.properties.detect { |c| c.name == :created_at } end # Act as a proxy for the base section configuration that actually # store the configurations. def method_missing(method_name, *args, &block) send(:base).send(method_name, *args, &block) end end end end ================================================ FILE: lib/rails_admin/config/proxyable/proxy.rb ================================================ # frozen_string_literal: true module RailsAdmin module Config module Proxyable class Proxy < BasicObject def initialize(object, bindings = {}) @object = object @bindings = bindings end # Bind variables to be used by the configuration options def bind(key, value = nil) if key.is_a?(::Hash) @bindings = key else @bindings[key] = value end self end def method_missing(method_name, *args, &block) if @object.respond_to?(method_name) reset = @object.bindings begin @object.bindings = @bindings response = @object.__send__(method_name, *args, &block) ensure @object.bindings = reset end response else super(method_name, *args, &block) end end end end end end ================================================ FILE: lib/rails_admin/config/proxyable.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/proxyable/proxy' module RailsAdmin module Config module Proxyable def bindings Thread.current[:rails_admin_bindings] ||= {} Thread.current[:rails_admin_bindings][self] end def bindings=(new_bindings) Thread.current[:rails_admin_bindings] ||= {} if new_bindings.nil? Thread.current[:rails_admin_bindings].delete(self) else Thread.current[:rails_admin_bindings][self] = new_bindings end end def with(bindings = {}) RailsAdmin::Config::Proxyable::Proxy.new(self, bindings) end end end end ================================================ FILE: lib/rails_admin/config/sections/base.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/inspectable' require 'rails_admin/config/has_fields' require 'rails_admin/config/has_groups' require 'rails_admin/config/has_description' module RailsAdmin module Config module Sections # Configuration of the show view for a new object class Base include RailsAdmin::Config::Proxyable include RailsAdmin::Config::Configurable include RailsAdmin::Config::Inspectable include RailsAdmin::Config::HasFields include RailsAdmin::Config::HasGroups include RailsAdmin::Config::HasDescription attr_reader :abstract_model, :parent, :root NAMED_INSTANCE_VARIABLES = %i[@parent @root @abstract_model].freeze def initialize(parent) @parent = parent @root = parent.root @abstract_model = root.abstract_model end end end end end ================================================ FILE: lib/rails_admin/config/sections/create.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/edit' module RailsAdmin module Config module Sections # Configuration of the edit view for a new object class Create < RailsAdmin::Config::Sections::Edit end end end end ================================================ FILE: lib/rails_admin/config/sections/edit.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/base' module RailsAdmin module Config module Sections # Configuration of the edit view for an existing object class Edit < RailsAdmin::Config::Sections::Base end end end end ================================================ FILE: lib/rails_admin/config/sections/export.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/base' module RailsAdmin module Config module Sections # Configuration of the navigation view class Export < RailsAdmin::Config::Sections::Base end end end end ================================================ FILE: lib/rails_admin/config/sections/list.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/base' module RailsAdmin module Config module Sections # Configuration of the list view class List < RailsAdmin::Config::Sections::Base register_instance_option :checkboxes? do true end register_instance_option :filters do [] end # Number of items listed per page register_instance_option :items_per_page do RailsAdmin::Config.default_items_per_page end # Positive value shows only prev, next links in pagination. # This is for avoiding count(*) query. register_instance_option :limited_pagination do false end register_instance_option :search_by do nil end register_instance_option :search_help do nil end register_instance_option :sort_by do parent.abstract_model.primary_key end register_instance_option :scopes do [] end register_instance_option :row_css_class do '' end register_deprecated_instance_option :sidescroll do ActiveSupport::Deprecation.warn('The sidescroll configuration option was removed, it is always enabled now.') end def fields_for_table visible_fields.partition(&:sticky?).flatten end register_deprecated_instance_option :sort_reverse do ActiveSupport::Deprecation.warn('The sort_reverse configuration option is deprecated and has no effect.') end end end end end ================================================ FILE: lib/rails_admin/config/sections/modal.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/edit' module RailsAdmin module Config module Sections class Modal < RailsAdmin::Config::Sections::Edit end end end end ================================================ FILE: lib/rails_admin/config/sections/nested.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/edit' module RailsAdmin module Config module Sections class Nested < RailsAdmin::Config::Sections::Edit end end end end ================================================ FILE: lib/rails_admin/config/sections/show.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/base' module RailsAdmin module Config module Sections class Show < RailsAdmin::Config::Sections::Base end end end end ================================================ FILE: lib/rails_admin/config/sections/update.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/sections/edit' module RailsAdmin module Config module Sections class Update < RailsAdmin::Config::Sections::Edit end end end end ================================================ FILE: lib/rails_admin/config/sections.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/string/inflections' require 'rails_admin/config/sections/base' require 'rails_admin/config/sections/edit' require 'rails_admin/config/sections/update' require 'rails_admin/config/sections/create' require 'rails_admin/config/sections/nested' require 'rails_admin/config/sections/modal' require 'rails_admin/config/sections/list' require 'rails_admin/config/sections/export' require 'rails_admin/config/sections/show' module RailsAdmin module Config # Sections describe different views in the RailsAdmin engine. Configurable sections are # list and navigation. # # Each section's class object can store generic configuration about that section (such as the # number of visible tabs in the main navigation), while the instances (accessed via model # configuration objects) store model specific configuration (such as the visibility of the # model). module Sections def self.included(klass) # Register accessors for all the sections in this namespace constants.each do |name| section = RailsAdmin::Config::Sections.const_get(name) name = name.to_s.underscore.to_sym klass.send(:define_method, name) do |&block| @sections ||= {} @sections[name] = section.new(self) unless @sections[name] @sections[name].instance_eval(&block) if block @sections[name] end end end end end end ================================================ FILE: lib/rails_admin/config.rb ================================================ # frozen_string_literal: true require 'rails_admin/config/lazy_model' require 'rails_admin/config/sections/list' require 'rails_admin/support/composite_keys_serializer' require 'active_support/core_ext/module/attribute_accessors' module RailsAdmin module Config # RailsAdmin is setup to try and authenticate with warden # If warden is found, then it will try to authenticate # # This is valid for custom warden setups, and also devise # If you're using the admin setup for devise, you should set RailsAdmin to use the admin # # @see RailsAdmin::Config.authenticate_with # @see RailsAdmin::Config.authorize_with DEFAULT_AUTHENTICATION = proc {} DEFAULT_AUTHORIZE = proc {} DEFAULT_AUDIT = proc {} DEFAULT_CURRENT_USER = proc {} class << self # Application title, can be an array of two elements attr_accessor :main_app_name # Configuration option to specify which models you want to exclude. attr_accessor :excluded_models # Configuration option to specify a allowlist of models you want to RailsAdmin to work with. # The excluded_models list applies against the allowlist as well and further reduces the models # RailsAdmin will use. # If included_models is left empty ([]), then RailsAdmin will automatically use all the models # in your application (less any excluded_models you may have specified). attr_accessor :included_models # Fields to be hidden in show, create and update views attr_reader :default_hidden_fields # Default items per page value used if a model level option has not # been configured attr_accessor :default_items_per_page # Default association limit attr_accessor :default_associated_collection_limit attr_reader :default_search_operator # Configuration option to specify which method names will be searched for # to be used as a label for object records. This defaults to [:name, :title] attr_accessor :label_methods # hide blank fields in show view if true attr_accessor :compact_show_view # Tell browsers whether to use the native HTML5 validations (novalidate form option). attr_accessor :browser_validations # set parent controller attr_reader :parent_controller # set settings for `protect_from_forgery` method # By default, it raises exception upon invalid CSRF tokens attr_accessor :forgery_protection_settings # Stores model configuration objects in a hash identified by model's class # name. # # @see RailsAdmin.config attr_reader :registry # Bootstrap CSS classes used for Navigation bar attr_accessor :navbar_css_classes # show Gravatar in Navigation bar attr_accessor :show_gravatar # accepts a hash of static links to be shown below the main navigation attr_accessor :navigation_static_links attr_accessor :navigation_static_label # Set where RailsAdmin fetches JS/CSS from, defaults to :sprockets attr_writer :asset_source # For customization of composite keys representation attr_accessor :composite_keys_serializer # Setup authentication to be run as a before filter # This is run inside the controller instance so you can setup any authentication you need to # # By default, the authentication will run via warden if available # and will run the default. # # If you use devise, this will authenticate the same as _authenticate_user!_ # # @example Devise admin # RailsAdmin.config do |config| # config.authenticate_with do # authenticate_admin! # end # end # # @example Custom Warden # RailsAdmin.config do |config| # config.authenticate_with do # warden.authenticate! scope: :paranoid # end # end # # @see RailsAdmin::Config::DEFAULT_AUTHENTICATION def authenticate_with(&blk) @authenticate = blk if blk @authenticate || DEFAULT_AUTHENTICATION end # Setup auditing/versioning provider that observe objects lifecycle def audit_with(*args, &block) extension = args.shift if extension klass = RailsAdmin::AUDITING_ADAPTERS[extension] klass.setup if klass.respond_to? :setup @audit = proc do @auditing_adapter = klass.new(*([self] + args).compact, &block) end elsif block @audit = block end @audit || DEFAULT_AUDIT end # Setup authorization to be run as a before filter # This is run inside the controller instance so you can setup any authorization you need to. # # By default, there is no authorization. # # @example Custom # RailsAdmin.config do |config| # config.authorize_with do # redirect_to root_path unless warden.user.is_admin? # end # end # # To use an authorization adapter, pass the name of the adapter. For example, # to use with CanCanCan[https://github.com/CanCanCommunity/cancancan/], pass it like this. # # @example CanCanCan # RailsAdmin.config do |config| # config.authorize_with :cancancan # end # # See the wiki[https://github.com/railsadminteam/rails_admin/wiki] for more on authorization. # # @see RailsAdmin::Config::DEFAULT_AUTHORIZE def authorize_with(*args, &block) extension = args.shift if extension klass = RailsAdmin::AUTHORIZATION_ADAPTERS[extension] klass.setup if klass.respond_to? :setup @authorize = proc do @authorization_adapter = klass.new(*([self] + args).compact, &block) end elsif block @authorize = block end @authorize || DEFAULT_AUTHORIZE end # Setup configuration using an extension-provided ConfigurationAdapter # # @example Custom configuration for role-based setup. # RailsAdmin.config do |config| # config.configure_with(:custom) do |config| # config.models = ['User', 'Comment'] # config.roles = { # 'Admin' => :all, # 'User' => ['User'] # } # end # end def configure_with(extension) configuration = RailsAdmin::CONFIGURATION_ADAPTERS[extension].new yield(configuration) if block_given? end # Setup a different method to determine the current user or admin logged in. # This is run inside the controller instance and made available as a helper. # # By default, _request.env["warden"].user_ or _current_user_ will be used. # # @example Custom # RailsAdmin.config do |config| # config.current_user_method do # current_admin # end # end # # @see RailsAdmin::Config::DEFAULT_CURRENT_USER def current_user_method(&block) @current_user = block if block @current_user || DEFAULT_CURRENT_USER end def default_search_operator=(operator) if %w[default like not_like starts_with ends_with is =].include? operator @default_search_operator = operator else raise ArgumentError.new("Search operator '#{operator}' not supported") end end # pool of all found model names from the whole application def models_pool (viable_models - excluded_models.collect(&:to_s)).uniq.sort end # Loads a model configuration instance from the registry or registers # a new one if one is yet to be added. # # First argument can be an instance of requested model, its class object, # its class name as a string or symbol or a RailsAdmin::AbstractModel # instance. # # If a block is given it is evaluated in the context of configuration instance. # # Returns given model's configuration # # @see RailsAdmin::Config.registry def model(entity, &block) key = case entity when RailsAdmin::AbstractModel entity.model.try(:name).try :to_sym when Class, ConstLoadSuppressor::ConstProxy entity.name.to_sym when String, Symbol entity.to_sym else entity.class.name.to_sym end @registry[key] ||= RailsAdmin::Config::LazyModel.new(key.to_s) @registry[key].add_deferred_block(&block) if block @registry[key] end def asset_source @asset_source ||= begin detected = defined?(Sprockets) ? :sprockets : :invalid unless ARGV.join(' ').include? 'rails_admin:install' warn <<~MSG [Warning] After upgrading RailsAdmin to 3.x you haven't set asset_source yet, using :#{detected} as the default. To suppress this message, run 'rails rails_admin:install' to setup the asset delivery method suitable to you. MSG end detected end end def default_hidden_fields=(fields) if fields.is_a?(Array) @default_hidden_fields = {} @default_hidden_fields[:edit] = fields @default_hidden_fields[:show] = fields else @default_hidden_fields = fields end end def parent_controller=(name) @parent_controller = name if defined?(RailsAdmin::ApplicationController) || defined?(RailsAdmin::MainController) RailsAdmin::Config::ConstLoadSuppressor.allowing do RailsAdmin.send(:remove_const, :ApplicationController) RailsAdmin.send(:remove_const, :MainController) load RailsAdmin::Engine.root.join('app/controllers/rails_admin/application_controller.rb') load RailsAdmin::Engine.root.join('app/controllers/rails_admin/main_controller.rb') end end end def total_columns_width=(_) ActiveSupport::Deprecation.warn('The total_columns_width configuration option is deprecated and has no effect.') end def sidescroll=(_) ActiveSupport::Deprecation.warn('The sidescroll configuration option was removed, it is always enabled now.') end # Setup actions to be used. def actions(&block) return unless block RailsAdmin::Config::Actions.reset RailsAdmin::Config::Actions.instance_eval(&block) end # Returns all model configurations # # @see RailsAdmin::Config.registry def models RailsAdmin::AbstractModel.all.collect { |m| model(m) } end # Reset all configurations to defaults. # # @see RailsAdmin::Config.registry def reset @compact_show_view = true @browser_validations = true @authenticate = nil @authorize = nil @audit = nil @current_user = nil @default_hidden_fields = {} @default_hidden_fields[:base] = [:_type] @default_hidden_fields[:edit] = %i[id _id created_at created_on deleted_at updated_at updated_on deleted_on] @default_hidden_fields[:show] = %i[id _id created_at created_on deleted_at updated_at updated_on deleted_on] @default_items_per_page = 20 @default_associated_collection_limit = 100 @default_search_operator = 'default' @excluded_models = [] @included_models = [] @label_methods = %i[name title] @main_app_name = proc { [Rails.application.engine_name.titleize.chomp(' Application'), 'Admin'] } @registry = {} @navbar_css_classes = %w[navbar-dark bg-primary border-bottom] @show_gravatar = true @navigation_static_links = {} @navigation_static_label = nil @asset_source = nil @composite_keys_serializer = RailsAdmin::Support::CompositeKeysSerializer @parent_controller = '::ActionController::Base' @forgery_protection_settings = {with: :exception} RailsAdmin::Config::Actions.reset RailsAdmin::AbstractModel.reset end # Reset a provided model's configuration. # # @see RailsAdmin::Config.registry def reset_model(model) key = model.is_a?(Class) ? model.name.to_sym : model.to_sym @registry.delete(key) end # Perform reset, then load RailsAdmin initializer again def reload! reset load RailsAdmin::Engine.config.initializer_path end # Get all models that are configured as visible sorted by their weight and label. # # @see RailsAdmin::Config::Hideable def visible_models(bindings) visible_models_with_bindings(bindings).sort do |a, b| if (weight_order = a.weight <=> b.weight) == 0 a.label.casecmp(b.label) else weight_order end end end private def viable_models included_models.collect(&:to_s).presence || begin @@system_models ||= # memoization for tests ([Rails.application] + Rails::Engine.subclasses.collect(&:instance)).flat_map do |app| (app.paths['app/models'].to_a + app.config.eager_load_paths).collect do |load_path| Dir.glob(app.root.join(load_path)).collect do |load_dir| path_prefix = "#{app.root.join(load_dir)}/" Dir.glob("#{load_dir}/**/*.rb").collect do |filename| # app/models/module/class.rb => module/class.rb => module/class => Module::Class filename.delete_prefix(path_prefix).chomp('.rb').camelize end end end end.flatten.reject { |m| m.starts_with?('Concerns::') } # rubocop:disable Style/MultilineBlockChain @@system_models + @registry.keys.collect(&:to_s) end end def visible_models_with_bindings(bindings) models.collect { |m| m.with(bindings) }.select do |m| m.visible? && RailsAdmin::Config::Actions.find(:index, bindings.merge(abstract_model: m.abstract_model)).try(:authorized?) && (!m.abstract_model.embedded? || m.abstract_model.cyclic?) end end end # Set default values for configuration options on load reset end end ================================================ FILE: lib/rails_admin/engine.rb ================================================ # frozen_string_literal: true require 'kaminari' require 'nested_form' require 'rails' require 'rails_admin' require 'rails_admin/extensions/url_for_extension' require 'rails_admin/version' require 'turbo-rails' module RailsAdmin class Engine < Rails::Engine isolate_namespace RailsAdmin attr_accessor :importmap config.action_dispatch.rescue_responses['RailsAdmin::ActionNotAllowed'] = :forbidden initializer 'RailsAdmin load UrlForExtension' do RailsAdmin::Engine.routes.singleton_class.prepend(RailsAdmin::Extensions::UrlForExtension) end initializer 'RailsAdmin reload config in development' do |app| config.initializer_path = app.root.join('config/initializers/rails_admin.rb') unless Rails.application.config.cache_classes ActiveSupport::Reloader.before_class_unload do RailsAdmin::Config.reload! end reloader = app.config.file_watcher.new([config.initializer_path], []) do # Do nothing, ActiveSupport::Reloader will trigger class_unload! anyway end app.reloaders << reloader app.reloader.to_run do reloader.execute_if_updated { require_unload_lock! } end reloader.execute end end initializer 'RailsAdmin precompile hook', group: :all do |app| case RailsAdmin.config.asset_source when :sprockets app.config.assets.precompile += %w[ rails_admin/application.js rails_admin/application.css ] app.config.assets.paths << RailsAdmin::Engine.root.join('src') require 'rails_admin/support/es_module_processor' Sprockets.register_bundle_processor 'application/javascript', RailsAdmin::Support::ESModuleProcessor when :importmap self.importmap = Importmap::Map.new.draw(app.root.join('config/importmap.rails_admin.rb')) end end # Check for required middlewares, users may forget to use them in Rails API mode config.after_initialize do |app| has_session_store = app.config.middleware.to_a.any? do |m| m.klass.try(:<=, ActionDispatch::Session::AbstractStore) || m.klass.try(:<=, ActionDispatch::Session::AbstractSecureStore) || m.klass.name =~ /^ActionDispatch::Session::/ end loaded = app.config.middleware.to_a.map(&:name) required = %w[ActionDispatch::Cookies ActionDispatch::Flash Rack::MethodOverride] missing = required - loaded unless missing.empty? && has_session_store configs = missing.map { |m| "config.middleware.use #{m}" } configs << "config.middleware.use #{app.config.session_store.try(:name) || 'ActionDispatch::Session::CookieStore'}, #{app.config.session_options}" unless has_session_store raise <<~ERROR Required middlewares for RailsAdmin are not added To fix this, add #{configs.join("\n ")} to config/application.rb. ERROR end RailsAdmin::Version.warn_with_js_version end end end ================================================ FILE: lib/rails_admin/extension.rb ================================================ # frozen_string_literal: true require 'rails_admin/extensions/controller_extension' module RailsAdmin EXTENSIONS = [] # rubocop:disable Style/MutableConstant AUTHORIZATION_ADAPTERS = {} # rubocop:disable Style/MutableConstant AUDITING_ADAPTERS = {} # rubocop:disable Style/MutableConstant CONFIGURATION_ADAPTERS = {} # rubocop:disable Style/MutableConstant # Extend RailsAdmin # # The extension may define various adapters (e.g., for authorization) and # register those via the options hash. def self.add_extension(extension_key, extension_definition, options = {}) options.assert_valid_keys(:authorization, :configuration, :auditing) EXTENSIONS << extension_key AUTHORIZATION_ADAPTERS[extension_key] = extension_definition::AuthorizationAdapter if options[:authorization] CONFIGURATION_ADAPTERS[extension_key] = extension_definition::ConfigurationAdapter if options[:configuration] AUDITING_ADAPTERS[extension_key] = extension_definition::AuditingAdapter if options[:auditing] end # Setup all extensions for testing def self.setup_all_extensions (AUTHORIZATION_ADAPTERS.values + AUDITING_ADAPTERS.values).each do |klass| klass.setup if klass.respond_to? :setup rescue # rubocop:disable Style/RescueStandardError # ignore errors end end end ================================================ FILE: lib/rails_admin/extensions/cancancan/authorization_adapter.rb ================================================ # frozen_string_literal: true module RailsAdmin module Extensions module CanCanCan # This adapter is for the CanCanCan[https://github.com/CanCanCommunity/cancancan] authorization library. class AuthorizationAdapter module ControllerExtension def current_ability # use _current_user instead of default current_user so it works with # whatever current user method is defined with RailsAdmin @current_ability ||= ability_class.new(_current_user) end end include RailsAdmin::Config::Configurable def self.setup RailsAdmin::Extensions::ControllerExtension.include ControllerExtension end # See the +authorize_with+ config method for where the initialization happens. def initialize(controller, ability = nil, &block) @controller = controller ability_class { ability } if ability instance_eval(&block) if block adapter = self ControllerExtension.define_method(:ability_class) do adapter.ability_class end @controller.current_ability.authorize! :access, :rails_admin end register_instance_option :ability_class do Ability end # This method is called in every controller action and should raise an exception # when the authorization fails. The first argument is the name of the controller # action as a symbol (:create, :bulk_delete, etc.). The second argument is the # AbstractModel instance that applies. The third argument is the actual model # instance if it is available. def authorize(action, abstract_model = nil, model_object = nil) return unless action action, subject = resolve_action_and_subject(action, abstract_model, model_object) @controller.current_ability.authorize!(action, subject) end # This method is called primarily from the view to determine whether the given user # has access to perform the action on a given model. It should return true when authorized. # This takes the same arguments as +authorize+. The difference is that this will # return a boolean whereas +authorize+ will raise an exception when not authorized. def authorized?(action, abstract_model = nil, model_object = nil) return unless action action, subject = resolve_action_and_subject(action, abstract_model, model_object) @controller.current_ability.can?(action, subject) end # This is called when needing to scope a database query. It is called within the list # and bulk_delete/destroy actions and should return a scope which limits the records # to those which the user can perform the given action on. def query(action, abstract_model) abstract_model.model.accessible_by(@controller.current_ability, action) end # This is called in the new/create actions to determine the initial attributes for new # records. It should return a hash of attributes which match what the user # is authorized to create. def attributes_for(action, abstract_model) @controller.current_ability.attributes_for(action, abstract_model&.model) end private def resolve_action_and_subject(action, abstract_model, model_object) subject = model_object || abstract_model&.model if subject [action, subject] else # For :dashboard compatibility [:read, action] end end end end end end ================================================ FILE: lib/rails_admin/extensions/cancancan.rb ================================================ # frozen_string_literal: true require 'rails_admin/extensions/cancancan/authorization_adapter' RailsAdmin.add_extension(:cancancan, RailsAdmin::Extensions::CanCanCan, authorization: true) ================================================ FILE: lib/rails_admin/extensions/controller_extension.rb ================================================ # frozen_string_literal: true module RailsAdmin module Extensions module ControllerExtension end end end ================================================ FILE: lib/rails_admin/extensions/paper_trail/auditing_adapter.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/string/strip' module RailsAdmin module Extensions module PaperTrail class VersionProxy def initialize(version, user_class = User) @version = version @user_class = user_class end def message @message = @version.event @version.respond_to?(:changeset) && @version.changeset.present? ? @message + ' [' + @version.changeset.to_a.collect { |c| "#{c[0]} = #{c[1][1]}" }.join(', ') + ']' : @message end def created_at @version.created_at end def table @version.item_type end def username begin @user_class.find(@version.whodunnit).try(:email) rescue StandardError nil end || @version.whodunnit end def item @version.item_id end end module ControllerExtension def user_for_paper_trail _current_user.try(:id) || _current_user end end class AuditingAdapter COLUMN_MAPPING = { table: :item_type, username: :whodunnit, item: :item_id, created_at: :created_at, message: :event, }.freeze E_USER_CLASS_NOT_SET = <<~ERROR Please set up PaperTrail's user class explicitly. config.audit_with :paper_trail do user_class { User } end ERROR E_VERSION_MODEL_NOT_SET = <<~ERROR Please set up PaperTrail's version model explicitly. config.audit_with :paper_trail do version_class { PaperTrail::Version } end If you have configured a model to use a custom version class (https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes) that configuration will take precedence over what you specify in `audit_with`. ERROR include RailsAdmin::Config::Configurable def self.setup raise 'PaperTrail not found' unless defined?(::PaperTrail) RailsAdmin::Extensions::ControllerExtension.include ControllerExtension end def initialize(controller, user_class_name = nil, version_class_name = nil, &block) @controller = controller @controller&.send(:set_paper_trail_whodunnit) user_class { user_class_name.to_s.constantize } if user_class_name version_class { version_class_name.to_s.constantize } if version_class_name instance_eval(&block) if block end register_instance_option :user_class do User rescue NameError raise E_USER_CLASS_NOT_SET end register_instance_option :version_class do PaperTrail::Version rescue NameError raise E_VERSION_MODEL_NOT_SET end register_instance_option :sort_by do {id: :desc} end def latest(count = 100) version_class. order(sort_by).includes(:item).limit(count). collect { |version| VersionProxy.new(version, user_class) } end def delete_object(_object, _model, _user) # do nothing end def update_object(_object, _model, _user, _changes) # do nothing end def create_object(_object, _abstract_model, _user) # do nothing end def listing_for_model(model, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) listing_for_model_or_object(model, nil, query, sort, sort_reverse, all, page, per_page) end def listing_for_object(model, object, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) listing_for_model_or_object(model, object, query, sort, sort_reverse, all, page, per_page) end protected # - model - a RailsAdmin::AbstractModel def listing_for_model_or_object(model, object, query, sort, sort_reverse, all, page, per_page) sort = if sort.present? {COLUMN_MAPPING[sort.to_sym] => sort_reverse ? :desc : :asc} else sort_by end current_page = page.presence || '1' versions = object.nil? ? versions_for_model(model) : object.public_send(model.model.versions_association_name) versions = versions.where('event LIKE ?', "%#{query}%") if query.present? versions = versions.order(sort) versions = versions.send(Kaminari.config.page_method_name, current_page).per(per_page) unless all paginated_proxies = Kaminari.paginate_array([], total_count: versions.try(:total_count) || versions.count) paginated_proxies = paginated_proxies.send( paginated_proxies.respond_to?(Kaminari.config.page_method_name) ? Kaminari.config.page_method_name : :page, current_page, ).per(per_page) versions.each do |version| paginated_proxies << VersionProxy.new(version, user_class) end paginated_proxies end def versions_for_model(model) model_name = model.model.name base_class_name = model.model.base_class.name options = if base_class_name == model_name {item_type: model_name} else {item_type: base_class_name, item_id: model.model.all} end version_class_for(model.model).where(options) end # PT can be configured to use [custom version # classes](https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes) # # ```ruby # has_paper_trail versions: { class_name: 'MyVersion' } # ``` def version_class_for(model) model.paper_trail.version_class end end end end end ================================================ FILE: lib/rails_admin/extensions/paper_trail.rb ================================================ # frozen_string_literal: true require 'rails_admin/extensions/paper_trail/auditing_adapter' RailsAdmin.add_extension(:paper_trail, RailsAdmin::Extensions::PaperTrail, auditing: true) ================================================ FILE: lib/rails_admin/extensions/pundit/authorization_adapter.rb ================================================ # frozen_string_literal: true module RailsAdmin module Extensions module Pundit # This adapter is for the Pundit[https://github.com/elabs/pundit] authorization library. # You can create another adapter for different authorization behavior, just be certain it # responds to each of the public methods here. class AuthorizationAdapter # This method is called first time only and used for setup def self.setup RailsAdmin::Extensions::ControllerExtension.include defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit end # See the +authorize_with+ config method for where the initialization happens. def initialize(controller) @controller = controller end # This method is called in every controller action and should raise an exception # when the authorization fails. The first argument is the name of the controller # action as a symbol (:create, :bulk_delete, etc.). The second argument is the # AbstractModel instance that applies. The third argument is the actual model # instance if it is available. def authorize(action, abstract_model = nil, model_object = nil) record = model_object || abstract_model&.model raise ::Pundit::NotAuthorizedError.new("not allowed to #{action} this #{record}") if action && !policy(record).send(action_for_pundit(action)) @controller.instance_variable_set(:@_pundit_policy_authorized, true) end # This method is called primarily from the view to determine whether the given user # has access to perform the action on a given model. It should return true when authorized. # This takes the same arguments as +authorize+. The difference is that this will # return a boolean whereas +authorize+ will raise an exception when not authorized. def authorized?(action, abstract_model = nil, model_object = nil) record = model_object || abstract_model&.model policy(record).send(action_for_pundit(action)) if action end # This is called when needing to scope a database query. It is called within the list # and bulk_delete/destroy actions and should return a scope which limits the records # to those which the user can perform the given action on. def query(_action, abstract_model) @controller.send(:policy_scope, abstract_model.model.all) rescue ::Pundit::NotDefinedError abstract_model.model.all end # This is called in the new/create actions to determine the initial attributes for new # records. It should return a hash of attributes which match what the user # is authorized to create. def attributes_for(action, abstract_model) record = abstract_model&.model policy(record).try(:attributes_for, action) || {} end private def policy(record) @controller.send(:policy, record) rescue ::Pundit::NotDefinedError ::ApplicationPolicy.new(@controller.send(:pundit_user), record) end def action_for_pundit(action) action[-1, 1] == '?' ? action : "#{action}?" end end end end end ================================================ FILE: lib/rails_admin/extensions/pundit.rb ================================================ # frozen_string_literal: true require 'rails_admin/extensions/pundit/authorization_adapter' RailsAdmin.add_extension(:pundit, RailsAdmin::Extensions::Pundit, authorization: true) ================================================ FILE: lib/rails_admin/extensions/url_for_extension.rb ================================================ # frozen_string_literal: true module RailsAdmin module Extensions module UrlForExtension def url_for(options, *args) case options[:id] when Array options[:id] = RailsAdmin.config.composite_keys_serializer.serialize(options[:id]) end super options, *args end end end end ================================================ FILE: lib/rails_admin/support/composite_keys_serializer.rb ================================================ # frozen_string_literal: true module RailsAdmin module Support module CompositeKeysSerializer def self.serialize(keys) keys.map { |key| key&.to_s&.gsub('_', '__') }.join('_') end def self.deserialize(string) string.split('_').map { |key| key&.gsub('__', '_') } end end end end ================================================ FILE: lib/rails_admin/support/csv_converter.rb ================================================ # frozen_string_literal: true require 'csv' module RailsAdmin class CSVConverter def initialize(objects = [], schema = nil) @fields = [] @associations = [] schema ||= {} return self if (@objects = objects).blank? @model = objects.dup.first.class @abstract_model = RailsAdmin::AbstractModel.new(@model) @model_config = @abstract_model.config @methods = [(schema[:only] || []) + (schema[:methods] || [])].flatten.compact @fields = @methods.collect { |m| export_field_for(m) }.compact @empty = ::I18n.t('admin.export.empty_value_for_associated_objects') schema_include = schema.delete(:include) || {} @associations = schema_include.each_with_object({}) do |(key, values), hash| association = export_field_for(key) next unless association&.association? model_config = association.associated_model_config abstract_model = model_config.abstract_model methods = [(values[:only] || []) + (values[:methods] || [])].flatten.compact hash[key] = { association: association, model: abstract_model.model, abstract_model: abstract_model, model_config: model_config, fields: methods.collect { |m| export_field_for(m, model_config) }.compact, } hash end end def to_csv(options = {}) if CSV::VERSION == '3.0.2' raise <<~MSG CSV library bundled with Ruby 2.6.0 has encoding issue, please upgrade Ruby to 2.6.1 or later. https://github.com/ruby/csv/issues/62 MSG end options = HashWithIndifferentAccess.new(options) encoding_to = Encoding.find(options[:encoding_to]) if options[:encoding_to].present? csv_string = generate_csv_string(options) csv_string = csv_string.encode(encoding_to, invalid: :replace, undef: :replace, replace: '?') if encoding_to # Add a BOM for utf8 encodings, helps with utf8 auto-detect for some versions of Excel. # Don't add if utf8 but user don't want to touch input encoding: # If user chooses utf8, they will open it in utf8 and BOM will disappear at reading. # But that way "English" users who don't bother and chooses to let utf8 by default won't get BOM added # and will not see it if Excel opens the file with a different encoding. csv_string = "\xEF\xBB\xBF#{csv_string}" if encoding_to == Encoding::UTF_8 [!options[:skip_header], (encoding_to || csv_string.encoding).to_s, csv_string] end private def export_field_for(method, model_config = @model_config) model_config.export.fields.detect { |f| f.name == method } end def generate_csv_string(options) generator_options = (options[:generator] || {}).symbolize_keys.delete_if { |_, value| value.blank? } method = @objects.respond_to?(:find_each) ? :find_each : :each CSV.generate(**generator_options) do |csv| csv << generate_csv_header unless options[:skip_header] @objects.send(method) do |object| csv << generate_csv_row(object) end end end def generate_csv_header @fields.collect do |field| ::I18n.t('admin.export.csv.header_for_root_methods', name: field.label, model: @abstract_model.pretty_name) end + @associations.flat_map do |_association_name, option_hash| option_hash[:fields].collect do |field| ::I18n.t('admin.export.csv.header_for_association_methods', name: field.label, association: option_hash[:association].label) end end end def generate_csv_row(object) @fields.collect do |field| field.with(object: object).export_value end + @associations.flat_map do |association_name, option_hash| associated_objects = [object.send(association_name)].flatten.compact option_hash[:fields].collect do |field| associated_objects.collect { |ao| field.with(object: ao).export_value.presence || @empty }.join(',') end end end end end ================================================ FILE: lib/rails_admin/support/datetime.rb ================================================ # frozen_string_literal: true module RailsAdmin module Support class Datetime # Ruby format options as a key and flatpickr format options as a value FLATPICKR_TRANSLATIONS = { '%A' => 'l', # The full weekday name ("Sunday") '%a' => 'D', # The abbreviated weekday name ("Sun") '%B' => 'F', # The full month name ("January") '%b' => 'M', # The abbreviated month name ("Jan") '%D' => 'm/d/y', # American date format mm/dd/yy '%d' => 'd', # Day of the month (01..31) '%-d' => 'j', # Day of the month (1..31) '%e' => 'j', # Day of the month (1..31) '%F' => 'Y-m-d', # ISO 8601 date format '%H' => 'H', # Hour of the day, 24-hour clock (00..23) '%-H' => 'H', # Hour of the day, 24-hour clock (0..23) '%h' => 'M', # Same as %b '%I' => 'G', # Hour of the day, 12-hour clock (01..12) '%-I' => 'h', # Hour of the day, 12-hour clock (1..12) '%k' => 'H', # Hour of the day, 24-hour clock (0..23) '%l' => 'h', # Hour of the day, 12-hour clock (1..12) '%-l' => 'h', # Hour of the day, 12-hour clock (1..12) '%M' => 'i', # Minute of the hour (00..59) '%-M' => 'i', # Minute of the hour (00..59) '%m' => 'm', # Month of the year (01..12) '%-m' => 'n', # Month of the year (1..12) '%P' => 'K', # Meridian indicator ('am' or 'pm') '%p' => 'K', # Meridian indicator ('AM' or 'PM') '%R' => 'H:i', # 24-hour time (%H:%M) '%r' => 'G:i:S K', # 12-hour time (%I:%M:%S %p) '%S' => 'S', # Second of the minute (00..60) '%-S' => 's', # Second of the minute (0..60) '%s' => 'U', # Number of seconds since 1970-01-01 00:00:00 UTC. '%T' => 'H:i:S', # 24-hour time (%H:%M:%S) '%U' => 'W', # Week number of the year. The week starts with Sunday. (00..53) '%w' => 'w', # Day of the week (Sunday is 0, 0..6) '%X' => 'H:i:S', # Same as %T '%x' => 'm/d/y', # Same as %D '%Y' => 'Y', # Year with century '%y' => 'y', # Year without a century (00..99) '%%' => '%', }.freeze class << self def to_flatpickr_format(strftime_format) strftime_format.gsub(/(? { 'adapter' => adapter, 'database' => database, 'username' => username, 'password' => (adapter == 'postgresql' ? 'postgres' : ''), 'host' => '127.0.0.1', 'encoding' => 'utf8', 'pool' => 5, 'timeout' => 5000, }, } filename = Rails.root.join('config/database.yml') File.open(filename, 'w') do |f| f.write(configuration.to_yaml) end end end ================================================ FILE: package.json ================================================ { "name": "rails_admin", "version": "3.3.0", "description": "RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data.", "homepage": "https://github.com/railsadminteam/rails_admin", "license": "MIT", "author": "Mitsuhiro Shibuya ", "files": [ "src" ], "main": "src/rails_admin/base.js", "scripts": { "link": "yarn link && cd spec/dummy_app && yarn link rails_admin", "format": "prettier -w ." }, "dependencies": { "@babel/runtime": "^7.16.7", "@fortawesome/fontawesome-free": ">=5.15.0 <7.0.0", "@hotwired/turbo-rails": "^7.1.0", "@popperjs/core": "^2.11.0", "@rails/ujs": "^6.1.4-1", "bootstrap": "^5.1.3", "flatpickr": "^4.6.9", "jquery": "^3.6.0", "jquery-ui": "^1.12.1 <1.14.0" }, "devDependencies": { "prettier": "^2.4.1" } } ================================================ FILE: rails_admin.gemspec ================================================ # frozen_string_literal: true require_relative 'lib/rails_admin/version' Gem::Specification.new do |spec| # If you add a dependency, please maintain alphabetical order spec.add_dependency 'activemodel-serializers-xml', '>= 1.0' spec.add_dependency 'csv' spec.add_dependency 'kaminari', '>= 0.14', '< 2.0' spec.add_dependency 'nested_form', '~> 0.3' spec.add_dependency 'rails', ['>= 6.0', '< 9'] spec.add_dependency 'turbo-rails', ['>= 1.0', '< 3'] spec.add_development_dependency 'bundler', '>= 1.0' spec.authors = ['Erik Michaels-Ober', 'Bogdan Gaza', 'Petteri Kaapa', 'Benoit Benezech', 'Mitsuhiro Shibuya'] spec.description = 'RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data.' spec.email = ['sferik@gmail.com', 'bogdan@cadmio.org', 'petteri.kaapa@gmail.com'] spec.files = Dir['Gemfile', 'LICENSE.md', 'README.md', 'Rakefile', 'package.json', 'app/**/*', 'config/**/*', 'lib/**/*', 'public/**/*', 'src/**/*', 'vendor/**/*'] spec.licenses = %w[MIT] spec.homepage = 'https://github.com/railsadminteam/rails_admin' spec.name = 'rails_admin' spec.require_paths = %w[lib] spec.required_ruby_version = '>= 2.6.0' spec.required_rubygems_version = '>= 1.8.11' spec.summary = 'Admin for Rails' spec.version = RailsAdmin::Version spec.post_install_message = <<~MSG ### Upgrading RailsAdmin from 2.x.x to 3.x.x ### Due to introduction of Webpack/Webpacker support, some additional dependencies and configuration will be needed. Running `bin/rails g rails_admin:install` will suggest required changes, based on the current setup of your app. For a complete list of changes, see https://github.com/railsadminteam/rails_admin/blob/master/CHANGELOG.md MSG end ================================================ FILE: spec/controllers/rails_admin/application_controller_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::ApplicationController, type: :controller do describe '#to_model_name' do it 'works with modules' do expect(controller.to_model_name('conversations~conversation')).to eq('Conversations::Conversation') end end describe 'helper method _get_plugin_name' do it 'works by default' do expect(controller.send(:_get_plugin_name)).to eq(['Dummy App', 'Admin']) end it 'works for static names' do RailsAdmin.config do |config| config.main_app_name = %w[static value] end expect(controller.send(:_get_plugin_name)).to eq(%w[static value]) end it 'works for dynamic names in the controller context' do RailsAdmin.config do |config| config.main_app_name = proc { |controller| [Rails.application.engine_name&.titleize, controller.params[:action].titleize] } end controller.params[:action] = 'dashboard' expect(controller.send(:_get_plugin_name)).to eq(['Dummy App Application', 'Dashboard']) end end describe '#_current_user' do it 'is public' do expect { controller._current_user }.not_to raise_error end end describe '#rails_admin_controller?' do it 'returns true' do expect(controller.send(:rails_admin_controller?)).to be true end end end ================================================ FILE: spec/controllers/rails_admin/main_controller_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::MainController, type: :controller do routes { RailsAdmin::Engine.routes } def get(action, params) super action, params: params end before do controller.instance_variable_set :@action, RailsAdmin::Config::Actions.find(:index) end describe '#check_for_cancel' do before do allow(controller).to receive(:back_or_index) { raise StandardError.new('redirected back') } end it 'redirects to back if params[:bulk_ids] is nil when params[:bulk_action] is present' do expect { get :bulk_delete, model_name: 'player', bulk_action: 'bulk_delete' }.to raise_error('redirected back') end it 'does not redirect to back if params[:bulk_ids] and params[:bulk_action] is present' do expect { get :bulk_delete, model_name: 'player', bulk_action: 'bulk_delete', bulk_ids: [1] }.not_to raise_error end end describe '#get_sort_hash' do context 'options sortable is a hash' do before do RailsAdmin.config('Player') do configure :team do sortable do :'team.name' end end end end it 'returns the option with no changes' do controller.params = {sort: 'team', model_name: 'players'} expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: :'team.name', sort_reverse: true) end end context 'when default sort_by points to a field with a table reference for sortable' do before do RailsAdmin.config('Player') do base do field :name do sortable 'teams.name' end end list do sort_by :name end end end it 'returns the query referenced in the sortable' do expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: 'teams.name', sort_reverse: true) end end context 'with a virtual field' do before do RailsAdmin.config('Player') do base do field :virtual do sortable :name end end list do sort_by :virtual end end end it 'returns the query referenced in the sortable' do expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: /["`]?players["`]?\.["`]?name["`]?/, sort_reverse: true) end end it 'works with belongs_to associations with label method virtual' do controller.params = {sort: 'parent_category', model_name: 'categories'} expect(controller.send(:get_sort_hash, RailsAdmin.config(Category))).to eq(sort: 'categories.parent_category_id', sort_reverse: true) end context 'using mongoid', mongoid: true do it 'gives back the remote table with label name, as it does not support joins' do controller.params = {sort: 'team', model_name: 'players'} expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: 'players.team_id', sort_reverse: true) end end context 'using active_record', active_record: true do let(:connection_config) do if ActiveRecord::Base.respond_to?(:connection_db_config) ActiveRecord::Base.connection_db_config.configuration_hash else ActiveRecord::Base.connection_config end end it 'gives back the local column' do controller.params = {sort: 'team', model_name: 'players'} expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: /^["`]teams["`]\.["`]name["`]$/, sort_reverse: true) end it 'quotes the table and column names it returns as :sort' do controller.params = {sort: 'team', model_name: 'players'} case connection_config[:adapter] when 'mysql2' expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))[:sort]).to eq '`teams`.`name`' else expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))[:sort]).to eq '"teams"."name"' end end end end describe '#bulk_action' do before do RailsAdmin.config do |config| config.actions do dashboard index do visible do raise # This shouldn't be invoked end end bulk_delete end end end it 'retrieves actions using :bulkable scope' do expect { post :bulk_action, params: {model_name: 'player', bulk_action: 'bulk_delete', bulk_ids: [1]} }.not_to raise_error end end describe '#list_entries called from view' do before do @teams = FactoryBot.create_list(:team, 21) controller.params = {model_name: 'teams'} end it 'paginates' do expect(controller.list_entries(RailsAdmin.config(Team), :index, nil, false).to_a.length).to eq(21) expect(controller.list_entries(RailsAdmin.config(Team), :index, nil, true).to_a.length).to eq(20) end end describe '#list_entries called from view with kaminari custom param_name' do before do @teams = FactoryBot.create_list(:team, 21) controller.params = {model_name: 'teams'} Kaminari.config.param_name = :pagina end after do Kaminari.config.param_name = :page end it 'paginates' do expect(controller.list_entries(RailsAdmin.config(Team), :index, nil, false).to_a.length).to eq(21) expect(controller.list_entries(RailsAdmin.config(Team), :index, nil, true).to_a.length).to eq(20) end end describe '#list_entries called with bulk_ids' do before do @teams = FactoryBot.create_list(:team, 21) controller.params = {model_name: 'teams', bulk_action: 'bulk_delete', bulk_ids: @teams.collect(&:id)} end it 'does not paginate' do expect(controller.list_entries(RailsAdmin.config(Team), :bulk_delete).to_a.length).to eq(21) end end describe '#list_entries for associated_collection' do before do @team = FactoryBot.create :team controller.params = {associated_collection: 'players', current_action: 'update', source_abstract_model: 'team', source_object_id: @team.id, model_name: 'player', action: 'index'} controller.get_model # set @model_config for Team end it "doesn't scope associated collection records when associated_collection_scope is nil" do @players = FactoryBot.create_list(:player, 2) RailsAdmin.config Team do field :players do associated_collection_scope false end end expect(controller.list_entries.to_a.length).to eq(@players.size) end it 'scopes associated collection records according to associated_collection_scope' do @players = FactoryBot.create_list(:player, 4) RailsAdmin.config Team do field :players do associated_collection_scope do proc { |scope| scope.limit(3) } end end end expect(controller.list_entries.to_a.length).to eq(3) end it 'scopes associated collection records according to bindings' do @team.revenue = BigDecimal('3') @team.save @players = FactoryBot.create_list(:player, 5) RailsAdmin.config Team do field :players do associated_collection_scope do team = bindings[:object] proc do |scope| scope.limit(team.revenue) end end end end expect(controller.list_entries.to_a.length).to eq(@team.revenue.to_i) end it 'limits associated collection records number to 30 if cache_all is false' do @players = FactoryBot.create_list(:player, 40) RailsAdmin.config Team do field :players do associated_collection_cache_all false end end expect(controller.list_entries.to_a.length).to eq(30) end it "doesn't limit associated collection records number to 30 if cache_all is true" do @players = FactoryBot.create_list(:player, 40) RailsAdmin.config Team do field :players do associated_collection_cache_all true end end expect(controller.list_entries.length).to eq(@players.size) end it 'orders associated collection records by id, descending' do @players = FactoryBot.create_list(:player, 3) expect(controller.list_entries.to_a).to eq(@players.sort_by(&:id).reverse) end end describe '#action_missing' do it 'raises error when action is not found' do expect(RailsAdmin::Config::Actions).to receive(:find).and_return(nil) expect { get :index, model_name: 'player' }.to raise_error AbstractController::ActionNotFound end end describe '#respond_to_missing?' do it 'returns the result based on existence of action' do expect(controller.send(:respond_to_missing?, :index, false)).to be true expect(controller.send(:respond_to_missing?, :invalid_action, false)).to be false end end describe '#get_collection' do let(:team) { FactoryBot.create :team } let!(:player) { FactoryBot.create :player, team: team } let(:model_config) { RailsAdmin.config(Team) } let(:abstract_model) { model_config.abstract_model } before do controller.params = {model_name: 'team'} end it 'performs eager-loading with `eager_load true`' do RailsAdmin.config Team do field :players do eager_load true end end expect(abstract_model).to receive(:all).with(hash_including(include: [:players]), nil).once.and_call_original controller.send(:get_collection, model_config, nil, false).to_a end it 'performs eager-loading with custom eager_load value' do RailsAdmin.config Team do field :players do eager_load players: :draft end end expect(abstract_model).to receive(:all).with(hash_including(include: [{players: :draft}]), nil).once.and_call_original controller.send(:get_collection, model_config, nil, false).to_a end context 'on export' do before do controller.instance_variable_set :@action, RailsAdmin::Config::Actions.find(:export) end it 'uses the export section' do RailsAdmin.config Team do export do field :players do eager_load true end end end expect(abstract_model).to receive(:all).with(hash_including(include: [:players]), nil).once.and_call_original controller.send(:get_collection, model_config, nil, false).to_a end end end describe 'index' do it "uses target model's primary key" do @user = FactoryBot.create :managing_user @team = FactoryBot.create :managed_team, user: @user get :index, model_name: 'managed_team', source_object_id: @user.id, source_abstract_model: 'managing_user', associated_collection: 'teams', current_action: :create, compact: true, format: :json expect(response.body).to match(/"id":"#{@team.id}"/) end context 'as JSON' do it 'returns strings' do FactoryBot.create :player, team: (FactoryBot.create :team) get :index, model_name: 'player', source_object_id: Team.first.id, source_abstract_model: 'team', associated_collection: 'players', current_action: :create, compact: true, format: :json expect(JSON.parse(response.body).first['id']).to be_a_kind_of String end end context 'when authorizing requests with pundit' do if defined?(Devise::Test) include Devise::Test::ControllerHelpers else include Devise::TestHelpers end controller(RailsAdmin::MainController) do include defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit after_action :verify_authorized end it 'performs authorization' do RailsAdmin.config do |c| c.authorize_with(:pundit) c.authenticate_with { warden.authenticate! scope: :user } c.current_user_method(&:current_user) end login_as FactoryBot.create :user, roles: [:admin] player = FactoryBot.create :player, team: (FactoryBot.create :team) expect { get :show, model_name: 'player', id: player.id }.not_to raise_error end end end describe 'sanitize_params_for!' do context 'with datetime' do before do ActionController::Parameters.permit_all_parameters = false RailsAdmin.config Comment do configure :created_at do show end end RailsAdmin.config NestedFieldTest do configure :created_at do show end end controller.params = ActionController::Parameters.new( 'field_test' => { 'unallowed_field' => "I shouldn't be here", 'datetime_field' => '2010-08-01T00:00:00', 'nested_field_tests_attributes' => { 'new_1330520162002' => { 'comment_attributes' => { 'unallowed_field' => "I shouldn't be here", 'created_at' => '2010-08-02T00:00:00', }, 'created_at' => '2010-08-03T00:00:00', }, }, 'comment_attributes' => { 'unallowed_field' => "I shouldn't be here", 'created_at' => '2010-08-04T00:00:00', }, }, ) controller.send(:sanitize_params_for!, :create, RailsAdmin.config(FieldTest), controller.params['field_test']) end after do ActionController::Parameters.permit_all_parameters = true end it 'sanitize params recursively in nested forms' do expect(controller.params[:field_test].to_h).to eq( 'datetime_field' => ::Time.zone.parse('Sun, 01 Aug 2010 00:00:00 UTC +00:00'), 'nested_field_tests_attributes' => { 'new_1330520162002' => { 'comment_attributes' => { 'created_at' => ::Time.zone.parse('Mon, 02 Aug 2010 00:00:00 UTC +00:00'), }, 'created_at' => ::Time.zone.parse('Tue, 03 Aug 2010 00:00:00 UTC +00:00'), }, }, 'comment_attributes' => { 'created_at' => ::Time.zone.parse('Wed, 04 Aug 2010 00:00:00 UTC +00:00'), }, ) end it 'enforces permit!' do expect(controller.params['field_test'].permitted?).to be_truthy expect(controller.params['field_test']['nested_field_tests_attributes'].values.first.permitted?).to be_truthy expect(controller.params['field_test']['comment_attributes'].permitted?).to be_truthy end end it 'allows for delete method with Carrierwave' do RailsAdmin.config FieldTest do field :carrierwave_asset field :carrierwave_assets field :dragonfly_asset field :paperclip_asset do delete_method :delete_paperclip_asset end if defined?(ActiveStorage) field :active_storage_asset do delete_method :remove_active_storage_asset end end if defined?(ActiveStorage) field :active_storage_assets do delete_method :remove_active_storage_assets end end if defined?(Shrine) field :shrine_asset do delete_method :remove_shrine_asset end end end controller.params = HashWithIndifferentAccess.new( 'field_test' => { 'carrierwave_asset' => 'test', 'carrierwave_asset_cache' => 'test', 'remove_carrierwave_asset' => 'test', 'carrierwave_assets' => 'test', 'dragonfly_asset' => 'test', 'remove_dragonfly_asset' => 'test', 'retained_dragonfly_asset' => 'test', 'paperclip_asset' => 'test', 'delete_paperclip_asset' => 'test', 'should_not_be_here' => 'test', }.merge(defined?(ActiveStorage) ? {'active_storage_asset' => 'test', 'remove_active_storage_asset' => 'test', 'active_storage_assets' => 'test', 'remove_active_storage_assets' => 'test'} : {}). merge(defined?(Shrine) ? {'shrine_asset' => 'test', 'remove_shrine_asset' => 'test'} : {}), ) controller.send(:sanitize_params_for!, :create, RailsAdmin.config(FieldTest), controller.params['field_test']) expect(controller.params[:field_test].to_h).to eq({ 'carrierwave_asset' => 'test', 'remove_carrierwave_asset' => 'test', 'carrierwave_asset_cache' => 'test', 'carrierwave_assets' => 'test', 'dragonfly_asset' => 'test', 'remove_dragonfly_asset' => 'test', 'retained_dragonfly_asset' => 'test', 'paperclip_asset' => 'test', 'delete_paperclip_asset' => 'test', }.merge(defined?(ActiveStorage) ? {'active_storage_asset' => 'test', 'remove_active_storage_asset' => 'test', 'active_storage_assets' => 'test', 'remove_active_storage_assets' => 'test'} : {}). merge(defined?(Shrine) ? {'shrine_asset' => 'test', 'remove_shrine_asset' => 'test'} : {})) end it 'allows for polymorphic associations parameters' do RailsAdmin.config Comment do field :commentable end controller.params = HashWithIndifferentAccess.new( 'comment' => { 'commentable_id' => 'test', 'commentable_type' => 'test', }, ) controller.send(:sanitize_params_for!, :create, RailsAdmin.config(Comment), controller.params['comment']) expect(controller.params[:comment].to_h).to eq( 'commentable_id' => 'test', 'commentable_type' => 'test', ) end end describe 'back_or_index' do before do allow(controller).to receive(:index_path).and_return(index_path) end let(:index_path) { '/' } it 'returns back to index when return_to is not defined' do controller.params = {} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to return_to url when it starts with same protocol and host' do return_to_url = "http://#{request.host}/teams" controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(return_to_url) end it 'returns back to return_to url when it contains a path' do return_to_url = '/teams' controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(return_to_url) end it 'returns back to index path when return_to path does not start with slash' do return_to_url = 'teams' controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to url does not start with full protocol' do return_to_url = "#{request.host}/teams" controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to url starts with double slash' do return_to_url = "//#{request.host}/teams" controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to url starts with triple slash' do return_to_url = "///#{request.host}/teams" controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to url does not have host' do return_to_url = 'http:///teams' controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to url starts with different protocol' do return_to_url = "other://#{request.host}/teams" controller.params = {return_to: return_to_url} expect(controller.send(:back_or_index)).to eq(index_path) end it 'returns back to index path when return_to does not start with the same protocol and host' do controller.params = {return_to: "http://google.com?#{request.host}"} expect(controller.send(:back_or_index)).to eq(index_path) end end end ================================================ FILE: spec/dummy_app/.browserslistrc ================================================ defaults ================================================ FILE: spec/dummy_app/.dockerignore ================================================ # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. # Ignore git directory. /.git/ # Ignore bundler config and Gemfile.lock. /.bundle /Gemfile.lock # Ignore all default key files. /config/master.key /config/credentials/*.key # Ignore all environment files. /.env* !/.env.example # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/ !/tmp/pids/.keep # Ignore storage (uploaded files in development and any SQLite databases). /db/*.sqlite3 /public/system /public/uploads /public/vite /storage/* !/storage/.keep /tmp/storage/* !/tmp/storage/ !/tmp/storage/.keep # Ignore assets. /node_modules/ /app/assets/builds/* !/app/assets/builds/.keep /public/assets ================================================ FILE: spec/dummy_app/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile ~/.gitignore_global # Ignore bundler config /.bundle /app/assets/builds/* !/app/assets/builds/.keep # Ignore the default SQLite database. /db/*.sqlite3 # Ignore all logfiles and tempfiles. /log/*.log /tmp /public/system /public/assets /public/packs /public/packs-test /public/vite* /node_modules /yarn-error.log /yarn.lock yarn-debug.log* .yarn-integrity ================================================ FILE: spec/dummy_app/Dockerfile ================================================ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile ARG RUBY_VERSION=3.1 FROM ruby:$RUBY_VERSION-slim AS base LABEL fly_launch_runtime="rails" # Rails app lives here WORKDIR /rails # Set production environment ENV RAILS_ENV="production" \ BUNDLE_WITHOUT="development:test" # Update gems and bundler RUN gem update --system --no-document && \ gem install -N bundler # Install packages needed to install nodejs RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install Node.js # ARG NODE_VERSION=18.16.0 # ENV PATH=/usr/local/node/bin:$PATH # RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ # /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ # rm -rf /tmp/node-build-master # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems and node modules RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential default-libmysqlclient-dev git libpq-dev libvips libyaml-dev node-gyp pkg-config python-is-python3 # Build options ENV PATH="/usr/local/node/bin:$PATH" # Copy application code COPY --link . . RUN sed -i "s/, path: '..\/..\/'//" Gemfile RUN bundle install && \ rm -rf ~/.bundle/ $BUNDLE_PATH/ruby/*/cache $BUNDLE_PATH/ruby/*/bundler/gems/*/.git # Install node modules # COPY --link package.json ./ # RUN npm install # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN sed -i "/link_tree ..\/..\/..\//d" app/assets/config/manifest.js RUN SECRET_KEY_BASE=DUMMY ./bin/rails assets:precompile db:setup # Final stage for app image FROM base # Install packages needed for deployment RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl imagemagick libsqlite3-0 libvips && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ mkdir public/system public/uploads && \ chown -R rails:rails db log tmp public/system public/uploads USER rails:rails # Deployment options ENV RAILS_LOG_TO_STDOUT="1" \ RAILS_SERVE_STATIC_FILES="true" \ RAILS_MAX_THREADS="1" # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD ["./bin/rails", "server"] ================================================ FILE: spec/dummy_app/Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' gem 'rails', '>= 7.0.0' group :active_record do platforms :jruby do gem 'activerecord-jdbcmysql-adapter', '>= 1.2' if ENV['CI_DB_ADAPTER'] == 'mysql2' gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2' if ENV['CI_DB_ADAPTER'] == 'postgresql' gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2' end platforms :ruby, :mswin, :mingw do gem 'mysql2', '>= 0.3.14' if ENV['CI_DB_ADAPTER'] == 'mysql2' gem 'pg', '>= 0.14' if ENV['CI_DB_ADAPTER'] == 'postgresql' gem 'sqlite3', '>= 1.3.0' end gem 'paper_trail', '>= 12.0' end gem 'carrierwave', '>= 2.0.0.rc', '< 3.0' gem 'devise', '>= 3.2' gem 'dragonfly', '~> 1.0' gem 'mini_magick', '>= 3.4' gem 'mlb', '>= 0.7' gem 'paperclip', '>= 3.4' gem 'puma' gem 'rails_admin', path: '../../' gem 'shrine', '~> 3.0' group :development, :test do gem 'cssbundling-rails', require: false gem 'importmap-rails', require: false gem 'vite_rails', require: false gem 'webpacker', require: false end # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sassc-rails', '~> 2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer' gem 'uglifier', '>= 1.3' end ================================================ FILE: spec/dummy_app/Procfile.dev ================================================ web: bin/rails server -p 3000 css: yarn build:css --watch vite: bin/vite dev ================================================ FILE: spec/dummy_app/Rakefile ================================================ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('config/application', __dir__) DummyApp::Application.load_tasks ================================================ FILE: spec/dummy_app/app/active_record/.gitkeep ================================================ ================================================ FILE: spec/dummy_app/app/active_record/abstract.rb ================================================ # frozen_string_literal: true class Abstract < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: spec/dummy_app/app/active_record/another_field_test.rb ================================================ # frozen_string_literal: true class AnotherFieldTest < ActiveRecord::Base has_many :nested_field_tests, inverse_of: :another_field_test end ================================================ FILE: spec/dummy_app/app/active_record/ball.rb ================================================ # frozen_string_literal: true class Ball < ActiveRecord::Base has_one :comment, as: :commentable validates_presence_of :color, on: :create def to_param color.present? ? color.downcase.tr(' ', '-') : id end end ================================================ FILE: spec/dummy_app/app/active_record/carrierwave_uploader.rb ================================================ # frozen_string_literal: true require 'mini_magick' class CarrierwaveUploader < CarrierWave::Uploader::Base # Include RMagick or ImageScience support: # include CarrierWave::RMagick include CarrierWave::MiniMagick # include CarrierWave::ImageScience # Choose what kind of storage to use for this uploader: storage :file # storage :fog # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end version :thumb do process resize_to_fill: [100, 100] end # Provide a default URL as a default if there hasn't been a file uploaded: # def default_url # "/images/fallback/" + [version_name, "default.png"].compact.join('_') # end # Process files as they are uploaded: # process scale: [200, 300] # # def scale(width, height) # # do something # end # Create different versions of your uploaded files: # version :thumb do # process scale: [50, 50] # end # Add an allowlist of extensions which are allowed to be uploaded. # For images you might use something like this: # def extension_allowlist # %w(jpg jpeg gif png) # end # Override the filename of the uploaded files: # Avoid using model.id or version_name here, see uploader/store.rb for details. # def filename # "something.jpg" if original_filename # end end ================================================ FILE: spec/dummy_app/app/active_record/category.rb ================================================ # frozen_string_literal: true class Category < ActiveRecord::Base belongs_to :parent_category, class_name: 'Category', optional: true end ================================================ FILE: spec/dummy_app/app/active_record/cms/basic_page.rb ================================================ # frozen_string_literal: true module Cms class BasicPage < ActiveRecord::Base self.table_name = :cms_basic_pages validates :title, :content, presence: true end end ================================================ FILE: spec/dummy_app/app/active_record/cms.rb ================================================ # frozen_string_literal: true module Cms def self.table_name_prefix 'cms_' end end ================================================ FILE: spec/dummy_app/app/active_record/comment/confirmed.rb ================================================ # frozen_string_literal: true class Comment class Confirmed < Comment default_scope { where(content: 'something') } end end ================================================ FILE: spec/dummy_app/app/active_record/comment.rb ================================================ # frozen_string_literal: true class Comment < ActiveRecord::Base include Taggable belongs_to :commentable, polymorphic: true, optional: true end ================================================ FILE: spec/dummy_app/app/active_record/concerns/taggable.rb ================================================ # frozen_string_literal: true module Taggable extend ActiveSupport::Concern # dummy end ================================================ FILE: spec/dummy_app/app/active_record/deeply_nested_field_test.rb ================================================ # frozen_string_literal: true class DeeplyNestedFieldTest < ActiveRecord::Base belongs_to :nested_field_test, inverse_of: :deeply_nested_field_tests end ================================================ FILE: spec/dummy_app/app/active_record/division.rb ================================================ # frozen_string_literal: true class Division < ActiveRecord::Base self.primary_key = :custom_id belongs_to :league, foreign_key: 'custom_league_id', optional: true has_many :teams validates_numericality_of(:custom_league_id, only_integer: true) validates_presence_of(:name) end ================================================ FILE: spec/dummy_app/app/active_record/draft.rb ================================================ # frozen_string_literal: true class Draft < ActiveRecord::Base belongs_to :team belongs_to :player validates_numericality_of(:player_id, only_integer: true) validates_numericality_of(:team_id, only_integer: true) validates_presence_of(:date) validates_numericality_of(:round, only_integer: true) validates_numericality_of(:pick, only_integer: true) validates_numericality_of(:overall, only_integer: true) end ================================================ FILE: spec/dummy_app/app/active_record/fan.rb ================================================ # frozen_string_literal: true class Fan < ActiveRecord::Base has_and_belongs_to_many :teams if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) has_many :fanships, inverse_of: :fan has_one :fanship, inverse_of: :fan end validates_presence_of(:name) end ================================================ FILE: spec/dummy_app/app/active_record/fanship.rb ================================================ # frozen_string_literal: true if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class Fanship < ActiveRecord::Base self.table_name = :fans_teams if defined?(CompositePrimaryKeys) self.primary_keys = :fan_id, :team_id else self.primary_key = :fan_id, :team_id end if defined?(CompositePrimaryKeys) || ActiveRecord.gem_version >= Gem::Version.new('7.2') has_many :favorite_players, foreign_key: %i[fan_id team_id], inverse_of: :fanship else has_many :favorite_players, query_constraints: %i[fan_id team_id], inverse_of: :fanship end belongs_to :fan, inverse_of: :fanships, optional: true belongs_to :team, optional: true end else class Fanship; end end ================================================ FILE: spec/dummy_app/app/active_record/favorite_player.rb ================================================ # frozen_string_literal: true if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class FavoritePlayer < ActiveRecord::Base if defined?(CompositePrimaryKeys) self.primary_keys = :fan_id, :team_id, :player_id else self.primary_key = :fan_id, :team_id, :player_id end if defined?(CompositePrimaryKeys) || ActiveRecord.gem_version >= Gem::Version.new('7.2') belongs_to :fanship, foreign_key: %i[fan_id team_id], inverse_of: :favorite_players else belongs_to :fanship, query_constraints: %i[fan_id team_id], inverse_of: :favorite_players end belongs_to :player end end ================================================ FILE: spec/dummy_app/app/active_record/field_test.rb ================================================ # frozen_string_literal: true class FieldTest < ActiveRecord::Base has_many :nested_field_tests, dependent: :destroy, inverse_of: :field_test accepts_nested_attributes_for :nested_field_tests, allow_destroy: true has_one :comment, as: :commentable accepts_nested_attributes_for :comment, allow_destroy: true has_attached_file :paperclip_asset, styles: {thumb: '100x100>'} attr_accessor :delete_paperclip_asset before_validation { self.paperclip_asset = nil if delete_paperclip_asset == '1' } ActiveRecord::Base.extend Dragonfly::Model ActiveRecord::Base.extend Dragonfly::Model::Validations dragonfly_accessor :dragonfly_asset mount_uploader :carrierwave_asset, CarrierwaveUploader mount_uploaders :carrierwave_assets, CarrierwaveUploader if ActiveRecord.gem_version < Gem::Version.new('7.1') serialize :carrierwave_assets, JSON else serialize :carrierwave_assets, coder: JSON end if defined?(ActiveStorage) has_one_attached :active_storage_asset attr_accessor :remove_active_storage_asset after_save { active_storage_asset.purge if remove_active_storage_asset == '1' } has_many_attached :active_storage_assets attr_accessor :remove_active_storage_assets after_save do Array(remove_active_storage_assets).each { |id| active_storage_assets.find_by_id(id)&.purge } end end include ShrineUploader.attachment(:shrine_asset) include ShrineVersioningUploader.attachment(:shrine_versioning_asset) has_rich_text :action_text_field if defined?(ActionText) if ActiveRecord.gem_version >= Gem::Version.new('7.0') enum :string_enum_field, {S: 's', M: 'm', L: 'l'} enum :integer_enum_field, %i[small medium large] else enum string_enum_field: {S: 's', M: 'm', L: 'l'} enum integer_enum_field: %i[small medium large] end validates :string_field, exclusion: {in: ['Invalid']} # to test file upload caching end ================================================ FILE: spec/dummy_app/app/active_record/hardball.rb ================================================ # frozen_string_literal: true class Hardball < Ball end ================================================ FILE: spec/dummy_app/app/active_record/image.rb ================================================ # frozen_string_literal: true class Image < ActiveRecord::Base has_attached_file :file, styles: {medium: '300x300>', thumb: '100x100>'} validates_attachment_presence :file end ================================================ FILE: spec/dummy_app/app/active_record/league.rb ================================================ # frozen_string_literal: true class League < ActiveRecord::Base has_many :divisions, foreign_key: 'custom_league_id' has_many :teams, -> { readonly }, through: :divisions has_many :players, through: :teams has_one :division, foreign_key: 'custom_league_id' validates_presence_of(:name) def custom_name "League '#{name}'" end end ================================================ FILE: spec/dummy_app/app/active_record/managed_team.rb ================================================ # frozen_string_literal: true class ManagedTeam < Team belongs_to :user, class_name: 'ManagingUser', foreign_key: :manager, primary_key: :email, optional: true, inverse_of: :teams end ================================================ FILE: spec/dummy_app/app/active_record/managing_user.rb ================================================ # frozen_string_literal: true class ManagingUser < User has_one :team, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user has_many :teams, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user end ================================================ FILE: spec/dummy_app/app/active_record/nested_fan.rb ================================================ # frozen_string_literal: true if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class NestedFan < Fan accepts_nested_attributes_for :fanships accepts_nested_attributes_for :fanship end end ================================================ FILE: spec/dummy_app/app/active_record/nested_favorite_player.rb ================================================ # frozen_string_literal: true if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class NestedFavoritePlayer < FavoritePlayer accepts_nested_attributes_for :fanship end end ================================================ FILE: spec/dummy_app/app/active_record/nested_field_test.rb ================================================ # frozen_string_literal: true class NestedFieldTest < ActiveRecord::Base belongs_to :field_test, optional: true, inverse_of: :nested_field_tests belongs_to :another_field_test, optional: true, inverse_of: :nested_field_tests has_one :comment, as: :commentable has_many :deeply_nested_field_tests, inverse_of: :nested_field_test accepts_nested_attributes_for :comment, allow_destroy: true, reject_if: proc { |attributes| attributes['content'].blank? } accepts_nested_attributes_for :deeply_nested_field_tests, allow_destroy: true end ================================================ FILE: spec/dummy_app/app/active_record/paper_trail_test/subclass_in_namespace.rb ================================================ # frozen_string_literal: true class PaperTrailTest < ActiveRecord::Base class SubclassInNamespace < self end end ================================================ FILE: spec/dummy_app/app/active_record/paper_trail_test.rb ================================================ # frozen_string_literal: true class PaperTrailTest < ActiveRecord::Base has_paper_trail end ================================================ FILE: spec/dummy_app/app/active_record/paper_trail_test_subclass.rb ================================================ # frozen_string_literal: true class PaperTrailTestSubclass < PaperTrailTest end ================================================ FILE: spec/dummy_app/app/active_record/paper_trail_test_with_custom_association.rb ================================================ # frozen_string_literal: true class PaperTrailTestWithCustomAssociation < ActiveRecord::Base self.table_name = :paper_trail_tests has_paper_trail versions: {class_name: 'Trail'} end ================================================ FILE: spec/dummy_app/app/active_record/player.rb ================================================ # frozen_string_literal: true class Player < ActiveRecord::Base belongs_to :team, optional: true, inverse_of: :players has_one :draft, dependent: :destroy has_many :comments, as: :commentable validates_presence_of(:name) validates_numericality_of(:number, only_integer: true) validates_uniqueness_of(:number, scope: :team_id, message: 'There is already a player with that number on this team') validates_each :name do |record, _attr, value| record.errors.add(:base, 'Player is cheating') if /on steroids/.match?(value.to_s) end if ActiveRecord.gem_version >= Gem::Version.new('7.0') enum :formation, {start: 'start', substitute: 'substitute'} else enum formation: {start: 'start', substitute: 'substitute'} end before_destroy :destroy_hook scope :rails_admin_search, ->(query) { where(name: query.reverse) } def destroy_hook; end end ================================================ FILE: spec/dummy_app/app/active_record/read_only_comment.rb ================================================ # frozen_string_literal: true class ReadOnlyComment < Comment def readonly? true end end ================================================ FILE: spec/dummy_app/app/active_record/restricted_team.rb ================================================ # frozen_string_literal: true class RestrictedTeam < Team has_many :players, foreign_key: :team_id, dependent: :restrict_with_error end ================================================ FILE: spec/dummy_app/app/active_record/shrine_uploader.rb ================================================ # frozen_string_literal: true class ShrineUploader < Shrine plugin :activerecord plugin :cached_attachment_data plugin :determine_mime_type plugin :pretty_location plugin :remove_attachment end ================================================ FILE: spec/dummy_app/app/active_record/shrine_versioning_uploader.rb ================================================ # frozen_string_literal: true class ShrineVersioningUploader < Shrine plugin :activerecord plugin :cached_attachment_data plugin :determine_mime_type plugin :pretty_location plugin :remove_attachment if Gem.loaded_specs['shrine'].version >= Gem::Version.create('3') plugin :derivatives Attacher.derivatives_processor do |original| { thumb: FakeIO.new('', filename: File.basename(original.path), content_type: File.extname(original.path)), } end end end ================================================ FILE: spec/dummy_app/app/active_record/team.rb ================================================ # frozen_string_literal: true class Team < ActiveRecord::Base has_many :players, -> { order :id }, inverse_of: :team has_and_belongs_to_many :fans has_many :comments, as: :commentable validates_numericality_of :division_id, only_integer: true validates_presence_of :manager validates_numericality_of :founded, only_integer: true, allow_blank: true validates_numericality_of :wins, only_integer: true validates_numericality_of :losses, only_integer: true validates_numericality_of :win_percentage validates_numericality_of :revenue, allow_nil: true belongs_to :division, optional: true if ActiveRecord.gem_version >= Gem::Version.new('7.0') enum :main_sponsor, %i[no_sponsor food_factory transportation_company bank energy_producer] else enum main_sponsor: %i[no_sponsor food_factory transportation_company bank energy_producer] end def player_names_truncated players.collect(&:name).join(', ')[0..32] end def color_enum ['white', 'black', 'red', 'green', 'blué'] end scope :green, -> { where(color: 'red') } scope :red, -> { where(color: 'red') } scope :white, -> { where(color: 'white') } rails_admin do field :color, :color end end ================================================ FILE: spec/dummy_app/app/active_record/trail.rb ================================================ # frozen_string_literal: true class Trail < PaperTrail::Version self.table_name = :custom_versions end ================================================ FILE: spec/dummy_app/app/active_record/two_level/namespaced/polymorphic_association_test.rb ================================================ # frozen_string_literal: true module TwoLevel module Namespaced class PolymorphicAssociationTest < ActiveRecord::Base has_many :comments, as: :commentable end end end ================================================ FILE: spec/dummy_app/app/active_record/two_level/namespaced.rb ================================================ # frozen_string_literal: true module TwoLevel module Namespaced def self.table_name_prefix 'two_level_namespaced_' end end end ================================================ FILE: spec/dummy_app/app/active_record/user/confirmed.rb ================================================ # frozen_string_literal: true class User class Confirmed < User end end ================================================ FILE: spec/dummy_app/app/active_record/user.rb ================================================ # frozen_string_literal: true class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, :lockable and :timeoutable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable if ActiveRecord.gem_version < Gem::Version.new('7.1') serialize :roles, Array else serialize :roles, coder: YAML, type: Array end # Add Paperclip support for avatars has_attached_file :avatar, styles: {medium: '300x300>', thumb: '100x100>'} attr_accessor :delete_avatar before_validation { self.avatar = nil if delete_avatar == '1' } def attr_accessible_role :custom_role end def roles_enum %i[admin user] end end ================================================ FILE: spec/dummy_app/app/active_record/without_table.rb ================================================ # frozen_string_literal: true class WithoutTable < ActiveRecord::Base end ================================================ FILE: spec/dummy_app/app/assets/builds/.keep ================================================ ================================================ FILE: spec/dummy_app/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_tree ../../../../../src .js //= link application.js //= link application.css ================================================ FILE: spec/dummy_app/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // //= require rails-ujs.esm //= require turbo ================================================ FILE: spec/dummy_app/app/assets/javascripts/rails-ujs.esm.js.erb ================================================ <%= depend_on_asset('rails-ujs').to_s.gsub(/context = this/, 'context = (this || globalThis)') %> ================================================ FILE: spec/dummy_app/app/assets/javascripts/rails_admin/custom/ui.js ================================================ window.domReadyTriggered = []; document.addEventListener("rails_admin.dom_ready", function () { window.domReadyTriggered.push("plainjs/dot"); }); $(document).on("rails_admin.dom_ready", function () { window.domReadyTriggered.push("jquery/dot"); }); ================================================ FILE: spec/dummy_app/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self */ ================================================ FILE: spec/dummy_app/app/assets/stylesheets/rails_admin/custom/theming.scss ================================================ .navbar-brand small { opacity: 0.99; } ================================================ FILE: spec/dummy_app/app/assets/stylesheets/rails_admin.scss ================================================ $fa-font-path: "."; @import "rails_admin/src/rails_admin/styles/base.scss"; @import "trix/dist/trix"; ================================================ FILE: spec/dummy_app/app/controllers/application_controller.rb ================================================ # frozen_string_literal: true class ApplicationController < ActionController::Base protect_from_forgery end ================================================ FILE: spec/dummy_app/app/controllers/players_controller.rb ================================================ # frozen_string_literal: true class PlayersController < ApplicationController def show @player = Player.find(params[:id]) end end ================================================ FILE: spec/dummy_app/app/eager_loaded/basketball.rb ================================================ # frozen_string_literal: true class Basketball < Ball end ================================================ FILE: spec/dummy_app/app/frontend/entrypoints/application.js ================================================ import "@rails/ujs"; import "@hotwired/turbo-rails"; ================================================ FILE: spec/dummy_app/app/frontend/entrypoints/rails_admin.js ================================================ import "~/stylesheets/rails_admin.scss"; import "rails_admin/src/rails_admin/base"; import "flatpickr/dist/l10n/fr.js"; import "trix"; import "@rails/actiontext"; import * as ActiveStorage from "@rails/activestorage"; ActiveStorage.start(); window.domReadyTriggered = []; document.addEventListener("rails_admin.dom_ready", function () { window.domReadyTriggered.push("plainjs/dot"); }); $(document).on("rails_admin.dom_ready", function () { window.domReadyTriggered.push("jquery/dot"); }); ================================================ FILE: spec/dummy_app/app/frontend/stylesheets/rails_admin.scss ================================================ $fa-font-path: "@fortawesome/fontawesome-free/webfonts"; @import "rails_admin/src/rails_admin/styles/base"; ================================================ FILE: spec/dummy_app/app/javascript/application.js ================================================ import Rails from "@rails/ujs"; import "@hotwired/turbo-rails"; Rails.start(); ================================================ FILE: spec/dummy_app/app/javascript/rails_admin.js ================================================ import "rails_admin/src/rails_admin/base"; import "flatpickr/dist/l10n/fr.js"; import "trix"; import "@rails/actiontext"; import * as ActiveStorage from "@rails/activestorage"; ActiveStorage.start(); window.domReadyTriggered = []; document.addEventListener("rails_admin.dom_ready", function () { window.domReadyTriggered.push("plainjs/dot"); }); $(document).on("rails_admin.dom_ready", function () { window.domReadyTriggered.push("jquery/dot"); }); ================================================ FILE: spec/dummy_app/app/javascript/rails_admin.scss ================================================ @import "rails_admin/src/rails_admin/styles/base.scss"; ================================================ FILE: spec/dummy_app/app/jobs/application_job.rb ================================================ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: spec/dummy_app/app/jobs/null_job.rb ================================================ # frozen_string_literal: true class NullJob < ApplicationJob queue_as :default def perform(*args) # Do nothing end end ================================================ FILE: spec/dummy_app/app/locales/models.en.yml ================================================ en: activerecord: &en_attributes attributes: fan: name: Their Name team: manager: Team Manager main_sponsor: Main Sponsor fans: Some Fans mongoid: *en_attributes fr: activerecord: &fr_attributes attributes: fan: name: Son nom team: manager: Manager de l'équipe fans: Quelques fans mongoid: *fr_attributes ================================================ FILE: spec/dummy_app/app/mailers/.gitkeep ================================================ ================================================ FILE: spec/dummy_app/app/mongoid/another_field_test.rb ================================================ # frozen_string_literal: true class AnotherFieldTest include Mongoid::Document has_many :nested_field_tests, inverse_of: :another_field_test end ================================================ FILE: spec/dummy_app/app/mongoid/ball.rb ================================================ # frozen_string_literal: true class Ball include Mongoid::Document include Mongoid::Timestamps field :color, type: String has_one :comment, as: :commentable validates_presence_of :color, on: :create def to_param color.present? ? color.downcase.tr(' ', '-') : id end end ================================================ FILE: spec/dummy_app/app/mongoid/carrierwave_uploader.rb ================================================ # frozen_string_literal: true require 'mini_magick' class CarrierwaveUploader < CarrierWave::Uploader::Base # Include RMagick or ImageScience support: # include CarrierWave::RMagick include CarrierWave::MiniMagick # include CarrierWave::ImageScience # Choose what kind of storage to use for this uploader: storage :file # storage :fog # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end version :thumb do process resize_to_fill: [100, 100] end # Provide a default URL as a default if there hasn't been a file uploaded: # def default_url # "/images/fallback/" + [version_name, "default.png"].compact.join('_') # end # Process files as they are uploaded: # process scale: [200, 300] # # def scale(width, height) # # do something # end # Create different versions of your uploaded files: # version :thumb do # process scale: [50, 50] # end # Add an allowlist of extensions which are allowed to be uploaded. # For images you might use something like this: # def extension_allowlist # %w(jpg jpeg gif png) # end # Override the filename of the uploaded files: # Avoid using model.id or version_name here, see uploader/store.rb for details. # def filename # "something.jpg" if original_filename # end end ================================================ FILE: spec/dummy_app/app/mongoid/category.rb ================================================ # frozen_string_literal: true class Category include Mongoid::Document belongs_to :parent_category, class_name: 'Category' end ================================================ FILE: spec/dummy_app/app/mongoid/cms/basic_page.rb ================================================ # frozen_string_literal: true module Cms class BasicPage include Mongoid::Document field :title, type: String field :content, type: String include Mongoid::Timestamps validates :title, :content, presence: true end end ================================================ FILE: spec/dummy_app/app/mongoid/cms.rb ================================================ # frozen_string_literal: true module Cms def self.table_name_prefix 'cms_' end end ================================================ FILE: spec/dummy_app/app/mongoid/comment/confirmed.rb ================================================ # frozen_string_literal: true class Comment class Confirmed < Comment default_scope -> { where(content: 'something') } end end ================================================ FILE: spec/dummy_app/app/mongoid/comment.rb ================================================ # frozen_string_literal: true class Comment include Mongoid::Document field :content, type: String include Mongoid::Timestamps include Taggable belongs_to :commentable, polymorphic: true end ================================================ FILE: spec/dummy_app/app/mongoid/concerns/taggable.rb ================================================ # frozen_string_literal: true module Taggable extend ActiveSupport::Concern # dummy end ================================================ FILE: spec/dummy_app/app/mongoid/deeply_nested_field_test.rb ================================================ # frozen_string_literal: true class DeeplyNestedFieldTest include Mongoid::Document include Mongoid::Timestamps field :title, type: String belongs_to :nested_field_test, inverse_of: :deeply_nested_field_tests end ================================================ FILE: spec/dummy_app/app/mongoid/division.rb ================================================ # frozen_string_literal: true class Division include Mongoid::Document include Mongoid::Timestamps field :name, type: String belongs_to :league, foreign_key: 'custom_league_id' has_many :teams validates_presence_of(:custom_league_id) validates_presence_of(:name) end ================================================ FILE: spec/dummy_app/app/mongoid/draft.rb ================================================ # frozen_string_literal: true class Draft include Mongoid::Document include Mongoid::Timestamps belongs_to :player belongs_to :team field :date, type: Date field :round, type: Integer field :pick, type: Integer field :overall, type: Integer field :college, type: String field :notes, type: String validates_presence_of(:player_id, only_integer: true) validates_presence_of(:team_id, only_integer: true) validates_presence_of(:date) validates_numericality_of(:round, only_integer: true) validates_numericality_of(:pick, only_integer: true) validates_numericality_of(:overall, only_integer: true) end ================================================ FILE: spec/dummy_app/app/mongoid/embed.rb ================================================ # frozen_string_literal: true class Embed include Mongoid::Document field :name, type: String embedded_in :field_test end ================================================ FILE: spec/dummy_app/app/mongoid/fan.rb ================================================ # frozen_string_literal: true class Fan include Mongoid::Document include Mongoid::Timestamps field :name, type: String has_and_belongs_to_many :teams validates_presence_of(:name) end ================================================ FILE: spec/dummy_app/app/mongoid/field_test.rb ================================================ # frozen_string_literal: true require 'rails_admin/adapters/mongoid' class FieldTest include Mongoid::Document include Mongoid::Paperclip include ActiveModel::ForbiddenAttributesProtection extend Dragonfly::Model field :name, type: String field :title, type: String field :subject, type: String field :short_text, type: String field :array_field, type: Array field :big_decimal_field, type: BigDecimal field :boolean_field, type: Boolean field :bson_object_id_field, type: RailsAdmin::Adapters::Mongoid::Bson::OBJECT_ID field :bson_binary_field, type: BSON::Binary field :date_field, type: Date field :datetime_field, type: DateTime field :time_with_zone_field, type: ActiveSupport::TimeWithZone field :default_field field :float_field, type: Float field :hash_field, type: Hash field :integer_field, type: Integer field :object_field, type: Object field :range_field, type: Range field :string_field, type: String field :symbol_field, type: Symbol field :text_field, type: String field :time_field, type: Time field :format, type: String field :open, type: Boolean field :restricted_field, type: String field :protected_field, type: String field :al, as: :aliased_field, type: String has_mongoid_attached_file :paperclip_asset, styles: {thumb: '100x100>'} field :shrine_asset_data, type: String include ShrineUploader::Attachment.new(:shrine_asset) field :shrine_versioning_asset_data, type: String include ShrineVersioningUploader::Attachment.new(:shrine_versioning_asset) has_many :nested_field_tests, dependent: :destroy, inverse_of: :field_test, autosave: true accepts_nested_attributes_for :nested_field_tests, allow_destroy: true # on creation, comment is not saved without autosave: true has_one :comment, as: :commentable, autosave: true accepts_nested_attributes_for :comment, allow_destroy: true embeds_many :embeds accepts_nested_attributes_for :embeds, allow_destroy: true attr_accessor :delete_paperclip_asset before_validation { self.paperclip_asset = nil if delete_paperclip_asset == '1' } field :dragonfly_asset_name field :dragonfly_asset_uid dragonfly_accessor :dragonfly_asset mount_uploader :carrierwave_asset, CarrierwaveUploader # carrierwave-mongoid does not support mount_uploaders yet: # https://github.com/carrierwaveuploader/carrierwave-mongoid/issues/138 mount_uploaders :carrierwave_assets, CarrierwaveUploader validates :short_text, length: {maximum: 255} end ================================================ FILE: spec/dummy_app/app/mongoid/hardball.rb ================================================ # frozen_string_literal: true class Hardball < Ball end ================================================ FILE: spec/dummy_app/app/mongoid/image.rb ================================================ # frozen_string_literal: true class Image include Mongoid::Document include Mongoid::Paperclip has_mongoid_attached_file :file, styles: {medium: '300x300>', thumb: '100x100>'} validates_attachment_presence :file end ================================================ FILE: spec/dummy_app/app/mongoid/league.rb ================================================ # frozen_string_literal: true class League include Mongoid::Document field :name, type: String include Mongoid::Timestamps has_many :divisions, foreign_key: 'custom_league_id' validates_presence_of(:name) def custom_name "League '#{name}'" end end ================================================ FILE: spec/dummy_app/app/mongoid/managed_team.rb ================================================ # frozen_string_literal: true class ManagedTeam < Team belongs_to :user, class_name: 'ManagingUser', foreign_key: :manager, primary_key: :email, inverse_of: :teams end ================================================ FILE: spec/dummy_app/app/mongoid/managing_user.rb ================================================ # frozen_string_literal: true class ManagingUser < User has_one :team, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user has_many :teams, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user has_and_belongs_to_many :players, foreign_key: :player_names, primary_key: :name, inverse_of: :nil has_and_belongs_to_many :balls, primary_key: :color, inverse_of: :nil end ================================================ FILE: spec/dummy_app/app/mongoid/nested_field_test.rb ================================================ # frozen_string_literal: true class NestedFieldTest include Mongoid::Document field :title, type: String belongs_to :field_test, inverse_of: :nested_field_tests belongs_to :another_field_test, inverse_of: :nested_field_tests include Mongoid::Timestamps has_many :deeply_nested_field_tests, inverse_of: :nested_field_test accepts_nested_attributes_for :deeply_nested_field_tests, allow_destroy: true has_one :comment, as: :commentable, autosave: true accepts_nested_attributes_for :comment, allow_destroy: true, reject_if: proc { |attributes| attributes['content'].blank? } end ================================================ FILE: spec/dummy_app/app/mongoid/player.rb ================================================ # frozen_string_literal: true class Player include Mongoid::Document include Mongoid::Timestamps include ActiveModel::ForbiddenAttributesProtection field :deleted_at, type: DateTime belongs_to :team, inverse_of: :players field :name, type: String field :position, type: String field :number, type: Integer field :retired, type: Boolean, default: false field :injured, type: Boolean, default: false field :born_on, type: Date field :notes, type: String field :suspended, type: Boolean, default: false field :formation, type: String validates_presence_of(:name) validates_numericality_of(:number, only_integer: true) validates_uniqueness_of(:number, scope: :team_id, message: 'There is already a player with that number on this team') validates_each :name do |record, _attr, value| record.errors.add(:base, 'Player is cheating') if /on steroids/.match?(value.to_s) end has_one :draft, dependent: :destroy has_many :comments, as: :commentable before_destroy :destroy_hook scope :rails_admin_search, ->(query) { where(name: query.reverse) } def destroy_hook; end end ================================================ FILE: spec/dummy_app/app/mongoid/read_only_comment.rb ================================================ # frozen_string_literal: true class ReadOnlyComment < Comment def readonly? true end end ================================================ FILE: spec/dummy_app/app/mongoid/restricted_team.rb ================================================ # frozen_string_literal: true class RestrictedTeam < Team has_many :players, foreign_key: :team_id, dependent: :restrict_with_error end ================================================ FILE: spec/dummy_app/app/mongoid/shrine_uploader.rb ================================================ # frozen_string_literal: true class ShrineUploader < Shrine plugin :mongoid plugin :cached_attachment_data plugin :determine_mime_type plugin :pretty_location plugin :remove_attachment end ================================================ FILE: spec/dummy_app/app/mongoid/shrine_versioning_uploader.rb ================================================ # frozen_string_literal: true class ShrineVersioningUploader < Shrine plugin :mongoid plugin :cached_attachment_data plugin :determine_mime_type plugin :pretty_location plugin :remove_attachment if Gem.loaded_specs['shrine'].version >= Gem::Version.create('3') plugin :derivatives Attacher.derivatives_processor do |original| { thumb: FakeIO.new('', filename: File.basename(original.path), content_type: File.extname(original.path)), } end end end ================================================ FILE: spec/dummy_app/app/mongoid/team.rb ================================================ # frozen_string_literal: true class Team include Mongoid::Document include Mongoid::Timestamps belongs_to :division field :name, type: String field :logo_url, type: String field :manager, type: String field :ballpark, type: String field :mascot, type: String field :founded, type: Integer field :wins, type: Integer field :losses, type: Integer field :win_percentage, type: Float field :revenue, type: BigDecimal field :color, type: String field :custom_field, type: String field :main_sponsor, type: Integer has_many :players, inverse_of: :team, order: :_id.asc has_and_belongs_to_many :fans has_many :comments, as: :commentable validates_presence_of :division_id, only_integer: true validates_presence_of :manager validates_numericality_of :founded, only_integer: true, allow_blank: true validates_numericality_of :wins, only_integer: true validates_numericality_of :losses, only_integer: true validates_numericality_of :win_percentage validates_numericality_of :revenue, allow_nil: true # needed to force these attributes to :string type validates_length_of :logo_url, maximum: 255 validates_length_of :manager, maximum: 100 validates_length_of :ballpark, maximum: 100 validates_length_of :mascot, maximum: 100 def player_names_truncated players.collect(&:name).join(', ')[0..32] end def color_enum ['white', 'black', 'red', 'green', 'blué'] end scope :green, -> { where(color: 'red') } scope :red, -> { where(color: 'red') } scope :white, -> { where(color: 'white') } end ================================================ FILE: spec/dummy_app/app/mongoid/two_level/namespaced/polymorphic_association_test.rb ================================================ # frozen_string_literal: true module TwoLevel module Namespaced class PolymorphicAssociationTest include Mongoid::Document field :name, type: String has_many :comments, as: :commentable end end end ================================================ FILE: spec/dummy_app/app/mongoid/user/confirmed.rb ================================================ # frozen_string_literal: true class User class Confirmed < User end end ================================================ FILE: spec/dummy_app/app/mongoid/user.rb ================================================ # frozen_string_literal: true class User include Mongoid::Document include Mongoid::Paperclip include ActiveModel::ForbiddenAttributesProtection # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, :lockable and :timeoutable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable ## Database authenticatable field :email, type: String field :encrypted_password, type: String ## Recoverable field :reset_password_token, type: String field :reset_password_sent_at, type: Time ## Rememberable field :remember_created_at, type: Time ## Trackable field :sign_in_count, type: Integer field :current_sign_in_at, type: Time field :last_sign_in_at, type: Time field :current_sign_in_ip, type: String field :last_sign_in_ip, type: String ## Encryptable # field :password_salt, type: String ## Confirmable # field :confirmation_token, type: String # field :confirmed_at, type: Time # field :confirmation_sent_at, type: Time # field :unconfirmed_email, type: String # Only if using reconfirmable ## Lockable # field :failed_attempts, type: Integer # Only if lock strategy is :failed_attempts # field :unlock_token, type: String # Only if unlock strategy is :email or :both # field :locked_at, type: Time # Token authenticatable # field :authentication_token, type: String ## Invitable # field :invitation_token, type: String include Mongoid::Timestamps # Add Paperclip support for avatars has_mongoid_attached_file :avatar, styles: {medium: '300x300>', thumb: '100x100>'} field :roles, type: Array def attr_accessible_role :custom_role end attr_accessor :delete_avatar before_validation { self.avatar = nil if delete_avatar == '1' } end ================================================ FILE: spec/dummy_app/app/views/layouts/application.html.erb ================================================ DummyApp <% case CI_ASSET %> <% when :webpacker %> <%= javascript_pack_tag "application" %> <% when :importmap %> <%= javascript_importmap_tags %> <% when :vite %> <%= vite_client_tag %> <%= vite_javascript_tag 'application' %> <% else %> <%= stylesheet_link_tag "application", media: "all" %> <%= javascript_include_tag "application", type: 'module' %> <% end %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: spec/dummy_app/app/views/players/show.html.erb ================================================

    <%= @player.name %>

    <%= link_to 'Back to admin', rails_admin.show_path(model_name: 'player', id: @player.id) %> ================================================ FILE: spec/dummy_app/babel.config.js ================================================ module.exports = function (api) { var validEnv = ["development", "test", "production"]; var currentEnv = api.env(); var isDevelopmentEnv = api.env("development"); var isProductionEnv = api.env("production"); var isTestEnv = api.env("test"); if (!validEnv.includes(currentEnv)) { throw new Error( "Please specify a valid `NODE_ENV` or " + '`BABEL_ENV` environment variables. Valid values are "development", ' + '"test", and "production". Instead, received: ' + JSON.stringify(currentEnv) + "." ); } return { presets: [ isTestEnv && [ "@babel/preset-env", { targets: { node: "current", }, }, ], (isProductionEnv || isDevelopmentEnv) && [ "@babel/preset-env", { forceAllTransforms: true, useBuiltIns: "entry", corejs: 3, modules: false, exclude: ["transform-typeof-symbol"], }, ], ].filter(Boolean), plugins: [ "babel-plugin-macros", "@babel/plugin-syntax-dynamic-import", isTestEnv && "babel-plugin-dynamic-import-node", "@babel/plugin-transform-destructuring", [ "@babel/plugin-proposal-class-properties", { loose: true, }, ], [ "@babel/plugin-proposal-object-rest-spread", { useBuiltIns: true, }, ], [ "@babel/plugin-proposal-private-methods", { loose: true, }, ], [ "@babel/plugin-proposal-private-property-in-object", { loose: true, }, ], [ "@babel/plugin-transform-runtime", { helpers: false, }, ], [ "@babel/plugin-transform-regenerator", { async: false, }, ], ].filter(Boolean), }; }; ================================================ FILE: spec/dummy_app/bin/bundle ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'bundle' is installed as part of a gem, and # this file is here to facilitate running it. # require "rubygems" m = Module.new do module_function def invoked_as_script? File.expand_path($0) == File.expand_path(__FILE__) end def env_var_version ENV["BUNDLER_VERSION"] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN bundler_version = a end next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ bundler_version = $1 update_index = i end bundler_version end def gemfile gemfile = ENV["BUNDLE_GEMFILE"] return gemfile if gemfile && !gemfile.empty? File.expand_path("../Gemfile", __dir__) end def lockfile lockfile = case File.basename(gemfile) when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) else "#{gemfile}.lock" end File.expand_path(lockfile) end def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ Regexp.last_match(1) end def bundler_requirement @bundler_requirement ||= env_var_version || cli_arg_version || bundler_requirement_for(lockfile_version) end def bundler_requirement_for(version) return "#{Gem::Requirement.default}.a" unless version bundler_gem_version = Gem::Version.new(version) requirement = bundler_gem_version.approximate_recommendation return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") requirement += ".a" if bundler_gem_version.prerelease? requirement end def load_bundler! ENV["BUNDLE_GEMFILE"] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do gem "bundler", bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do require "bundler/version" end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" exit 42 end def activation_error_handling yield nil rescue StandardError, LoadError => e e end end m.load_bundler! if m.invoked_as_script? load Gem.bin_path("bundler", "bundle") end ================================================ FILE: spec/dummy_app/bin/dev ================================================ #!/usr/bin/env bash if ! command -v foreman &> /dev/null then echo "Installing foreman..." gem install foreman fi foreman start -f Procfile.dev "$@" ================================================ FILE: spec/dummy_app/bin/docker-entrypoint ================================================ #!/bin/bash -e exec "${@}" ================================================ FILE: spec/dummy_app/bin/importmap ================================================ #!/usr/bin/env ruby require_relative "../config/application" require "importmap/commands" ================================================ FILE: spec/dummy_app/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: spec/dummy_app/bin/rake ================================================ #!/usr/bin/env ruby require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: spec/dummy_app/bin/setup ================================================ #!/usr/bin/env ruby require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') or system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: spec/dummy_app/bin/update ================================================ #!/usr/bin/env ruby require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system 'bundle check' or system! 'bundle install' puts "\n== Updating database ==" system! 'bin/rails db:migrate' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: spec/dummy_app/bin/vite ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'vite' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("vite_ruby", "vite") ================================================ FILE: spec/dummy_app/bin/webpack ================================================ #!/usr/bin/env ruby ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" ENV["NODE_ENV"] ||= "development" require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) require "bundler/setup" require "webpacker" require "webpacker/webpack_runner" APP_ROOT = File.expand_path("..", __dir__) Dir.chdir(APP_ROOT) do Webpacker::WebpackRunner.run(ARGV) end ================================================ FILE: spec/dummy_app/bin/webpack-dev-server ================================================ #!/usr/bin/env ruby ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" ENV["NODE_ENV"] ||= "development" require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) require "bundler/setup" require "webpacker" require "webpacker/dev_server_runner" APP_ROOT = File.expand_path("..", __dir__) Dir.chdir(APP_ROOT) do Webpacker::DevServerRunner.run(ARGV) end ================================================ FILE: spec/dummy_app/config/application.rb ================================================ # frozen_string_literal: true require File.expand_path('boot', __dir__) require 'action_controller/railtie' require 'action_mailer/railtie' begin require CI_ORM.to_s require "#{CI_ORM}/railtie" rescue LoadError # ignore errors end require 'active_storage/engine' if CI_ORM == :active_record require 'action_text/engine' if CI_ORM == :active_record case CI_ASSET when :webpacker require 'webpacker' when :sprockets, :webpack require 'sprockets/railtie' when :importmap require 'sprockets/railtie' require 'importmap-rails' when :vite require 'vite_rails' end # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups, CI_ORM) module DummyApp class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. config.load_defaults Rails.version[0, 3] (CI_TARGET_ORMS - [CI_ORM]).each { |orm| config.paths.add "app/#{orm}", eager_load: false } config.eager_load_paths = (config.try(:all_eager_load_paths) || config.eager_load_paths).reject { |p| p =~ %r{/app/([^/]+)} && !%W[controllers jobs locales mailers #{CI_ORM}].include?(Regexp.last_match[1]) } config.eager_load_paths += %W[#{config.root}/app/eager_loaded] config.autoload_paths += %W[#{config.root}/lib] config.i18n.load_path += Dir[Rails.root.join('app', 'locales', '*.{rb,yml}').to_s] if CI_ORM == :active_record config.active_record.time_zone_aware_types = %i[datetime time] config.active_record.yaml_column_permitted_classes = [Symbol] if [ActiveRecord::Base, ActiveRecord].any? { |klass| klass.respond_to?(:yaml_column_permitted_classes=) } end config.active_storage.service = :local if defined?(ActiveStorage) config.active_storage.replace_on_assign_to_many = false if defined?(ActiveStorage) && ActiveStorage.version < Gem::Version.create('6.1') case CI_ASSET when :webpack config.assets.precompile += %w[rails_admin.js rails_admin.css] when :importmap config.assets.paths << RailsAdmin::Engine.root.join('src') config.assets.precompile += %w[rails_admin.js rails_admin.css] config.importmap.cache_sweepers << RailsAdmin::Engine.root.join('src') end initializer :ignore_unused_assets_path, after: :append_assets_path, group: :all do |app| case CI_ASSET when :webpack, :importmap app.config.assets.paths.delete(Rails.root.join('app', 'assets', 'javascripts').to_s) when :sprockets app.config.assets.paths.delete(Rails.root.join('app', 'assets', 'builds').to_s) end end end end ================================================ FILE: spec/dummy_app/config/boot.rb ================================================ # frozen_string_literal: true CI_ORM = (ENV['CI_ORM'] || :active_record).to_sym unless defined?(CI_ORM) CI_TARGET_ORMS = %i[active_record mongoid].freeze CI_ASSET = (ENV['CI_ASSET'] || :sprockets).to_sym unless defined?(CI_ASSET) ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. ================================================ FILE: spec/dummy_app/config/database.yml ================================================ sqlite: &sqlite adapter: sqlite3 pool: 5 timeout: 5000 postgresql: adapter: postgresql database: rails_admin username: postgres password: host: 127.0.0.1 encoding: utf8 pool: 5 timeout: 5000 mysql: adapter: mysql2 database: rails_admin username: root password: host: 127.0.0.1 encoding: utf8 pool: 5 timeout: 5000 development: <<: *sqlite database: db/development.sqlite3 test: <<: *sqlite database: db/test.sqlite3 production: <<: *sqlite database: db/production.sqlite3 ================================================ FILE: spec/dummy_app/config/dockerfile.yml ================================================ # generated by dockerfile-rails --- options: label: fly_launch_runtime: rails sentry: false ================================================ FILE: spec/dummy_app/config/environment.rb ================================================ # frozen_string_literal: true # Load the Rails application. require File.expand_path('application', __dir__) # Initialize the Rails application. DummyApp::Application.initialize! ================================================ FILE: spec/dummy_app/config/environments/development.rb ================================================ # frozen_string_literal: true DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = {'Cache-Control' => 'public, max-age=172800'} else config.action_controller.perform_caching = false config.cache_store = :null_store end # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load if CI_ORM == :active_record if config.respond_to?(:assets) # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. config.assets.raise_runtime_errors = true end # Raises error for missing translations # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker end ================================================ FILE: spec/dummy_app/config/environments/production.rb ================================================ # frozen_string_literal: true DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? config.static_cache_control = 'public, max-age=31536000' # Compress JavaScripts and CSS. # config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Action Cable endpoint configuration # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :info # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :request_id ] # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "dummy_app_#{Rails.env}" # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Disable yjit to save memory. config.yjit = false end ================================================ FILE: spec/dummy_app/config/environments/test.rb ================================================ # frozen_string_literal: true DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. if config.respond_to?(:public_file_server) config.public_file_server.enabled = true config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'} else config.serve_static_files = true config.static_cache_control = 'public, max-age=3600' end # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = Rails.gem_version >= Gem::Version.new('7.1') ? :none : false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Randomize the order test cases are executed. config.active_support.test_order = :random # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end ================================================ FILE: spec/dummy_app/config/importmap.rails_admin.rb ================================================ # frozen_string_literal: true # Pin npm packages by running ./bin/importmap pin 'rails_admin', preload: true pin 'rails_admin/src/rails_admin/base', to: 'rails_admin/base.js' pin '@hotwired/turbo', to: 'https://ga.jspm.io/npm:@hotwired/turbo@7.1.0/dist/turbo.es2017-esm.js' pin '@hotwired/turbo-rails', to: 'https://ga.jspm.io/npm:@hotwired/turbo-rails@7.1.3/app/javascript/turbo/index.js' pin '@popperjs/core', to: 'https://ga.jspm.io/npm:@popperjs/core@2.11.5/dist/esm/popper.js' pin '@rails/actioncable/src', to: 'https://ga.jspm.io/npm:@rails/actioncable@7.0.3-1/src/index.js' pin '@rails/actiontext', to: 'https://ga.jspm.io/npm:@rails/actiontext@7.0.3-1/app/javascript/actiontext/index.js' pin '@rails/activestorage', to: 'https://ga.jspm.io/npm:@rails/activestorage@7.0.3-1/app/assets/javascripts/activestorage.esm.js' pin '@rails/ujs', to: 'https://ga.jspm.io/npm:@rails/ujs@7.0.3-1/lib/assets/compiled/rails-ujs.js' pin 'bootstrap', to: 'https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js' pin 'flatpickr', to: 'https://ga.jspm.io/npm:flatpickr@4.6.13/dist/flatpickr.js' pin 'flatpickr/', to: 'https://ga.jspm.io/npm:flatpickr@4.6.13/' pin 'jquery', to: 'https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js' pin 'jquery-ui/', to: 'https://ga.jspm.io/npm:jquery-ui@1.13.1/' pin 'trix', to: 'https://ga.jspm.io/npm:trix@2.0.0-beta.0/dist/trix.js' ================================================ FILE: spec/dummy_app/config/importmap.rb ================================================ # frozen_string_literal: true # Pin npm packages by running ./bin/importmap pin 'application', preload: true pin '@hotwired/turbo-rails', to: 'https://ga.jspm.io/npm:@hotwired/turbo-rails@7.1.3/app/javascript/turbo/index.js' pin '@rails/ujs', to: 'https://ga.jspm.io/npm:@rails/ujs@6.0.5/lib/assets/compiled/rails-ujs.js' pin '@hotwired/turbo', to: 'https://ga.jspm.io/npm:@hotwired/turbo@7.1.0/dist/turbo.es2017-esm.js' pin '@rails/actioncable/src', to: 'https://ga.jspm.io/npm:@rails/actioncable@7.0.3/src/index.js' ================================================ FILE: spec/dummy_app/config/initializers/application_controller_renderer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) ================================================ FILE: spec/dummy_app/config/initializers/assets.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' if Rails.application.config.respond_to?(:assets) # Add additional assets to the asset load path # Rails.application.config.assets.paths << Emoji.images_path Rails.application.config.assets.paths << Rails.root.join('node_modules/@fortawesome/fontawesome-free/webfonts') if Rails.application.config.respond_to?(:assets) # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) ================================================ FILE: spec/dummy_app/config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: spec/dummy_app/config/initializers/cookies_serializer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # This is a new Rails 5.0 default, so introduced as a config to ensure apps made with earlier versions of Rails aren't affected when upgrading. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: spec/dummy_app/config/initializers/cors.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Avoid CORS issues when API is called from the frontend app. # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. # Read more: https://github.com/cyu/rack-cors # Rails.application.config.middleware.insert_before 0, Rack::Cors do # allow do # origins 'example.com' # # resource '*', # headers: :any, # methods: [:get, :post, :put, :patch, :delete, :options, :head] # end # end ================================================ FILE: spec/dummy_app/config/initializers/devise.rb ================================================ # frozen_string_literal: true # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. config.secret_key = '5c72fe9ca98a9862e235c7fb8277e3f6a02d41e0f941beebaa5271a712c46da90f5e4a04fde095d7f2f219cd41ec014c72d995837424164ae7ffc0f44eaa3742' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. require "devise/orm/#{CI_ORM}" # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is # just :email. You can configure it to use [:username, :subdomain], so for # authenticating a user, both parameters are required. Remember that those # parameters are used only when authenticating and not when retrieving from # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. # config.authentication_keys = [ :email ] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the # find_for_authentication method and considered in your model lookup. For instance, # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. # The same considerations mentioned for authentication_keys also apply to request_keys. # config.request_keys = [] # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. config.case_insensitive_keys = [:email] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. config.strip_whitespace_keys = [:email] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the # given strategies, for example, `config.params_authenticatable = [:database]` will # enable it only for database (email + password) authentication. # config.params_authenticatable = true # Tell if authentication through HTTP Auth is enabled. False by default. # It can be set to an array that will enable http authentication only for the # given strategies, for example, `config.http_authenticatable = [:token]` will # enable it only for token authentication. The supported strategies are: # :database = Support basic authentication with authentication key + password # :token = Support basic authentication with token authentication key # :token_options = Support token authentication with options as defined in # http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html # config.http_authenticatable = false # If http headers should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true # The realm used in Http Basic Authentication. 'Application' by default. # config.http_authentication_realm = 'Application' # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. # config.paranoid = true # By default Devise will store the user in session. You can skip storage for # :http_auth and :token_auth by adding those symbols to the array below. # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by # passing skip: :sessions to `devise_for` in your config/routes.rb config.skip_session_storage = [:http_auth] # By default, Devise cleans up the CSRF token on authentication to # avoid CSRF token fixation attacks. This means that, when using AJAX # requests for sign in and sign up, you need to get a new CSRF token # from the server. You can disable this option at your own risk. # config.clean_up_csrf_token_on_authentication = true # ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 10. If # using other encryptors, it sets how many times you want the password re-encrypted. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use # a value less than 10 in other environments. config.stretches = Rails.env.test? ? 1 : 10 # Setup a pepper to generate the encrypted password. # config.pepper = 'db4c2356895fab849584ee71a3467a20720ff9b629f27a0da70bd4176773c5360e25d453f1801c69e2e0e61f3a9b5631cda52d7da99b4ced83ea66940db3ce0e' # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without # confirming their account. For instance, if set to 2.days, the user will be # able to access the website for two days without confirming their account, # access will be blocked just in the third day. Default is 0.days, meaning # the user cannot access the website without confirming their account. # config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before their # token becomes invalid. For example, if set to 3.days, the user can confirm # their account within 3 days after the mail was sent, but on the fourth day # their account can't be confirmed with the token any more. # Default is nil, meaning there is no restriction on how long a user can take # before confirming their account. # config.confirm_within = 3.days # If true, requires any email changes to be confirmed (exactly the same way as # initial account confirmation) to be applied. Requires additional unconfirmed_email # db field (see migrations). Until confirmed new email is stored in # unconfirmed email column, and copied to email column on successful confirmation. config.reconfirmable = true # Defines which key will be used when confirming an account # config.confirmation_keys = [ :email ] # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false # Options to be passed to the created cookie. For instance, you can set # secure: true in order to force SSL only cookies. # config.rememberable_options = {} # ==> Configuration for :validatable # Range for password length. Default is 8..128. config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly # to give user feedback and not to assert the e-mail validity. # config.email_regexp = /\A[^@]+@[^@]+\z/ # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. # config.timeout_in = 30.minutes # If true, expires auth token on session timeout. # config.expire_auth_token_on_timeout = false # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. # config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account # config.unlock_keys = [ :email ] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. # config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. # config.maximum_attempts = 20 # Time interval to unlock the account if :time is enabled as unlock_strategy. # config.unlock_in = 1.hour # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account # config.reset_password_keys = [ :email ] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 6.hours # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) # and :restful_authentication_sha1 (then you should set stretches to 10, and copy # REST_AUTH_SITE_KEY to pepper). # # Require the `devise-encryptable` gem when using anything other than bcrypt # config.encryptor = :sha512 # ==> Configuration for :token_authenticatable # Defines name of the authentication token params key # config.token_authentication_key = :auth_token # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. # config.scoped_views = false # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). # config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. # config.sign_out_all_scopes = true # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like # :html, should redirect to the sign in page when the user does not have # access, but formats like :xml or :json, should return 401. # # If you have any extra navigational formats, like :iphone or :mobile, you # should add them to the navigational formats lists. # # The "*/*" below is required to match Internet Explorer requests. # config.navigational_formats = ['*/*', :html] # The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = :delete # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # # config.warden do |manager| # manager.intercept_401 = false # manager.default_strategies(scope: :user).unshift :some_external_strategy # end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine # is mountable, there are some extra configurations to be taken into account. # The following options are available, assuming the engine is mounted as: # # mount MyEngine, at: '/my_engine' # # The router that invoked `devise_for`, in the example above, would be: # config.router_name = :my_engine # # When using omniauth, Devise cannot automatically set Omniauth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' end ================================================ FILE: spec/dummy_app/config/initializers/dragonfly.rb ================================================ # frozen_string_literal: true require 'dragonfly' # Logger Dragonfly.logger = Rails.logger # Configure Dragonfly.app.configure do plugin :imagemagick protect_from_dos_attacks true secret '904547b2be647f0e11a76933b3220d6bd2fb83f380ac760125e4daa3b9504b4e' url_format '/media/:job/:name' datastore(:file, root_path: Rails.root.join('public/system/dragonfly', Rails.env), server_root: Rails.root.join('public')) end # Mount as middleware Rails.application.middleware.use Dragonfly::Middleware ================================================ FILE: spec/dummy_app/config/initializers/filter_parameter_logging.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: spec/dummy_app/config/initializers/inflections.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: spec/dummy_app/config/initializers/mime_types.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: spec/dummy_app/config/initializers/mongoid.rb ================================================ # frozen_string_literal: true if CI_ORM == :mongoid filename = if Mongoid.respond_to?(:belongs_to_required_by_default=) 'mongoid6.yml' else 'mongoid5.yml' end ::Mongoid.load!(Rails.root.join('config', filename)) end ================================================ FILE: spec/dummy_app/config/initializers/rails_admin.rb ================================================ # frozen_string_literal: true RailsAdmin.config do |c| c.asset_source = CI_ASSET c.model Team do include_all_fields field :color, :hidden end if Rails.env.production? # Live demo configuration c.main_app_name = ['RailsAdmin', 'Live Demo'] c.included_models = %w[Comment Division Draft Fan FieldTest League NestedFieldTest Player Team User] c.model 'FieldTest' do configure :paperclip_asset do visible false end end c.model 'League' do configure :players do visible false end end end end ================================================ FILE: spec/dummy_app/config/initializers/secret_token.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure your secret_key_base is kept private # if you're sharing your code publicly. DummyApp::Application.config.secret_key_base = '11cefe91820bbc7c4ef81114d5b8e9237bce9c1d5d1b4a2f8b333e151dfa3ff43b3db8d0d3b4dc414f15113f28c5d779e829c3cb3fbcdc7c5a94fe7fdcb2c81c' ================================================ FILE: spec/dummy_app/config/initializers/session_patch.rb ================================================ # frozen_string_literal: true require 'action_dispatch/middleware/session/abstract_store' # When ORM was switched, but another ORM's model class still exists in session # (Devise saves User model to session), ActionDispatch raises ActionDispatch::Session::SessionRestoreError # and app can't be started unless you delete your browser's cookie data. # To prevent this situation, detect this problem here and reset session data # so user can make another login via Devise. ActionDispatch::Session::StaleSessionCheck.module_eval do def stale_session_check! yield rescue ArgumentError {} end end ================================================ FILE: spec/dummy_app/config/initializers/session_store.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_dummy_app_session' ================================================ FILE: spec/dummy_app/config/initializers/shrine.rb ================================================ # frozen_string_literal: true require 'shrine' require 'shrine/storage/memory' Shrine.storages = { cache: Shrine::Storage::Memory.new, store: Shrine::Storage::Memory.new, } ================================================ FILE: spec/dummy_app/config/initializers/wrap_parameters.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: spec/dummy_app/config/locales/devise.en.yml ================================================ # Additional translations at https://github.com/plataformatec/devise/wiki/I18n en: devise: confirmations: confirmed: "Your account was successfully confirmed." confirmed_and_signed_in: "Your account was successfully confirmed. You are now signed in." send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." failure: already_authenticated: "You are already signed in." inactive: "Your account is not activated yet." invalid: "Invalid email or password." invalid_token: "Invalid authentication token." locked: "Your account is locked." not_found_in_database: "Invalid email or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your account before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" reset_password_instructions: subject: "Reset password instructions" unlock_instructions: subject: "Unlock Instructions" omniauth_callbacks: failure: 'Could not authenticate you from %{kind} because "%{reason}".' success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." send_instructions: "You will receive an email with instructions about how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password was changed successfully. You are now signed in." updated_not_active: "Your password was changed successfully." registrations: destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." signed_up: "Welcome! You have signed up successfully." signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." updated: "You updated your account successfully." sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." unlocks: send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: already_confirmed: "was already confirmed, please try signing in" confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" expired: "has expired, please request a new one" not_found: "not found" not_locked: "was not locked" not_saved: one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:" ================================================ FILE: spec/dummy_app/config/locales/en.yml ================================================ en: admin: help: team: name: "Team Name Help Text." ================================================ FILE: spec/dummy_app/config/locales/fr.yml ================================================ fr: date: formats: default: "%d/%m/%Y" short: "%e %b" compact: "%d/%m/%y" long: "%e %B %Y" day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi] abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam] month_names: [ ~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre, ] abbr_month_names: [ ~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc., ] order: - :day - :month - :year time: formats: default: "%d %B %Y %H:%M:%S" short: "%d %b %H:%M" compact: "%d/%m/%y %H:%M" long: "%A %d %B %Y %H:%M" verb: "%d %B %Y" am: "am" pm: "pm" ================================================ FILE: spec/dummy_app/config/mongoid5.yml ================================================ defaults: &defaults use_utc: false use_activesupport_time_zone: true development: <<: *defaults database: dummy_app_development clients: default: database: dummy_app_development hosts: - 127.0.0.1:27017 test: <<: *defaults clients: default: database: dummy_app_test hosts: - 127.0.0.1:27017 ================================================ FILE: spec/dummy_app/config/mongoid6.yml ================================================ defaults: &defaults use_utc: false use_activesupport_time_zone: true options: belongs_to_required_by_default: false development: <<: *defaults database: dummy_app_development clients: default: database: dummy_app_development hosts: - 127.0.0.1:27017 test: <<: *defaults clients: default: database: dummy_app_test hosts: - 127.0.0.1:27017 ================================================ FILE: spec/dummy_app/config/puma.rb ================================================ # frozen_string_literal: true threads 0, ENV.fetch('RAILS_MAX_THREADS', 3) port ENV.fetch('PORT', 3000) ================================================ FILE: spec/dummy_app/config/routes.rb ================================================ # frozen_string_literal: true DummyApp::Application.routes.draw do # Needed for :show_in_app tests resources :players, only: [:show] devise_for :users mount RailsAdmin::Engine => '/admin', as: 'rails_admin' root to: 'rails_admin/main#dashboard' end ================================================ FILE: spec/dummy_app/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. development: secret_key_base: fe661de3f58b9d446d992466c6e623394828bb6a0af4853bdef9de6ac17b847f418c2aa5778afa4af7b34906df5075e4d5dc6f4b61def924b7ae58f886608770 test: secret_key_base: 3bc2c7ebeb21e93fd8cb1166ae0cdb560c2fa8417e5dd016f691765b5e5a455f53841c01906dfd3b779beed3ad5062be664426001cf9e54ec0874c273a8e64eb # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ================================================ FILE: spec/dummy_app/config/storage.yml ================================================ local: service: Disk root: <%= Rails.root.join('public', 'system') %> ================================================ FILE: spec/dummy_app/config/vite.json ================================================ { "all": { "sourceCodeDir": "app/frontend", "watchAdditionalPaths": ["../../src"] }, "development": { "autoBuild": true, "publicOutputDir": "vite", "port": 3036 }, "test": { "autoBuild": false, "publicOutputDir": "vite" } } ================================================ FILE: spec/dummy_app/config/webpack/development.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || "development"; const environment = require("./environment"); module.exports = environment.toWebpackConfig(); ================================================ FILE: spec/dummy_app/config/webpack/environment.js ================================================ const { environment } = require("@rails/webpacker"); module.exports = environment; ================================================ FILE: spec/dummy_app/config/webpack/production.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || "production"; const environment = require("./environment"); module.exports = environment.toWebpackConfig(); ================================================ FILE: spec/dummy_app/config/webpack/test.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || "development"; const environment = require("./environment"); module.exports = environment.toWebpackConfig(); ================================================ FILE: spec/dummy_app/config/webpacker.yml ================================================ # Note: You must restart bin/webpack-dev-server for changes to take effect default: &default source_path: app/javascript source_entry_path: . public_root_path: public public_output_path: packs cache_path: tmp/cache/webpacker webpack_compile_output: true # Additional paths webpack should lookup modules # ['app/assets', 'engine/foo/app/assets'] additional_paths: ["node_modules/rails_admin/src"] # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false # Extract and emit a css file extract_css: true static_assets_extensions: - .jpg - .jpeg - .png - .gif - .tiff - .ico - .svg - .eot - .otf - .ttf - .woff - .woff2 extensions: - .mjs - .js - .sass - .scss - .css - .module.sass - .module.scss - .module.css - .png - .svg - .gif - .jpeg - .jpg development: <<: *default compile: true # Reference: https://webpack.js.org/configuration/dev-server/ dev_server: https: false host: localhost port: 3035 public: localhost:3035 hmr: false # Inline should be set to true if using HMR inline: true overlay: true compress: true disable_host_check: true use_local_ip: false quiet: false pretty: false headers: "Access-Control-Allow-Origin": "*" watch_options: ignored: "**/node_modules/**" test: <<: *default compile: true additional_paths: ["src"] # relative from the project root # Compile test packs to a separate directory public_output_path: packs-test production: <<: *default # Production depends on precompilation of packs prior to booting for performance. compile: false # Extract and emit a css file extract_css: true # Cache manifest.json for performance cache_manifest: true ================================================ FILE: spec/dummy_app/config.ru ================================================ # frozen_string_literal: true # This file is used by Rack-based servers to start the application. require ::File.expand_path('config/environment', __dir__) run DummyApp::Application ================================================ FILE: spec/dummy_app/db/migrate/00000000000001_create_divisions_migration.rb ================================================ # frozen_string_literal: true class CreateDivisionsMigration < ActiveRecord::Migration[5.0] def self.up create_table :divisions do |t| t.timestamps null: false t.integer :league_id t.string :name, limit: 50, null: false end end def self.down drop_table(:divisions) end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000002_create_drafts_migration.rb ================================================ # frozen_string_literal: true class CreateDraftsMigration < ActiveRecord::Migration[5.0] def self.up create_table :drafts do |t| t.timestamps null: false t.integer :player_id t.integer :team_id t.date :date t.integer :round t.integer :pick t.integer :overall t.string :college, limit: 100 t.text :notes end end def self.down drop_table :drafts end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000003_create_leagues_migration.rb ================================================ # frozen_string_literal: true class CreateLeaguesMigration < ActiveRecord::Migration[5.0] def self.up create_table :leagues do |t| t.timestamps null: false t.string :name, limit: 50, null: false end end def self.down drop_table :leagues end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000004_create_players_migration.rb ================================================ # frozen_string_literal: true class CreatePlayersMigration < ActiveRecord::Migration[5.0] def self.up create_table :players do |t| t.timestamps null: false t.datetime :deleted_at t.integer :team_id t.string :name, limit: 100, null: false t.string :position, limit: 50 t.integer :number, null: false t.boolean :retired, default: false t.boolean :injured, default: false t.date :born_on t.text :notes end end def self.down drop_table :players end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000005_create_teams_migration.rb ================================================ # frozen_string_literal: true class CreateTeamsMigration < ActiveRecord::Migration[5.0] def self.up create_table :teams do |t| t.timestamps null: false t.integer :league_id t.integer :division_id t.string :name, limit: 50 t.string :logo_url, limit: 255 t.string :manager, limit: 100, null: false t.string :ballpark, limit: 100 t.string :mascot, limit: 100 t.integer :founded t.integer :wins t.integer :losses t.float :win_percentage end end def self.down drop_table :teams end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000006_devise_create_users.rb ================================================ # frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[5.0] def self.up create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: '' t.string :encrypted_password, null: false, default: '' ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable t.integer :sign_in_count, default: 0 t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip ## Encryptable t.string :password_salt ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at # Token authenticatable # t.string :authentication_token t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true end def self.down drop_table :users end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000007_create_histories_table.rb ================================================ # frozen_string_literal: true class CreateHistoriesTable < ActiveRecord::Migration[5.0] def self.up create_table :histories do |t| t.string :message # title, name, or object_id t.string :username t.integer :item t.string :table t.timestamps null: false end add_index(:histories, %i[item table]) end def self.down drop_table :histories end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000008_create_fans_migration.rb ================================================ # frozen_string_literal: true class CreateFansMigration < ActiveRecord::Migration[5.0] def self.up create_table :fans do |t| t.timestamps null: false t.string :name, limit: 100, null: false end end def self.down drop_table :fans end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000009_create_fans_teams_migration.rb ================================================ # frozen_string_literal: true class CreateFansTeamsMigration < ActiveRecord::Migration[5.0] def self.up create_table :fans_teams, id: false do |t| t.integer :fan_id, :team_id end end def self.down drop_table :fans_teams end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000010_add_revenue_to_team_migration.rb ================================================ # frozen_string_literal: true class AddRevenueToTeamMigration < ActiveRecord::Migration[5.0] def self.up add_column :teams, :revenue, :decimal, precision: 18, scale: 2 end def self.down remove_column :teams, :revenue end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000011_add_suspended_to_player_migration.rb ================================================ # frozen_string_literal: true class AddSuspendedToPlayerMigration < ActiveRecord::Migration[5.0] def self.up add_column :players, :suspended, :boolean, default: false end def self.down remove_column :players, :suspended end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000012_add_avatar_columns_to_user.rb ================================================ # frozen_string_literal: true class AddAvatarColumnsToUser < ActiveRecord::Migration[5.0] def self.up add_column :users, :avatar_file_name, :string add_column :users, :avatar_content_type, :string add_column :users, :avatar_file_size, :integer add_column :users, :avatar_updated_at, :datetime end def self.down remove_column :users, :avatar_file_name remove_column :users, :avatar_content_type remove_column :users, :avatar_file_size remove_column :users, :avatar_updated_at end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000013_add_roles_to_user.rb ================================================ # frozen_string_literal: true class AddRolesToUser < ActiveRecord::Migration[5.0] def self.up add_column :users, :roles, :string end def self.down remove_column :users, :roles end end ================================================ FILE: spec/dummy_app/db/migrate/00000000000014_add_color_to_team_migration.rb ================================================ # frozen_string_literal: true class AddColorToTeamMigration < ActiveRecord::Migration[5.0] def self.up add_column :teams, :color, :string end def self.down remove_column :teams, :color end end ================================================ FILE: spec/dummy_app/db/migrate/20101223222233_create_rel_tests.rb ================================================ # frozen_string_literal: true class CreateRelTests < ActiveRecord::Migration[5.0] def self.up create_table :rel_tests do |t| t.integer :league_id t.integer :division_id, null: false t.integer :player_id t.timestamps null: false end end def self.down drop_table :rel_tests end end ================================================ FILE: spec/dummy_app/db/migrate/20110103205808_create_comments.rb ================================================ # frozen_string_literal: true class CreateComments < ActiveRecord::Migration[5.0] def self.up create_table :comments do |t| t.integer :commentable_id t.string :commentable_type t.text :content t.timestamps null: false end end def self.down drop_table :comments end end ================================================ FILE: spec/dummy_app/db/migrate/20110123042530_rename_histories_to_rails_admin_histories.rb ================================================ # frozen_string_literal: true class RenameHistoriesToRailsAdminHistories < ActiveRecord::Migration[5.0] def self.up rename_table :histories, :rails_admin_histories end def self.down rename_table :rails_admin_histories, :histories end end ================================================ FILE: spec/dummy_app/db/migrate/20110224184303_create_field_tests.rb ================================================ # frozen_string_literal: true class CreateFieldTests < ActiveRecord::Migration[5.0] def self.up create_table :field_tests do |t| t.string :string_field t.text :text_field t.integer :integer_field t.float :float_field t.decimal :decimal_field t.datetime :datetime_field t.timestamp :timestamp_field t.time :time_field t.date :date_field t.boolean :boolean_field t.timestamps null: false end end def self.down drop_table :field_tests end end ================================================ FILE: spec/dummy_app/db/migrate/20110328193014_create_cms_basic_pages.rb ================================================ # frozen_string_literal: true class CreateCmsBasicPages < ActiveRecord::Migration[5.0] def self.up create_table :cms_basic_pages do |t| t.string :title t.text :content t.timestamps null: false end end def self.down drop_table :cms_basic_pages end end ================================================ FILE: spec/dummy_app/db/migrate/20110329183136_remove_league_id_from_teams.rb ================================================ # frozen_string_literal: true class RemoveLeagueIdFromTeams < ActiveRecord::Migration[5.0] def self.up remove_column :teams, :league_id end def self.down add_column :teams, :league_id, :integer end end ================================================ FILE: spec/dummy_app/db/migrate/20110607152842_add_format_to_field_test.rb ================================================ # frozen_string_literal: true class AddFormatToFieldTest < ActiveRecord::Migration[5.0] def self.up add_column :field_tests, :format, :string end def self.down remove_column :field_tests, :format end end ================================================ FILE: spec/dummy_app/db/migrate/20110714095433_create_balls.rb ================================================ # frozen_string_literal: true class CreateBalls < ActiveRecord::Migration[5.0] def self.up create_table :balls, force: true do |t| t.string :color t.timestamps null: false end end def self.down drop_table :balls end end ================================================ FILE: spec/dummy_app/db/migrate/20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb ================================================ # frozen_string_literal: true class AddProtectedFieldAndRestrictedFieldToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :restricted_field, :string add_column :field_tests, :protected_field, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20110901131551_change_division_primary_key.rb ================================================ # frozen_string_literal: true class ChangeDivisionPrimaryKey < ActiveRecord::Migration[5.0] def up drop_table :divisions create_table :divisions, primary_key: 'custom_id' do |t| t.timestamps null: false t.integer :league_id t.string :name, limit: 50, null: false end end def down drop_table :divisions create_table :divisions do |t| t.timestamps null: false t.integer :league_id t.string :name, limit: 50, null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20110901142530_rename_league_id_foreign_key_on_divisions.rb ================================================ # frozen_string_literal: true class RenameLeagueIdForeignKeyOnDivisions < ActiveRecord::Migration[5.0] def change rename_column :divisions, :league_id, :custom_league_id end end ================================================ FILE: spec/dummy_app/db/migrate/20110901150912_set_primary_key_not_null_for_divisions.rb ================================================ # frozen_string_literal: true class SetPrimaryKeyNotNullForDivisions < ActiveRecord::Migration[5.0] def up drop_table :divisions create_table :divisions, id: false do |t| t.timestamps null: false t.primary_key :custom_id t.integer :custom_league_id t.string :name, limit: 50, null: false end end def down drop_table :divisions create_table :divisions, primary_key: :custom_id do |t| t.timestamps null: false t.integer :custom_league_id t.string :name, limit: 50, null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20110901154834_change_length_for_rails_admin_histories.rb ================================================ # frozen_string_literal: true class ChangeLengthForRailsAdminHistories < ActiveRecord::Migration[5.0] def up change_column :rails_admin_histories, :message, :text end def down change_column :rails_admin_histories, :message, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb ================================================ # frozen_string_literal: true class AddDragonflyAndCarrierwaveToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :paperclip_asset_file_name, :string add_column :field_tests, :dragonfly_asset_uid, :string add_column :field_tests, :carrierwave_asset, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20111115041025_add_type_to_balls.rb ================================================ # frozen_string_literal: true class AddTypeToBalls < ActiveRecord::Migration[5.0] def change add_column :balls, :type, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20111123092549_create_nested_field_tests.rb ================================================ # frozen_string_literal: true class CreateNestedFieldTests < ActiveRecord::Migration[5.0] def change create_table :nested_field_tests do |t| t.string :title t.integer :field_test_id t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20111130075338_add_dragonfly_asset_name_to_field_tests.rb ================================================ # frozen_string_literal: true class AddDragonflyAssetNameToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :dragonfly_asset_name, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20111215083258_create_foo_bars.rb ================================================ # frozen_string_literal: true class CreateFooBars < ActiveRecord::Migration[5.0] def change create_table :foo_bars do |t| t.string :title t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20120117151733_add_custom_field_to_teams.rb ================================================ # frozen_string_literal: true class AddCustomFieldToTeams < ActiveRecord::Migration[5.0] def change add_column :teams, :custom_field, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20120118122004_add_categories.rb ================================================ # frozen_string_literal: true class AddCategories < ActiveRecord::Migration[5.0] def change create_table :categories do |t| t.integer :parent_category_id end end end ================================================ FILE: spec/dummy_app/db/migrate/20120319041705_drop_rel_tests.rb ================================================ # frozen_string_literal: true class DropRelTests < ActiveRecord::Migration[5.0] def self.up drop_table :rel_tests end def self.down create_table :rel_tests do |t| t.integer :league_id t.integer :division_id, null: false t.integer :player_id t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20120720075608_create_another_field_tests.rb ================================================ # frozen_string_literal: true class CreateAnotherFieldTests < ActiveRecord::Migration[5.0] def change create_table :another_field_tests do |t| t.timestamps null: false end add_column :nested_field_tests, :another_field_test_id, :integer end end ================================================ FILE: spec/dummy_app/db/migrate/20120928075608_create_images.rb ================================================ # frozen_string_literal: true class CreateImages < ActiveRecord::Migration[5.0] def change create_table :images do |t| t.string :file_file_name t.string :file_content_type t.bigint :file_file_size t.datetime :file_updated_at t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20140412075608_create_deeply_nested_field_tests.rb ================================================ # frozen_string_literal: true class CreateDeeplyNestedFieldTests < ActiveRecord::Migration[5.0] def change create_table :deeply_nested_field_tests do |t| t.belongs_to :nested_field_test t.string :title t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20140826093220_create_paper_trail_tests.rb ================================================ # frozen_string_literal: true class CreatePaperTrailTests < ActiveRecord::Migration[5.0] def change create_table :paper_trail_tests do |t| t.string :name t.timestamps null: false end end end ================================================ FILE: spec/dummy_app/db/migrate/20140826093552_create_versions.rb ================================================ # frozen_string_literal: true class CreateVersions < ActiveRecord::Migration[5.0] def change create_table :versions do |t| t.string :item_type, null: false t.integer :item_id, null: false t.string :event, null: false t.string :whodunnit t.text :object t.datetime :created_at end add_index :versions, %i[item_type item_id] end end ================================================ FILE: spec/dummy_app/db/migrate/20150815102450_add_refile_to_field_tests.rb ================================================ # frozen_string_literal: true class AddRefileToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :refile_asset_id, :string add_column :field_tests, :refile_asset_filename, :string add_column :field_tests, :refile_asset_size, :string add_column :field_tests, :refile_asset_content_type, :string end end ================================================ FILE: spec/dummy_app/db/migrate/20151027181550_change_field_test_id_to_nested_field_tests.rb ================================================ # frozen_string_literal: true class ChangeFieldTestIdToNestedFieldTests < ActiveRecord::Migration[5.0] def change change_column :nested_field_tests, :field_test_id, :integer, null: false end end ================================================ FILE: spec/dummy_app/db/migrate/20160728152942_add_main_sponsor_to_teams.rb ================================================ # frozen_string_literal: true class AddMainSponsorToTeams < ActiveRecord::Migration[5.0] def change add_column :teams, :main_sponsor, :integer, default: 0, null: false end end ================================================ FILE: spec/dummy_app/db/migrate/20160728153058_add_formation_to_players.rb ================================================ # frozen_string_literal: true class AddFormationToPlayers < ActiveRecord::Migration[5.0] def change add_column :players, :formation, :string, default: 'substitute', null: false end end ================================================ FILE: spec/dummy_app/db/migrate/20171229220713_add_enum_fields_to_field_tests.rb ================================================ # frozen_string_literal: true class AddEnumFieldsToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :string_enum_field, :string add_column :field_tests, :integer_enum_field, :integer end end ================================================ FILE: spec/dummy_app/db/migrate/20180701084251_create_active_storage_tables.active_storage.rb ================================================ # frozen_string_literal: true # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.0] def change create_table :active_storage_blobs do |t| t.string :key, null: false t.string :filename, null: false t.string :content_type t.text :metadata if t.respond_to? :bigint t.bigint :byte_size, null: false else t.integer :byte_size, null: false end t.string :checksum, null: false t.datetime :created_at, null: false t.index [:key], unique: true end create_table :active_storage_attachments do |t| t.string :name, null: false t.references :record, null: false, polymorphic: true, index: false t.references :blob, null: false t.datetime :created_at, null: false t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true end end end ================================================ FILE: spec/dummy_app/db/migrate/20180707101855_add_carrierwave_assets_to_field_tests.rb ================================================ # frozen_string_literal: true class AddCarrierwaveAssetsToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :carrierwave_assets, :string, after: :carrierwave_asset end end ================================================ FILE: spec/dummy_app/db/migrate/20181029101829_add_shrine_data_to_field_tests.rb ================================================ # frozen_string_literal: true class AddShrineDataToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :shrine_asset_data, :text add_column :field_tests, :shrine_versioning_asset_data, :text end end ================================================ FILE: spec/dummy_app/db/migrate/20190531065324_create_action_text_tables.action_text.rb ================================================ # frozen_string_literal: true class CreateActionTextTables < ActiveRecord::Migration[5.0] def change create_table :action_text_rich_texts do |t| t.string :name, null: false t.text :body, size: :long t.references :record, null: false, polymorphic: true, index: false t.timestamps t.index %i[record_type record_id name], name: 'index_action_text_rich_texts_uniqueness', unique: true end end end ================================================ FILE: spec/dummy_app/db/migrate/20201127111952_update_active_storage_tables.rb ================================================ # frozen_string_literal: true class UpdateActiveStorageTables < ActiveRecord::Migration[5.0] def change add_column :active_storage_blobs, :service_name, :string, null: false, default: 'local' create_table :active_storage_variant_records do |t| t.belongs_to :blob, null: false, index: false t.string :variation_digest, null: false t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true end end end ================================================ FILE: spec/dummy_app/db/migrate/20210811121027_create_two_level_namespaced_polymorphic_association_tests.rb ================================================ # frozen_string_literal: true class CreateTwoLevelNamespacedPolymorphicAssociationTests < ActiveRecord::Migration[5.0] def change create_table :two_level_namespaced_polymorphic_association_tests do |t| t.string :name t.timestamps end end end ================================================ FILE: spec/dummy_app/db/migrate/20210812115908_create_custom_versions.rb ================================================ # frozen_string_literal: true class CreateCustomVersions < ActiveRecord::Migration[5.0] def change create_table :custom_versions do |t| t.string :item_type, null: false t.integer :item_id, null: false t.string :event, null: false t.string :whodunnit t.text :object t.datetime :created_at end add_index :custom_versions, %i[item_type item_id] end end ================================================ FILE: spec/dummy_app/db/migrate/20211011235734_add_bool_field_open.rb ================================================ # frozen_string_literal: true class AddBoolFieldOpen < ActiveRecord::Migration[6.0] def change add_column :field_tests, :open, :boolean end end ================================================ FILE: spec/dummy_app/db/migrate/20220416102741_create_composite_key_tables.rb ================================================ # frozen_string_literal: true class CreateCompositeKeyTables < ActiveRecord::Migration[6.0] def change add_column :fans_teams, :since, :date create_table :favorite_players, primary_key: %i[fan_id team_id player_id] do |t| t.integer :fan_id, null: false t.integer :team_id, null: false t.integer :player_id, null: false t.string :reason t.timestamps end end end ================================================ FILE: spec/dummy_app/db/migrate/20240921171953_add_non_nullable_boolean_field.rb ================================================ # frozen_string_literal: true class AddNonNullableBooleanField < ActiveRecord::Migration[6.0] def change add_column :field_tests, :non_nullable_boolean_field, :boolean, null: false, default: false end end ================================================ FILE: spec/dummy_app/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2024_09_21_171953) do create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" t.string "record_type", null: false t.integer "record_id", null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.integer "record_id", null: false t.integer "blob_id", null: false t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.bigint "byte_size", null: false t.string "checksum", null: false t.datetime "created_at", precision: nil, null: false t.string "service_name", default: "local", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end create_table "active_storage_variant_records", force: :cascade do |t| t.integer "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end create_table "another_field_tests", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "balls", force: :cascade do |t| t.string "color" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "type" end create_table "categories", force: :cascade do |t| t.integer "parent_category_id" end create_table "cms_basic_pages", force: :cascade do |t| t.string "title" t.text "content" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "comments", force: :cascade do |t| t.integer "commentable_id" t.string "commentable_type" t.text "content" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "custom_versions", force: :cascade do |t| t.string "item_type", null: false t.integer "item_id", null: false t.string "event", null: false t.string "whodunnit" t.text "object" t.datetime "created_at", precision: nil t.index ["item_type", "item_id"], name: "index_custom_versions_on_item_type_and_item_id" end create_table "deeply_nested_field_tests", force: :cascade do |t| t.integer "nested_field_test_id" t.string "title" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["nested_field_test_id"], name: "index_deeply_nested_field_tests_on_nested_field_test_id" end create_table "divisions", primary_key: "custom_id", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "custom_league_id" t.string "name", limit: 50, null: false end create_table "drafts", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "player_id" t.integer "team_id" t.date "date" t.integer "round" t.integer "pick" t.integer "overall" t.string "college", limit: 100 t.text "notes" end create_table "fans", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "name", limit: 100, null: false end create_table "fans_teams", id: false, force: :cascade do |t| t.integer "fan_id" t.integer "team_id" t.date "since" end create_table "favorite_players", primary_key: ["fan_id", "team_id", "player_id"], force: :cascade do |t| t.integer "fan_id", null: false t.integer "team_id", null: false t.integer "player_id", null: false t.string "reason" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "field_tests", force: :cascade do |t| t.string "string_field" t.text "text_field" t.integer "integer_field" t.float "float_field" t.decimal "decimal_field" t.datetime "datetime_field", precision: nil t.datetime "timestamp_field", precision: nil t.time "time_field" t.date "date_field" t.boolean "boolean_field" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "format" t.string "restricted_field" t.string "protected_field" t.string "paperclip_asset_file_name" t.string "dragonfly_asset_uid" t.string "carrierwave_asset" t.string "dragonfly_asset_name" t.string "refile_asset_id" t.string "refile_asset_filename" t.string "refile_asset_size" t.string "refile_asset_content_type" t.string "string_enum_field" t.integer "integer_enum_field" t.string "carrierwave_assets" t.text "shrine_asset_data" t.text "shrine_versioning_asset_data" t.boolean "open" t.boolean "non_nullable_boolean_field", default: false, null: false end create_table "foo_bars", force: :cascade do |t| t.string "title" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "images", force: :cascade do |t| t.string "file_file_name" t.string "file_content_type" t.bigint "file_file_size" t.datetime "file_updated_at", precision: nil t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "leagues", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "name", limit: 50, null: false end create_table "nested_field_tests", force: :cascade do |t| t.string "title" t.integer "field_test_id", null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "another_field_test_id" end create_table "paper_trail_tests", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "players", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.datetime "deleted_at", precision: nil t.integer "team_id" t.string "name", limit: 100, null: false t.string "position", limit: 50 t.integer "number", null: false t.boolean "retired", default: false t.boolean "injured", default: false t.date "born_on" t.text "notes" t.boolean "suspended", default: false t.string "formation", default: "substitute", null: false end create_table "rails_admin_histories", force: :cascade do |t| t.text "message" t.string "username" t.integer "item" t.string "table" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["item", "table"], name: "index_rails_admin_histories_on_item_and_table" end create_table "teams", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "division_id" t.string "name", limit: 50 t.string "logo_url", limit: 255 t.string "manager", limit: 100, null: false t.string "ballpark", limit: 100 t.string "mascot", limit: 100 t.integer "founded" t.integer "wins" t.integer "losses" t.float "win_percentage" t.decimal "revenue", precision: 18, scale: 2 t.string "color" t.string "custom_field" t.integer "main_sponsor", default: 0, null: false end create_table "two_level_namespaced_polymorphic_association_tests", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at", precision: nil t.datetime "remember_created_at", precision: nil t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at", precision: nil t.datetime "last_sign_in_at", precision: nil t.string "current_sign_in_ip" t.string "last_sign_in_ip" t.string "password_salt" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "avatar_file_name" t.string "avatar_content_type" t.integer "avatar_file_size" t.datetime "avatar_updated_at", precision: nil t.string "roles" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end create_table "versions", force: :cascade do |t| t.string "item_type", null: false t.integer "item_id", null: false t.string "event", null: false t.string "whodunnit" t.text "object" t.datetime "created_at", precision: nil t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" end end ================================================ FILE: spec/dummy_app/db/seeds.rb ================================================ # frozen_string_literal: true require 'mlb' user_model = RailsAdmin::AbstractModel.new(User) league_model = RailsAdmin::AbstractModel.new(League) division_model = RailsAdmin::AbstractModel.new(Division) team_model = RailsAdmin::AbstractModel.new(Team) player_model = RailsAdmin::AbstractModel.new(Player) user_model.new(email: 'username@example.com', password: 'password', password_confirmation: 'password').save MLB::Teams.all(season: Time.now.year).each do |mlb_team| league = league_model.where(name: mlb_team.league.name).first unless league league = league_model.model.new(name: mlb_team.league.name) league.save! end division = division_model.where(name: mlb_team.division.name).first unless division division = division_model.model.new(name: mlb_team.division.name, league: league) division.save! end team = team_model.where(name: mlb_team.name).first unless team team = team_model.model.new(name: mlb_team.name, logo_url: mlb_team.link, manager: 'None', ballpark: mlb_team.venue.name, founded: mlb_team.first_year_of_play, wins: 0, losses: 0, win_percentage: 0.0, division: division) team.save! end mlb_team.roster.reject { |roster| roster.jersey_number.nil? }.each do |roster| player_model.model.new(name: roster.player.full_name, number: roster.jersey_number, position: roster.position.name, team: team).save end end puts "Seeded #{league_model.count} leagues, #{division_model.count} divisions, #{team_model.count} teams and #{player_model.count} players" ================================================ FILE: spec/dummy_app/doc/README_FOR_APP ================================================ Use this README file to introduce your application and point to useful places in the API for learning more. Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. ================================================ FILE: spec/dummy_app/fly.toml ================================================ # fly.toml app configuration file generated for rails-admin on 2023-08-06T18:22:31+09:00 # # See https://fly.io/docs/reference/configuration/ for information about how to use this file. # app = "rails-admin" primary_region = "iad" console_command = "/rails/bin/rails console" [build] [http_service] internal_port = 3000 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 0 processes = ["app"] [[statics]] guest_path = "/rails/public" url_prefix = "/" ================================================ FILE: spec/dummy_app/lib/assets/.gitkeep ================================================ ================================================ FILE: spec/dummy_app/lib/does_not_load_autoload_paths_not_in_eager_load.rb ================================================ # frozen_string_literal: true module DoesNotLoadAutoloadPathsNotInEagerLoad raise 'This file is in app.paths.autoload but not app.paths.eager_load and ' \ ' should not be autoloaded by rails_admin' end ================================================ FILE: spec/dummy_app/lib/tasks/.gitkeep ================================================ ================================================ FILE: spec/dummy_app/package.json ================================================ { "name": "dummy_app", "private": true, "version": "0.1.0", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@rails/actiontext": "^7.0.3-1", "@rails/activestorage": "^7.0.3-1", "@rails/webpacker": "5.4.3", "rails_admin": "file:../..", "trix": "^2.0.0-beta.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12" }, "devDependencies": { "vite": "^5.0", "vite-plugin-ruby": ">=5.0 <6", "webpack-dev-server": "^3" }, "scripts": { "build": "webpack --config webpack.config.js", "build:css": "sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules" } } ================================================ FILE: spec/dummy_app/postcss.config.js ================================================ module.exports = { plugins: [ require("postcss-import"), require("postcss-flexbugs-fixes"), require("postcss-preset-env")({ autoprefixer: { flexbox: "no-2009", }, stage: 3, }), ], }; ================================================ FILE: spec/dummy_app/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

    You may have mistyped the address or the page may have moved.

    ================================================ FILE: spec/dummy_app/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

    Maybe you tried to change something you didn't have access to.

    ================================================ FILE: spec/dummy_app/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

    ================================================ FILE: spec/dummy_app/public/robots.txt ================================================ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / ================================================ FILE: spec/dummy_app/public/system/dragonfly/development/2011/11/24/10_36_27_888_Pensive_Parakeet.jpg.meta ================================================ {: nameI"Pensive Parakeet.jpg:ET:model_classI"FieldTest;F:model_attachment:dragonfly_asset ================================================ FILE: spec/dummy_app/public/system/dragonfly/development/2011/11/30/08_54_39_906_Costa_Rican_Frog.jpg.meta ================================================ {: nameI"Costa Rican Frog.jpg:ET:model_classI"FieldTest;F:model_attachment:dragonfly_asset ================================================ FILE: spec/dummy_app/vendor/javascript/.keep ================================================ ================================================ FILE: spec/dummy_app/vite.config.ts ================================================ import { defineConfig } from "vite"; import RubyPlugin from "vite-plugin-ruby"; export default defineConfig({ plugins: [RubyPlugin()], }); ================================================ FILE: spec/dummy_app/webpack.config.js ================================================ const path = require("path"); const webpack = require("webpack"); module.exports = { mode: "production", devtool: "source-map", entry: { application: "./app/javascript/application.js", rails_admin: "./app/javascript/rails_admin.js", }, output: { filename: "[name].js", sourceMapFilename: "[name].js.map", path: path.resolve(__dirname, "app/assets/builds"), }, plugins: [ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), ], }; ================================================ FILE: spec/factories.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :player do sequence(:name) { |n| "Player #{n}" } sequence(:number) { |n| n } sequence(:position) { |n| "Position #{n}" } end factory :draft do date { 1.week.ago } sequence(:round) sequence(:pick) sequence(:overall) sequence(:college) { |n| "College #{n}" } association :team association :player end factory :team do sequence(:division_id) sequence(:name) { |n| "Team #{n}" } sequence(:manager) { |n| "Manager #{n}" } sequence(:founded) sequence(:wins) sequence(:losses) sequence(:win_percentage) factory :managed_team, class: ManagedTeam factory :restricted_team, class: RestrictedTeam end factory :league do sequence(:name) { |n| "League #{n}" } end factory :division do sequence(:custom_league_id) sequence(:name) { |n| "Division #{n}" } end factory :fan do sequence(:name) { |n| "Fan #{n}" } end factory :fanship do association :fan association :team end factory :favorite_player do association :fanship association :player end factory :user do sequence(:email) { |n| "username_#{n}@example.com" } sequence(:password) { |_n| 'password' } factory :user_confirmed, class: User::Confirmed factory :managing_user, class: ManagingUser end factory :field_test do end factory :comment do sequence(:content) do |n| <<-LOREM_IPSUM Lorém --#{n}-- ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." LOREM_IPSUM end factory :comment_confirmed, class: Comment::Confirmed do content { 'something' } end end factory :ball do color { %w[red blue green yellow purple brown black white].sample } end factory :hardball do color { 'blue' } end factory :image do file { File.open(Rails.root.join('public', 'robots.txt')) } end factory :paper_trail_test do sequence(:name) { |n| "name #{n}" } factory :paper_trail_test_subclass, parent: :paper_trail_test, class: 'PaperTrailTestSubclass' factory :paper_trail_test_subclass_in_namespace, parent: :paper_trail_test, class: 'PaperTrailTest::SubclassInNamespace' factory :paper_trail_test_with_custom_association, parent: :paper_trail_test, class: 'PaperTrailTestWithCustomAssociation' end factory :two_level_namespaced_polymorphic_association_test, class: 'TwoLevel::Namespaced::PolymorphicAssociationTest' do sequence(:name) { |n| "name #{n}" } end end ================================================ FILE: spec/fixtures/test.txt ================================================ test ================================================ FILE: spec/helpers/rails_admin/application_helper_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::ApplicationHelper, type: :helper do describe '#authorized?' do let(:abstract_model) { RailsAdmin.config(FieldTest).abstract_model } it 'doesn\'t use unpersisted objects' do expect(helper).to receive(:action).with(:edit, abstract_model, nil).and_call_original helper.authorized?(:edit, abstract_model, FactoryBot.build(:field_test)) end end describe 'with #authorized? stubbed' do before do allow(controller).to receive(:authorized?).and_return(true) end describe '#current_action?' do it 'returns true if current_action, false otherwise' do @action = RailsAdmin::Config::Actions.find(:index) expect(helper.current_action?(RailsAdmin::Config::Actions.find(:index))).to be_truthy expect(helper.current_action?(RailsAdmin::Config::Actions.find(:show))).not_to be_truthy end end describe '#action' do it 'returns action by :custom_key' do RailsAdmin.config do |config| config.actions do dashboard do custom_key :my_custom_dashboard_key end end end expect(helper.action(:my_custom_dashboard_key)).to be end it 'returns only visible actions' do RailsAdmin.config do |config| config.actions do dashboard do visible false end end end expect(helper.action(:dashboard)).to be_nil end it 'returns only visible actions, passing all bindings' do RailsAdmin.config do |config| config.actions do member :test_bindings do visible do bindings[:controller].is_a?(ActionView::TestCase::TestController) && bindings[:abstract_model].model == Team && bindings[:object].is_a?(Team) end end end end expect(helper.action(:test_bindings, RailsAdmin::AbstractModel.new(Team), Team.new)).to be expect(helper.action(:test_bindings, RailsAdmin::AbstractModel.new(Team), Player.new)).to be_nil expect(helper.action(:test_bindings, RailsAdmin::AbstractModel.new(Player), Team.new)).to be_nil end end describe '#actions' do it 'returns actions by type' do abstract_model = RailsAdmin::AbstractModel.new(Player) object = FactoryBot.create :player expect(helper.actions(:all, abstract_model, object).collect(&:custom_key)).to eq(%i[dashboard index show new edit export delete bulk_delete history_show history_index show_in_app]) expect(helper.actions(:root, abstract_model, object).collect(&:custom_key)).to eq([:dashboard]) expect(helper.actions(:collection, abstract_model, object).collect(&:custom_key)).to eq(%i[index new export bulk_delete history_index]) expect(helper.actions(:member, abstract_model, object).collect(&:custom_key)).to eq(%i[show edit delete history_show show_in_app]) end it 'only returns visible actions, passing bindings correctly' do RailsAdmin.config do |config| config.actions do member :test_bindings do visible do bindings[:controller].is_a?(ActionView::TestCase::TestController) && bindings[:abstract_model].model == Team && bindings[:object].is_a?(Team) end end end end expect(helper.actions(:all, RailsAdmin::AbstractModel.new(Team), Team.new).collect(&:custom_key)).to eq([:test_bindings]) expect(helper.actions(:all, RailsAdmin::AbstractModel.new(Team), Player.new).collect(&:custom_key)).to eq([]) expect(helper.actions(:all, RailsAdmin::AbstractModel.new(Player), Team.new).collect(&:custom_key)).to eq([]) end end describe '#logout_method' do it 'defaults to :delete when Devise is not defined' do allow(Object).to receive(:defined?).with(Devise).and_return(false) expect(helper.logout_method).to eq(:delete) end it 'uses first sign out method from Devise when it is defined' do allow(Object).to receive(:defined?).with(Devise).and_return(true) expect(Devise).to receive(:sign_out_via).and_return(%i[whatever_defined_on_devise something_ignored]) expect(helper.logout_method).to eq(:whatever_defined_on_devise) end end describe '#wording_for' do it 'gives correct wording even if action is not visible' do RailsAdmin.config do |config| config.actions do index do visible false end end end expect(helper.wording_for(:menu, :index)).to eq('List') end it 'passes correct bindings' do expect(helper.wording_for(:title, :edit, RailsAdmin::AbstractModel.new(Team), Team.new(name: 'the avengers'))).to eq("Edit Team 'the avengers'") end it 'defaults correct bindings' do @action = RailsAdmin::Config::Actions.find :edit @abstract_model = RailsAdmin::AbstractModel.new(Team) @object = Team.new(name: 'the avengers') expect(helper.wording_for(:title)).to eq("Edit Team 'the avengers'") end it 'does not try to use the wrong :label_metod' do @abstract_model = RailsAdmin::AbstractModel.new(Draft) @object = Draft.new expect(helper.wording_for(:link, :new, RailsAdmin::AbstractModel.new(Team))).to eq('Add a new Team') end end describe '#breadcrumb' do it 'gives us a breadcrumb' do @action = RailsAdmin::Config::Actions.find(:edit, abstract_model: RailsAdmin::AbstractModel.new(Team), object: FactoryBot.create(:team, name: 'the avengers')) bc = helper.breadcrumb expect(bc).to match(/Dashboard/) # dashboard expect(bc).to match(/Teams/) # list expect(bc).to match(/the avengers/) # show expect(bc).to match(/Edit/) # current (edit) end end describe '#menu_for' do it 'passes model and object as bindings and generates a menu, excluding non-get actions' do RailsAdmin.config do |config| config.actions do dashboard index do visible do bindings[:abstract_model].model == Team end end show do visible do bindings[:object].instance_of?(Team) end end delete do http_methods %i[post put delete] end end end @action = RailsAdmin::Config::Actions.find :show @abstract_model = RailsAdmin::AbstractModel.new(Team) @object = FactoryBot.create(:team, name: 'the avengers') expect(helper.menu_for(:root)).to match(/Dashboard/) expect(helper.menu_for(:collection, @abstract_model)).to match(/List/) expect(helper.menu_for(:member, @abstract_model, @object)).to match(/Show/) @abstract_model = RailsAdmin::AbstractModel.new(Player) @object = Player.new expect(helper.menu_for(:collection, @abstract_model)).not_to match(/List/) expect(helper.menu_for(:member, @abstract_model, @object)).not_to match(/Show/) end it 'excludes non-get actions' do RailsAdmin.config do |config| config.actions do dashboard do http_methods %i[post put delete] end end end @action = RailsAdmin::Config::Actions.find :dashboard expect(helper.menu_for(:root)).not_to match(/Dashboard/) end it 'shows actions which are marked as show_in_menu' do I18n.backend.store_translations( :en, admin: {actions: { shown_in_menu: {menu: 'Look this'}, }} ) RailsAdmin.config do |config| config.actions do dashboard do show_in_menu false end root :shown_in_menu, :dashboard do action_name :dashboard show_in_menu true end end end @action = RailsAdmin::Config::Actions.find :dashboard expect(helper.menu_for(:root)).not_to match(/Dashboard/) expect(helper.menu_for(:root)).to match(/Look this/) end it 'should render allow an action to have link_target as config' do RailsAdmin.config do |config| config.actions do dashboard index show do link_target :_blank end end end @action = RailsAdmin::Config::Actions.find :show @abstract_model = RailsAdmin::AbstractModel.new(Team) @object = FactoryBot.create(:team, name: 'the avengers') expect(helper.menu_for(:member, @abstract_model, @object)).to match(/_blank/) end end describe '#main_navigation' do it 'shows included models' do RailsAdmin.config do |config| config.included_models = [Ball, Comment] end expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Balls).*(Comments)/m) end it 'does not draw empty navigation labels' do RailsAdmin.config do |config| config.included_models = [Ball, Comment, Comment::Confirmed] config.model Comment do navigation_label 'Commentz' end config.model Comment::Confirmed do label_plural 'Confirmed' end end expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Balls).*(Commentz).*(Confirmed)/m) expect(helper.main_navigation).not_to match(/(btn-toggle).*(Navigation).*(Balls).*(Commentz).*(Confirmed).*(Comment)/m) end it 'does not show unvisible models' do RailsAdmin.config do |config| config.included_models = [Ball, Comment] config.model Comment do hide end end result = helper.main_navigation expect(result).to match(/(btn-toggle).*(Navigation).*(Balls)/m) expect(result).not_to match('Comments') end it 'shows children of hidden models' do # https://github.com/railsadminteam/rails_admin/issues/978 RailsAdmin.config do |config| config.included_models = [Ball, Hardball] config.model Ball do hide end end expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Hardballs)/m) end it 'shows children of excluded models' do RailsAdmin.config do |config| config.included_models = [Hardball] end expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Hardballs)/m) end it 'nests in navigation label' do RailsAdmin.config do |config| config.included_models = [Comment] config.model Comment do navigation_label 'commentable' end end expect(helper.main_navigation).to match(/(btn-toggle).*(commentable).*(Comments)/m) end it 'nests in parent model' do RailsAdmin.config do |config| config.included_models = [Player, Comment] config.model Comment do parent Player end end expect(helper.main_navigation).to match(/(Players).* (nav-level-1).*(Comments)/m) end it 'orders' do RailsAdmin.config do |config| config.included_models = [Player, Comment] end expect(helper.main_navigation).to match(/(Comments).*(Players)/m) RailsAdmin.config(Comment) do weight 1 end expect(helper.main_navigation).to match(/(Players).*(Comments)/m) end end describe '#root_navigation' do it 'shows actions which are marked as show_in_sidebar' do I18n.backend.store_translations( :en, admin: {actions: { shown_in_sidebar: {menu: 'Look this'}, }} ) RailsAdmin.config do |config| config.actions do dashboard do show_in_sidebar false end root :shown_in_sidebar, :dashboard do action_name :dashboard show_in_sidebar true end end end expect(helper.root_navigation).not_to match(/Dashboard/) expect(helper.root_navigation).to match(/Look this/) end it 'allows grouping by sidebar_label' do I18n.backend.store_translations( :en, admin: { actions: { foo: {menu: 'Foo'}, bar: {menu: 'Bar'}, }, } ) RailsAdmin.config do |config| config.actions do dashboard do show_in_sidebar true sidebar_label 'One' end root :foo, :dashboard do action_name :dashboard show_in_sidebar true sidebar_label 'Two' end root :bar, :dashboard do action_name :dashboard show_in_sidebar true sidebar_label 'Two' end end end expect(helper.strip_tags(helper.root_navigation).delete(' ')).to eq 'OneDashboardTwoFooBar' end end describe '#static_navigation' do it 'shows not show static nav if no static links defined' do RailsAdmin.config do |config| config.navigation_static_links = {} end expect(helper.static_navigation).to be_empty end it 'shows links if defined' do RailsAdmin.config do |config| config.navigation_static_links = { 'Test Link' => 'http://www.google.com', } end expect(helper.static_navigation).to match(/Test Link/) end it 'shows default header if navigation_static_label not defined in config' do RailsAdmin.config do |config| config.navigation_static_links = { 'Test Link' => 'http://www.google.com', } end expect(helper.static_navigation).to match(I18n.t('admin.misc.navigation_static_label')) end it 'shows custom header if defined' do RailsAdmin.config do |config| config.navigation_static_label = 'Test Header' config.navigation_static_links = { 'Test Link' => 'http://www.google.com', } end expect(helper.static_navigation).to match(/Test Header/) end end describe '#bulk_menu' do it 'includes all visible bulkable actions' do RailsAdmin.config do |config| config.actions do index collection :zorg do bulkable true action_name :zorg_action end collection :blub do bulkable true visible do bindings[:abstract_model].model == Team end end end end # Preload all models to prevent I18n being cleared in Mongoid builds RailsAdmin::AbstractModel.all en = {admin: {actions: { zorg: {bulk_link: 'Zorg all these %{model_label_plural}'}, blub: {bulk_link: 'Blub all these %{model_label_plural}'}, }}} I18n.backend.store_translations(:en, en) @abstract_model = RailsAdmin::AbstractModel.new(Team) result = helper.bulk_menu expect(result).to match('zorg_action') expect(result).to match('Zorg all these Teams') expect(result).to match('blub') expect(result).to match('Blub all these Teams') result_2 = helper.bulk_menu(RailsAdmin::AbstractModel.new(Player)) expect(result_2).to match('zorg_action') expect(result_2).to match('Zorg all these Players') expect(result_2).not_to match('blub') expect(result_2).not_to match('Blub all these Players') end end describe '#edit_user_link' do subject { helper.edit_user_link } let(:user) { FactoryBot.create(:user) } before { allow(helper).to receive(:_current_user).and_return(user) } it 'shows the edit action link of the user' do is_expected.to match(%r{href="[^"]+/admin/user/#{user.id}/edit"}) end it 'shows the gravatar icon' do is_expected.to include('gravatar') end context "when the user doesn't have the email column" do let(:user) { FactoryBot.create(:player) } it 'shows nothing' do is_expected.to be nil end end context 'when gravatar is disabled' do before { RailsAdmin.config.show_gravatar = false } it "doesn't show the gravatar icon" do is_expected.not_to include('gravatar') end end context 'when the user is not authorized to perform edit' do before do allow_any_instance_of(RailsAdmin::Config::Actions::Edit).to receive(:authorized?).and_return(false) end it 'shows gravatar and email without a link' do is_expected.to include('gravatar') is_expected.to include(user.email) is_expected.not_to match('href') end it 'shows only email without a link when gravatar is disabled' do RailsAdmin.config do |config| config.show_gravatar = false end is_expected.not_to include('gravatar') is_expected.not_to match('href') is_expected.to include(user.email) is_expected.to match("#{user.email}") end end end end describe '#flash_alert_class' do it 'makes errors red with alert-danger' do expect(helper.flash_alert_class('error')).to eq('alert-danger') end it 'makes alerts yellow with alert-warning' do expect(helper.flash_alert_class('alert')).to eq('alert-warning') end it 'makes notices blue with alert-info' do expect(helper.flash_alert_class('notice')).to eq('alert-info') end it 'prefixes others with "alert-"' do expect(helper.flash_alert_class('foo')).to eq('alert-foo') end end end ================================================ FILE: spec/helpers/rails_admin/form_builder_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::FormBuilder', type: :helper do describe '#generate' do before do RailsAdmin.config Player do create do include_all_fields field :number, :hidden end end allow(helper).to receive(:authorized?).and_return(true) (@object = Player.new).save @builder = RailsAdmin::FormBuilder.new(:player, @object, helper, {}) allow(@builder).to receive(:field_for).and_return('field') action = double allow(action).to receive(:enabled?).and_return true helper.instance_variable_set :@action, action end it 'does not add additional error div from default ActionView::Base.field_error_proc' do expect(@builder.generate(action: :create, model_config: RailsAdmin.config(Player))).not_to have_css('.field_with_errors') expect(@builder.generate(action: :create, model_config: RailsAdmin.config(Player))).to have_css('.control-group.error') end it 'hidden fields should be wrapper' do expect(@builder.generate(action: :create, model_config: RailsAdmin.config(Player))).to match('control-group row mb-3 hidden_type number_field') end end describe '#object_infos' do before do allow(helper).to receive(:authorized?).and_return(true) @object = Fan.create!(name: 'foo') @builder = RailsAdmin::FormBuilder.new(:fan, @object, helper, {}) end it 'returns a tag with infos' do expect(@builder.object_infos).to eql '' end context 'when object_label\'s type is symbol' do before { @object.name = :foo } it 'does not break' do expect(@builder.object_infos).to eql '' end end end end ================================================ FILE: spec/helpers/rails_admin/main_helper_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::MainHelper, type: :helper do describe '#rails_admin_form_for' do let(:html_form) do helper.rails_admin_form_for(FieldTest.new, url: new_path(model_name: 'field_test')) {} end let(:html_form_with_attrs) do helper.rails_admin_form_for(FieldTest.new, url: new_path(model_name: 'field_test'), html: {class: 'example'}) {} end context 'with html5 browser_validations enabled' do before do RailsAdmin.config.browser_validations = true RailsAdmin.config FieldTest do field :address, :string do required true end end end it 'should not add novalidate attribute to the html form tag' do expect(html_form).to_not include 'novalidate' end end context 'with html5 browser_validations disabled' do before do RailsAdmin.config.browser_validations = false RailsAdmin.config FieldTest do field :address, :string do required true end end end it 'should add novalidate attribute to the html form tag' do expect(html_form).to include 'novalidate="novalidate"' end it 'should add novalidate attribute to the html form tag with html attributes' do expect(html_form_with_attrs).to include 'novalidate="novalidate"' end end end end ================================================ FILE: spec/integration/actions/base_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Base action', type: :request do subject { page } describe '#enabled?' do it 'prevents the access to unauthorized actions' do RailsAdmin.config do |config| config.actions do index do except %w[FieldTest] end end end expect { visit index_path(model_name: 'field_test') }.to raise_error 'RailsAdmin::ActionNotAllowed' end describe 'in form action' do before do RailsAdmin.config do |config| config.actions do index new edit do except %w[Player Team] end end end end describe 'for filterling-select widget' do it 'hides modal links to disabled actions' do visit new_path(model_name: 'player') expect(page).to have_link 'Add a new Team' expect(page).not_to have_link 'Edit this Team' end end describe 'for filterling-multiselect widget' do it 'hides edit link to another model' do visit new_path(model_name: 'team') expect(page).to have_link 'Add a new Player' expect(page).not_to have_link 'Edit this Player' end end end context 'when used with #visible?' do let!(:player) { FactoryBot.create(:player) } before do RailsAdmin.config do |config| config.actions do index show edit do enabled false visible true end end end end it 'allows disabled links to be shown' do visit index_path(model_name: 'player') is_expected.to have_css('.edit_member_link.disabled span', text: /Edit/, visible: false) visit show_path(model_name: 'player', id: player.id) is_expected.to have_css('.edit_member_link.disabled a[href*="void(0)"]') end end end end ================================================ FILE: spec/integration/actions/bulk_delete_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'BulkDelete action', type: :request do subject { page } describe 'confirmation page' do before do @players = FactoryBot.create_list(:player, 2) end it 'shows names of to-be-deleted players' do post(bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: @players.collect(&:id))) @players.each { |player| expect(response.body).to include(player.name) } end it 'redirects to list when all of the records is already deleted' do expect(Player.count).to eq @players.length player_ids = [@players.first.id] @players.first.destroy # delete selected object before send request to show bulk_delete confirmation. post(bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: player_ids)) expect(response.response_code).to eq 302 index_url = response.headers['Location'] expect(URI.parse(index_url).path).to eq(index_path(model_name: 'player')) visit index_url is_expected.to have_no_content(@players.first.name) is_expected.to have_content(@players.last.name) end it 'returns error message for DELETE request if the records are already deleted' do expect(Player.count).to eq @players.length player_ids = [@players.first.id] @players.first.destroy # delete selected object before send request to delete it. delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: player_ids)) expect(response.response_code).to eq 302 expect(flash[:error]).to match(/0 players failed to be deleted/i) end it 'returns error message for DELETE request without bulk_ids' do expect(Player.count).to eq @players.length delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: '')) expect(response.response_code).to eq 302 expect(flash[:error]).to match(/0 players failed to be deleted/i) end end context 'on destroy' do before do @players = Array.new(3) { FactoryBot.create(:player) } @delete_ids = @players[0..1].collect(&:id) # NOTE: This uses an internal, unsupported capybara API which could break at any moment. We # should refactor this test so that it either A) uses capybara's supported API (only GET # requests via visit) or B) just uses Rack::Test (and doesn't use capybara for browser # interaction like click_button). page.driver.browser.reset_host! page.driver.browser.process :post, bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: @delete_ids, '_method' => 'post') click_button "Yes, I'm sure" end it 'does not contain deleted records' do expect(RailsAdmin::AbstractModel.new('Player').all.pluck(:id)).to eq([@players[2].id]) expect(page).to have_selector('.alert-success', text: '2 Players successfully deleted') end end context 'on cancel' do before do @players = Array.new(3) { FactoryBot.create(:player) } @delete_ids = @players[0..1].collect(&:id) visit index_path(model_name: 'player') @delete_ids.each { |id| find(%(input[name="bulk_ids[]"][value="#{id}"])).click } click_link 'Selected items' click_link 'Delete selected Players' end it 'does not delete records', js: true do find_button('Cancel').trigger('click') is_expected.to have_text 'No actions were taken' expect(RailsAdmin::AbstractModel.new('Player').count).to eq(3) end end context 'with composite primary keys', composite_primary_keys: true do let!(:fanships) { FactoryBot.create_list(:fanship, 3) } it 'provides check boxes for bulk operation' do visit index_path(model_name: 'fanship') fanships.each { |fanship| is_expected.to have_css(%(input[name="bulk_ids[]"][value="#{fanship.id}"])) } end it 'deletes selected records' do delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'fanship', bulk_ids: fanships[0..1].map { |fanship| RailsAdmin::Support::CompositeKeysSerializer.serialize(fanship.id) })) expect(flash[:success]).to match(/2 Fanships successfully deleted/) expect(Fanship.all).to eq fanships[2..2] end end end ================================================ FILE: spec/integration/actions/dashboard_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Dashboard action', type: :request do subject { page } it 'shows statistics by default' do allow(RailsAdmin.config(Player).abstract_model).to receive(:count).and_return(0) expect(RailsAdmin.config(Player).abstract_model).to receive(:count) visit dashboard_path end it 'does not show statistics if turned off' do RailsAdmin.config do |c| c.included_models = [Player] c.actions do dashboard do statistics false end index # mandatory end end expect(RailsAdmin.config(Player).abstract_model).not_to receive(:count) visit dashboard_path end it 'does not show history if turned off', active_record: true do RailsAdmin.config do |c| c.audit_with :paper_trail, 'User', 'PaperTrail::Version' c.included_models = [PaperTrailTest] c.actions do dashboard do history false end index # mandatory new history_index end end with_versioning do visit new_path(model_name: 'paper_trail_test') fill_in 'paper_trail_test[name]', with: 'Jackie Robinson' click_button 'Save' end visit dashboard_path is_expected.not_to have_content 'Jackie Robinson' end it 'counts are different for same-named models in different modules' do allow(RailsAdmin.config(User::Confirmed).abstract_model).to receive(:count).and_return(10) allow(RailsAdmin.config(Comment::Confirmed).abstract_model).to receive(:count).and_return(0) visit dashboard_path expect(find('tr.user_confirmed_links .progress').text).to eq '10' expect(find('tr.comment_confirmed_links .progress').text).to eq '0' end it 'most recent change dates are different for same-named models in different modules' do user_create = 10.days.ago comment_create = 20.days.ago FactoryBot.create(:user_confirmed, created_at: user_create) FactoryBot.create(:comment_confirmed, created_at: comment_create) visit dashboard_path expect(find('tr.user_confirmed_links')).to have_content '10 days ago' expect(find('tr.comment_confirmed_links')).to have_content '20 days ago' end describe 'I18n' do around do |example| I18n.config.available_locales = I18n.config.available_locales + [:xx] I18n.backend.class.send(:include, I18n::Backend::Pluralization) I18n.backend.store_translations :xx, admin: { misc: { ago: 'back', }, }, datetime: { distance_in_words: { x_days: { one: '1 day', }, }, } I18n.locale = :xx example.run I18n.locale = :en I18n.config.available_locales = I18n.config.available_locales - [:xx] end it "fallbacks to 'ago' when 'time_ago' is not available" do FactoryBot.create(:player, created_at: 1.day.ago) visit dashboard_path expect(page).to have_content '1 day back' end end end ================================================ FILE: spec/integration/actions/delete_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Delete action', type: :request do subject { page } it 'shows "Delete model"' do @draft = FactoryBot.create :draft @player = @draft.player @comment = @player.comments.create visit delete_path(model_name: 'player', id: @player.id) is_expected.to have_content('delete this player') is_expected.to have_link(@player.name, href: "/admin/player/#{@player.id}") is_expected.to have_link("Draft ##{@draft.id}", href: "/admin/draft/#{@draft.id}") is_expected.to have_link("Comment ##{@comment.id}", href: "/admin/comment/#{@comment.id}") end context 'with missing object' do before do visit delete_path(model_name: 'player', id: 1) end it 'raises NotFound' do expect(page.driver.status_code).to eq(404) end end context 'with show action disabled' do before do RailsAdmin.config.actions do dashboard index delete end @draft = FactoryBot.create :draft @player = @draft.player @comment = @player.comments.create visit delete_path(model_name: 'player', id: @player.id) end it 'shows "Delete model"' do is_expected.to have_content('delete this player') is_expected.not_to have_selector("a[href=\"/admin/player/#{@player.id}\"]") is_expected.not_to have_selector("a[href=\"/admin/draft/#{@draft.id}\"]") is_expected.not_to have_selector("a[href=\"/admin/comment/#{@comment.id}\"]") end end context 'on deleting an object which has an associated item without id' do before do @player = FactoryBot.create :player allow_any_instance_of(Player).to receive(:draft).and_return(Draft.new) visit delete_path(model_name: 'player', id: @player.id) end it 'shows "Delete model"' do is_expected.not_to have_content('Routing Error') is_expected.to have_content('delete this player') is_expected.to have_link(@player.name, href: "/admin/player/#{@player.id}") end end context 'on deleting an object which has many associated item' do before do comments = FactoryBot.create_list :comment, 20 @player = FactoryBot.create :player, comments: comments visit delete_path(model_name: 'player', id: @player.id) end it 'shows only ten first plus x mores', skip_mongoid: true do is_expected.to have_selector('.comment', count: 10) is_expected.to have_content('Plus 10 more Comments') end end context 'on destroy' do before do @player = FactoryBot.create :player visit delete_path(model_name: 'player', id: @player.id) click_button "Yes, I'm sure" @player = RailsAdmin::AbstractModel.new('Player').first end it 'destroys an object' do expect(@player).to be_nil end it 'shows success message' do is_expected.to have_content('Player successfully deleted') end end context 'with destroy errors' do before do allow_any_instance_of(Player).to receive(:destroy_hook) { throw :abort } @player = FactoryBot.create :player visit delete_path(model_name: 'player', id: @player.id) click_button "Yes, I'm sure" end it 'does not destroy an object' do expect(@player.reload).to be end it 'shows error message' do is_expected.to have_content('Player failed to be deleted') end it 'returns status code 406' do expect(page.status_code).to eq(406) end end context 'on destroy error by dependent: :restrict_with_error' do let!(:player) { FactoryBot.create :player, team: FactoryBot.create(:restricted_team) } before do visit delete_path(model_name: 'restricted_team', id: player.team.id) click_button "Yes, I'm sure" is_expected.to have_content('Restricted team failed to be deleted') end it 'shows error message', active_record: true do is_expected.to have_content('Cannot delete record because dependent players exist') end it 'shows error message', mongoid: true do is_expected.to have_content('Players is not empty and prevents the document from being destroyed') end end context 'on cancel' do before do @player = FactoryBot.create :player visit delete_path(model_name: 'player', id: @player.id) end it 'does not destroy an object', js: true do find_button('Cancel').trigger('click') is_expected.to have_text 'No actions were taken' expect(RailsAdmin::AbstractModel.new('Player').first).to be end end context 'with missing object' do before do delete delete_path(model_name: 'player', id: 1) end it 'raises NotFound' do expect(response.code).to eq('404') end end context 'when navigated to delete from show page' do it 'redirects to the index instead of trying to show the deleted object' do @player = FactoryBot.create :player visit show_path(model_name: 'player', id: @player.id) click_link 'Delete' click_button "Yes, I'm sure" expect(URI.parse(page.current_url).path).to eq(index_path(model_name: 'player')) end it 'stays on the delete page' do allow_any_instance_of(Player).to receive(:destroy_hook) { throw :abort } @player = FactoryBot.create :player visit show_path(model_name: 'player', id: @player.id) click_link 'Delete' click_button "Yes, I'm sure" expect(URI.parse(page.current_url).path).to eq(delete_path(model_name: 'player', id: @player.id)) end end context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'deletes the object' do visit delete_path(model_name: 'fanship', id: fanship.id) click_button "Yes, I'm sure" is_expected.to have_content('Fanship successfully deleted') expect(Fanship.all).to be_empty end end end ================================================ FILE: spec/integration/actions/edit_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Edit action', type: :request do subject { page } describe 'page' do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) end it 'shows "Edit model"' do is_expected.to have_content('Edit Player') end it 'shows required fields as "Required"' do is_expected.to have_selector('div', text: /Name\s*Required/) is_expected.to have_selector('div', text: /Number\s*Required/) end it 'shows non-required fields as "Optional"' do expect(find('#player_position_field .form-text')).to have_content('Optional') expect(find('#player_born_on_field .form-text')).to have_content('Optional') expect(find('#player_notes_field .form-text')).to have_content('Optional') end it 'checks required fields to have required attribute set' do expect(find_field('player_name')[:required]).to be_present expect(find_field('player_number')[:required]).to be_present end it 'checks optional fields to not have required attribute set' do expect(find_field('player_position')[:required]).to be_blank end end describe 'css hooks' do it 'is present' do visit new_path(model_name: 'team') is_expected.to have_selector('#team_division_id_field.belongs_to_association_type.division_field') end end describe 'field grouping' do it 'is hideable' do RailsAdmin.config Team do edit do group :default do label 'Hidden group' hide end end end visit new_path(model_name: 'team') # Should not have the group header is_expected.to have_no_selector('legend', text: 'Hidden Group') # Should not have any of the group's fields either is_expected.to have_no_selector('select#team_division') is_expected.to have_no_selector('input#team_name') is_expected.to have_no_selector('input#team_logo_url') is_expected.to have_no_selector('input#team_manager') is_expected.to have_no_selector('input#team_ballpark') is_expected.to have_no_selector('input#team_mascot') is_expected.to have_no_selector('input#team_founded') is_expected.to have_no_selector('input#team_wins') is_expected.to have_no_selector('input#team_losses') is_expected.to have_no_selector('input#team_win_percentage') is_expected.to have_no_selector('input#team_revenue') end it 'hides association groupings' do RailsAdmin.config Team do edit do group :players do label 'Players' field :players hide end end end visit new_path(model_name: 'team') # Should not have the group header is_expected.to have_no_selector('legend', text: 'Players') # Should not have any of the group's fields either is_expected.to have_no_selector('select#team_player_ids') end it 'is renameable' do RailsAdmin.config Team do edit do group :default do label 'Renamed group' end end end visit new_path(model_name: 'team') # NOTE: capybara 2.0 is exceedingly reluctant to reveal the text of invisible elements. This was # the least terrible option I was able to find. It would probably be better to refactor the test # so the label we're looking for is displayed. expect(find('legend', visible: false).native.text).to include('Renamed group') end describe 'help' do before do class HelpTest < Tableless column :name, 'varchar(50)' column :division, :varchar end RailsAdmin.config.included_models = [HelpTest, Team] end after(:each) do # restore validation setting HelpTest._validators[:name] = [] HelpTest.reset_callbacks(:validate) end context 'using mongoid', skip_active_record: true do it 'uses the db column size for the maximum length' do visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length up to 255.') end it 'returns nil for the maximum length' do visit new_path(model_name: 'team') expect(find('#team_custom_field_field .form-text')).not_to have_content('Length') end end context 'using active_record', skip_mongoid: true do it 'uses the db column size for the maximum length' do visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length up to 50.') end it 'uses the :minimum setting from the validation' do HelpTest.class_eval do validates_length_of :name, minimum: 1 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-50.') end it 'uses the minimum of db column size or :maximum setting from the validation' do HelpTest.class_eval do validates_length_of :name, maximum: 51 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length up to 50.') end end it 'shows help section if present' do RailsAdmin.config HelpTest do edit do group :default do help 'help paragraph to display' end end end visit new_path(model_name: 'help_test') is_expected.to have_selector('fieldset>p', text: 'help paragraph to display') end it 'does not show help if not present' do RailsAdmin.config HelpTest do edit do group :default do label 'no help' end end end visit new_path(model_name: 'help_test') is_expected.not_to have_selector('fieldset>p') end it 'is able to display multiple help if there are multiple sections' do RailsAdmin.config HelpTest do edit do group :default do field :name help 'help for default' end group :other_section do label 'Other Section' field :division help 'help for other section' end end end visit new_path(model_name: 'help_test') is_expected.to have_selector('fieldset>p', text: 'help for default') is_expected.to have_selector('fieldset>p', text: 'help for other section') is_expected.to have_selector('fieldset>p', count: 2) end it 'uses the :is setting from the validation' do HelpTest.class_eval do validates_length_of :name, is: 3 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length of 3.') end it 'uses the :maximum setting from the validation' do HelpTest.class_eval do validates_length_of :name, maximum: 49 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length up to 49.') end it 'uses the :minimum and :maximum from the validation' do HelpTest.class_eval do validates_length_of :name, minimum: 1, maximum: 49 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-49.') end it 'uses the range from the validation' do HelpTest.class_eval do validates_length_of :name, in: 1..49 end visit new_path(model_name: 'help_test') expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-49.') end it 'does not show help for hidden fields' do RailsAdmin.config HelpTest do edit do field :name, :hidden end end visit new_path(model_name: 'help_test') expect(page).not_to have_css('.form-text') end end it 'has accessor for its fields' do RailsAdmin.config Team do edit do group :default do field :name field :logo_url end group :belongs_to_associations do label "Belong's to associations" field :division end group :basic_info do field :manager end end end visit new_path(model_name: 'team') is_expected.to have_selector('legend', text: 'Basic info', visible: false) is_expected.to have_selector('legend', text: 'Basic info', visible: true) is_expected.to have_selector('legend', text: "Belong's to associations") is_expected.to have_selector('label', text: 'Name') is_expected.to have_selector('label', text: 'Logo url') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('.control-group', count: 4) end it 'has accessor for its fields by type' do RailsAdmin.config Team do edit do group :default do field :name field :logo_url end group :other do field :division_id field :manager field :ballpark fields_of_type :string do label { "#{label} (STRING)" } end end end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Name') is_expected.to have_selector('label', text: 'Logo url') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('label', text: 'Manager (STRING)') is_expected.to have_selector('label', text: 'Ballpark (STRING)') end end describe 'fields' do it 'shows all by default' do visit new_path(model_name: 'team') is_expected.to have_selector('select#team_division_id') is_expected.to have_selector('input#team_name') is_expected.to have_selector('input#team_logo_url') is_expected.to have_selector('input#team_manager') is_expected.to have_selector('input#team_ballpark') is_expected.to have_selector('input#team_mascot') is_expected.to have_selector('input#team_founded') is_expected.to have_selector('input#team_wins') is_expected.to have_selector('input#team_losses') is_expected.to have_selector('input#team_win_percentage') is_expected.to have_selector('input#team_revenue') is_expected.to have_selector('select#team_player_ids') is_expected.to have_selector('select#team_fan_ids') end it 'appears in order defined' do RailsAdmin.config Team do edit do field :manager field :division field :name end end visit new_path(model_name: 'team') is_expected.to have_selector(:xpath, "//*[contains(@class, 'field')][1]//*[@id='team_manager']") is_expected.to have_selector(:xpath, "//*[contains(@class, 'field')][2]//*[@id='team_division_id']") is_expected.to have_selector(:xpath, "//*[contains(@class, 'field')][3]//*[@id='team_name']") end it 'only shows the defined fields if some fields are defined' do RailsAdmin.config Team do edit do field :division field :name end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('label', text: 'Name') is_expected.to have_selector('.control-group', count: 2) end describe 'I18n awarly' do after :each do I18n.locale = :en end it 'delegates the label option to the ActiveModel API and memoizes it' do RailsAdmin.config Team do edit do field :manager field :fans end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Team Manager') is_expected.to have_selector('label', text: 'Some Fans') I18n.locale = :fr visit new_path(model_name: 'team') is_expected.to have_selector('label', text: "Manager de l'équipe") is_expected.to have_selector('label', text: 'Quelques fans') end end it 'is renameable' do RailsAdmin.config Team do edit do field :manager do label 'Renamed field' end field :division field :name end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Renamed field') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('label', text: 'Name') end it 'is renameable by type' do RailsAdmin.config Team do edit do fields_of_type :string do label { "#{label} (STRING)" } end end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('label', text: 'Name (STRING)') is_expected.to have_selector('label', text: 'Logo url (STRING)') is_expected.to have_selector('label', text: 'Manager (STRING)') is_expected.to have_selector('label', text: 'Ballpark (STRING)') is_expected.to have_selector('label', text: 'Mascot (STRING)') is_expected.to have_selector('label', text: 'Founded') is_expected.to have_selector('label', text: 'Wins') is_expected.to have_selector('label', text: 'Losses') is_expected.to have_selector('label', text: 'Win percentage') is_expected.to have_selector('label', text: 'Revenue') is_expected.to have_selector('label', text: 'Players') is_expected.to have_selector('label', text: 'Fans') end it 'is globally renameable by type' do RailsAdmin.config Team do edit do fields_of_type :string do label { "#{label} (STRING)" } end end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Division') is_expected.to have_selector('label', text: 'Name (STRING)') is_expected.to have_selector('label', text: 'Logo url (STRING)') is_expected.to have_selector('label', text: 'Manager (STRING)') is_expected.to have_selector('label', text: 'Ballpark (STRING)') is_expected.to have_selector('label', text: 'Mascot (STRING)') is_expected.to have_selector('label', text: 'Founded') is_expected.to have_selector('label', text: 'Wins') is_expected.to have_selector('label', text: 'Losses') is_expected.to have_selector('label', text: 'Win percentage') is_expected.to have_selector('label', text: 'Revenue') is_expected.to have_selector('label', text: 'Players') is_expected.to have_selector('label', text: 'Fans') end it 'is flaggable as read only and be configurable with formatted_value' do RailsAdmin.config Team do edit do field :name do read_only true formatted_value do "I'm outputted in the form" end end end end visit new_path(model_name: 'team') is_expected.to have_content("I'm outputted in the form") end it 'is hideable' do RailsAdmin.config Team do edit do field :manager do hide end field :division field :name end end visit new_path(model_name: 'team') is_expected.to have_no_selector('#team_manager') is_expected.to have_selector('#team_division_id') is_expected.to have_selector('#team_name') end it 'is hideable by type' do RailsAdmin.config Team do edit do fields_of_type :string do hide end end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Division') is_expected.to have_no_selector('label', text: 'Name') is_expected.to have_no_selector('label', text: 'Logo url') is_expected.to have_no_selector('label', text: 'Manager') is_expected.to have_no_selector('label', text: 'Ballpark') is_expected.to have_no_selector('label', text: 'Mascot') is_expected.to have_selector('label', text: 'Founded') is_expected.to have_selector('label', text: 'Wins') is_expected.to have_selector('label', text: 'Losses') is_expected.to have_selector('label', text: 'Win percentage') is_expected.to have_selector('label', text: 'Revenue') is_expected.to have_selector('label', text: 'Players') is_expected.to have_selector('label', text: 'Fans') end it 'is globally hideable by type' do RailsAdmin.config Team do edit do fields_of_type :string do hide end end end visit new_path(model_name: 'team') is_expected.to have_selector('label', text: 'Division') is_expected.to have_no_selector('label', text: 'Name') is_expected.to have_no_selector('label', text: 'Logo url') is_expected.to have_no_selector('label', text: 'Manager') is_expected.to have_no_selector('label', text: 'Ballpark') is_expected.to have_no_selector('label', text: 'Mascot') is_expected.to have_selector('label', text: 'Founded') is_expected.to have_selector('label', text: 'Wins') is_expected.to have_selector('label', text: 'Losses') is_expected.to have_selector('label', text: 'Win percentage') is_expected.to have_selector('label', text: 'Revenue') is_expected.to have_selector('label', text: 'Players') is_expected.to have_selector('label', text: 'Fans') end it 'has option to customize the help text' do RailsAdmin.config Team do edit do field :manager do help "#{help} Additional help text for manager field." end field :division field :name end end visit new_path(model_name: 'team') expect(find('#team_manager_field .form-text')).to have_content('Required. Length up to 100. Additional help text for manager field.') expect(find('#team_division_id_field .form-text')).to have_content('Required') expect(find('#team_name_field .form-text')).not_to have_content('Additional help text') end it 'has option to override required status' do RailsAdmin.config Team do edit do field :manager do optional true end field :division do optional true end field :name do required true end end end visit new_path(model_name: 'team') expect(find('#team_manager_field .form-text')).to have_content('Optional') expect(find('#team_division_id_field .form-text')).to have_content('Optional') expect(find('#team_name_field .form-text')).to have_content(I18n.translate('admin.help.team.name')) end describe 'inline_add' do it 'can hide the add button on an associated field' do RailsAdmin.config Player do edit do field :team do inline_add false end field :draft do inline_add false end field :comments do inline_add false end end end visit new_path(model_name: 'player') is_expected.to have_no_selector('a', text: 'Add a new Team') is_expected.to have_no_selector('a', text: 'Add a new Draft') is_expected.to have_no_selector('a', text: 'Add a new Comment') end it 'can show the add button on an associated field' do RailsAdmin.config Player do edit do field :team do inline_add true end field :draft do inline_add true end field :comments do inline_add true end end end visit new_path(model_name: 'player') is_expected.to have_selector('a', text: 'Add a new Team') is_expected.to have_selector('a', text: 'Add a new Draft') is_expected.to have_selector('a', text: 'Add a new Comment') end context 'when the associated model is invisible' do before do RailsAdmin.config do |config| [Team, Draft, Comment].each do |model| config.model model do visible false end end end end it 'does not prevent showing the add button' do visit new_path(model_name: 'player') is_expected.to have_selector('a', text: 'Add a new Team') is_expected.to have_selector('a', text: 'Add a new Draft') is_expected.to have_selector('a', text: 'Add a new Comment') end end end describe 'inline_edit' do it 'can hide the edit button on an associated field' do RailsAdmin.config Player do edit do field :team do inline_edit false end field :draft do inline_edit false end end end visit new_path(model_name: 'player') is_expected.to have_no_selector('a', text: 'Edit this Team') is_expected.to have_no_selector('a', text: 'Edit this Draft') end it 'can show the edit button on an associated field' do RailsAdmin.config Player do edit do field :team do inline_edit true end field :draft do inline_edit true end end end visit new_path(model_name: 'player') is_expected.to have_selector('a', text: 'Edit this Team') is_expected.to have_selector('a', text: 'Edit this Draft') end context 'when the associated model is invisible' do before do RailsAdmin.config do |config| [Team, Draft].each do |model| config.model model do visible false end end end end it 'does not prevent showing the edit button' do visit new_path(model_name: 'player') is_expected.to have_selector('a', text: 'Edit this Team') is_expected.to have_selector('a', text: 'Edit this Draft') end end end end context 'with missing object' do before do visit edit_path(model_name: 'player', id: 1) end it 'raises NotFound' do expect(page.driver.status_code).to eq(404) end end context 'with a readonly object' do let(:comment) { FactoryBot.create :comment, (CI_ORM == :mongoid ? {_type: 'ReadOnlyComment'} : {}) } it 'raises ActionNotAllowed' do expect { visit edit_path(model_name: 'read_only_comment', id: comment.id) }.to raise_error 'RailsAdmin::ActionNotAllowed' end end context 'with missing label', given: ['a player exists', 'three teams with no name exist'] do before do @player = FactoryBot.create :player @teams = Array.new(3) { FactoryBot.create :team, name: '' } visit edit_path(model_name: 'player', id: @player.id) end end context 'with overridden to_param' do before do @ball = FactoryBot.create :ball visit edit_path(model_name: 'ball', id: @ball.id) end it 'displays a link to the delete page' do is_expected.to have_selector "a[href$='/admin/ball/#{@ball.id}/delete']" end end context 'on cancel' do before do @player = FactoryBot.create :player visit '/admin/player' click_link 'Edit' end it 'sends back to previous URL', js: true do find_button('Cancel').trigger('click') is_expected.to have_text 'No actions were taken' expect(page.current_path).to eq('/admin/player') end it 'allows submit even if client-side validation is not satisfied', js: true do fill_in 'player[name]', with: '' find_button('Cancel').trigger('click') is_expected.to have_text 'No actions were taken' end end context 'with errors' do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) end it 'returns to edit page' do fill_in 'player[name]', with: '' click_button 'Save' # first(:button, "Save").click expect(page.driver.status_code).to eq(406) is_expected.to have_selector "form[action='#{edit_path(model_name: 'player', id: @player.id)}']" end end describe 'add another' do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' click_button 'Save' # first(:button, "Save").click @player = RailsAdmin::AbstractModel.new('Player').first end it 'updates an object with correct attributes' do expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end describe 'update and edit', js: true do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' find_button('Save and edit').trigger('click') end it 'updates an object with correct attributes' do is_expected.to have_text 'Player successfully updated' expect(page.current_path).to eq("/admin/player/#{@player.id}/edit") @player.reload expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end context 'with a submit button with custom value', js: true do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) execute_script %{$('.form-actions [name="_save"]').attr('name', 'player[name]').attr('value', 'Jackie Robinson')} find_button('Save').trigger('click') is_expected.to have_text 'Player successfully updated' end it 'submits the value' do @player.reload expect(@player.name).to eq('Jackie Robinson') end end context 'with missing object' do before do put edit_path(model_name: 'player', id: 1), params: {player: {name: 'Jackie Robinson', number: 42, position: 'Second baseman'}} end it 'raises NotFound' do expect(response.code).to eq('404') end end context 'with invalid object' do before do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: 'a' fill_in 'player[position]', with: 'Second baseman' click_button 'Save' # first(:button, "Save").click @player.reload end it 'shows an error message' do expect(Capybara.string(body)).to have_content('Player failed to be updated') end end context 'with overridden to_param' do before do @ball = FactoryBot.create :ball visit edit_path(model_name: 'ball', id: @ball.id) fill_in 'ball[color]', with: 'gray' click_button 'Save and edit' @ball.reload end it 'updates an object with correct attributes' do expect(@ball.color).to eq('gray') end end context 'on update of STI subclass on superclass view' do before do @hardball = FactoryBot.create :hardball visit edit_path(model_name: 'ball', id: @hardball.id) fill_in 'ball[color]', with: 'cyan' click_button 'Save and edit' @hardball.reload end it 'updates an object with correct attributes' do expect(@hardball.color).to eq('cyan') end end context "with a field with 'format' as a name (conflicts with Kernel#format)" do it 'is updatable without any error' do RailsAdmin.config FieldTest do edit do field :format end end visit new_path(model_name: 'field_test') fill_in 'field_test[format]', with: 'test for format' click_button 'Save' # first(:button, "Save").click @record = RailsAdmin::AbstractModel.new('FieldTest').first expect(@record.format).to eq('test for format') end end context "with a field with 'open' as a name" do it 'is updatable without any error' do RailsAdmin.config FieldTest do edit do field :open do nullable false end end end record = FieldTest.create visit edit_path(model_name: 'field_test', id: record.id) expect do check 'field_test[open]' click_button 'Save' end.to change { record.reload.open }.from(nil).to(true) end end context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'edits the object' do visit edit_path(model_name: 'fanship', id: fanship.id) fill_in 'Since', with: '2000-01-23' click_button 'Save' expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) end context 'using custom serializer' do before do RailsAdmin.config.composite_keys_serializer = Class.new do def self.serialize(keys) keys.join(',') end def self.deserialize(string) string.split(',') end end end it 'edits the object' do visit edit_path(model_name: 'fanship', id: "#{fanship.fan_id},#{fanship.team_id}") fill_in 'Since', with: '2000-01-23' click_button 'Save' expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) end end context 'receiving invalid id' do it 'returns 404' do visit edit_path(model_name: 'fanship', id: '11') expect(page.driver.status_code).to eq(404) is_expected.to have_content("Fanship with id '11' could not be found") end end end end ================================================ FILE: spec/integration/actions/export_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'csv' RSpec.describe 'Export action', type: :request do subject { page } let!(:player) { FactoryBot.create(:player) } it 'exports to CSV' do visit export_path(model_name: 'player') click_button 'Export to csv' is_expected.to have_content player.name end it 'exports to JSON' do visit export_path(model_name: 'player') click_button 'Export to json' is_expected.to have_content player.name end it 'exports to XML' do pending "Mongoid does not support to_xml's :include option" if CI_ORM == :mongoid visit export_path(model_name: 'player') click_button 'Export to xml' is_expected.to have_content player.name end it 'works with Turbo Drive enabled', js: true do visit export_path(model_name: 'player') page.execute_script 'console.error = function(error) { throw error }' expect { find_button('Export to csv').trigger('click') }.not_to raise_error end it 'does not break when nothing is checked' do visit export_path(model_name: 'comment') all('input[type="checkbox"]').each(&:uncheck) expect { click_button 'Export to csv' }.not_to raise_error end describe 'with associations' do let!(:players) { FactoryBot.create_list(:player, 3) } let(:team) { FactoryBot.create :team } let(:draft) { FactoryBot.create :draft } let(:comments) { FactoryBot.create_list(:comment, 2) } before do player.team = team player.draft = draft player.comments = comments player.save end it 'exports to CSV with default schema, containing properly translated header and follow configuration' do RailsAdmin.config do |c| c.model Player do include_all_fields field :name do export_value do "#{value} exported" end end field :json_field, :json do formatted_value do '{}' end end end end visit export_path(model_name: 'player') is_expected.to have_content 'Select fields to export' select " ','", from: 'csv_options_generator_col_sep' click_button 'Export to csv' csv = CSV.parse page.driver.response.body.force_encoding('utf-8') # comes through as us-ascii on some platforms expect(csv[0]).to match_array ['Id', 'Created at', 'Updated at', 'Deleted at', 'Name', 'Position', 'Number', 'Retired', 'Injured', 'Born on', 'Notes', 'Suspended', 'Formation', 'Json field', 'Id [Team]', 'Created at [Team]', 'Updated at [Team]', 'Name [Team]', 'Logo url [Team]', 'Team Manager [Team]', 'Ballpark [Team]', 'Mascot [Team]', 'Founded [Team]', 'Wins [Team]', 'Losses [Team]', 'Win percentage [Team]', 'Revenue [Team]', 'Color [Team]', 'Custom field [Team]', 'Main Sponsor [Team]', 'Id [Draft]', 'Created at [Draft]', 'Updated at [Draft]', 'Date [Draft]', 'Round [Draft]', 'Pick [Draft]', 'Overall [Draft]', 'College [Draft]', 'Notes [Draft]', 'Id [Comments]', 'Content [Comments]', 'Created at [Comments]', 'Updated at [Comments]'] expect(csv.flatten).to include("#{player.name} exported") expect(csv.flatten).to include(player.team.name) expect(csv.flatten).to include(player.draft.college) expect(csv.flatten.join(' ')).to include(player.comments.first.content.split("\n").first.strip) expect(csv.flatten.join(' ')).to include(player.comments.second.content.split("\n").first.strip) end let(:custom_schema) do # removed schema=>only=>created_at { 'only' => [PK_COLUMN.to_s, 'updated_at', 'deleted_at', 'name', 'position', 'number', 'retired', 'injured', 'born_on', 'notes', 'suspended'], 'include' => { 'team' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'name', 'logo_url', 'manager', 'ballpark', 'mascot', 'founded', 'wins', 'losses', 'win_percentage', 'revenue', 'color']}, 'draft' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'date', 'round', 'pick', 'overall', 'college', 'notes']}, 'comments' => {'only' => [PK_COLUMN.to_s, 'content', 'created_at', 'updated_at']}, }, } end it 'exports to CSV with custom schema' do page.driver.post(export_path(model_name: 'player', schema: custom_schema, csv: true, all: true, csv_options: {generator: {col_sep: ','}})) csv = CSV.parse page.driver.response.body expect(csv[0]).not_to include('Created at') end it 'exports polymorphic fields the easy way for now' do visit export_path(model_name: 'comment') select " ','", from: 'csv_options_generator_col_sep' click_button 'Export to csv' csv = CSV.parse page.driver.response.body expect(csv[0]).to match_array ['Id', 'Commentable', 'Commentable type', 'Content', 'Created at', 'Updated at'] csv[1..].each do |line| expect(line[csv[0].index('Commentable')]).to eq(player.id.to_s) expect(line[csv[0].index('Commentable type')]).to eq(player.class.to_s) end end end context 'on cancel' do before do @player = FactoryBot.create :player visit export_path(model_name: 'player') end it 'does nothing', js: true do find_button('Cancel').trigger('click') is_expected.to have_text 'No actions were taken' end end describe 'bulk export' do it 'is supported' do visit index_path(model_name: 'player') click_link 'Export found Players' is_expected.to have_content('Select fields to export') end describe 'with model scope' do let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } before do RailsAdmin.config do |config| config.model Comment::Confirmed do scope { Comment::Confirmed.unscoped } end end end it 'overrides default_scope' do page.driver.post(export_path(model_name: 'comment~confirmed', schema: {only: ['content']}, csv: true, all: true, csv_options: {generator: {col_sep: ','}}, bulk_ids: comments.map(&:id))) csv = CSV.parse page.driver.response.body expect(csv.flatten).to match_array %w[Content something anything] end end end context 'with composite primary keys', composite_primary_keys: true do let!(:fanship) { FactoryBot.create(:fanship) } it 'exports to CSV' do visit export_path(model_name: 'fanship') click_button 'Export to csv' is_expected.to have_content fanship.fan.name is_expected.to have_content fanship.team.name end end end ================================================ FILE: spec/integration/actions/history_index_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'HistoryIndex action', type: :request, active_record: true do subject { page } let(:user) { FactoryBot.create :user } let(:paper_trail_test) { FactoryBot.create :paper_trail_test } before(:each) do RailsAdmin.config do |config| config.audit_with :paper_trail, 'User', 'PaperTrail::Version' end PaperTrail::Version.delete_all with_versioning do PaperTrail.request.whodunnit = user.id 30.times do |i| paper_trail_test.update!(name: "updated name #{i}") end end end it 'shows the history' do visit history_index_path(model_name: 'paper_trail_test') is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 20) end it 'supports pagination' do visit history_index_path(model_name: 'paper_trail_test', page: 2) is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 11) end it 'supports sorting', js: true do visit history_index_path(model_name: 'paper_trail_test') find('th.header', text: 'Item').click is_expected.to have_css('th.item.headerSortDown') end context "when Kaminari's custom param_name is set" do before { Kaminari.config.param_name = :pagina } after { Kaminari.config.param_name = :page } it 'picks the page value from params' do visit history_index_path(model_name: 'paper_trail_test', pagina: 2) is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 11) end end end ================================================ FILE: spec/integration/actions/history_show_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'HistoryShow action', type: :request, active_record: true do let(:user) { FactoryBot.create :user } let(:paper_trail_test) { FactoryBot.create :paper_trail_test } before(:each) do RailsAdmin.config do |config| config.audit_with :paper_trail, 'User', 'PaperTrail::Version' end PaperTrail::Version.delete_all with_versioning do PaperTrail.request.whodunnit = user.id 30.times do |i| paper_trail_test.update!(name: "updated name #{i}") end end end it 'shows the history' do visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id) expect(all('table#history tbody tr').count).to eq(20) end it 'supports pagination' do visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id, page: 2) expect(all('table#history tbody tr').count).to eq(11) end context "when Kaminari's custom param_name is set" do before { Kaminari.config.param_name = :pagina } after { Kaminari.config.param_name = :page } it 'picks the page value from params' do visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id, pagina: 2) expect(all('table#history tbody tr').count).to eq(11) end end end ================================================ FILE: spec/integration/actions/index_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Index action', type: :request do subject { page } describe 'page' do it 'shows "List of Models", should show filters and should show column headers' do RailsAdmin.config.default_items_per_page = 1 2.times { FactoryBot.create :player } # two pages of players visit index_path(model_name: 'player') is_expected.to have_content('List of Players') is_expected.to have_content('Created at') is_expected.to have_content('Updated at') # it "shows the show, edit and delete links" do is_expected.to have_selector("li[title='Show'] a") is_expected.to have_selector("li[title='Edit'] a") is_expected.to have_selector("li[title='Delete'] a") # it "has the search box with some prompt text" do is_expected.to have_selector("input[placeholder='Filter']") # https://github.com/railsadminteam/rails_admin/issues/362 # test that no link uses the "wildcard route" with the main # controller and list method # it "does not use the 'wildcard route'" do is_expected.to have_selector("a[href*='all=true']") # make sure we're fully testing pagination is_expected.to have_no_selector("a[href^='/rails_admin/main/list']") end end describe 'css hooks' do it 'is present' do RailsAdmin.config Team do list do field :name end end FactoryBot.create :team visit index_path(model_name: 'team') is_expected.to have_selector('th.header.string_type.name_field') is_expected.to have_selector('td.string_type.name_field') end end describe 'with querying and filtering' do before do @teams = Array.new(2) do FactoryBot.create(:team) end @players = [ FactoryBot.create(:player, retired: true, injured: true, team: @teams[0]), FactoryBot.create(:player, retired: true, injured: false, team: @teams[0]), FactoryBot.create(:player, retired: false, injured: true, team: @teams[1]), FactoryBot.create(:player, retired: false, injured: false, team: @teams[1]), ] @comment = FactoryBot.create(:comment, commentable: @players[2]) end it 'allows to query on any attribute' do RailsAdmin.config Player do list do field :name field :team field :injured field :retired end end visit index_path(model_name: 'player', query: @players[0].name) is_expected.to have_content(@players[0].name) (1..3).each do |i| is_expected.to have_no_content(@players[i].name) end end it 'allows to clear the search query box', js: true do visit index_path(model_name: 'player', query: @players[0].name) is_expected.not_to have_content(@players[1].name) find_button('Reset filters').click is_expected.to have_content(@players[1].name) end it 'allows to filter on one attribute' do RailsAdmin.config Player do list do field :name field :team field :injured field :retired end end visit index_path(model_name: 'player', f: {injured: {'1' => {v: 'true'}}}) is_expected.to have_content(@players[0].name) is_expected.to have_no_content(@players[1].name) is_expected.to have_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to combine filters on two different attributes' do RailsAdmin.config Player do list do field :name field :team field :injured field :retired end end visit index_path(model_name: 'player', f: {retired: {'1' => {v: 'true'}}, injured: {'1' => {v: 'true'}}}) is_expected.to have_content(@players[0].name) (1..3).each do |i| is_expected.to have_no_content(@players[i].name) end end it 'allows to filter on belongs_to relationships' do RailsAdmin.config Player do list do field :name field :team field :injured field :retired end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams[0].name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to filter on has_one relationships' do @draft = FactoryBot.create(:draft, player: @players[1], college: 'University of Alabama') RailsAdmin.config Player do list do field :name field :draft do searchable :college end end end visit index_path(model_name: 'player', f: {draft: {'1' => {v: 'Alabama'}}}) is_expected.to have_content(@players[1].name) is_expected.to have_css('tbody .name_field', count: 1) end it 'allows to disable search on attributes' do RailsAdmin.config Player do list do field :position field :name do searchable false end end end visit index_path(model_name: 'player', query: @players[0].name) is_expected.to have_no_content(@players[0].name) end it 'allows to search a belongs_to attribute over the base table' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable Player => :team_id end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.id}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a belongs_to attribute over the target table' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable Team => :name end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a belongs_to attribute over the target table with a table name specified as a hash' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable teams: :name end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a belongs_to attribute over the target table with a table name specified as a string' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable 'teams.name' end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a belongs_to attribute over the label method by default' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a belongs_to attribute over the target table when an attribute is specified' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable :name end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search over more than one attribute' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :team do searchable [:name, {Player => :team_id}] end end end visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}, '2' => {v: @teams.first.id, o: 'is'}}}) is_expected.to have_content(@players[0].name) is_expected.to have_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) # same with a different id visit index_path(model_name: 'player', f: {team: {'1' => {v: @teams.first.name}, '2' => {v: @teams.last.id, o: 'is'}}}) is_expected.to have_no_content(@players[0].name) is_expected.to have_no_content(@players[1].name) is_expected.to have_no_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'allows to search a has_many attribute over the target table' do RailsAdmin.config Player do list do field PK_COLUMN field :name field :comments do searchable :content end end end visit index_path(model_name: 'player', f: {comments: {'1' => {v: @comment.content}}}) is_expected.to have_no_content(@players[0].name) is_expected.to have_no_content(@players[1].name) is_expected.to have_content(@players[2].name) is_expected.to have_no_content(@players[3].name) end it 'displays base filters when no filters are present in the params' do RailsAdmin.config Player do list { filters(%i[name team]) } field :name do default_filter_operator 'is' end field :team do filterable true end end visit index_path(model_name: 'player') expect(JSON.parse(find('#filters_box')['data-options']).map(&:symbolize_keys)).to match_array [ { index: 1, label: 'Name', name: 'name', type: 'string', value: '', operator: 'is', operators: %w[_discard like not_like is starts_with ends_with], }, { index: 2, label: 'Team', name: 'team', type: 'belongs_to_association', value: '', operator: nil, operators: %w[_discard like not_like is starts_with ends_with _separator _present _blank], }, ] end it 'shows the help text below the search box' do RailsAdmin.config Player do list do search_help 'Use this box to search!' end end visit index_path(model_name: 'player') is_expected.to have_css('.form-text', text: /Use this box/) end end describe 'fields' do before do if defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) RailsAdmin.config Fan do configure(:fanships) { hide } configure(:fanship) { hide } end end end it 'shows all by default' do visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Created at', 'Updated at', 'Their Name', 'Teams'] end it 'hides some fields on demand with a block' do RailsAdmin.config Fan do list do exclude_fields_if do type == :datetime end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Their Name', 'Teams'] end it 'hides some fields on demand with fields list' do RailsAdmin.config Fan do list do exclude_fields :created_at, :updated_at end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Their Name', 'Teams'] end it 'adds some fields on demand with a block' do RailsAdmin.config Fan do list do include_fields_if do type != :datetime end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Their Name', 'Teams'] end it 'shows some fields on demand with fields list, respect ordering and configure them' do RailsAdmin.config Fan do list do fields :name, PK_COLUMN do label do "Modified #{label}" end end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Modified Id', 'Modified Their Name'] end it 'shows all fields if asked' do RailsAdmin.config Fan do list do include_all_fields field PK_COLUMN field :name end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Created at', 'Updated at', 'Their Name', 'Teams'] end it 'appears in order defined' do RailsAdmin.config Fan do list do field :updated_at field :name field PK_COLUMN field :created_at end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to eq(['Updated at', 'Their Name', 'Id', 'Created at']) end it 'only lists the defined fields if some fields are defined' do RailsAdmin.config Fan do list do field PK_COLUMN field :name end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to eq(['Id', 'Their Name']) is_expected.to have_no_selector('th:nth-child(4).header') end it 'delegates the label option to the ActiveModel API' do RailsAdmin.config Fan do list do field :name end end visit index_path(model_name: 'fan') expect(find('th:nth-child(2)')).to have_content('Their Name') end it 'is renameable' do RailsAdmin.config Fan do list do field PK_COLUMN do label 'Identifier' end field :name end end visit index_path(model_name: 'fan') expect(find('th:nth-child(2)')).to have_content('Identifier') expect(find('th:nth-child(3)')).to have_content('Their Name') end it 'is renameable by type' do RailsAdmin.config Fan do list do fields_of_type :datetime do label { "#{label} (datetime)" } end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Created at (datetime)', 'Updated at (datetime)', 'Their Name', 'Teams'] end it 'is globally renameable by type' do RailsAdmin.config Fan do list do fields_of_type :datetime do label { "#{label} (datetime)" } end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Created at (datetime)', 'Updated at (datetime)', 'Their Name', 'Teams'] end it 'is sortable by default' do visit index_path(model_name: 'fan') is_expected.to have_selector('th:nth-child(2).header') is_expected.to have_selector('th:nth-child(3).header') is_expected.to have_selector('th:nth-child(4).header') is_expected.to have_selector('th:nth-child(5).header') end it 'has option to disable sortability' do RailsAdmin.config Fan do list do field PK_COLUMN do sortable false end field :name end end visit index_path(model_name: 'fan') is_expected.to have_no_selector('th:nth-child(2).header') is_expected.to have_selector('th:nth-child(3).header') end it 'has option to disable sortability by type' do RailsAdmin.config Fan do list do fields_of_type :datetime do sortable false end field PK_COLUMN field :name field :created_at field :updated_at end end visit index_path(model_name: 'fan') is_expected.to have_selector('th:nth-child(2).header') is_expected.to have_selector('th:nth-child(3).header') is_expected.to have_no_selector('th:nth-child(4).header') is_expected.to have_no_selector('th:nth-child(5).header') end it 'has option to disable sortability by type globally' do RailsAdmin.config Fan do list do fields_of_type :datetime do sortable false end field PK_COLUMN field :name field :created_at field :updated_at end end visit index_path(model_name: 'fan') is_expected.to have_selector('th:nth-child(2).header') is_expected.to have_selector('th:nth-child(3).header') is_expected.to have_no_selector('th:nth-child(4).header') is_expected.to have_no_selector('th:nth-child(5).header') end it 'has option to hide fields by type' do RailsAdmin.config Fan do list do fields_of_type :datetime do hide end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Their Name', 'Teams'] end it 'has option to hide fields by type globally' do RailsAdmin.config Fan do list do fields_of_type :datetime do hide end end end visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). to match_array ['Id', 'Their Name', 'Teams'] end it 'has option to customize column width' do RailsAdmin.config Fan do list do field PK_COLUMN do column_width 200 end field :name field :created_at field :updated_at end end @fans = FactoryBot.create_list(:fan, 2) visit index_path(model_name: 'fan') # NOTE: Capybara really doesn't want us to look at invisible text. This test # could break at any moment. expect(find('style').native.text).to include("#list th.#{PK_COLUMN}_field") expect(find('style').native.text).to include("#list td.#{PK_COLUMN}_field") end it 'has option to customize output formatting' do RailsAdmin.config Fan do list do field PK_COLUMN field :name do formatted_value do value.to_s.upcase end end field :created_at field :updated_at end end @fans = FactoryBot.create_list(:fan, 2).sort_by(&:id) visit index_path(model_name: 'fan') expect(find('tbody tr:nth-child(1) td:nth-child(3)')).to have_content(@fans[1].name.upcase) expect(find('tbody tr:nth-child(2) td:nth-child(3)')).to have_content(@fans[0].name.upcase) end it 'has a simple option to customize output formatting of date fields' do RailsAdmin.config Fan do list do field PK_COLUMN field :name field :created_at do date_format :short end field :updated_at end end @fans = FactoryBot.create_list(:fan, 2) visit index_path(model_name: 'fan') is_expected.to have_selector('tbody tr:nth-child(1) td:nth-child(4)', text: /\d{2} \w{3} \d{1,2}:\d{1,2}/) end it 'has option to customize output formatting of date fields' do RailsAdmin.config Fan do list do field PK_COLUMN field :name field :created_at do strftime_format '%Y-%m-%d' end field :updated_at end end @fans = FactoryBot.create_list(:fan, 2) visit index_path(model_name: 'fan') is_expected.to have_selector('tbody tr:nth-child(1) td:nth-child(4)', text: /\d{4}-\d{2}-\d{2}/) end it 'allows addition of virtual fields (object methods)' do RailsAdmin.config Team do list do field PK_COLUMN field :name field :player_names_truncated end end @team = FactoryBot.create :team @players = FactoryBot.create_list :player, 2, team: @team visit index_path(model_name: 'team') expect(find('tbody tr:nth-child(1) td:nth-child(4)')).to have_content(@players.sort_by(&:id).collect(&:name).join(', ')) end describe 'with title attribute' do it 'does not allow XSS' do RailsAdmin.config Team do list do field :name end end @team = FactoryBot.create :team, name: '" onclick="alert()" "' visit index_path(model_name: 'team') expect(find('tbody tr:nth-child(1) td:nth-child(2)')['onclick']).to be_nil expect(find('tbody tr:nth-child(1) td:nth-child(2)')['title']).to eq '" onclick="alert()" "' end it 'does not break values with HTML tags' do RailsAdmin.config Player do list do field :team end end @player = FactoryBot.create :player, team: FactoryBot.create(:team) visit index_path(model_name: 'player') expect(find('tbody tr:nth-child(1) td:nth-child(2)')['title']).to eq @player.team.name end end end context 'when no record exists' do before do visit index_path(model_name: 'player') end it 'shows "No records found" message' do is_expected.to have_content('No records found') end end context 'without pagination' do before do @players = FactoryBot.create_list(:player, 2) visit index_path(model_name: 'player') end it 'shows "2 results"' do is_expected.to have_content('2 players') end end context 'with pagination' do def visit_page(page) visit index_path(model_name: 'player', page: page) end before do FactoryBot.create_list :player, 3 end describe 'with limited_pagination=false' do before { RailsAdmin.config.default_items_per_page = 1 } it 'page 1' do visit_page(1) within('ul.pagination') do expect(find('li:first')).to have_content('« Prev') expect(find('li:last')).to have_content('Next »') expect(find('li.active')).to have_content('1') end end it 'page 2' do visit_page(2) within('ul.pagination') do expect(find('li:first')).to have_content('« Prev') expect(find('li:last')).to have_content('Next »') expect(find('li.active')).to have_content('2') end end it 'page 3' do visit_page(3) within('ul.pagination') do expect(find('li:first')).to have_content('« Prev') expect(find('li:last')).to have_content('Next »') expect(find('li.active')).to have_content('3') end end end context 'with limited_pagination=true' do before do RailsAdmin.config.default_items_per_page = 1 allow(RailsAdmin::AbstractModel.new(Player).config.list). to receive(:limited_pagination). and_return(true) end it 'page 1' do visit_page(1) within('ul.pagination') do expect(find('li:first')).not_to have_content('« Prev') expect(find('li:last')).to have_content('Next »') end end it 'page 2' do visit_page(2) within('ul.pagination') do expect(find('li:first')).to have_content('« Prev') expect(find('li:last')).to have_content('Next »') end end it 'page 3' do visit_page(3) within('ul.pagination') do expect(find('li:first')).to have_content('« Prev') expect(find('li:last')).to have_content('Next »') end end end describe 'number of items per page' do before do FactoryBot.create_list :league, 2 end it 'is configurable per model' do RailsAdmin.config League do list do items_per_page 1 end end visit index_path(model_name: 'league') is_expected.to have_selector('tbody tr', count: 1) visit index_path(model_name: 'player') is_expected.to have_selector('tbody tr', count: 3) end end end context 'on showing all' do it 'responds successfully with a single model' do FactoryBot.create :player visit index_path(model_name: 'player', all: true) expect(find('div.total-count')).to have_content('1 player') expect(find('div.total-count')).not_to have_content('1 players') end it 'responds successfully with multiple models' do FactoryBot.create_list(:player, 2) visit index_path(model_name: 'player', all: true) expect(find('div.total-count')).to have_content('2 players') end end context 'with pagination disabled by :associated_collection' do it 'responds successfully' do @team = FactoryBot.create :team Array.new(2) { FactoryBot.create :player, team: @team } visit index_path(model_name: 'player', associated_collection: 'players', compact: true, current_action: 'update', source_abstract_model: 'team', source_object_id: @team.id) expect(find('div.total-count')).to have_content('2 players') end end describe 'sorting' do let(:today) { Date.today } let(:players) do [{name: 'Jackie Robinson', created_at: today, team_id: rand(99_999), number: 42}, {name: 'Deibinson Romero', created_at: (today - 2.days), team_id: rand(99_999), number: 13}, {name: 'Sandy Koufax', created_at: (today - 1.days), team_id: rand(99_999), number: 32}] end let(:leagues) do [{name: 'American', created_at: (today - 1.day)}, {name: 'Florida State', created_at: (today - 2.days)}, {name: 'National', created_at: today}] end let(:player_names_by_date) { players.sort_by { |p| p[:created_at] }.collect { |p| p[:name] } } let(:league_names_by_date) { leagues.sort_by { |l| l[:created_at] }.collect { |l| l[:name] } } before { @players = players.collect { |h| Player.create(h) } } it 'has reverse direction by default' do RailsAdmin.config Player do list do sort_by :created_at field :name end end visit index_path(model_name: 'player') player_names_by_date.reverse.each_with_index do |name, i| expect(find("tbody tr:nth-child(#{i + 1})")).to have_content(name) end end it 'allows change direction by using field configuration' do RailsAdmin.config Player do list do sort_by :created_at configure :created_at do sort_reverse false end field :name end end visit index_path(model_name: 'player') player_names_by_date.each_with_index do |name, i| expect(find("tbody tr:nth-child(#{i + 1})")).to have_content(name) end end it 'can be activated by clicking the table header', js: true do visit index_path(model_name: 'player') find('th.header', text: 'Name').trigger('click') is_expected.to have_css('th.name_field.headerSortDown') expect(all('tbody td.name_field').map(&:text)).to eq @players.map(&:name).sort end end context 'on listing as compact json' do it 'has_content an array with 2 elements and contain an array of elements with keys id and label' do FactoryBot.create_list(:player, 2) get index_path(model_name: 'player', compact: true, format: :json) expect(ActiveSupport::JSON.decode(response.body).length).to eq(2) ActiveSupport::JSON.decode(response.body).each do |object| expect(object).to have_key('id') expect(object).to have_key('label') end end end describe 'with search operator' do let(:player) { FactoryBot.create :player } before do expect(Player.count).to eq(0) end it 'finds the player if the query matches the default search operator' do RailsAdmin.config do |config| config.default_search_operator = 'ends_with' config.model Player do list { field :name } end end visit index_path(model_name: 'player', query: player.name[2, -1]) is_expected.to have_content(player.name) end it 'does not find the player if the query does not match the default search operator' do RailsAdmin.config do |config| config.default_search_operator = 'ends_with' config.model Player do list { field :name } end end visit index_path(model_name: 'player', query: player.name[0, 2]) is_expected.to have_no_content(player.name) end it 'finds the player if the query matches the specified search operator' do RailsAdmin.config Player do list do field :name do search_operator 'starts_with' end end end visit index_path(model_name: 'player', query: player.name[0, 2]) is_expected.to have_content(player.name) end it 'does not find the player if the query does not match the specified search operator' do RailsAdmin.config Player do list do field :name do search_operator 'starts_with' end end end visit index_path(model_name: 'player', query: player.name[1..]) is_expected.to have_no_content(player.name) end end describe 'with custom search' do before do RailsAdmin.config do |config| config.model Player do list do search_by :rails_admin_search end end end end let!(:players) do [FactoryBot.create(:player, name: 'Joe'), FactoryBot.create(:player, name: 'George')] end it 'performs search using given scope' do visit index_path(model_name: 'player', query: 'eoJ') is_expected.to have_content(players[0].name) is_expected.to have_no_content(players[1].name) end end context 'with overridden to_param' do before do @ball = FactoryBot.create :ball visit index_path(model_name: 'ball') end it 'shows the show, edit and delete links with valid url' do is_expected.to have_selector("td a[href$='/admin/ball/#{@ball.id}']") is_expected.to have_selector("td a[href$='/admin/ball/#{@ball.id}/edit']") is_expected.to have_selector("td a[href$='/admin/ball/#{@ball.id}/delete']") end end describe 'with model scope' do context 'without default scope' do let!(:teams) { %w[red yellow blue].map { |color| FactoryBot.create :team, color: color } } it 'works', active_record: true do RailsAdmin.config do |config| config.model Team do scope { Team.where(color: %w[red blue]) } end end visit index_path(model_name: 'team') expect(all(:css, 'td.color_field').map(&:text)).to match_array %w[red blue] end it 'works', mongoid: true do RailsAdmin.config do |config| config.model Team do scope { Team.any_in(color: %w[red blue]) } end end visit index_path(model_name: 'team') expect(all(:css, 'td.color_field').map(&:text)).to match_array %w[red blue] end end context 'with default_scope' do let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } before do RailsAdmin.config do |config| config.model Comment::Confirmed do scope { Comment::Confirmed.unscoped } end end end it 'can be overriden' do visit index_path(model_name: 'comment~confirmed') expect(all(:css, 'td.content_field').map(&:text)).to match_array %w[something anything] end end end describe 'with scopes' do before do RailsAdmin.config do |config| config.model Team do list do scopes [nil, :red, :white] end end end @teams = [ FactoryBot.create(:team, color: 'red'), FactoryBot.create(:team, color: 'red'), FactoryBot.create(:team, color: 'white'), FactoryBot.create(:team, color: 'black'), ] end it 'displays configured scopes' do visit index_path(model_name: 'team') expect(find('#scope_selector li:first')).to have_content('All') expect(find('#scope_selector li:nth-child(2)')).to have_content('Red') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') expect(find('#scope_selector li a.active')).to have_content('All') end it 'shows only scoped records' do visit index_path(model_name: 'team') is_expected.to have_content(@teams[0].name) is_expected.to have_content(@teams[1].name) is_expected.to have_content(@teams[2].name) is_expected.to have_content(@teams[3].name) visit index_path(model_name: 'team', scope: 'red') expect(find('#scope_selector li a.active')).to have_content('Red') is_expected.to have_content(@teams[0].name) is_expected.to have_content(@teams[1].name) is_expected.to have_no_content(@teams[2].name) is_expected.to have_no_content(@teams[3].name) visit index_path(model_name: 'team', scope: 'white') expect(find('#scope_selector li a.active')).to have_content('White') is_expected.to have_no_content(@teams[0].name) is_expected.to have_no_content(@teams[1].name) is_expected.to have_content(@teams[2].name) is_expected.to have_no_content(@teams[3].name) end it 'shows all records instead when scope not in list' do visit index_path(model_name: 'team', scope: 'green') is_expected.to have_content(@teams[0].name) is_expected.to have_content(@teams[1].name) is_expected.to have_content(@teams[2].name) is_expected.to have_content(@teams[3].name) end describe 'i18n' do before :each do en = {admin: {scopes: { _all: 'every', red: 'krasnyj', }}} I18n.backend.store_translations(:en, en) end after { I18n.reload! } context 'global' do it 'displays configured scopes' do visit index_path(model_name: 'team') expect(find('#scope_selector li:first')).to have_content('every') expect(find('#scope_selector li:nth-child(2)')).to have_content('krasnyj') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') expect(find('#scope_selector li a.active')).to have_content('every') end end context 'per model' do before :each do en = {admin: {scopes: {team: { _all: 'any', red: 'kr', }}}} I18n.backend.store_translations(:en, en) end after { I18n.reload! } it 'displays configured scopes' do visit index_path(model_name: 'team') expect(find('#scope_selector li:first')).to have_content('any') expect(find('#scope_selector li:nth-child(2)')).to have_content('kr') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') expect(find('#scope_selector li a.active')).to have_content('any') end end end end describe 'row CSS class' do before do RailsAdmin.config do |config| config.model Team do list do row_css_class { 'my_class' } end end end @teams = [ FactoryBot.create(:team, color: 'red'), FactoryBot.create(:team, color: 'red'), FactoryBot.create(:team, color: 'white'), FactoryBot.create(:team, color: 'black'), ] end it 'appends the CSS class to the model row class' do visit index_path(model_name: 'team') expect(page).to have_css('tr.team_row.my_class') end end describe 'checkboxes?' do describe 'default is enabled' do before do RailsAdmin.config FieldTest do list end end it 'displays checkboxes on index' do @records = FactoryBot.create_list :field_test, 3 visit index_path(model_name: 'field_test') checkboxes = all(:xpath, './/form[@id="bulk_form"]//input[@type="checkbox"]') expect(checkboxes.length).to be > 0 expect(page).to have_content('Selected items') end end describe 'false' do before do RailsAdmin.config FieldTest do list do checkboxes false end end end it 'does not display any checkboxes on index' do @records = FactoryBot.create_list :field_test, 3 visit index_path(model_name: 'field_test') checkboxes = all(:xpath, './/form[@id="bulk_form"]//input[@type="checkbox"]') expect(checkboxes.length).to eq 0 expect(page).not_to have_content('Selected items') end end end describe 'sidescroll' do all_team_columns = ['', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', 'Team Manager', 'Ballpark', 'Mascot', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Color', 'Custom field', 'Main Sponsor', 'Players', 'Some Fans', 'Comments', ''] it 'displays all fields on one page' do FactoryBot.create_list :team, 3 visit index_path(model_name: 'team') cols = all('th').collect(&:text) expect(cols[0..4]).to eq(all_team_columns[0..4]) expect(cols).to contain_exactly(*all_team_columns) end it 'allows fields to be sticky' do RailsAdmin.config Team do list do configure(:division) { sticky true } configure(:name) { sticky true } end end FactoryBot.create_list :team, 3 visit index_path(model_name: 'team') cols = all('th').collect(&:text) expect(cols[0..4]).to eq(['', 'Division', 'Name', 'Id', 'Created at']) expect(cols).to contain_exactly(*all_team_columns) expect(page).to have_selector('.name_field.sticky') expect(page).to have_selector('.division_field.sticky') end it 'displays all fields with no checkboxes' do RailsAdmin.config Team do list do checkboxes false end end FactoryBot.create_list :team, 3 visit index_path(model_name: 'team') cols = all('th').collect(&:text) expect(cols[0..3]).to eq(all_team_columns[1..4]) expect(cols).to contain_exactly(*all_team_columns[1..]) end end context 'with composite primary keys', composite_primary_keys: true do let!(:fanships) { FactoryBot.create_list(:fanship, 3) } it 'shows the list' do visit index_path(model_name: 'fanship') expect(all('th').collect(&:text)[0..3]).to eq(['', 'Fan', 'Team', 'Since']) fanships.each do |fanship| is_expected.to have_content fanship.fan.name is_expected.to have_content fanship.team.name end is_expected.to have_content '3 fanships' end context 'using custom serializer' do before do RailsAdmin.config.composite_keys_serializer = Class.new do def self.serialize(keys) keys.join(',') end def self.deserialize(string) string.split(',') end end end it 'shows the member action links accordingly' do visit index_path(model_name: 'fanship') is_expected.to have_css(%(a[href$="/admin/fanship/#{fanships[0].fan_id},#{fanships[0].team_id}/edit"])) end end end end ================================================ FILE: spec/integration/actions/new_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'New action', type: :request do subject { page } describe 'page' do before do visit new_path(model_name: 'player') end it 'shows "New Model"' do is_expected.to have_content('New Player') end it 'shows required fields as "Required"' do is_expected.to have_selector('div', text: /Name\s*Required/) is_expected.to have_selector('div', text: /Number\s*Required/) end it 'shows non-required fields as "Optional"' do is_expected.to have_selector('#player_position_field .form-text', text: 'Optional') is_expected.to have_selector('#player_born_on_field .form-text', text: 'Optional') is_expected.to have_selector('#player_notes_field .form-text', text: 'Optional') end # https://github.com/railsadminteam/rails_admin/issues/362 # test that no link uses the "wildcard route" with the main # controller and new method it "does not use the 'wildcard route'" do is_expected.to have_no_selector("a[href^='/rails_admin/main/new']") end end context 'with missing label' do before do FactoryBot.create :team, name: '' visit new_path(model_name: 'player') end end context 'with parameters for pre-population' do it 'populates form field when corresponding parameters are passed in' do visit new_path(model_name: 'player', player: {name: 'Sam'}) expect(page).to have_css('input[value=Sam]') end it 'prepropulates has_one relationships' do @draft = FactoryBot.create :draft @player = FactoryBot.create :player, name: 'has_one association prepopulated' visit new_path(model_name: 'player', player: {draft_id: @draft.id}) expect(page).to have_css("select#player_draft_id option[selected='selected'][value='#{@draft.id}']") end it 'prepropulates has_many relationships' do @player = FactoryBot.create :player, name: 'has_many association prepopulated' visit new_path(model_name: 'team', team: {player_ids: [@player.id]}) expect(page).to have_css("select#team_player_ids option[selected='selected'][value='#{@player.id}']") end end context 'with namespaced model' do it 'has correct input field names' do visit new_path(model_name: 'cms~basic_page') is_expected.to have_selector('label[for=cms_basic_page_title]') is_expected.to have_selector("input#cms_basic_page_title[name='cms_basic_page[title]']") is_expected.to have_selector('label[for=cms_basic_page_content]') is_expected.to have_selector("textarea#cms_basic_page_content[name='cms_basic_page[content]']") end context 'with parameters for pre-population' do it 'populates form field when corresponding parameters are passed in' do visit new_path(model_name: 'cms~basic_page', cms_basic_page: {title: 'Hello'}) expect(page).to have_css('input[value=Hello]') end end it 'creates object with correct attributes' do visit new_path(model_name: 'cms~basic_page') fill_in 'cms_basic_page[title]', with: 'Hello' fill_in 'cms_basic_page[content]', with: 'World' expect do click_button 'Save' # first(:button, "Save").click end.to change(Cms::BasicPage, :count).by(1) end end context 'on create' do before do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' click_button 'Save' @player = RailsAdmin::AbstractModel.new('Player').first end it 'creates an object with correct attributes' do expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end context 'on create and edit' do before do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' click_button 'Save and edit' @player = RailsAdmin::AbstractModel.new('Player').first end it 'creates an object with correct attributes' do expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end context 'on create and add another', js: true do before do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' find_button('Save and add another').trigger('click') end it 'creates an object with correct attributes' do is_expected.to have_text 'Player successfully created' expect(page.current_path).to eq('/admin/player/new') @player = RailsAdmin::AbstractModel.new('Player').first expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end context 'with uniqueness constraint violated', given: 'a player exists' do before do @team = FactoryBot.create :team @player = FactoryBot.create :player, team: @team post new_path(model_name: 'player', player: {name: @player.name, number: @player.number.to_s, position: @player.position, team_id: @team.id}) end it 'shows an error message' do expect(response.body).to include('There is already a player with that number on this team') end end context 'with invalid object' do before do post new_path(model_name: 'player', player: {id: 1}) end it 'shows an error message' do expect(response.body).to include('Player failed to be created') end end context 'with object with errors on base' do before do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson on steroids' click_button 'Save and add another' end it 'shows error base error message in flash' do is_expected.to have_content('Player failed to be created') is_expected.to have_content('Player is cheating') end end context 'with a readonly object' do it 'shows non-editable form' do RailsAdmin.config do |config| config.model ReadOnlyComment do edit do field :content end end end visit new_path(model_name: 'read_only_comment') is_expected.not_to have_css('textarea[name="read_only_comment[content]"]') is_expected.to have_css('button[name="_save"]:disabled') end end context 'with composite primary keys', composite_primary_keys: true do let!(:fan) { FactoryBot.create(:fan) } let!(:team) { FactoryBot.create(:team) } it 'creates an object' do visit new_path(model_name: 'fanship') select(fan.name, from: 'Fan') select(team.name, from: 'Team') expect { click_button 'Save' }.to change { Fanship.count }.by(1) expect(Fanship.first.attributes.fetch_values('fan_id', 'team_id')).to eq [fan.id, team.id] end end end ================================================ FILE: spec/integration/actions/show_in_app_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'ShowInApp action', type: :request do subject { page } describe 'link' do let!(:player) { FactoryBot.create :player } it 'has the data-turbo: false attribute' do visit index_path(model_name: 'player') is_expected.to have_selector(%(li[title="Show in app"] a[data-turbo="false"])) click_link 'Show' is_expected.to have_selector(%(a[data-turbo="false"]), text: 'Show in app') end end end ================================================ FILE: spec/integration/actions/show_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Show action', type: :request do subject { page } let(:team) { FactoryBot.create :team } describe 'page' do it 'has History, Edit, Delete, Details and attributes' do @player = FactoryBot.create :player visit show_path(model_name: 'player', id: @player.id) is_expected.to have_selector('a', text: 'History') is_expected.to have_selector('a', text: 'Edit') is_expected.to have_selector('a', text: 'Delete') is_expected.to have_content('Details') is_expected.to have_content('Name') is_expected.to have_content(@player.name) is_expected.to have_content('Number') is_expected.to have_content(@player.number) end end context 'with invalid id' do it 'raises NotFound' do visit '/admin/players/123this-id-doesnt-exist' expect(page.driver.status_code).to eq(404) end end describe 'JSON show view' do before do @player = FactoryBot.create :player visit uri end let(:uri) { show_path(model_name: 'player', id: @player.id, format: :json) } let(:body) { page.body } it 'creates a JSON uri' do expect(uri).to eq("/admin/player/#{@player.id}.json") end it 'contains the JSONified object' do expect(JSON.parse(body)).to eq JSON.parse @player.reload.to_json end end context 'when compact_show_view is enabled' do it 'hides nil fields in show view by default' do visit show_path(model_name: 'team', id: team.id) is_expected.not_to have_css('.logo_url_field') end it 'hides blank fields in show view by default' do team.update logo_url: '' visit show_path(model_name: 'team', id: team.id) is_expected.not_to have_css('.logo_url_field') end it 'is disactivable' do RailsAdmin.config do |c| c.compact_show_view = false end visit show_path(model_name: 'team', id: team.id) is_expected.to have_css('.logo_url_field') end describe 'with boolean field' do let(:player) { FactoryBot.create :player, retired: false } it 'does not hide false value' do visit show_path(model_name: 'player', id: player.id) is_expected.to have_css('.retired_field') end end end context 'when compact_show_view is disabled' do before do RailsAdmin.config do |c| c.compact_show_view = false end end it 'shows nil fields' do team.update logo_url: nil visit show_path(model_name: 'team', id: team.id) is_expected.to have_css('.logo_url_field') end it 'shows blank fields' do team.update logo_url: '' visit show_path(model_name: 'team', id: team.id) is_expected.to have_css('.logo_url_field') end end describe 'bindings' do it 'should be present' do RailsAdmin.config Team do |_c| show do field :name do show do bindings[:object] && bindings[:view] && bindings[:controller] end end end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('div .name_field.string_type') end end describe 'css hooks' do it 'is present' do visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('div .name_field.string_type') end end describe 'field grouping' do before do RailsAdmin.config do |c| c.compact_show_view = false end end it 'is hideable' do RailsAdmin.config Team do show do group :default do hide end end end visit show_path(model_name: 'team', id: team.id) is_expected.not_to have_selector('h4', text: 'Basic info') %w[ division name logo_url manager ballpark mascot founded wins losses win_percentage revenue ].each do |field| is_expected.not_to have_selector(".#{field}_field") end end it 'hides association groupings by the name of the association' do RailsAdmin.config Team do show do group :players do hide end end end visit show_path(model_name: 'team', id: team.id) is_expected.not_to have_selector('h4', text: 'Players') end it 'is renameable' do RailsAdmin.config Team do show do group :default do label 'Renamed group' end end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('h4', text: 'Renamed group') end it 'has accessor for its fields' do RailsAdmin.config Team do show do group :default do field :name field :logo_url end group :belongs_to_associations do label "Belong's to associations" field :division end end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('h4', text: 'Basic info') is_expected.to have_selector('h4', text: "Belong's to associations") is_expected.to have_selector('.name_field') is_expected.to have_selector('.logo_url_field') is_expected.to have_selector('.division_field') end it 'has accessor for its fields by type' do RailsAdmin.config Team do show do group :default do field :name field :logo_url end group :other do field :division field :manager field :ballpark fields_of_type :string do label { "#{label} (STRING)" } end end end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.card-header', text: 'Name') is_expected.to have_selector('.card-header', text: 'Logo url') is_expected.to have_selector('.card-header', text: 'Division') is_expected.to have_selector('.card-header', text: 'Manager (STRING)') is_expected.to have_selector('.card-header', text: 'Ballpark (STRING)') end end describe 'fields' do before do RailsAdmin.config do |c| c.compact_show_view = false end end it 'shows all by default' do visit show_path(model_name: 'team', id: team.id) %w[ division name logo_url manager ballpark mascot founded wins losses win_percentage revenue players fans ].each do |field| is_expected.to have_selector(".#{field}_field") end end it 'only shows the defined fields and appear in order defined' do RailsAdmin.config Team do show do field :manager field :division field :name end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.manager_field') is_expected.to have_selector('.division_field') is_expected.to have_selector('.name_field') end it 'delegates the label option to the ActiveModel API' do RailsAdmin.config Team do show do field :manager field :fans end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.card-header', text: 'Team Manager') is_expected.to have_selector('.card-header', text: 'Some Fans') end it 'is renameable' do RailsAdmin.config Team do show do field :manager do label 'Renamed field' end field :division field :name end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.card-header', text: 'Renamed field') is_expected.to have_selector('.card-header', text: 'Division') is_expected.to have_selector('.card-header', text: 'Name') end it 'is renameable by type' do RailsAdmin.config Team do show do fields_of_type :string do label { "#{label} (STRING)" } end end end visit show_path(model_name: 'team', id: team.id) [ 'Division', 'Name (STRING)', 'Logo url (STRING)', 'Manager (STRING)', 'Ballpark (STRING)', 'Mascot (STRING)', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans' ].each do |text| is_expected.to have_selector('.card-header', text: text) end end it 'is globally renameable by type' do RailsAdmin.config Team do show do fields_of_type :string do label { "#{label} (STRING)" } end end end visit show_path(model_name: 'team', id: team.id) [ 'Division', 'Name (STRING)', 'Logo url (STRING)', 'Manager (STRING)', 'Ballpark (STRING)', 'Mascot (STRING)', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans' ].each do |text| is_expected.to have_selector('.card-header', text: text) end end it 'is hideable' do RailsAdmin.config Team do show do field :manager do hide end field :division field :name end end visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.division_field') is_expected.to have_selector('.name_field') end it 'is hideable by type' do RailsAdmin.config Team do show do fields_of_type :string do hide end end end visit show_path(model_name: 'team', id: team.id) ['Name', 'Logo url', 'Manager', 'Ballpark', 'Mascot'].each do |text| is_expected.not_to have_selector('.card-header', text: text) end ['Division', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans'].each do |text| is_expected.to have_selector('.card-header', text: text) end end it 'is globally hideable by type' do RailsAdmin.config Team do show do fields_of_type :string do hide end end end visit show_path(model_name: 'team', id: team.id) ['Name', 'Logo url', 'Manager', 'Ballpark', 'Mascot'].each do |text| is_expected.not_to have_selector('.card-header', text: text) end ['Division', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans'].each do |text| is_expected.to have_selector('.card-header', text: text) end end end describe 'virtual field' do let(:team) { FactoryBot.create :team, name: 'foobar' } context 'with formatted_value defined' do before do RailsAdmin.config Team do show do field :truncated_name do formatted_value do bindings[:object].name.truncate(5) end end end end end it 'shows up correctly' do visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.truncated_name_field') is_expected.to have_selector('.card', text: 'fo...') end end context 'without formatted_value' do before do RailsAdmin.config Team do show do field :truncated_name do pretty_value do bindings[:object].name.truncate(5) end end end end end it 'raises error along with suggestion' do expect { visit show_path(model_name: 'team', id: team.id) }.to raise_error(/you should declare 'formatted_value'/) end end end describe 'with model scope' do let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } before do RailsAdmin.config do |config| config.model Comment::Confirmed do scope { Comment::Confirmed.unscoped } end end end it 'overrides default_scope' do visit show_path(model_name: 'comment~confirmed', id: comments[0].id) is_expected.to have_selector('.card-body', text: 'something') visit show_path(model_name: 'comment~confirmed', id: comments[1].id) is_expected.to have_selector('.card-body', text: 'anything') end end context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'shows the object' do visit show_path(model_name: 'fanship', id: fanship.id) is_expected.to have_link(fanship.fan.name) is_expected.to have_link(fanship.team.name) end end end ================================================ FILE: spec/integration/auditing/paper_trail_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin PaperTrail auditing', active_record: true do before(:each) do RailsAdmin.config do |config| config.audit_with :paper_trail end end shared_examples :paper_on_object_creation do describe 'on object creation', type: :request do subject { page } before do @user = FactoryBot.create :user RailsAdmin::Config.authenticate_with { warden.authenticate! scope: :user } RailsAdmin::Config.current_user_method(&:current_user) login_as @user with_versioning do visit new_path(model_name: paper_model_name) fill_in paper_field_name, with: 'Jackie Robinson' click_button 'Save' @object = RailsAdmin::AbstractModel.new(paper_class_name).first end end it 'records a version' do expect(@object.versions.size).to eq 1 expect(@object.versions.first.whodunnit).to eq @user.id.to_s end end end shared_examples :paper_history do describe 'model history fetch' do before(:each) do PaperTrail::Version.delete_all @model = RailsAdmin::AbstractModel.new(paper_class_name) @user = FactoryBot.create :user @paper_trail_test = FactoryBot.create(paper_factory) with_versioning do # `PaperTrail.whodunnit` deprecated in PT 9, will be removed in 10. # Use `PaperTrail.request.whodunnit` instead. if PaperTrail.respond_to?(:request) PaperTrail.request.whodunnit = @user.id else PaperTrail.whodunnit = @user.id end 30.times do |i| @paper_trail_test.update!(name: "updated name #{i}") end end end it 'creates versions' do expect(paper_class_name.constantize.version_class_name.constantize.count).to eq(30) end describe 'model history fetch with auditing adapter' do before(:all) do @adapter = RailsAdmin::Extensions::PaperTrail::AuditingAdapter.new(nil) end it 'fetches on page of history' do versions = @adapter.listing_for_model @model, nil, false, false, false, nil, 20 expect(versions.total_count).to eq(30) expect(versions.count).to eq(20) end it 'respects RailsAdmin::Config.default_items_per_page' do RailsAdmin.config.default_items_per_page = 15 versions = @adapter.listing_for_model @model, nil, false, false, false, nil expect(versions.total_count).to eq(30) expect(versions.count).to eq(15) end it 'sets correct next page' do versions = @adapter.listing_for_model @model, nil, false, false, false, 2, 10 expect(versions.next_page).to eq(3) end it 'can fetch all history' do versions = @adapter.listing_for_model @model, nil, false, false, true, nil, 20 expect(versions.total_count).to eq(30) expect(versions.count).to eq(30) end describe 'returned version objects' do before(:each) do @padinated_listing = @adapter.listing_for_model @model, nil, false, false, false, nil @version = @padinated_listing.first end it '#username returns user email' do expect(@version.username).to eq(@user.email) end it '#message returns event' do expect(@version.message).to eq('update') end describe 'changed item attributes' do it '#item returns item.id' do expect(@version.item).to eq(@paper_trail_test.id) end it '#table returns item class name' do expect(@version.table.to_s).to eq(@model.model.base_class.name) end end context 'with Kaminari' do before do Kaminari.config.page_method_name = :per_page_kaminari end after do Kaminari.config.page_method_name = :page end let(:paginated_array) { Kaminari::PaginatableArray.new.page('1') } it "supports pagination when Kaminari's page_method_name is customized" do expect(PaperTrail::Version).to receive(:per_page_kaminari).twice.and_return(@padinated_listing) allow(Kaminari).to receive(:paginate_array).and_return(paginated_array) expect(paginated_array).to receive(:per_page_kaminari).twice.and_return(paginated_array) @adapter.listing_for_model @model, nil, false, false, false, nil @adapter.listing_for_object @model, @paper_trail_test, nil, false, false, false, nil end it "does not break when Kaminari's page_method_name is not applied to Kaminari::PaginatableArray" do expect(PaperTrail::Version).to receive(:per_page_kaminari).twice.and_return(@padinated_listing) allow(Kaminari).to receive(:paginate_array).and_return(paginated_array) allow(paginated_array).to receive(:respond_to).with(:per_page_kaminari).and_return(false) expect(paginated_array).to receive(:page).twice.and_return(paginated_array) @adapter.listing_for_model @model, nil, false, false, false, nil @adapter.listing_for_object @model, @paper_trail_test, nil, false, false, false, nil end end end end end end context 'with a normal PaperTrail model' do let(:paper_class_name) { 'PaperTrailTest' } let(:paper_factory) { :paper_trail_test } it_behaves_like :paper_on_object_creation do let(:paper_model_name) { 'paper_trail_test' } let(:paper_field_name) { 'paper_trail_test[name]' } end it_behaves_like :paper_history end context 'with a subclassed PaperTrail model' do let(:paper_class_name) { 'PaperTrailTestSubclass' } let(:paper_factory) { :paper_trail_test_subclass } it_behaves_like :paper_on_object_creation do let(:paper_model_name) { 'paper_trail_test_subclass' } let(:paper_field_name) { 'paper_trail_test_subclass[name]' } end it_behaves_like :paper_history end context 'with a namespaced PaperTrail model' do let(:paper_class_name) { 'PaperTrailTest::SubclassInNamespace' } let(:paper_factory) { :paper_trail_test_subclass_in_namespace } it_behaves_like :paper_on_object_creation do let(:paper_model_name) { 'paper_trail_test~subclass_in_namespace' } let(:paper_field_name) { 'paper_trail_test_subclass_in_namespace[name]' } end it_behaves_like :paper_history end context 'with a PaperTrail model with custom version association name' do let(:paper_class_name) { 'PaperTrailTestWithCustomAssociation' } let(:paper_factory) { :paper_trail_test_with_custom_association } it_behaves_like :paper_history end end ================================================ FILE: spec/integration/authentication/devise_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin Devise Authentication', type: :request do subject { page } let!(:user) { FactoryBot.create :user } before do RailsAdmin.config do |config| config.authenticate_with do warden.authenticate! scope: :user end config.current_user_method(&:current_user) end end it 'supports logging-in', js: true do visit dashboard_path fill_in 'Email', with: user.email fill_in 'Password', with: 'password' click_button 'Log in' is_expected.to have_css 'body.rails_admin' end it 'supports logging-out', js: true do login_as user visit dashboard_path click_link 'Log out' is_expected.to have_content 'Log in' end end ================================================ FILE: spec/integration/authorization/cancancan_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin CanCanCan Authorization', type: :request do class Ability include CanCan::Ability def initialize(user) can :access, :rails_admin if user.roles.include? :admin can :read, :dashboard if user.roles.include? :test_exception can :access, :rails_admin can :manage, :all can :show_in_app, :all can %i[update destroy], Player cannot %i[update destroy], Player, retired: true else can :manage, Player if user.roles.include? :manage_player can :read, Player, retired: false if user.roles.include? :read_player can :create, Player, suspended: true if user.roles.include? :create_player can :update, Player, retired: false if user.roles.include? :update_player can :destroy, Player, retired: false if user.roles.include? :destroy_player can :history, Player, retired: false if user.roles.include? :history_player can :show_in_app, Player, retired: false if user.roles.include? :show_in_app_player end end end class AdminAbility include CanCan::Ability def initialize(user) can :access, :rails_admin if user.roles.include? :admin can :show_in_app, :all can :manage, :all end end subject { page } before do RailsAdmin.config do |c| c.authorize_with(:cancancan) c.authenticate_with { warden.authenticate! scope: :user } c.current_user_method(&:current_user) end @player_model = RailsAdmin::AbstractModel.new(Player) @user = FactoryBot.create :user login_as @user end describe 'with no roles' do before do @user.update(roles: []) end it 'GET /admin should raise CanCan::AccessDenied' do expect { visit dashboard_path }.to raise_error(CanCan::AccessDenied) end it 'GET /admin/player should raise CanCan::AccessDenied' do expect { visit index_path(model_name: 'player') }.to raise_error(CanCan::AccessDenied) end end describe 'with read player role' do before do @user.update(roles: %i[admin read_player]) end it 'GET /admin should show Player but not League' do visit dashboard_path is_expected.to have_content('Player') is_expected.not_to have_content('League') is_expected.not_to have_content('Add new') end it 'GET /admin/player should render successfully but not list retired players and not show new, edit, or delete actions' do # ensure :name column to be shown RailsAdmin.config Player do list do field :name end end @players = [ FactoryBot.create(:player, retired: false), FactoryBot.create(:player, retired: true), ] visit index_path(model_name: 'player') is_expected.to have_content(@players[0].name) is_expected.not_to have_content(@players[1].name) is_expected.not_to have_content('Add new') is_expected.to have_css('.show_member_link') is_expected.not_to have_css('.edit_member_link') is_expected.not_to have_css('.delete_member_link') is_expected.not_to have_css('.history_show_member_link') is_expected.not_to have_css('.show_in_app_member_link') end it 'GET /admin/team should raise CanCan::AccessDenied' do expect { visit index_path(model_name: 'team') }.to raise_error(CanCan::AccessDenied) end it 'GET /admin/player/new should raise CanCan::AccessDenied' do expect { visit new_path(model_name: 'player') }.to raise_error(CanCan::AccessDenied) end end describe 'with create and read player role' do before do @user.update(roles: %i[admin read_player create_player]) end it 'GET /admin/player/new should render and create record upon submission' do visit new_path(model_name: 'player') is_expected.not_to have_content('Save and edit') is_expected.not_to have_content('Delete') is_expected.to have_content('Save and add another') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' click_button 'Save' # first(:button, "Save").click is_expected.not_to have_content('Edit') @player = RailsAdmin::AbstractModel.new('Player').first expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') expect(@player).to be_suspended # suspended is inherited behavior based on permission end it 'POST /admin/player/new with unauthorized attribute value should raise access denied' do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' choose name: 'player[suspended]', option: '0' expect { click_button 'Save' }.to raise_error(CanCan::AccessDenied) end it 'GET /admin/player/1/edit should raise access denied' do @player = FactoryBot.create :player expect { visit edit_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) end end describe 'with update and read player role' do before do @user.update(roles: %i[admin read_player update_player]) end it 'GET /admin/player/1/edit should render and update record upon submission' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) is_expected.to have_content('Save and edit') is_expected.not_to have_content('Save and add another') is_expected.not_to have_content('Add new') is_expected.not_to have_content('Delete') is_expected.not_to have_content('History') is_expected.not_to have_content('Show in app') fill_in 'player[name]', with: 'Jackie Robinson' click_button 'Save' # click_button "Save" # first(:button, "Save").click @player.reload expect(@player.name).to eq('Jackie Robinson') end it 'GET /admin/player/1/edit with retired player should raise access denied' do @player = FactoryBot.create :player, retired: true expect { visit edit_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) end it 'PUT /admin/player/new with unauthorized attribute value should raise access denied' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) choose name: 'player[retired]', option: '1' expect { click_button 'Save' }.to raise_error(CanCan::AccessDenied) end it 'GET /admin/player/1/delete should raise access denied' do @player = FactoryBot.create :player expect { visit delete_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) end end describe 'with history role' do it 'shows links to history action' do @user.update(roles: %i[admin read_player history_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') is_expected.to have_css('.show_member_link') is_expected.not_to have_css('.edit_member_link') is_expected.not_to have_css('.delete_member_link') is_expected.to have_css('.history_show_member_link') visit show_path(model_name: 'player', id: @player.id) is_expected.to have_content('Show') is_expected.not_to have_content('Edit') is_expected.not_to have_content('Delete') is_expected.to have_content('History') end end describe 'with show in app role' do it 'shows links to show in app action' do @user.update(roles: %i[admin read_player show_in_app_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') is_expected.to have_css('.show_member_link') is_expected.not_to have_css('.edit_member_link') is_expected.not_to have_css('.delete_member_link') is_expected.not_to have_css('.history_show_member_link') is_expected.to have_css('.show_in_app_member_link') visit show_path(model_name: 'player', id: @player.id) is_expected.to have_content('Show') is_expected.not_to have_content('Edit') is_expected.not_to have_content('Delete') is_expected.not_to have_content('History') is_expected.to have_content('Show in app') end end describe 'with all roles' do it 'shows links to all actions' do @user.update(roles: %i[admin manage_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') is_expected.to have_css('.show_member_link') is_expected.to have_css('.edit_member_link') is_expected.to have_css('.delete_member_link') is_expected.to have_css('.history_show_member_link') is_expected.to have_css('.show_in_app_member_link') visit show_path(model_name: 'player', id: @player.id) is_expected.to have_content('Show') is_expected.to have_content('Edit') is_expected.to have_content('Delete') is_expected.to have_content('History') is_expected.to have_content('Show in app') end end describe 'with destroy and read player role' do before do @user.update(roles: %i[admin read_player destroy_player]) end it 'GET /admin/player/1/delete should render and destroy record upon submission' do @player = FactoryBot.create :player player_id = @player.id visit delete_path(model_name: 'player', id: player_id) click_button "Yes, I'm sure" expect(@player_model.get(player_id)).to be_nil end it 'GET /admin/player/1/delete with retired player should raise access denied' do @player = FactoryBot.create :player, retired: true expect { visit delete_path(model_name: 'player', id: @player.id) }.to raise_error(CanCan::AccessDenied) end it 'GET /admin/player/bulk_delete should render records which are authorized to' do active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true post bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) expect(response.body).to include(active_player.name) expect(response.body).not_to include(retired_player.name) end it 'POST /admin/player/bulk_destroy should destroy records which are authorized to' do active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true delete bulk_delete_path(model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) expect(@player_model.get(active_player.id)).to be_nil expect(@player_model.get(retired_player.id)).not_to be_nil end end describe 'with exception role' do it 'GET /admin/player/bulk_delete should render records which are authorized to' do @user.update(roles: %i[admin test_exception]) active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true post bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) expect(response.body).to include(active_player.name) expect(response.body).not_to include(retired_player.name) end it 'POST /admin/player/bulk_destroy should destroy records which are authorized to' do @user.update(roles: %i[admin test_exception]) active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true delete bulk_delete_path(model_name: 'player', bulk_ids: [active_player, retired_player].collect(&:id)) expect(@player_model.get(active_player.id)).to be_nil expect(@player_model.get(retired_player.id)).not_to be_nil end end describe 'with a custom admin ability' do before do RailsAdmin.config do |c| c.authorize_with :cancancan do ability_class { AdminAbility } end end @user = FactoryBot.create :user login_as @user end describe 'with admin role only' do before do @user.update(roles: [:admin]) end it 'GET /admin/team should render successfully' do visit index_path(model_name: 'team') expect(page.status_code).to eq(200) end it 'GET /admin/player/new should render successfully' do visit new_path(model_name: 'player') expect(page.status_code).to eq(200) end it 'GET /admin/player/1/edit should render successfully' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) expect(page.status_code).to eq(200) end it 'GET /admin/player/1/edit with retired player should render successfully' do @player = FactoryBot.create :player, retired: true visit edit_path(model_name: 'player', id: @player.id) expect(page.status_code).to eq(200) end it 'GET /admin/player/1/delete should render successfully' do @player = FactoryBot.create :player visit delete_path(model_name: 'player', id: @player.id) expect(page.status_code).to eq(200) end it 'GET /admin/player/1/delete with retired player should render successfully' do @player = FactoryBot.create :player, retired: true visit delete_path(model_name: 'player', id: @player.id) expect(page.status_code).to eq(200) end end end end ================================================ FILE: spec/integration/authorization/pundit_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin Pundit Authorization', type: :request do subject { page } before do RailsAdmin.config do |c| c.authorize_with(:pundit) c.authenticate_with { warden.authenticate! scope: :user } c.current_user_method(&:current_user) end @player_model = RailsAdmin::AbstractModel.new(Player) @user = FactoryBot.create :user, roles: [] login_as @user end describe 'with no roles' do it 'GET /admin should raise Pundit::NotAuthorizedError' do expect { visit dashboard_path }.to raise_error(Pundit::NotAuthorizedError) end it 'GET /admin/player should raise Pundit::NotAuthorizedError' do expect { visit index_path(model_name: 'player') }.to raise_error(Pundit::NotAuthorizedError) end end describe 'with read player role' do before do @user.update(roles: %i[admin read_player]) end it 'GET /admin should show Player but not League' do visit dashboard_path is_expected.to have_content('Player') is_expected.not_to have_content('League') is_expected.not_to have_content('Add new') end it 'GET /admin/team should raise Pundit::NotAuthorizedError' do expect { visit index_path(model_name: 'team') }.to raise_error(Pundit::NotAuthorizedError) end it 'GET /admin/player/1/edit should raise access denied' do @player = FactoryBot.create :player expect { visit edit_path(model_name: 'player', id: @player.id) }.to raise_error(Pundit::NotAuthorizedError) end end describe 'with admin role' do before do @user.update(roles: %i[admin manage_player]) end it 'GET /admin should show Player but not League' do visit dashboard_path is_expected.to have_content('Player') end it 'GET /admin/player/new should render and create record upon submission' do visit new_path(model_name: 'player') is_expected.to have_content('Save and edit') is_expected.not_to have_content('Delete') is_expected.to have_content('Save and add another') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' click_button 'Save' is_expected.not_to have_content('Edit') @player = RailsAdmin::AbstractModel.new('Player').first expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end describe 'with all roles' do it 'shows links to all actions' do @user.update(roles: %i[admin manage_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') is_expected.to have_css('.show_member_link') is_expected.to have_css('.edit_member_link') is_expected.to have_css('.delete_member_link') is_expected.to have_css('.history_show_member_link') is_expected.to have_css('.show_in_app_member_link') visit show_path(model_name: 'player', id: @player.id) is_expected.to have_content('Show') is_expected.to have_content('Edit') is_expected.to have_content('Delete') is_expected.to have_content('History') is_expected.to have_content('Show in app') end end describe 'with create and read player role' do before do @user.update(roles: %i[admin read_player create_player]) end it 'POST /admin/player/new with unauthorized attribute value should raise access denied' do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' choose name: 'player[suspended]', option: '0' expect { click_button 'Save' }.to raise_error(Pundit::NotAuthorizedError) end end describe 'with update and read player role' do before do @user.update(roles: %i[admin read_player update_player]) end it 'PUT /admin/player/new with unauthorized attribute value should raise access denied' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) choose name: 'player[retired]', option: '1' expect { click_button 'Save' }.to raise_error(Pundit::NotAuthorizedError) end end context 'when ApplicationController already has pundit_user' do let(:admin) { FactoryBot.create :user, roles: [:admin] } before do RailsAdmin.config.parent_controller = 'ApplicationController' allow_any_instance_of(ApplicationController).to receive(:pundit_user).and_return(admin) end it 'uses original pundit_user' do pending 'no way to dynamically change superclass' expect { visit dashboard_path }.not_to raise_error end end context 'when custom authorization key is set' do before do RailsAdmin.config do |c| c.actions do dashboard index do authorization_key :rails_admin_index end end end end it 'uses the custom key' do expect { visit index_path(model_name: 'team') }.not_to raise_error end end context 'when custom authorization key is suffixed with ?' do before do @user.update(roles: [:admin]) RailsAdmin.config do |c| c.actions do dashboard do authorization_key :dashboard? end index end end end it 'does not append ? on policy check' do expect_any_instance_of(ApplicationPolicy).not_to receive(:'dashboard??') visit dashboard_path end end end ================================================ FILE: spec/integration/fields/action_text_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' if defined?(ActionText) RSpec.describe 'ActionText field', type: :request, js: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :action_text_field end end end it 'works without error' do allow(ConsoleLogger).to receive(:warn).with(/ActionText assets should be loaded statically/) expect { visit new_path(model_name: 'field_test') }.not_to raise_error is_expected.to have_selector('trix-toolbar') end if RailsAdmin.config.asset_source == :sprockets && Rails.gem_version < Gem::Version.new('7.0') it 'shows a warning if ActionText assets are loaded dynamically' do expect(ConsoleLogger).to receive(:warn).with(/ActionText assets should be loaded statically/) visit new_path(model_name: 'field_test') is_expected.to have_selector('trix-toolbar') end it 'allows suppressing the warning' do RailsAdmin.config FieldTest do edit do field :action_text_field do warn_dynamic_load false end end end expect(ConsoleLogger).not_to receive(:warn).with(/ActionText assets should be loaded statically/) visit new_path(model_name: 'field_test') is_expected.to have_selector('trix-toolbar') end end end end ================================================ FILE: spec/integration/fields/active_record_enum_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'ActiveRecordEnum field', type: :request, active_record: true do subject { page } describe 'for string-keyed enum' do before do RailsAdmin.config FieldTest do edit do field :string_enum_field do default_value 'M' end end end end it 'auto-detects enumeration' do visit new_path(model_name: 'field_test') is_expected.to have_selector('.enum_type select') is_expected.not_to have_selector('.enum_type select[multiple]') expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[S M L] end it 'shows current value as selected' do visit edit_path(model_name: 'field_test', id: FieldTest.create(string_enum_field: 'L')) expect(find('.enum_type select').value).to eq 'l' end it 'can be updated' do visit edit_path(model_name: 'field_test', id: FieldTest.create(string_enum_field: 'S')) select 'L' click_button 'Save' expect(FieldTest.first.string_enum_field).to eq 'L' end it 'pre-populates default value' do visit new_path(model_name: 'field_test') expect(find('.enum_type select').value).to eq 'm' end end describe 'for integer-keyed enum' do before do RailsAdmin.config FieldTest do edit do field :integer_enum_field do default_value :medium end end end end it 'auto-detects enumeration' do visit new_path(model_name: 'field_test') is_expected.to have_selector('.enum_type select') is_expected.not_to have_selector('.enum_type select[multiple]') expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[small medium large] end it 'shows current value as selected' do visit edit_path(model_name: 'field_test', id: FieldTest.create(integer_enum_field: :large)) expect(find('.enum_type select').value).to eq '2' end it 'can be updated' do visit edit_path(model_name: 'field_test', id: FieldTest.create(integer_enum_field: :small)) select 'large' click_button 'Save' expect(FieldTest.first.integer_enum_field).to eq 'large' end it 'pre-populates default value' do visit new_path(model_name: 'field_test') expect(find('.enum_type select').value).to eq '1' end end end ================================================ FILE: spec/integration/fields/active_storage_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'ActiveStorage field', type: :request, active_record: true do subject { page } let(:field_test) { FactoryBot.create :field_test } before do # To suppress 'SQLite3::BusyException: database is locked' exception @original = page.driver.browser.url_blacklist # rubocop:disable Naming/InclusiveLanguage page.driver.browser.url_blacklist = [%r{/rails/active_storage/representations}] # rubocop:disable Naming/InclusiveLanguage end after { page.driver.browser.url_blacklist = @original } # rubocop:disable Naming/InclusiveLanguage describe 'direct upload', js: true do before do RailsAdmin.config FieldTest do edit do field(:active_storage_asset) { direct true } end end end it 'works' do visit edit_path(model_name: 'field_test', id: field_test.id) attach_file 'Active storage asset', file_path('test.jpg') expect_any_instance_of(ActiveStorage::DirectUploadsController).to receive(:create).and_call_original click_button 'Save' expect(page).to have_content 'Field test successfully updated' field_test.reload expect(field_test.active_storage_asset.filename).to eq 'test.jpg' end end end ================================================ FILE: spec/integration/fields/base_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Base field', type: :request do subject { page } describe '#default_value' do it 'is set for all types of input fields' do RailsAdmin.config do |config| config.excluded_models = [] config.model(FieldTest) do field :string_field do default_value 'string_field default_value' end field :text_field do default_value 'string_field text_field' end field :boolean_field do default_value true end field :date_field do default_value Date.today end end end visit new_path(model_name: 'field_test') # In Rails 3.2.3 behavior of textarea has changed to insert newline after the opening tag, # but Capybara's RackTest driver is not up to this behavior change. # (https://github.com/jnicklas/capybara/issues/677) # So we manually cut off first newline character as a workaround here. expect(find_field('field_test[string_field]').value.gsub(/^\n/, '')).to eq('string_field default_value') expect(find_field('field_test[text_field]').value.gsub(/^\n/, '')).to eq('string_field text_field') expect(find('[name="field_test[date_field]"]', visible: false).value).to eq(Date.today.to_s) expect(has_checked_field?('field_test[boolean_field]')).to be_truthy end it 'sets default value for selects' do RailsAdmin.config(Team) do field :color, :enum do default_value 'black' enum do %w[black white] end end end visit new_path(model_name: 'team') expect(find_field('team[color]').value).to eq('black') end it 'renders custom value next time if error happend' do RailsAdmin.config(Team) do field :name do render do raise ZeroDivisionError unless bindings[:object].persisted? 'Custom Name' end end end expect { visit new_path(model_name: 'team') }.to raise_error(/ZeroDivisionError/) record = FactoryBot.create(:team) visit edit_path(model_name: 'team', id: record.id) expect(page).to have_content('Custom Name') end end describe '#bindings' do it 'is available' do RailsAdmin.config do |config| config.excluded_models = [] end RailsAdmin.config Category do field :parent_category do visible do !bindings[:object].new_record? end end end visit new_path(model_name: 'category') is_expected.to have_no_css('#category_parent_category_id') click_button 'Save' # first(:button, "Save").click visit edit_path(model_name: 'category', id: Category.first) is_expected.to have_css('#category_parent_category_id') click_button 'Save' # first(:button, "Save").click is_expected.to have_content('Category successfully updated') end end end ================================================ FILE: spec/integration/fields/belongs_to_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'BelongsToAssociation field', type: :request do subject { page } it 'does not add a related id to the belongs_to create team link' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) is_expected.to have_selector("a[data-link='/admin/team/new?modal=true']") end describe 'on create' do let!(:draft) { FactoryBot.create :draft } let(:team) { FactoryBot.create :team } it 'shows selects' do visit new_path(model_name: 'player') is_expected.to have_selector('select#player_team_id') end context 'with default_value' do before do id = team.id RailsAdmin.config Player do configure :team do default_value id end end end it 'shows the value as selected' do visit new_path(model_name: 'player') expect(find('select#player_team_id').value).to eq team.id.to_s end end end describe 'on show' do before do @team = FactoryBot.create :team @player = FactoryBot.create :player, team_id: @team.id visit show_path(model_name: 'player', id: @player.id) end it 'shows associated objects' do is_expected.to have_css("a[href='/admin/team/#{@team.id}']") end end context 'with custom primary_key option' do let(:users) { FactoryBot.create_list :managing_user, 2 } let!(:teams) { [FactoryBot.create(:managed_team, manager: users[0].email), FactoryBot.create(:managed_team)] } before do RailsAdmin.config.included_models = [ManagedTeam, ManagingUser] RailsAdmin.config ManagedTeam do field :user end end it 'allows update' do visit edit_path(model_name: 'managed_team', id: teams[0].id) expect(page).to have_css("select#managed_team_manager option[value=\"#{users[0].email}\"]") select("ManagingUser ##{users[1].id}", from: 'User') click_button 'Save' teams[0].reload expect(teams[0].user).to eq users[1] end context 'when fetching associated objects via xhr' do before do RailsAdmin.config ManagedTeam do field(:user) { associated_collection_cache_all false } end end it 'allows update', js: true do visit edit_path(model_name: 'managed_team', id: teams[0].id) find('input.ra-filtering-select-input').set('M') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("ManagingUser ##{users[1].id}")).click()} click_button 'Save' teams[0].reload expect(teams[0].user).to eq users[1] end end end context 'with composite foreign keys', composite_primary_keys: true do let!(:fanship) { FactoryBot.create(:fanship) } let(:favorite_player) { FactoryBot.create(:favorite_player) } describe 'via default field' do it 'allows update' do visit edit_path(model_name: 'favorite_player', id: favorite_player.id) is_expected.to have_select('Fanship', selected: "Fanship ##{favorite_player.fanship.id}") select("Fanship ##{fanship.id}", from: 'Fanship') click_button 'Save' is_expected.to have_content 'Favorite player successfully updated' expect(FavoritePlayer.all.map(&:fanship)).to eq [fanship] end context 'with invalid key' do before do allow_any_instance_of(RailsAdmin::Config::Fields::Types::BelongsToAssociation). to receive(:collection).and_return([["Fanship ##{fanship.id}", 'invalid']]) end it 'fails to update' do visit edit_path(model_name: 'favorite_player', id: favorite_player.id) select("Fanship ##{fanship.id}", from: 'Fanship') click_button 'Save' is_expected.to have_content 'Fanship must exist' end end end describe 'via remote-sourced field' do before do RailsAdmin.config FavoritePlayer do field :fanship do associated_collection_cache_all false end end end it 'allows update', js: true do visit edit_path(model_name: 'favorite_player', id: favorite_player.id) find('.fanship_field input.ra-filtering-select-input').set(fanship.fan_id) page.execute_script("document.querySelector('.fanship_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Fanship ##{fanship.id}")).click()} click_button 'Save' is_expected.to have_content 'Favorite player successfully updated' expect(FavoritePlayer.all.map(&:fanship)).to eq [fanship] end end describe 'via nested field' do it 'allows update' do visit edit_path(model_name: 'nested_favorite_player', id: favorite_player.id) fill_in 'Since', with: '2020-01-23' click_button 'Save' is_expected.to have_content 'Nested favorite player successfully updated' expect(favorite_player.reload.fanship.since).to eq Date.new(2020, 1, 23) end end end end ================================================ FILE: spec/integration/fields/boolean_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Boolean field', type: :request do subject { page } let(:field_test) { FactoryBot.create :field_test } context 'if nullable' do before do RailsAdmin.config FieldTest do field :boolean_field end end it 'shows 3 radio buttons' do visit new_path(model_name: 'field_test') is_expected.to have_content 'New Field test' expect(all('[name="field_test[boolean_field]"]').map { |e| e['value'] }).to eq ['1', '0', ''] end it 'can be updated' do visit edit_path(model_name: 'field_test', id: field_test.id) # change the value to true and assert the values find('.boolean_type label.success').click click_button 'Save and edit' # validate that the success button rendered and is active expect(page).to have_selector('.boolean_type input[value="1"][checked]') # validate the value is true expect(field_test.reload.boolean_field).to be true # change the value to false and assert the values find('.boolean_type label.danger').click click_button 'Save and edit' expect(page).to have_selector('.boolean_type input[value="0"][checked]') expect(field_test.reload.boolean_field).to be false # change the value to nil and assert the values find('.boolean_type label.default').click click_button 'Save and edit' expect(page).to have_selector('.boolean_type input[value=""][checked]') expect(field_test.reload.boolean_field).to be nil end end context 'when the boolean is in an embedded document' do before do RailsAdmin.config FieldTest do field :comment end RailsAdmin.config Comment do field :content, :boolean end end it 'can be updated', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) # toggle open the embedded document section find('#field_test_comment_attributes_field .add_nested_fields').click # set the value to false and assert the values find('.boolean_type label.danger').click click_button 'Save and edit' expect(field_test.reload.comment.content).to eq '0' end end context 'if not nullable' do before do RailsAdmin.config FieldTest do field :boolean_field do nullable false end end end it 'shows a checkbox' do visit new_path(model_name: 'field_test') is_expected.to have_content 'New Field test' is_expected.to have_css '[type="checkbox"][name="field_test[boolean_field]"]' end it 'can be updated' do visit edit_path(model_name: 'field_test', id: field_test.id) find('.boolean_type input').check click_button 'Save and edit' expect(field_test.reload.boolean_field).to be true find('.boolean_type input').uncheck click_button 'Save and edit' expect(field_test.reload.boolean_field).to be false end end context 'if the database column is not nullable', active_record: true do before do RailsAdmin.config FieldTest do field :non_nullable_boolean_field end end it 'shows a checkbox' do visit new_path(model_name: 'field_test') is_expected.to have_content 'New Field test' is_expected.to have_css '[type="checkbox"][name="field_test[non_nullable_boolean_field]"]' end it 'can be updated' do visit edit_path(model_name: 'field_test', id: field_test.id) find('.boolean_type input').check click_button 'Save and edit' expect(field_test.reload.non_nullable_boolean_field).to be true find('.boolean_type input').uncheck click_button 'Save and edit' expect(field_test.reload.non_nullable_boolean_field).to be false end end end ================================================ FILE: spec/integration/fields/carrierwave_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Carrierwave field', type: :request, active_record: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :string_field field :carrierwave_asset end end end it 'supports caching an uploaded file' do visit new_path(model_name: 'field_test') attach_file 'Carrierwave asset', file_path('test.jpg') fill_in 'field_test[string_field]', with: 'Invalid' click_button 'Save' expect(page).to have_content 'Field test failed to be created' fill_in 'field_test[string_field]', with: '' click_button 'Save' expect(FieldTest.first.carrierwave_asset.file).to exist end end ================================================ FILE: spec/integration/fields/ck_editor_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'CKEditor field', type: :request do subject { page } it 'works without error', js: true do RailsAdmin.config Draft do edit do field :notes, :ck_editor end end expect { visit new_path(model_name: 'draft') }.not_to raise_error is_expected.to have_selector('#cke_draft_notes') end end ================================================ FILE: spec/integration/fields/code_mirror_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'CodeMirror field', type: :request do subject { page } it 'works without error', js: true do RailsAdmin.config Draft do edit do field :notes, :code_mirror end end expect { visit new_path(model_name: 'draft') }.not_to raise_error is_expected.to have_selector('.CodeMirror') end end ================================================ FILE: spec/integration/fields/color_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Color field', type: :request do subject { page } it 'uses HTML5 color picker' do RailsAdmin.config Team do field :color, :color end visit new_path(model_name: 'team') is_expected.to have_selector('#team_color[type="color"]') end end ================================================ FILE: spec/integration/fields/date_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Date field', type: :request do subject { page } before do RailsAdmin.config FieldTest do field :id field :date_field end end describe 'filtering' do let!(:field_tests) do [FactoryBot.create(:field_test, date_field: Date.new(2021, 1, 2)), FactoryBot.create(:field_test, date_field: Date.new(2021, 1, 3))] end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '2021-01-03T00:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end it 'does not break when the condition is not filled' do visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) is_expected.to have_content '2 field test' end context 'with server timezone changed' do around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '2021-01-03T00:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end end end context 'on create' do it 'is initially blank' do visit new_path(model_name: 'field_test') expect(find('[name="field_test[date_field]"]', visible: false).value).to be_blank end it 'persists the value' do visit new_path(model_name: 'field_test') find('[name="field_test[date_field]"]', visible: false).set('2021-01-02T00:00:00') click_button 'Save' expect(FieldTest.count).to eq 1 expect(FieldTest.first.date_field).to eq Date.new(2021, 1, 2) end it 'ignores the time part' do visit new_path(model_name: 'field_test') find('[name="field_test[date_field]"]', visible: false).set('2021-01-02T12:34:00') click_button 'Save' expect(FieldTest.count).to eq 1 expect(FieldTest.first.date_field).to eq Date.new(2021, 1, 2) end end context 'on update' do let(:field_test) { FactoryBot.create :field_test, date_field: Date.new(2021, 1, 2) } it 'updates the value' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[date_field]"]', visible: false).value).to eq '2021-01-02T00:00:00' find('[name="field_test[date_field]"]', visible: false).set('2021-02-03T00:00:00') click_button 'Save' field_test.reload expect(field_test.date_field).to eq Date.new(2021, 2, 3) end end context 'with server timezone changed' do let(:field_test) { FactoryBot.create :field_test, date_field: Date.new(2015, 10, 8) } around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'is not altered by just saving untouched' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[date_field]"]', visible: false).value).to eq '2015-10-08T00:00:00' click_button 'Save' expect { field_test.reload }.not_to change(field_test, :date_field) end end end ================================================ FILE: spec/integration/fields/datetime_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Datetime field', type: :request do subject { page } before do RailsAdmin.config FieldTest do edit do field :datetime_field end end end describe 'filtering' do let!(:field_tests) do [FactoryBot.create(:field_test, datetime_field: DateTime.new(2021, 1, 2, 3, 45)), FactoryBot.create(:field_test, datetime_field: DateTime.new(2021, 1, 2, 4, 45))] end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '2021-01-02T04:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end it 'does not break when the condition is not filled' do visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) is_expected.to have_content '2 field test' end context 'with server timezone changed' do around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '2021-01-01T22:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end end end context 'on create' do it 'is initially blank' do visit new_path(model_name: 'field_test') expect(find('[name="field_test[datetime_field]"]', visible: false).value).to be_blank end it 'persists the value' do visit new_path(model_name: 'field_test') find('[name="field_test[datetime_field]"]', visible: false).set('2021-01-02T03:45:00') click_button 'Save' expect(FieldTest.count).to eq 1 expect(FieldTest.first.datetime_field).to eq DateTime.new(2021, 1, 2, 3, 45) end end context 'on update' do let(:field_test) { FactoryBot.create :field_test, datetime_field: DateTime.new(2021, 1, 2, 3, 45) } it 'updates the value' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' find('[name="field_test[datetime_field]"]', visible: false).set('2021-02-03T04:55:00') click_button 'Save' field_test.reload expect(field_test.datetime_field).to eq DateTime.new(2021, 2, 3, 4, 55) end end context 'with server timezone changed' do let(:field_test) { FactoryBot.create :field_test, datetime_field: DateTime.new(2015, 10, 8, 6, 45) } around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'treats the datetime set by the browser as local time' do visit edit_path(model_name: 'field_test', id: field_test.id) find('[name="field_test[datetime_field]"]', visible: false).set('2021-02-03T04:55:00') click_button 'Save' field_test.reload expect(field_test.datetime_field.iso8601).to eq '2021-02-03T04:55:00-06:00' end it 'is not altered by just saving untouched' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2015-10-08T01:45:00' click_button 'Save' expect { field_test.reload }.not_to change(field_test, :datetime_field) end end end ================================================ FILE: spec/integration/fields/enum_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Enum field', type: :request, active_record: true do subject { page } before do allow_any_instance_of(Team).to receive(:color_enum).and_return(%w[blue green red]) end describe 'for single value' do before do RailsAdmin.config Team do field :color end end it 'shows a single-value edit form' do visit new_path(model_name: 'team') is_expected.to have_selector('.enum_type select') is_expected.not_to have_selector('.enum_type select[multiple]') expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[blue green red] end it 'uses the filtering-select widget for selection', js: true do visit new_path(model_name: 'team') is_expected.to have_selector('.enum_type .filtering-select') end end describe 'for multiple values' do before do RailsAdmin.config Team do field :color do multiple true end end end it 'shows a multiple-value edit form' do visit new_path(model_name: 'team') is_expected.to have_selector('.enum_type select') is_expected.to have_selector('.enum_type select[multiple]') expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[blue green red] end it 'uses the filtering-multiselect widget for selection', js: true do visit new_path(model_name: 'team') is_expected.to have_selector('.enum_type .ra-multiselect') end end end ================================================ FILE: spec/integration/fields/file_upload_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'FileUpload field', type: :request do subject { page } before do RailsAdmin.config FieldTest do field :string_field, :file_upload do delete_method 'boolean_field' def resource_url(_thumb = false) value end end end end let(:field_test) { FactoryBot.create :field_test, string_field: 'http://localhost/dummy.jpg' } it 'supports deletion', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('#field_test_boolean_field', visible: false)).not_to be_checked click_link "Delete 'String field'" expect(find('#field_test_boolean_field', visible: false)).to be_checked end it 'shows a inline preview', js: true do visit new_path(model_name: 'field_test') attach_file 'String field', file_path('test.jpg') is_expected.to have_selector('#field_test_string_field_field img.preview') end end ================================================ FILE: spec/integration/fields/floara_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Floara field', type: :request do subject { page } it 'works without error', js: true do RailsAdmin.config Draft do edit do field :notes, :froala end end expect { visit new_path(model_name: 'draft') }.not_to raise_error is_expected.to have_selector('.fr-box') end it 'should include custom froala configuration' do RailsAdmin.config Draft do edit do field :notes, :froala do config_options inlineMode: false css_location 'stub_css.css' js_location 'stub_js.js' end end end visit new_path(model_name: 'draft') is_expected.to have_selector('textarea#draft_notes[data-richtext="froala-wysiwyg"][data-options]') end end ================================================ FILE: spec/integration/fields/has_and_belongs_to_many_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'HasAndBelongsToManyAssociation field', type: :request do subject { page } context 'on create' do before do @teams = FactoryBot.create_list(:team, 3) post new_path(model_name: 'fan', fan: {name: 'John Doe', team_ids: [@teams[0].id]}) @fan = RailsAdmin::AbstractModel.new('Fan').first end it 'creates an object with correct associations' do @teams[0].reload expect(@fan.teams).to include(@teams[0]) expect(@fan.teams).not_to include(@teams[1]) expect(@fan.teams).not_to include(@teams[2]) end end describe 'on update' do before do @teams = FactoryBot.create_list(:team, 3) @fan = FactoryBot.create :fan, teams: [@teams[0]] visit edit_path(model_name: 'fan', id: @fan.id) end it 'shows associated objects' do is_expected.to have_selector '#fan_team_ids' do |select| options = select.all 'option' expect(options[0]['selected']).to eq 'selected' expect(options[1]['selected']).to eq nil expect(options[2]['selected']).to eq nil end end end describe 'on show' do before do @player = FactoryBot.create :player @comment1 = FactoryBot.create :comment, commentable: @player @comment2 = FactoryBot.create :comment, commentable: @player @comment3 = FactoryBot.create :comment, commentable: FactoryBot.create(:player) visit show_path(model_name: 'player', id: @player.id) end it 'shows associated objects' do is_expected.to have_css("a[href='/admin/comment/#{@comment1.id}']") is_expected.to have_css("a[href='/admin/comment/#{@comment2.id}']") is_expected.not_to have_css("a[href='/admin/comment/#{@comment3.id}']") end end context "with Mongoid's custom primary_key option", mongoid: true do let(:user) { FactoryBot.create :managing_user, players: [players[0]], balls: [balls[0]] } let!(:players) { FactoryBot.create_list(:player, 2) } let!(:balls) { %w[red blue].map { |color| FactoryBot.create(:ball, color: color) } } before do RailsAdmin.config ManagingUser do field :players field :balls end end it 'allows update' do visit edit_path(model_name: 'managing_user', id: user.id) expect(find("select#managing_user_player_names option[value=\"#{players[0].name}\"]")).to be_selected expect(find("select#managing_user_ball_ids option[value=\"#{balls[0].color}\"]")).to be_selected select(players[1].name, from: 'Players') select(balls[1].rails_admin_default_object_label_method, from: 'Balls') click_button 'Save' expect(ManagingUser.first.players).to match_array players expect(ManagingUser.first.balls).to match_array balls end end end ================================================ FILE: spec/integration/fields/has_many_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'HasManyAssociation field', type: :request do subject { page } it 'adds a related id to the has_many create team link' do @team = FactoryBot.create :team visit edit_path(model_name: 'team', id: @team.id) is_expected.to have_selector("a[data-link='/admin/player/new?modal=true&player%5Bteam_id%5D=#{@team.id}']") end context 'when an association is readonly' do it 'is not editable' do @league = FactoryBot.create :league visit edit_path(model_name: 'league', id: @league.id) is_expected.not_to have_selector('select#league_team_ids') is_expected.to have_selector('select#league_division_ids') # decoy, fails if naming scheme changes end end describe 'has many associations through more than one association' do it 'is not editable' do @league = FactoryBot.create :league visit edit_path(model_name: 'league', id: @league.id) expect(page).to have_selector('select#league_division_ids') expect(page).to_not have_selector('select#league_player_ids') end end describe 'on create' do before do @divisions = Array.new(3) { Division.create!(name: "div #{Time.now.to_f}", league: League.create!(name: "league #{Time.now.to_f}")) } end it 'shows selects' do visit new_path(model_name: 'league') is_expected.to have_selector('select#league_division_ids') end it 'creates an object with correct associations' do post new_path(model_name: 'league', league: {name: 'National League', division_ids: [@divisions[0].id]}) @league = RailsAdmin::AbstractModel.new('League').all.to_a.last @divisions[0].reload expect(@league.divisions).to include(@divisions[0]) expect(@league.divisions).not_to include(@divisions[1]) expect(@league.divisions).not_to include(@divisions[2]) end context 'with default_value' do before do ids = [@divisions[2].id] RailsAdmin.config League do configure :divisions do default_value ids end end end it 'shows the value as selected' do visit new_path(model_name: 'league') expect(find('select#league_division_ids').value).to eq [@divisions[2].id.to_s] end end end context 'on update' do it 'is fillable and emptyable', active_record: true do @league = FactoryBot.create :league @divisions = Array.new(3) { Division.create!(name: "div #{Time.now.to_f}", league: League.create!(name: "league #{Time.now.to_f}")) } put edit_path(model_name: 'league', id: @league.id, league: {name: 'National League', division_ids: [@divisions[0].id]}) @league.reload expect(@league.name).to eq('National League') @divisions[0].reload expect(@league.divisions).to include(@divisions[0]) expect(@league.divisions).not_to include(@divisions[1]) expect(@league.divisions).not_to include(@divisions[2]) put edit_path(model_name: 'league', id: @league.id, league: {division_ids: ['']}) @league.reload expect(@league.divisions).to be_empty end context 'with embedded model', mongoid: true do it 'is editable' do @record = FactoryBot.create :field_test 2.times.each { |i| @record.embeds.create name: "embed #{i}" } visit edit_path(model_name: 'field_test', id: @record.id) fill_in 'field_test_embeds_attributes_0_name', with: 'embed 1 edited' page.find('#field_test_embeds_attributes_1__destroy', visible: false).set('true') click_button 'Save' # first(:button, "Save").click @record.reload expect(@record.embeds.length).to eq(1) expect(@record.embeds[0].name).to eq('embed 1 edited') end end end context 'on show' do context 'with embedded model', mongoid: true do it "does not show link to individual object's page" do @record = FactoryBot.create :field_test 2.times.each { |i| @record.embeds.create name: "embed #{i}" } visit show_path(model_name: 'field_test', id: @record.id) is_expected.not_to have_link('embed 0') is_expected.not_to have_link('embed 1') end end end context 'on list' do context 'with embedded model', mongoid: true do it "does not show link to individual object's page" do RailsAdmin.config FieldTest do list do field :embeds end end @record = FactoryBot.create :field_test 2.times.each { |i| @record.embeds.create name: "embed #{i}" } visit index_path(model_name: 'field_test') is_expected.not_to have_link('embed 0') is_expected.not_to have_link('embed 1') end end end context 'with not nullable foreign key', active_record: true do before do RailsAdmin.config FieldTest do edit do field :nested_field_tests do nested_form false end end end @field_test = FactoryBot.create :field_test end it 'don\'t allow to remove element', js: true do visit edit_path(model_name: 'FieldTest', id: @field_test.id) is_expected.not_to have_selector('a.ra-multiselect-item-remove') is_expected.not_to have_selector('a.ra-multiselect-item-remove-all') end end context 'with nullable foreign key', active_record: true do before do RailsAdmin.config Team do edit do field :players end end @team = FactoryBot.create :team end it 'allow to remove element', js: true do visit edit_path(model_name: 'Team', id: @team.id) is_expected.to have_selector('a.ra-multiselect-item-remove') is_expected.to have_selector('a.ra-multiselect-item-remove-all') end end context 'with custom primary_key option' do let(:user) { FactoryBot.create :managing_user } let!(:teams) { [FactoryBot.create(:managed_team, manager: user.email), FactoryBot.create(:managed_team)] } before do RailsAdmin.config.included_models = [ManagingUser, ManagedTeam] RailsAdmin.config ManagingUser do field :teams end end it 'allows update' do visit edit_path(model_name: 'managing_user', id: user.id) expect(find("select#managing_user_team_ids option[value=\"#{teams[0].id}\"]")).to have_content teams[0].name select(teams[1].name, from: 'Teams') click_button 'Save' is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.teams).to match_array teams end context 'when fetching associated objects via xhr' do before do RailsAdmin.config ManagingUser do field(:teams) { associated_collection_cache_all false } end end it 'allows update', js: true do visit edit_path(model_name: 'managing_user', id: user.id) find('input.ra-multiselect-search').set('T') find('.ra-multiselect-collection option', text: teams[1].name).select_option find('.ra-multiselect-item-add').click click_button 'Save' is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.teams).to match_array teams end end end context 'with composite foreign keys', composite_primary_keys: true do let(:fan) { FactoryBot.create(:fan) } let!(:fanships) { FactoryBot.create_list(:fanship, 3) } describe 'via default field' do before do RailsAdmin.config Fan do field :name field :fanships end end it 'shows the current selection' do visit edit_path(model_name: 'fan', id: fanships[0].fan.id) is_expected.to have_select('Fanships', selected: "Fanship ##{fanships[0].id}") end it 'allows update' do visit edit_path(model_name: 'fan', id: fan.id) select("Fanship ##{fanships[0].id}", from: 'Fanships') select("Fanship ##{fanships[1].id}", from: 'Fanships') click_button 'Save' is_expected.to have_content 'Fan successfully updated' expect(fan.reload.fanships.map(&:team_id)).to match_array fanships.map(&:team_id)[0..1] end context 'with invalid key' do before do allow_any_instance_of(RailsAdmin::Config::Fields::Types::HasManyAssociation). to receive(:collection).and_return([["Fanship ##{fanships[0].id}", 'invalid']]) end it 'fails to update' do visit edit_path(model_name: 'fan', id: fan.id) select("Fanship ##{fanships[0].id}", from: 'Fanships') expect { click_button 'Save' }.to raise_error ActiveRecord::RecordNotFound end end end describe 'via remote-sourced field' do before do RailsAdmin.config Fan do field :name field :fanships do associated_collection_cache_all false end end end it 'allows update', js: true do visit edit_path(model_name: 'fan', id: fan.id) find('input.ra-multiselect-search').set('F') find('.ra-multiselect-collection option', text: "Fanship ##{fanships[0].id}").select_option find('.ra-multiselect-collection option', text: "Fanship ##{fanships[1].id}").select_option find('.ra-multiselect-item-add').click click_button 'Save' is_expected.to have_content 'Fan successfully updated' expect(fan.reload.fanships.map(&:team_id)).to match_array fanships.map(&:team_id)[0..1] end end describe 'via nested field' do let!(:team) { FactoryBot.create :team } let!(:fanships) { FactoryBot.create_list(:fanship, 2, fan: fan) } before do RailsAdmin.config NestedFan do field :name field :fanships end end it 'allows update' do visit edit_path(model_name: 'nested_fan', id: fan.id) select(team.name, from: 'nested_fan_fanships_attributes_0_team_id') fill_in 'nested_fan_fanships_attributes_1_since', with: '2020-01-23' click_button 'Save' is_expected.to have_content 'Nested fan successfully updated' expect(fan.fanships[0].team).to eq team expect(fan.fanships[1].since).to eq Date.new(2020, 1, 23) end end end end ================================================ FILE: spec/integration/fields/has_one_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'HasOneAssociation field', type: :request do subject { page } it 'adds a related id to the has_one create draft link' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) is_expected.to have_selector("a[data-link='/admin/draft/new?draft%5Bplayer_id%5D=#{@player.id}&modal=true']") end context 'on create' do before do @draft = FactoryBot.create :draft end it 'shows selects' do visit new_path(model_name: 'player') is_expected.to have_selector('select#player_draft_id') end it 'creates an object with correct associations' do visit new_path(model_name: 'player') fill_in 'Name', with: 'Jackie Robinson' fill_in 'Number', with: @draft.player.number + 1 select("Draft ##{@draft.id}", from: 'Draft') click_button 'Save' is_expected.to have_content 'Player successfully created' @player = Player.where(name: 'Jackie Robinson').first @draft.reload expect(@player.draft).to eq(@draft) end context 'with default_value' do before do id = @draft.id RailsAdmin.config Player do configure :draft do default_value id end end end it 'shows the value as selected' do visit new_path(model_name: 'player') expect(find('select#player_draft_id').value).to eq @draft.id.to_s end end end context 'on update' do before do @drafts = FactoryBot.create_list :draft, 2 @player = FactoryBot.create :player, draft: @drafts[0] visit edit_path(model_name: 'player', id: @player.id) end it 'updates an object with correct associations' do select("Draft ##{@drafts[1].id}", from: 'Draft') click_button 'Save' @player.reload expect(@player.draft).to eq(@drafts[1]) end it 'clears the current selection' do select('', from: 'Draft') click_button 'Save' @player.reload expect(@player.draft).to be nil end end describe 'on show' do before do @player = FactoryBot.create :player @draft = FactoryBot.create :draft, player: @player visit show_path(model_name: 'player', id: @player.id) end it 'shows associated objects' do is_expected.to have_css("a[href='/admin/draft/#{@draft.id}']") end end context 'with custom primary_key option' do let(:user) { FactoryBot.create :managing_user } let!(:team) { FactoryBot.create(:managed_team) } before do RailsAdmin.config.included_models = [ManagingUser, ManagedTeam] RailsAdmin.config ManagingUser do field :team end end it 'allows update' do visit edit_path(model_name: 'managing_user', id: user.id) select(team.name, from: 'Team') click_button 'Save' is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.team).to eq team end context 'when fetching associated objects via xhr' do before do RailsAdmin.config ManagingUser do field(:team) { associated_collection_cache_all false } end end it 'allows update', js: true do visit edit_path(model_name: 'managing_user', id: user.id) find('input.ra-filtering-select-input').set('T') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("#{team.name}")).click()} click_button 'Save' is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.team).to eq team end end end context 'with composite foreign keys', composite_primary_keys: true do let(:fan) { FactoryBot.create(:fan) } let!(:fanship) { FactoryBot.create(:fanship, fan: fan) } describe 'via default field' do before do RailsAdmin.config Fan do field :name field :fanship end end it 'allows create' do visit new_path(model_name: 'fan') fill_in 'Name', with: 'someone' select("Fanship ##{fanship.id}", from: 'Fanship') click_button 'Save' is_expected.to have_content 'Fan successfully created' expect(Fan.where(name: 'someone').first.fanship.team_id).to eq fanship.team_id end it 'shows the current selection' do visit edit_path(model_name: 'fan', id: fanship.fan_id) is_expected.to have_select('Fanship', selected: "Fanship ##{fanship.id}") end end describe 'via remote-sourced field' do before do RailsAdmin.config Fan do field :name field :fanship do associated_collection_cache_all false end end end it 'allows create', js: true do visit new_path(model_name: 'fan') fill_in 'Name', with: 'someone' find('.fanship_field input.ra-filtering-select-input').set(fanship.fan_id) page.execute_script("document.querySelector('.fanship_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Fanship ##{fanship.id}")).click()} click_button 'Save' is_expected.to have_content 'Fan successfully created' expect(Fan.where(name: 'someone').first.fanship.team_id).to eq fanship.team_id end end describe 'via nested field' do let!(:team) { FactoryBot.create :team } before do RailsAdmin.config NestedFan do field :name field :fanship end end it 'allows update' do visit edit_path(model_name: 'nested_fan', id: fanship.fan_id) select(team.name, from: 'Team') fill_in 'Since', with: '2020-01-23' click_button 'Save' is_expected.to have_content 'Nested fan successfully updated' expect(fan.fanship.team).to eq team expect(fan.fanship.since).to eq Date.new(2020, 1, 23) end end end end ================================================ FILE: spec/integration/fields/hidden_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Hidden field', type: :request do subject { page } describe '#default_value' do before do RailsAdmin::Config.authenticate_with { warden.authenticate! scope: :user } RailsAdmin::Config.current_user_method(&:current_user) login_as User.create( email: 'username@example.com', password: 'password', ) end before do RailsAdmin.config Player do include_all_fields edit do field :name, :hidden do default_value do bindings[:view]._current_user.email end end end end end it 'shows up with default value, hidden' do visit new_path(model_name: 'player') is_expected.to have_selector("#player_name[type=hidden][value='username@example.com']", visible: false) is_expected.not_to have_selector("#player_name[type=hidden][value='toto@example.com']", visible: false) end it 'does not show label' do is_expected.not_to have_selector('label', text: 'Name') end it 'does not show help block' do is_expected.not_to have_xpath("id('player_name')/../p[@class='help-block']") end it 'submits the field value' do visit new_path(model_name: 'player') find("#player_name[type=hidden][value='username@example.com']", visible: false).set('someone@example.com') fill_in 'Number', with: 1 click_button 'Save' is_expected.to have_content('Player successfully created') expect(Player.first.name).to eq 'someone@example.com' end end end ================================================ FILE: spec/integration/fields/multiple_active_storage_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'MultipleActiveStorage field', type: :request, active_record: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :active_storage_assets end end # To suppress 'SQLite3::BusyException: database is locked' exception @original = page.driver.browser.url_blacklist # rubocop:disable Naming/InclusiveLanguage page.driver.browser.url_blacklist = [%r{/rails/active_storage/representations}] # rubocop:disable Naming/InclusiveLanguage end after { page.driver.browser.url_blacklist = @original } # rubocop:disable Naming/InclusiveLanguage it 'supports uploading multiple files', js: true do visit new_path(model_name: 'field_test') attach_file 'Active storage assets', [file_path('test.jpg'), file_path('test.png')] click_button 'Save' is_expected.to have_content 'Field test successfully created' expect(FieldTest.first.active_storage_assets.map { |image| image.filename.to_s }).to match_array ['test.jpg', 'test.png'] end context 'when working with existing files' do let(:field_test) { FactoryBot.create(:field_test, active_storage_assets: ['test.jpg', 'test.png'].map { |img| {io: File.open(file_path(img)), filename: img} }) } it 'supports appending a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) attach_file 'Active storage assets', [file_path('test.gif')] click_button 'Save' is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.jpg', 'test.png', 'test.gif'] end it 'supports deleting a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) click_link "Delete 'Active storage assets' #1" click_button 'Save' is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.png'] end end describe 'direct upload', js: true do let(:field_test) { FactoryBot.create :field_test } before do RailsAdmin.config FieldTest do edit do configure(:active_storage_assets) { direct true } end end end it 'works' do visit edit_path(model_name: 'field_test', id: field_test.id) attach_file 'Active storage assets', [file_path('test.jpg')] expect_any_instance_of(ActiveStorage::DirectUploadsController).to receive(:create).and_call_original click_button 'Save' expect(page).to have_content 'Field test successfully updated' field_test.reload expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.jpg'] end end end ================================================ FILE: spec/integration/fields/multiple_carrierwave_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'MultipleCarrierwave field', type: :request, active_record: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :carrierwave_assets end end end it 'supports uploading multiple files', js: true do visit new_path(model_name: 'field_test') attach_file 'Carrierwave assets', [file_path('test.jpg'), file_path('test.png')] click_button 'Save' is_expected.to have_content 'Field test successfully created' expect(FieldTest.first.carrierwave_assets.map { |image| File.basename(image.url) }).to match_array ['test.jpg', 'test.png'] end context 'when working with existing files' do let(:field_test) { FactoryBot.create(:field_test, carrierwave_assets: ['test.jpg', 'test.png'].map { |img| File.open(file_path(img)) }) } it 'supports appending a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) attach_file 'Carrierwave assets', [file_path('test.gif')] click_button 'Save' is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.jpg', 'test.png', 'test.gif'] end it 'supports deleting a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) click_link "Delete 'Carrierwave assets' #1" click_button 'Save' is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.png'] end it 'supports reordering files', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) page.execute_script File.read(File.expand_path('../../../vendor/assets/javascripts/rails_admin/jquery3.js', __dir__)) page.execute_script File.read(File.expand_path('../../support/jquery.simulate.drag-sortable.js', __dir__)) page.execute_script %{$(".ui-sortable-handle:first-child").simulateDragSortable({move: 1});} click_button 'Save' is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.png', 'test.jpg'] end end end ================================================ FILE: spec/integration/fields/multiple_file_upload_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'MultipleFileUpload field', type: :request do subject { page } before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload do attachment do delete_value { value } def resource_url(_thumb = false) value end end delete_method 'boolean_field' reorderable true def value bindings[:object].safe_send(name)&.split end end end end let(:field_test) { FactoryBot.create :field_test, string_field: 'http://localhost/1.jpg http://localhost/2.jpg' } it 'supports deletion', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) click_link "Delete 'String field' #1" expect(page.all(:css, '[name="field_test[boolean_field][]"]:checked', visible: false).map(&:value)).to eq %w[http://localhost/1.jpg] end it 'shows a inline preview', js: true do visit new_path(model_name: 'field_test') attach_file 'String field', file_path('test.jpg') is_expected.to have_selector('#field_test_string_field_field img.preview') end end ================================================ FILE: spec/integration/fields/paperclip_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Paperclip field', type: :request do subject { page } it 'shows a file upload field' do RailsAdmin.config User do edit do field :avatar end end visit new_path(model_name: 'user') is_expected.to have_selector('input#user_avatar') end end ================================================ FILE: spec/integration/fields/polymorphic_assosiation_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'PolymorphicAssociation field', type: :request do subject { page } context 'on create' do it 'is editable', js: true do @players = ['Jackie Robinson', 'Rob Wooten'].map { |name| FactoryBot.create :player, name: name } visit new_path(model_name: 'comment') select 'Player', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} click_button 'Save' is_expected.to have_content 'Comment successfully created' expect(Comment.first.commentable).to eq @players[0] end it 'uses base class for models with inheritance' do @hardball = FactoryBot.create :hardball post new_path(model_name: 'comment', comment: {commentable_type: 'Hardball', commentable_id: @hardball.id}) @comment = Comment.first expect(@comment.commentable_type).to eq 'Ball' expect(@comment.commentable).to eq @hardball end it 'clears the selected id on type change', js: true do @players = ['Jackie Robinson', 'Rob Wooten'].map { |name| FactoryBot.create :player, name: name } visit new_path(model_name: 'comment') select 'Player', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} select 'Team', from: 'comment[commentable_type]' expect(find('#comment_commentable_id', visible: false).value).to eq '' end context 'when the associated model is declared in a two-level namespace' do it 'successfully saves the record', js: true do polymorphic_association_tests = ['Jackie Robinson', 'Rob Wooten'].map do |name| FactoryBot.create(:two_level_namespaced_polymorphic_association_test, name: name) end visit new_path(model_name: 'comment') select 'Polymorphic association test', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} click_button 'Save' is_expected.to have_content 'Comment successfully created' expect(Comment.first.commentable).to eq polymorphic_association_tests.first end end end context 'on update' do let(:team) { FactoryBot.create :team, name: 'Los Angeles Dodgers' } let(:comment) { FactoryBot.create :comment, commentable: team } let!(:players) { ['Jackie Robinson', 'Rob Wooten', 'Scott Hairston'].map { |name| FactoryBot.create :player, name: name } } it 'is editable', js: true do visit edit_path(model_name: 'comment', id: comment.id) expect(find('select#comment_commentable_type').value).to eq 'Team' expect(find('select#comment_commentable_id', visible: false).value).to eq team.id.to_s find('input.ra-filtering-select-input').set('Los') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all('ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Los Angeles Dodgers'] select 'Player', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all('ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Rob Wooten', 'Jackie Robinson'] page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} click_button 'Save' is_expected.to have_content 'Comment successfully updated' expect(comment.reload.commentable).to eq players[0] end it 'is visible in the owning end' do visit edit_path(model_name: 'team', id: team.id) is_expected.to have_selector('select#team_comment_ids') end context 'with records in different models share the same id', js: true do let!(:players) { [FactoryBot.create(:player, id: team.id, name: 'Jackie Robinson')] } it 'clears the selected id on type change', js: true do visit edit_path(model_name: 'comment', id: comment.id) select 'Player', from: 'comment[commentable_type]' click_button 'Save' is_expected.to have_content 'Comment successfully updated' expect(comment.reload.commentable).to eq nil end it 'updates correctly', js: true do visit edit_path(model_name: 'comment', id: comment.id) select 'Player', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} click_button 'Save' is_expected.to have_content 'Comment successfully updated' expect(comment.reload.commentable).to eq players[0] end end end context 'on show' do before do @player = FactoryBot.create :player @comment = FactoryBot.create :comment, commentable: @player visit show_path(model_name: 'comment', id: @comment.id) end it 'shows associated object' do is_expected.to have_css("a[href='/admin/player/#{@player.id}']") end end context 'on list' do before :each do @team = FactoryBot.create :team @comment = FactoryBot.create :comment, commentable: @team end it 'works like belongs to associations in the list view' do visit index_path(model_name: 'comment') is_expected.to have_content(@team.name) end end end ================================================ FILE: spec/integration/fields/serialized_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Serialized field', type: :request do subject { page } context 'with serialized objects' do before do RailsAdmin.config do |c| c.model User do configure :roles, :serialized end end @user = FactoryBot.create :user visit edit_path(model_name: 'user', id: @user.id) fill_in 'user[roles]', with: %(['admin', 'user']) click_button 'Save' # first(:button, "Save").click @user.reload end it 'saves the serialized data' do expect(@user.roles).to eq(%w[admin user]) end end context 'with serialized objects of Mongoid', mongoid: true do before do @field_test = FactoryBot.create :field_test visit edit_path(model_name: 'field_test', id: @field_test.id) end it 'saves the serialized data' do fill_in 'field_test[array_field]', with: '[4, 2]' fill_in 'field_test[hash_field]', with: '{ a: 6, b: 2 }' click_button 'Save' # first(:button, "Save").click @field_test.reload expect(@field_test.array_field).to eq([4, 2]) expect(@field_test.hash_field).to eq('a' => 6, 'b' => 2) end it 'clears data when empty string is passed' do fill_in 'field_test[array_field]', with: '' fill_in 'field_test[hash_field]', with: '' click_button 'Save' # first(:button, "Save").click @field_test.reload expect(@field_test.array_field).to eq(nil) expect(@field_test.hash_field).to eq(nil) end end end ================================================ FILE: spec/integration/fields/shrine_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Shrine field', type: :request, active_record: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :string_field field :shrine_asset end end end it 'supports caching an uploaded file', js: true do visit new_path(model_name: 'field_test') attach_file 'Shrine asset', file_path('test.jpg') fill_in 'field_test[string_field]', with: 'Invalid' click_button 'Save' expect(page).to have_content 'Field test failed to be created' fill_in 'field_test[string_field]', with: '' click_button 'Save' expect(FieldTest.first.shrine_asset).to exist end end ================================================ FILE: spec/integration/fields/simple_mde_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'SimpleMDE field', type: :request do subject { page } it 'works without error', js: true do RailsAdmin.config Draft do edit do field :notes, :simple_mde end end expect { visit new_path(model_name: 'draft') }.not_to raise_error is_expected.to have_selector('a[title="Markdown Guide"]') end end ================================================ FILE: spec/integration/fields/time_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Time field', type: :request, active_record: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :time_field end end end describe 'filtering' do let!(:field_tests) do [FactoryBot.create(:field_test, time_field: DateTime.new(2000, 1, 1, 3, 45)), FactoryBot.create(:field_test, time_field: DateTime.new(2000, 1, 1, 4, 45))] end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '2000-01-01T04:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end it 'does not break when the condition is not filled' do visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) is_expected.to have_content '2 field test' end context 'with server timezone changed' do around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'correctly returns a record' do visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '2000-01-01T22:00:00', nil], o: 'between'}}}) is_expected.to have_content '1 field test' is_expected.to have_content field_tests[1].id end end end context 'on create' do it 'is initially blank' do visit new_path(model_name: 'field_test') expect(find('[name="field_test[time_field]"]', visible: false).value).to be_blank end it 'persists the value' do visit new_path(model_name: 'field_test') find('[name="field_test[time_field]"]', visible: false).set('2000-01-01T03:45:00') click_button 'Save' expect(FieldTest.count).to eq 1 expect(FieldTest.first.time_field).to eq DateTime.new(2000, 1, 1, 3, 45) end it 'ignores the date part' do visit new_path(model_name: 'field_test') find('[name="field_test[time_field]"]', visible: false).set('2021-01-02T03:45:00') click_button 'Save' expect(FieldTest.first.time_field).to eq DateTime.new(2000, 1, 1, 3, 45) end end context 'on update' do let(:field_test) { FactoryBot.create :field_test, time_field: DateTime.new(2021, 1, 2, 3, 45) } it 'updates the value' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[time_field]"]', visible: false).value).to eq '2000-01-01T03:45:00' find('[name="field_test[time_field]"]', visible: false).set('2021-02-03T04:55:00') click_button 'Save' field_test.reload expect(field_test.time_field).to eq DateTime.new(2000, 1, 1, 4, 55) end end context 'with server timezone changed' do let(:field_test) { FactoryBot.create :field_test, time_field: DateTime.new(2000, 1, 1, 6, 45) } around do |example| original = Time.zone Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') example.run Time.zone = original end it 'treats the datetime set by the browser as local time' do visit edit_path(model_name: 'field_test', id: field_test.id) find('[name="field_test[time_field]"]', visible: false).set('2000-01-01T04:55:00') click_button 'Save' field_test.reload expect(field_test.time_field.iso8601).to eq '2000-01-01T04:55:00-06:00' end it 'is not altered by just saving untouched' do visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('[name="field_test[time_field]"]', visible: false).value).to eq '2000-01-01T00:45:00' click_button 'Save' expect { field_test.reload }.not_to change(field_test, :time_field) end end end ================================================ FILE: spec/integration/fields/wysihtml5_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Wysihtml5 field', type: :request do subject { page } it 'works without error', js: true do RailsAdmin.config Draft do edit do field :notes, :wysihtml5 end end expect { visit new_path(model_name: 'draft') }.not_to raise_error is_expected.to have_selector('.wysihtml5-toolbar') end it 'should include custom wysihtml5 configuration' do RailsAdmin.config Draft do edit do field :notes, :wysihtml5 do config_options image: false css_location 'stub_css.css' js_location 'stub_js.js' end end end visit new_path(model_name: 'draft') is_expected.to have_selector('textarea#draft_notes[data-richtext="bootstrap-wysihtml5"][data-options]') end end ================================================ FILE: spec/integration/rails_admin_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin, type: :request do subject { page } before do RailsAdmin::Config.authenticate_with { warden.authenticate! scope: :user } RailsAdmin::Config.current_user_method(&:current_user) login_as User.create( email: 'username@example.com', password: 'password', ) end # A common mistake for translators is to forget to change the YAML file's # root key from en to their own locale (as people tend to use the English # file as template for a new translation). describe 'localization' do it 'defaults to English' do RailsAdmin.config.included_models = [] visit dashboard_path is_expected.to have_content('Site Administration') is_expected.to have_content('Dashboard') end end describe 'html head' do before { visit dashboard_path } # NOTE: the [href^="/asset... syntax matches the start of a value. The reason # we just do that is to avoid being confused by rails' asset_ids. it 'loads stylesheets in header' do case RailsAdmin.config.asset_source when :sprockets is_expected.to have_selector('head link[href^="/assets/rails_admin"][href$=".css"]', visible: false) when :webpacker is_expected.to have_no_selector('head link[href~="rails_admin"][href$=".css"]', visible: false) end end it 'loads javascript files in body' do case RailsAdmin.config.asset_source when :sprockets is_expected.to have_selector('head script[src^="/assets/rails_admin"][src$=".js"]', visible: false) when :webpacker is_expected.to have_selector('head script[src^="/packs-test/js/rails_admin"][src$=".js"]', visible: false) end end end describe 'custom theming' do before { visit dashboard_path } if CI_ASSET == :sprockets it 'applies the style overridden by assets in the application', js: true do expect(find('.navbar-brand small').style('opacity')).to eq({'opacity' => '0.99'}) end end end describe 'navbar css class' do it 'is set by default' do expect(RailsAdmin.config.navbar_css_classes).to eq(%w[navbar-dark bg-primary border-bottom]) end it 'can be configured' do RailsAdmin.config do |config| config.navbar_css_classes = %w[navbar-light border-bottom] end visit dashboard_path is_expected.to have_css('nav.navbar.navbar-light.border-bottom') end end describe 'sidebar navigation', js: true do it 'is collapsible' do visit dashboard_path is_expected.to have_css('.sidebar .nav-link', text: 'Players') click_button 'Navigation' is_expected.to have_css('.sidebar .btn-toggle.collapsed') is_expected.not_to have_css('.sidebar #navigation.show') is_expected.not_to have_css('.sidebar .nav-link', text: 'Players') end it 'persists over a page transition' do visit dashboard_path click_button 'Navigation' is_expected.to have_css('.sidebar .btn-toggle.collapsed') is_expected.not_to have_css('.sidebar #navigation.show') find('.player_links .show a').trigger('click') is_expected.to have_content 'List of Players' is_expected.not_to have_css('.sidebar .nav-link', text: 'Players') end end describe '_current_user' do # https://github.com/railsadminteam/rails_admin/issues/549 it 'is accessible from the list view' do RailsAdmin.config Player do list do field :name do visible do bindings[:view]._current_user.email == 'username@example.com' end end field :team do visible do bindings[:view]._current_user.email == 'foo@example.com' end end end end visit index_path(model_name: 'player') is_expected.to have_selector('.header.name_field') is_expected.not_to have_selector('.header.team_field') end end describe 'secondary navigation' do it 'has Gravatar image' do visit dashboard_path is_expected.to have_selector('ul.navbar-nav img[src*="gravatar.com"]') end it "does not show Gravatar when user doesn't have email method" do allow_any_instance_of(User).to receive(:respond_to?).and_return(true) allow_any_instance_of(User).to receive(:respond_to?).with(:email).and_return(false) allow_any_instance_of(User).to receive(:respond_to?).with(:devise_scope).and_return(false) visit dashboard_path is_expected.not_to have_selector('ul.nav.pull-right li img') end it 'does not cause error when email is nil' do allow_any_instance_of(User).to receive(:email).and_return(nil) visit dashboard_path is_expected.to have_selector('body.rails_admin') end it 'shows a log out link' do visit dashboard_path is_expected.to have_content 'Log out' end it 'has bg-danger class on log out link' do visit dashboard_path is_expected.to have_selector('.bg-danger') end it 'has links for actions which are marked as show_in_navigation' do I18n.backend.store_translations( :en, admin: {actions: { shown_in_navigation: {menu: 'Look this'}, }} ) RailsAdmin.config do |config| config.actions do dashboard do show_in_navigation false end root :shown_in_navigation, :dashboard do action_name :dashboard show_in_navigation true end end end visit dashboard_path is_expected.not_to have_css '.root_links li', text: 'Dashboard' is_expected.to have_css '.root_links li', text: 'Look this' end end describe 'CSRF protection' do before do allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true) end it 'is enforced' do visit new_path(model_name: 'league') fill_in 'league[name]', with: 'National league' find('input[name="authenticity_token"]', visible: false).set('invalid token') expect { click_button 'Save' }.to raise_error ActionController::InvalidAuthenticityToken end end describe 'Turbo Drive', js: true do let(:player) { FactoryBot.create :player } it 'does not trigger JS errors by going away from and back to RailsAdmin' do visit show_path(model_name: 'player', id: player.id) click_link 'Show in app' click_link 'Back to admin' is_expected.to have_content 'Details for Player' end it 'triggers rails_admin.dom_ready right after a validation error' do visit edit_path(model_name: 'player', id: player.id) fill_in 'player[name]', with: 'on steroids' find_button('Save').trigger 'click' is_expected.to have_content 'Player failed to be updated' is_expected.to have_css '.filtering-select[data-input-for="player_team_id"]' end it 'does not prefetch pages' do allow_any_instance_of(RailsAdmin::Config::Actions::Index).to receive(:controller).and_raise('index prefetched') visit dashboard_path find('.sidebar a.nav-link[href$="/player"]').hover sleep 0.3 # Turbo waits 100ms before prefetch end end describe 'dom_ready events', js: true do it 'trigger properly' do visit dashboard_path expect(evaluate_script('domReadyTriggered')).to match_array %w[plainjs/dot jquery/dot] end end context 'with invalid model name' do it "redirects to dashboard and inform the user the model wasn't found" do visit '/admin/whatever' expect(page.driver.status_code).to eq(404) expect(find('.alert-danger')).to have_content("Model 'Whatever' could not be found") end end context 'with invalid action' do it "redirects to balls index and inform the user the id wasn't found" do visit '/admin/ball/545-typo' expect(page.driver.status_code).to eq(404) expect(find('.alert-danger')).to have_content("Ball with id '545-typo' could not be found") end end end ================================================ FILE: spec/integration/widgets/datetimepicker_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Datetimepicker widget', type: :request, js: true do subject { page } before do RailsAdmin.config FieldTest do edit do field :datetime_field end end end it 'is initially blank' do visit new_path(model_name: 'field_test') expect(find('[name="field_test[datetime_field]"]', visible: false).value).to be_blank expect(find('[name="field_test[datetime_field]"] + input').value).to be_blank end it 'populates the value selected by the Datetime picker into the hidden_field' do visit new_path(model_name: 'field_test') is_expected.to have_css '.form-control.flatpickr-input', visible: false page.execute_script <<-JS document.querySelector('#field_test_datetime_field')._flatpickr.setDate('2015-10-08 14:00:00'); JS expect(find('#field_test_datetime_field + input').value).to eq 'October 08, 2015 14:00' expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2015-10-08T14:00:00' end it 'populates the value entered in the text field into the hidden_field' do visit new_path(model_name: 'field_test') find('#field_test_datetime_field + input').set 'January 2, 2021 03:45' expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' expect(find('#field_test_datetime_field + input').value).to eq 'January 02, 2021 03:45' end it 'works with a different format' do RailsAdmin.config FieldTest do edit do configure(:datetime_field) { strftime_format '%Y-%m-%d' } end end visit new_path(model_name: 'field_test') find('#field_test_datetime_field + input').set '2021-01-02' expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T00:00:00' expect(find('#field_test_datetime_field + input').value).to eq '2021-01-02' end it 'supports custom flatpickr_format' do RailsAdmin.config FieldTest do edit do configure(:datetime_field) { flatpickr_format 'H\Hi\MS\S' } end end visit new_path(model_name: 'field_test') is_expected.to have_css '.form-control.flatpickr-input', visible: false page.execute_script <<-JS document.querySelector('#field_test_datetime_field')._flatpickr.setDate('2015-10-08 12:34:56'); JS expect(find('#field_test_datetime_field + input').value).to eq '12H34M56S' end context 'with locale set' do around(:each) do |example| original = I18n.default_locale I18n.default_locale = :fr example.run I18n.default_locale = original end it 'shows and accepts the value in the given locale' do visit new_path(model_name: 'field_test', field_test: {datetime_field: '2021-01-02T03:45:00'}) expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' expect(find('#field_test_datetime_field + input').value).to eq 'samedi 02 janvier 2021 03:45' find('#field_test_datetime_field + input').set 'mercredi 03 février 2021 04:55' expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-02-03T04:55:00' end end end ================================================ FILE: spec/integration/widgets/filter_box_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Filter box widget', type: :request, js: true do subject { page } it 'adds filters' do RailsAdmin.config Player do field :name field :position end visit index_path(model_name: 'player') is_expected.to have_no_css('#filters_box .filter') click_link 'Add filter' click_link 'Name' within('#filters_box') do is_expected.to have_css('.filter', count: 1) is_expected.to have_css('.filter select[name^="f[name]"]') end click_link 'Add filter' click_link 'Position' within('#filters_box') do is_expected.to have_css('.filter', count: 2) is_expected.to have_css('.filter select[name^="f[position]"]') end end it 'removes filters' do RailsAdmin.config Player do field :name field :position end visit index_path(model_name: 'player') is_expected.to have_no_css('#filters_box .filter') click_link 'Add filter' click_link 'Name' click_link 'Add filter' click_link 'Position' within('#filters_box') do is_expected.to have_css('.filter', count: 2) click_button 'Name' is_expected.to have_no_css('.filter select[name^="f[name]"]') click_button 'Position' is_expected.to have_no_css('.filter') end end it 'hides redundant filter options for required fields' do RailsAdmin.config Player do list do field :name do required true end field :team end end visit index_path(model_name: 'player', f: {name: {'1' => {v: ''}}, team: {'2' => {v: ''}}}) within(:select, name: 'f[name][1][o]') do expect(page.all('option').map(&:value)).to_not include('_present', '_blank') end within(:select, name: 'f[team][2][o]') do expect(page.all('option').map(&:value)).to include('_present', '_blank') end end it 'supports limiting filter operators' do RailsAdmin.config Player do list do field :name do filter_operators %w[is starts_with _present] end end end visit index_path(model_name: 'player') is_expected.to have_no_css('#filters_box .filter') click_link 'Add filter' click_link 'Name' within(:select, name: /f\[name\]\[\d+\]\[o\]/) do expect(page.all('option').map(&:value)).to eq %w[is starts_with _present] end end it 'does not cause duplication when using browser back' do RailsAdmin.config Player do field :name end visit index_path(model_name: 'player', f: {name: {'1' => {v: 'a'}}}) find(%([href$="/admin/player/export"])).click is_expected.to have_content 'Export Players' page.go_back is_expected.to have_content 'List of Players' expect(all(:css, '#filters_box div.filter').count).to eq 1 end describe 'for boolean field' do before do RailsAdmin.config FieldTest do field :boolean_field end end it 'is filterable with true and false' do visit index_path(model_name: 'field_test') click_link 'Add filter' click_link 'Boolean field' within('#filters_box') do expect(page.all('option').map(&:value)).to include('true', 'false') end end end describe 'for date field' do before do RailsAdmin.config FieldTest do field :date_field end end it 'populates the value selected by the Datetimepicker into the hidden_field' do visit index_path(model_name: 'field_test') click_link 'Add filter' click_link 'Date field' expect(find('[name^="f[date_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank page.execute_script <<-JS document.querySelector('.form-control.date')._flatpickr.setDate('2015-10-08'); JS expect(find('[name^="f[date_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2015-10-08T00:00:00' end end describe 'for datetime field' do before do RailsAdmin.config FieldTest do field :datetime_field end end it 'populates the value selected by the Datetimepicker into the hidden_field' do visit index_path(model_name: 'field_test') click_link 'Add filter' click_link 'Datetime field' expect(find('[name^="f[datetime_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank page.execute_script <<-JS document.querySelector('.form-control.datetime')._flatpickr.setDate('2015-10-08 14:00:00'); JS expect(find('[name^="f[datetime_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2015-10-08T14:00:00' end end describe 'for enum field' do before do RailsAdmin.config Team do field :color, :enum end end it 'supports multiple selection mode' do visit index_path(model_name: 'team') click_link 'Add filter' click_link 'Color' expect(all('#filters_box option').map(&:text)).to include 'white', 'black', 'red', 'green', 'blué' find('.filter .switch-select .fa-plus').click expect(find('#filters_box select')['multiple']).to be true expect(find('#filters_box select')['name']).to match(/\[\]$/) end context 'with the filter pre-populated' do it 'does not break' do visit index_path(model_name: 'team', f: {color: {'1' => {v: 'red'}}}) is_expected.to have_css('.filter select[name^="f[color]"]') expect(find('.filter select[name^="f[color]"]').value).to eq 'red' expect(all('#filters_box option').map(&:text)).to include 'white', 'black', 'red', 'green', 'blué' end end end describe 'for time field', active_record: true do before do RailsAdmin.config FieldTest do field :time_field end end it 'populates the value selected by the Datetimepicker into the hidden_field' do visit index_path(model_name: 'field_test') click_link 'Add filter' click_link 'Time field' expect(find('[name^="f[time_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank page.execute_script <<-JS document.querySelector('.form-control.datetime')._flatpickr.setDate('2000-01-01 14:00:00'); JS expect(find('[name^="f[time_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2000-01-01T14:00:00' end end describe 'for has_one association field' do before do RailsAdmin.config Player do field :draft do searchable :college end end end it 'is filterable' do visit index_path(model_name: 'player') click_link 'Add filter' click_link 'Draft' expect(page).to have_css '[name^="f[draft]"][name$="[o]"]' expect(page).to have_css '[name^="f[draft]"][name$="[v]"]' end end end ================================================ FILE: spec/integration/widgets/filtering_multi_select_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Filtering multi-select widget', type: :request, js: true do subject { page } let(:team) { FactoryBot.create :team } let!(:players) { ['Cory Burns', 'Leonys Martin', 'Matt Garza'].map { |name| FactoryBot.create :player, name: name } } before do RailsAdmin.config Team do field :players end end context 'on create' do before { visit new_path(model_name: 'team') } it 'is initially unset' do expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Cory Burns', 'Matt Garza'] expect(find('.ra-multiselect-selection').text).to be_empty end it 'supports filtering' do find('input.ra-multiselect-search').set('Alex') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_content 'No objects found' find('input.ra-multiselect-search').set('Ma') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_content 'Leonys' expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza'] end it 'sets ids of the selected items' do find('.ra-multiselect-collection option', text: /Cory/).select_option within('.ra-multiselect-center') { click_link 'Add new' } expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns'] expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza'] expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s] end end context 'on update' do let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' } before { visit edit_path(model_name: 'team', id: team.id) } it 'additionally selects items' do expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [player.id.to_s] find('.ra-multiselect-collection option', text: /Cory/).select_option within('.ra-multiselect-center') { click_link 'Add new' } expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s, player.id.to_s] end it 'deselects the current selection' do find('.ra-multiselect-selection option', text: /Elvis/).select_option within('.ra-multiselect-center') { click_link 'Remove' } expect(all('.ra-multiselect-selection option').map(&:text)).to be_empty expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Cory Burns', 'Leonys Martin', 'Matt Garza', 'Elvis Andrus'] expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty end end describe 'Choose all button' do it 'picks all available items' do visit edit_path(model_name: 'team', id: team.id) click_link 'Choose all' expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players.map(&:id).map(&:to_s) end end describe 'Clear all button' do let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' } it 'removes all selected items' do visit edit_path(model_name: 'team', id: team.id) find('.ra-multiselect-collection option', text: /Cory/).select_option within('.ra-multiselect-center') { click_link 'Add new' } expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns', 'Elvis Andrus'] click_link 'Clear all' expect(all(:css, '#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty end end context 'when using remote requests' do before do RailsAdmin.config Team do field(:players) { associated_collection_cache_all false } end visit edit_path(model_name: 'team', id: team.id) end it 'supports filtering' do find('input.ra-multiselect-search').set('Alex') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_content 'No objects found' players[2].update name: 'Adam Rosales' find('input.ra-multiselect-search').set('Ma') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_content 'Leonys' expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin'] end describe 'Choose all button' do it 'does not pick the placeholder for selection' do click_link 'Choose all' expect(page).not_to have_css('#team_player_ids option', visible: false) expect(page).not_to have_css('.ra-multiselect-selection option') end it 'picks all available items' do find('input.ra-multiselect-search').set('Ma') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_css('.ra-multiselect-collection option', text: /Matt/) click_link 'Choose all' expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players[1..2].map(&:id).map(&:to_s) end end end it 'does not cause duplication when using browser back' do visit new_path(model_name: 'team') find(%([href$="/admin/team/export"])).click is_expected.to have_content 'Export Teams' page.go_back is_expected.to have_content 'New Team' expect(all(:css, 'input.ra-multiselect-search').count).to eq 1 end describe 'dynamic scoping' do let!(:team) { FactoryBot.create :team, division: FactoryBot.create(:division) } let(:division) { FactoryBot.create(:division) } let!(:teams) { ['Los Angeles Dodgers', 'Texas Rangers'].map { |name| FactoryBot.create :team, name: name, division: division } } before do RailsAdmin.config Team do field :name field :division end RailsAdmin.config Fan do field :division, :enum do enum { Division.pluck(:name, CI_ORM == :active_record ? :custom_id : :id).to_h } def value nil end def parse_input(params) params.delete :division end end field :teams do dynamically_scope_by :division end end visit new_path(model_name: 'fan') end it 'changes selection candidates based on value of the specified field' do expect(all('#fan_team_ids option', visible: false).map(&:value).filter(&:present?)).to be_empty select division.name, from: 'Division', visible: false find('input.ra-multiselect-search').set('e') page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_content 'Dodgers' expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] end end end ================================================ FILE: spec/integration/widgets/filtering_select_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Filtering select widget', type: :request, js: true do subject { page } let!(:teams) { ['Los Angeles Dodgers', 'Texas Rangers'].map { |name| FactoryBot.create :team, name: name } } let(:player) { FactoryBot.create :player, team: teams[0] } before do RailsAdmin.config Player do field :team field :number end end context 'on create' do before { visit new_path(model_name: 'player') } it 'is initially unset' do expect(find('input.ra-filtering-select-input').value).to be_empty expect(find('#player_team_id', visible: false).value).to be_empty end it 'supports filtering' do find('input.ra-filtering-select-input').set('ge') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] find('input.ra-filtering-select-input').set('Los') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Los Angeles Dodgers'] find('input.ra-filtering-select-input').set('Mets') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['No objects found'] end it 'sets id of the selected item' do find('input.ra-filtering-select-input').set('Tex') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} expect(find('#player_team_id', visible: false).value).to eq teams[1].id.to_s end end context 'on update' do it 'changes the selected value' do visit edit_path(model_name: 'player', id: player.id) expect(find('#player_team_id', visible: false).value).to eq teams[0].id.to_s find('input.ra-filtering-select-input').set('Tex') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} expect(find('#player_team_id', visible: false).value).to eq teams[1].id.to_s end it 'clears the current selection with making the search box empty' do visit edit_path(model_name: 'player', id: player.id) find('input.ra-filtering-select-input').set('') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keyup'))") expect(find('#player_team_id', visible: false).value).to be_empty end it 'clears the current selection with selecting the clear option' do visit edit_path(model_name: 'player', id: player.id) within('.filtering-select') { find('.dropdown-toggle').click } find('a.ui-menu-item-wrapper', text: /Clear/).click expect(find('#player_team_id', visible: false).value).to be_empty end context 'when the field is required' do before do RailsAdmin.config Player do field(:team) { required true } end visit edit_path(model_name: 'player', id: player.id) end it 'does not show the clear option' do within('.filtering-select') { find('.dropdown-toggle').click } is_expected.not_to have_css('a.ui-menu-item-wrapper', text: /Clear/) end end end it 'prevents duplication when using browser back and forward' do player visit index_path(model_name: 'player') find(%([href$="/admin/player/#{player.id}/edit"])).click is_expected.to have_content 'Edit Player' page.go_back is_expected.to have_content 'List of Players' page.go_forward is_expected.to have_content 'Edit Player' expect(all(:css, 'input.ra-filtering-select-input').count).to eq 1 end it 'does not lose options on browser back' do visit edit_path(model_name: 'player', id: player.id) find('.team_field .dropdown-toggle').click find('li.ui-menu-item a', text: /Clear/).click click_link 'Show' is_expected.to have_content 'Details for Player' page.go_back find('.team_field input.ra-filtering-select-input').set('Los') page.execute_script("document.querySelector('.team_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a', text: 'Los Angeles Dodgers') end context 'when using remote requests' do before do RailsAdmin.config Player do field :team do associated_collection_cache_all false end end visit new_path(model_name: 'player') end it 'supports filtering' do find('input.ra-filtering-select-input').set('ge') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] teams[0].update name: 'Cincinnati Reds' find('input.ra-filtering-select-input').set('Red') page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Cincinnati Reds'] end end describe 'dynamic scoping' do let!(:players) { FactoryBot.create_list :player, 2, team: teams[1] } let!(:freelancer) { FactoryBot.create :player, team: nil } context 'with single field' do before do player RailsAdmin.config Draft do field :team field :player do dynamically_scope_by :team end end visit new_path(model_name: 'draft') end it 'changes selection candidates based on value of the specified field' do expect(all('#draft_player_id option', visible: false).map(&:value).filter(&:present?)).to be_empty find('[data-input-for="draft_team_id"] input.ra-filtering-select-input').set('Tex') page.execute_script(%{document.querySelector('[data-input-for="draft_team_id"] input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))}) is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array players.map(&:name) end it 'allows filtering by blank value' do within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array [freelancer.name] end end context 'with multiple fields' do before do player RailsAdmin.config Draft do field :team field :player do dynamically_scope_by [:team, {round: :number}] end field :round end visit new_path(model_name: 'draft', draft: {team_id: teams[1].id}) end it 'changes selection candidates based on value of the specified fields' do fill_in 'draft[round]', with: players[1].number within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array [players[1].name] end end end end ================================================ FILE: spec/integration/widgets/nested_many_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Nested many widget', type: :request, js: true do subject { page } let(:field_test) { FactoryBot.create :field_test } let(:nested_field_tests) { %w[1 2].map { |i| NestedFieldTest.create! title: "title #{i}", field_test: field_test } } before do RailsAdmin.config(FieldTest) do field :nested_field_tests end end it 'adds a new nested item' do visit edit_path(model_name: 'field_test', id: field_test.id) find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click expect(page).to have_selector('.fields.tab-pane.active', visible: true) # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.nested_field_tests.length).to eq(1) end it 'edits a nested item' do nested_field_tests visit edit_path(model_name: 'field_test', id: field_test.id) fill_in 'field_test_nested_field_tests_attributes_0_title', with: 'nested field test title 1 edited', visible: false edited_id = find('#field_test_nested_field_tests_attributes_0_id', visible: false).value # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.nested_field_tests.find(edited_id).title).to eq('nested field test title 1 edited') end it 'deletes a nested item' do nested_field_tests visit edit_path(model_name: 'field_test', id: field_test.id) find('#field_test_nested_field_tests_attributes_0__destroy', visible: false).set('true') # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.reload.nested_field_tests.map(&:id)).to eq [nested_field_tests[1].id] end it 'sets bindings[:object] to nested object', js: false do RailsAdmin.config(NestedFieldTest) do nested do field :title do label do bindings[:object].class.name end end end end nested_field_tests visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('#field_test_nested_field_tests_attributes_0_title_field')).to have_content('NestedFieldTest') end it 'is deactivatable' do visit new_path(model_name: 'field_test') is_expected.to have_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') RailsAdmin.config(FieldTest) do configure :nested_field_tests do nested_form false end end visit new_path(model_name: 'field_test') is_expected.to have_no_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') end it 'is closable after adding a new item' do visit new_path(model_name: 'field_test') within('#field_test_nested_field_tests_attributes_field') do find('.add_nested_fields').click expect(page).to have_selector('.tab-content.collapse.show') expect(page).to have_selector('.nav .nav-link.active', visible: true) expect(page).to have_selector('.fields.tab-pane.active', visible: true) find(':scope > .controls .toggler').click expect(page).not_to have_selector('.tab-content.collapse.show') expect(page).not_to have_selector('.nav .nav-link.active', visible: true) expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) end end it 'closes after removing all items' do visit new_path(model_name: 'field_test') within('#field_test_nested_field_tests_attributes_field') do find('.add_nested_fields').click expect(page).to have_selector('.tab-content.collapse.show') find(':scope > .tab-content > .fields > .remove_nested_fields', visible: false).click expect(page).not_to have_selector('.nav .nav-link.active', visible: true) expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) end end context 'with nested_attributes_options given' do before do allow(FieldTest.nested_attributes_options).to receive(:[]).with(any_args). and_return(allow_destroy: true) end it 'does not show destroy button except for newly created when :allow_destroy is false', js: false do nested_field_tests allow(FieldTest.nested_attributes_options).to receive(:[]).with(:nested_field_tests). and_return(allow_destroy: false) visit edit_path(model_name: 'field_test', id: field_test.id) expect(find('#field_test_nested_field_tests_attributes_0_title').value).to eq('title 1') is_expected.not_to have_selector('form .remove_nested_fields') expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( /]* class="remove_nested_fields"[^>]*>/, ) end end context "when a field which have the same name of nested_in field's" do it "does not hide fields which are not associated with nesting parent field's model" do visit new_path(model_name: 'field_test') is_expected.not_to have_selector('select#field_test_nested_field_tests_attributes_new_nested_field_tests_field_test_id') expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( /]* id="field_test_nested_field_tests_attributes_new_nested_field_tests_another_field_test_id"[^>]*>/, ) end it 'hides fields that are deeply nested with inverse_of' do visit new_path(model_name: 'field_test') expect(page.body).to_not include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_nested_field_test_id_field') expect(page.body).to include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_title') end end context 'when XSS attack is attempted' do it 'does not break on adding a new item' do allow(I18n).to receive(:t).and_call_original expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Nested field test').and_return('') visit edit_path(model_name: 'field_test', id: field_test.id) find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click end it 'does not break on editing an existing item' do NestedFieldTest.create! title: '', field_test: field_test visit edit_path(model_name: 'field_test', id: field_test.id) end end end ================================================ FILE: spec/integration/widgets/nested_one_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Nested one widget', type: :request, js: true do subject { page } let(:field_test) { FactoryBot.create :field_test } before do RailsAdmin.config(FieldTest) do field :comment end end it 'adds an nested item' do visit edit_path(model_name: 'field_test', id: field_test.id) find('#field_test_comment_attributes_field .add_nested_fields').click fill_in 'field_test_comment_attributes_content', with: 'nested comment content' # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.reload.comment.content.strip).to eq('nested comment content') end it 'deletes the nested item' do FactoryBot.create :comment, commentable: field_test visit edit_path(model_name: 'field_test', id: field_test.id) find('.comment_field .toggler').click find('.comment_field .remove_nested_fields', visible: false).click # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.reload.comment).to be nil end it 'is optional' do visit edit_path(model_name: 'field_test', id: field_test.id) click_button 'Save' expect(field_test.reload.comment).to be_nil end it 'is closable after adding a new item' do visit new_path(model_name: 'field_test') within('#field_test_comment_attributes_field') do find('.add_nested_fields').click expect(page).to have_selector('.tab-content.collapse.show') expect(page).to have_selector('.fields.tab-pane.active', visible: true) find(':scope > .controls .toggler').click expect(page).not_to have_selector('.tab-content.collapse.show') expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) end end it 'closes after removing the item' do visit new_path(model_name: 'field_test') within('#field_test_comment_attributes_field') do find('.add_nested_fields').click expect(page).to have_selector('.tab-content.collapse.show') find(':scope > .tab-content > .fields > .remove_nested_fields', visible: false).click expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) end end context 'when XSS attack is attempted' do it 'does not break on adding a new item' do allow(I18n).to receive(:t).and_call_original expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Comment').and_return('') visit edit_path(model_name: 'field_test', id: field_test.id) find('#field_test_comment_attributes_field .add_nested_fields').click end it 'does not break on adding an existing item' do RailsAdmin.config Comment do object_label_method :content end FactoryBot.create :comment, content: '', commentable: field_test visit edit_path(model_name: 'field_test', id: field_test.id) end end context 'when the nested field contains a required field' do before do RailsAdmin.config Comment do configure :content do required true end end end it 'is not affected by form required validation' do FactoryBot.create :comment, commentable: field_test, content: '' visit edit_path(model_name: 'field_test', id: field_test.id) find('.comment_field .toggler').click find('.comment_field .remove_nested_fields', visible: false).click # trigger click via JS, workaround for instability in CI execute_script %(document.querySelector('button[name="_save"]').click()) is_expected.to have_content('Field test successfully updated') expect(field_test.reload.comment).to be nil end end end ================================================ FILE: spec/integration/widgets/remote_form_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'Remote form widget', type: :request, js: true do subject { page } describe 'modal' do it 'supports focusing on sub-modals' do visit new_path(model_name: 'division') click_link 'Add a new League' is_expected.to have_content 'New League' is_expected.not_to have_css '#modal.modal-static' execute_script %($(document.body).append($(''))) find('#sub-modal input').click is_expected.to have_css '#sub-modal input:focus' end end context 'with filtering select widget' do let(:league) { FactoryBot.create :league } let(:division) { FactoryBot.create :division, league: league } before do RailsAdmin.config Division do field :league end RailsAdmin.config League do field :name end end it 'creates an associated record' do visit new_path(model_name: 'division') click_link 'Add a new League' is_expected.to have_content 'New League' fill_in 'Name', with: 'National League' find('#modal .save-action').click expect(find('#division_custom_league_id', visible: false).value).to eq League.first.id.to_s expect(League.pluck(:name)).to eq ['National League'] end it 'updates the associated record' do visit edit_path(model_name: 'division', id: division.id) expect(find('#division_custom_league_id', visible: false).value).to eq league.id.to_s click_link 'Edit this League' is_expected.to have_content "Edit League '#{league.name}'" fill_in 'Name', with: 'National League' find('#modal .save-action').click expect(find('#division_custom_league_id', visible: false).value).to eq league.id.to_s expect(league.reload.name).to eq 'National League' end end context 'with filtering multi-select widget' do let(:leagues) { FactoryBot.create_list :league, 2 } let!(:division) { FactoryBot.create :division, name: 'National League Central', league: leagues[0] } before do RailsAdmin.config League do field :divisions end RailsAdmin.config Division do field :name field :league end end it 'creates an associated record and adds into selection' do visit edit_path(model_name: 'league', id: leagues[1].id) click_link 'Add a new Division' is_expected.to have_content 'New Division' fill_in 'Name', with: 'National League West' find(%(#division_custom_league_id option[value="#{leagues[0].id}"]), visible: false).select_option find('#modal .save-action').click is_expected.to have_css('.ra-multiselect-selection option', text: 'National League West') new_division = Division.where(name: 'National League West').first expect(new_division).not_to be nil expect(find('#league_division_ids', visible: false).value).to eq [new_division.id.to_s] end it 'updates an unselected associated record with leaving it unselected' do visit edit_path(model_name: 'league', id: leagues[1].id) find('.ra-multiselect-collection option', text: division.name).double_click is_expected.to have_content "Edit Division 'National League Central'" fill_in 'Name', with: 'National League East' find('#modal .save-action').click is_expected.to have_css('.ra-multiselect-collection option', text: 'National League East') expect(find('#league_division_ids', visible: false).value).to eq [] expect(division.reload.name).to eq 'National League East' end it 'updates a selected associated record' do visit edit_path(model_name: 'league', id: leagues[0].id) find('.ra-multiselect-selection option', text: division.name).double_click is_expected.to have_content "Edit Division 'National League Central'" fill_in 'Name', with: 'National League East' find('#modal .save-action').click expect(find('#league_division_ids', visible: false).value).to eq [division.id.to_s] expect(division.reload.name).to eq 'National League East' end context 'with inline_edit set to false' do before do RailsAdmin.config League do field :divisions do inline_edit false end end end it 'does not open the modal with double click' do visit edit_path(model_name: 'league', id: leagues[1].id) find('.ra-multiselect-collection option', text: division.name).double_click is_expected.not_to have_content "Edit Division 'National League Central'" end end end context 'with file upload' do before do RailsAdmin.config NestedFieldTest do field :field_test end RailsAdmin.config FieldTest do field :carrierwave_asset end end it 'submits successfully' do visit new_path(model_name: 'nested_field_test') click_link 'Add a new Field test' is_expected.to have_content 'New Field test' attach_file 'Carrierwave asset', file_path('test.jpg') find('#modal .save-action').click is_expected.to have_css('option', text: /FieldTest #/, visible: false) expect(FieldTest.first.carrierwave_asset.file.size).to eq 1575 end end context 'with validation errors' do before do RailsAdmin.config Team do field :players end RailsAdmin.config Player do field :name field :number field :team end end context 'on create' do it 'shows the error messages' do visit new_path(model_name: 'team') click_link 'Add a new Player' is_expected.to have_content 'New Player' find('#player_name').set('on steroids') find('#modal .save-action').click is_expected.to have_css('#modal') is_expected.to have_content 'Player is cheating' is_expected.to have_css '.text-danger', text: 'is not a number' end end context 'on update' do let!(:player) { FactoryBot.create :player, name: 'Cheater' } it 'shows the error messages' do visit new_path(model_name: 'team') find('option', text: 'Cheater').double_click is_expected.to have_content "Edit Player 'Cheater'" find('#player_name').set('Cheater on steroids') find('#player_number').set('') find('#modal .save-action').click is_expected.to have_css('#modal') is_expected.to have_content 'Player is cheating' is_expected.to have_css '.text-danger', text: 'is not a number' end end end end ================================================ FILE: spec/orm/active_record.rb ================================================ # frozen_string_literal: true require 'rails_admin/adapters/active_record' ActiveRecord::Base.connection.data_sources.each do |table| ActiveRecord::Base.connection.drop_table(table) end def silence_stream(stream) old_stream = stream.dup stream.reopen(/mswin|mingw/.match?(RbConfig::CONFIG['host_os']) ? 'NUL:' : '/dev/null') stream.sync = true yield ensure stream.reopen(old_stream) old_stream.close end silence_stream($stdout) do if ActiveRecord::Migrator.respond_to? :migrate ActiveRecord::Migrator.migrate File.expand_path('../dummy_app/db/migrate', __dir__) else ActiveRecord::MigrationContext.new( *([File.expand_path('../dummy_app/db/migrate', __dir__)] + (ActiveRecord::MigrationContext.instance_method(:initialize).arity == 2 ? [ActiveRecord::SchemaMigration] : [])), ).migrate end end class Tableless < ActiveRecord::Base class << self def load_schema # do nothing end def columns @columns ||= [] end def column(name, sql_type = nil, default = nil, null = true) cast_type = connection.send(:lookup_cast_type, sql_type.to_s) define_attribute(name.to_s, cast_type) columns << if ActiveRecord.version > Gem::Version.new('6.0') type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new( sql_type: sql_type.to_s, type: cast_type.type, limit: cast_type.limit, precision: cast_type.precision, scale: cast_type.scale, ) ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, type_metadata, null) else ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, cast_type, sql_type.to_s, null) end end def columns_hash @columns_hash ||= columns.collect { |column| [column.name, column] }.to_h end def column_names @column_names ||= columns.collect(&:name) end def column_defaults @column_defaults ||= columns.collect { |column| [column.name, nil] }.each_with_object({}) do |e, a| a[e[0]] = e[1] a end end def attribute_types @attribute_types ||= columns.collect { |column| [column.name, lookup_attribute_type(column.type)] }.to_h end def table_exists? true end def primary_key 'id' end private def lookup_attribute_type(type) ActiveRecord::Type.lookup({datetime: :time}[type] || type) end end # Override the save method to prevent exceptions. def save(validate = true) validate ? valid? : true end end ## # Column length detection seems to be broken for PostgreSQL. # This is a workaround.. # Refs. https://github.com/rails/rails/commit/b404613c977a5cc31c6748723e903fa5a0709c3b # if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def lookup_cast_type(sql_type) oid = execute("SELECT #{quote(sql_type)}::regtype::oid", 'SCHEMA').first['oid'].to_i type_map.lookup(oid, sql_type) end end end ================================================ FILE: spec/orm/mongoid.rb ================================================ # frozen_string_literal: true require 'rails_admin/adapters/mongoid' Paperclip.logger = Logger.new(nil) class Tableless include Mongoid::Document class << self def column(name, sql_type = 'string', default = nil, _null = true) # ignore length sql_type = sql_type.to_s.sub(/\(.*\)/, '').to_sym field(name, type: {integer: Integer, string: String, text: String, date: Date, datetime: DateTime}[sql_type], default: default) end end end ================================================ FILE: spec/policies.rb ================================================ # frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def show? user.roles.include? :admin end def destroy? false end def history? user.roles.include? :admin end def show_in_app? user.roles.include? :admin end def dashboard? user.roles.include? :admin end def index? false end def new? user.roles.include? :admin end alias create? new? def edit? user.roles.include? :admin end alias update? edit? def export? user.roles.include? :admin end def rails_admin_index? true end end class PlayerPolicy < ApplicationPolicy def new? (user.roles.include?(:manage_player) || (user.roles.include?(:create_player) && (!record.is_a?(Player) || record.suspended))) end alias create? new? def edit? (user.roles.include?(:manage_player) || (user.roles.include?(:update_player) && (!record.is_a?(Player) || !record.retired))) end alias update? edit? def destroy? (user.roles.include? :manage_player) end def index? user.roles.include? :admin end end ================================================ FILE: spec/rails_admin/abstract_model_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::AbstractModel do describe '.all' do it 'returns abstract models for all models' do expect(RailsAdmin::AbstractModel.all.map(&:model)).to include Player, Team end it 'does not pick up a model without table', active_record: true do expect(RailsAdmin::AbstractModel.all.map(&:model)).not_to include WithoutTable end end describe '.new' do context 'on ActiveRecord::NoDatabaseError', active_record: true do before do expect(WithoutTable).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError) end it 'does not raise error and returns nil' do expect(RailsAdmin::AbstractModel.new('WithoutTable')).to eq nil end end context 'on ActiveRecord::ConnectionNotEstablished', active_record: true do before do expect(WithoutTable).to receive(:table_exists?).and_raise(ActiveRecord::ConnectionNotEstablished) end it 'does not raise error and returns nil' do expect(RailsAdmin::AbstractModel.new('WithoutTable')).to eq nil end end end describe '#to_s' do it 'returns model\'s name' do expect(RailsAdmin::AbstractModel.new(Cms::BasicPage).to_s).to eq Cms::BasicPage.to_s end end describe '#to_param' do it 'turns namespaces into prefixes with ~' do expect(RailsAdmin::AbstractModel.new('Cms::BasicPage').to_param).to eq('cms~basic_page') end end describe 'filters' do before do @abstract_model = RailsAdmin::AbstractModel.new('FieldTest') end context 'ActiveModel::ForbiddenAttributesProtection' do it 'is present' do @abstract_model.model.ancestors.collect(&:to_s).include?('ActiveModel::ForbiddenAttributesProtection') end end context 'on ActiveRecord native enum', active_record: true do shared_examples 'filter on enum' do before do %w[S M L].each do |size| FactoryBot.create(:field_test, string_enum_field: size) end %w[small medium large].each do |size| FactoryBot.create(:field_test, integer_enum_field: size) end end let(:model) { RailsAdmin::AbstractModel.new('FieldTest') } let(:filters) { {enum_field => {'1' => {v: filter_value, o: 'is'}}} } subject(:elements) { model.all(filters: filters) } it 'lists elements by value' do expect(elements.count).to eq(expected_elements_count) expect(elements.map(&enum_field.to_sym)).to all(eq(enum_label)) end end context 'when enum is integer enum' do it_behaves_like 'filter on enum' do let(:filter_value) { 0 } let(:enum_field) { 'integer_enum_field' } let(:enum_label) { 'small' } let(:expected_elements_count) { 1 } end end context 'when enum is string enum where label <> value' do it_behaves_like 'filter on enum' do let(:filter_value) { 's' } let(:enum_field) { 'string_enum_field' } let(:enum_label) { 'S' } let(:expected_elements_count) { 1 } end end end context 'on dates' do before do [Date.new(2012, 1, 1), Date.new(2012, 1, 2), Date.new(2012, 1, 3), Date.new(2012, 1, 4)].each do |date| FactoryBot.create(:field_test, date_field: date) end end it 'lists elements within outbound limits' do expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-03'], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-02'], o: 'between'}}}).count).to eq(1) expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-03', ''], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '', '2012-01-02'], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['2012-01-02'], o: 'default'}}}).count).to eq(1) end end context 'on datetimes' do before do I18n.locale = :en FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 1, 23, 59, 59)) FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 2, 0, 0, 0)) FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 3, 23, 59, 59)) FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 4, 0, 0, 0)) end it 'lists elements within outbound limits' do expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-02T00:00:00', '2012-01-03T23:59:59'], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-02T00:00:00', '2012-01-03T12:00:00'], o: 'between'}}}).count).to eq(1) expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-03T12:00:00', ''], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '', '2012-01-02T12:00:00'], o: 'between'}}}).count).to eq(2) expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['2012-01-02T00:00:00'], o: 'default'}}}).count).to eq(1) end end end context 'with Kaminari' do before do @paged = Player.page(1) Kaminari.config.page_method_name = :per_page_kaminari @abstract_model = RailsAdmin::AbstractModel.new('Player') end after do Kaminari.config.page_method_name = :page end it "supports pagination when Kaminari's page_method_name is customized" do expect(Player).to receive(:per_page_kaminari).once.and_return(@paged) @abstract_model.all(sort: PK_COLUMN, page: 1, per: 2) end end describe 'each_associated_children' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') @draft = FactoryBot.build :draft @comments = FactoryBot.build_list :comment, 2 @player = FactoryBot.build :player, draft: @draft, comments: @comments end it 'should return has_one and has_many associations with its children' do @abstract_model.each_associated_children(@player) do |association, children| expect(children).to eq case association.name when :draft [@draft] when :comments @comments end end end end end ================================================ FILE: spec/rails_admin/active_record_extension_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require File.expand_path('../../config/initializers/active_record_extensions', __dir__) RSpec.describe 'ActiveRecord::Base', active_record: true do describe '#safe_send' do it 'only calls #read_attribute once' do @player = Player.new @player.number = 23 original_method = @player.method(:read_attribute) expect(@player).to receive(:read_attribute).exactly(1).times do |*args| original_method.call(*args) end expect(@player.safe_send(:number)).to eq(23) end end end ================================================ FILE: spec/rails_admin/adapters/active_record/association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'timecop' RSpec.describe 'RailsAdmin::Adapters::ActiveRecord::Association', active_record: true do before :all do RailsAdmin::AbstractModel.reset_polymorphic_parents class ARBlog < Tableless has_many :a_r_posts has_many :a_r_comments, as: :commentable belongs_to :librarian, polymorphic: true end class ARPost < Tableless belongs_to :a_r_blog has_and_belongs_to_many :a_r_categories has_many :a_r_comments, as: :commentable end class ARCategory < Tableless has_and_belongs_to_many :a_r_posts, -> { readonly } has_and_belongs_to_many :scoped_posts, ->(category) { where(id: category.id) }, class_name: 'ARPost' belongs_to :librarian, polymorphic: true end class ARUser < Tableless has_one :a_r_profile has_many :a_r_categories, as: :librarian end class ARProfile < Tableless belongs_to :a_r_user has_many :a_r_blogs, as: :librarian end class ARComment < Tableless belongs_to :commentable, polymorphic: true end @blog = RailsAdmin::AbstractModel.new(ARBlog) @post = RailsAdmin::AbstractModel.new(ARPost) @category = RailsAdmin::AbstractModel.new(ARCategory) @user = RailsAdmin::AbstractModel.new(ARUser) @profile = RailsAdmin::AbstractModel.new(ARProfile) @comment = RailsAdmin::AbstractModel.new(ARComment) end after :all do RailsAdmin::AbstractModel.reset_polymorphic_parents end it 'lists associations' do expect(@post.associations.collect { |a| a.name.to_s }).to include(*%w[a_r_blog a_r_categories a_r_comments]) end it 'list associations types in supported [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do # ActiveRecord 4.1 converts has_and_belongs_to_many association to has_many expect((@post.associations + @blog.associations + @user.associations).collect(&:type).uniq.collect(&:to_s)).to include(*%w[belongs_to has_many has_one]) end describe 'belongs_to association' do subject { @post.associations.detect { |a| a.name == :a_r_blog } } it 'returns correct values' do expect(subject.pretty_name).to eq 'A r blog' expect(subject.type).to eq :belongs_to expect(subject.klass).to eq ARBlog expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :a_r_blog_id expect(subject.foreign_type).to be_nil expect(subject.key_accessor).to eq :a_r_blog_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end end describe 'has_many association' do subject { @blog.associations.detect { |a| a.name == :a_r_posts } } it 'returns correct values' do expect(subject.pretty_name).to eq 'A r posts' expect(subject.type).to eq :has_many expect(subject.klass).to eq ARPost expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :ar_blog_id expect(subject.foreign_type).to be_nil expect(subject.key_accessor).to eq :a_r_post_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end end describe 'has_many association' do let(:league) { RailsAdmin::AbstractModel.new(League) } context 'for direct has many' do let(:association) { league.associations.detect { |a| a.name == :divisions } } it 'returns correct values' do expect(association.type).to eq :has_many expect(association.klass).to eq Division expect(association.read_only?).to be_falsey expect(association.foreign_key_nullable?).to be_truthy end end context 'for has many through marked as readonly' do let(:association) { league.associations.detect { |a| a.name == :teams } } it 'returns correct values' do expect(association.type).to eq :has_many expect(association.klass).to eq Team expect(association.read_only?).to be_truthy expect(association.foreign_key_nullable?).to be_truthy end end context 'for has many through multiple associations' do let(:association) { league.associations.detect { |a| a.name == :players } } it 'returns correct values' do expect(association.type).to eq :has_many expect(association.klass).to eq Player expect(association.read_only?).to be_truthy end end end describe 'has_many association with not nullable foreign key' do let(:field_test) { RailsAdmin::AbstractModel.new(FieldTest) } let(:association) { field_test.associations.detect { |a| a.name == :nested_field_tests } } context 'for direct has many' do it 'returns correct values' do expect(association.foreign_key_nullable?).to be_falsey end end context 'when foreign_key is passed as Symbol' do before do class FieldTestWithSymbolForeignKey < FieldTest has_many :nested_field_tests, dependent: :destroy, inverse_of: :field_test, foreign_key: :field_test_id end end let(:field_test) { RailsAdmin::AbstractModel.new(FieldTestWithSymbolForeignKey) } it 'does not break' do expect(association.foreign_key_nullable?).to be_falsey end end end describe 'has_and_belongs_to_many association' do subject { @post.associations.detect { |a| a.name == :a_r_categories } } it 'returns correct values' do expect(subject.pretty_name).to eq 'A r categories' expect(subject.klass).to eq ARCategory expect(subject.primary_key).to eq :id expect(subject.foreign_type).to be_nil expect(subject.foreign_key_nullable?).to be_truthy expect(subject.key_accessor).to eq :a_r_category_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end context 'with a scope given' do subject { @category.associations.detect { |a| a.name == :a_r_posts } } it 'does not break' do expect(subject.read_only?).to be_truthy end end context 'with a scope that receives an argument given' do subject { @category.associations.detect { |a| a.name == :scoped_posts } } it 'ignores the scope' do expect(subject.read_only?).to be_falsey end end end describe 'polymorphic belongs_to association' do before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[ARBlog ARPost ARCategory ARUser ARProfile ARComment]) } subject { @comment.associations.detect { |a| a.name == :commentable } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Commentable' expect(subject.type).to eq :belongs_to expect(subject.klass).to eq [ARBlog, ARPost] expect(subject.primary_key).to be_nil expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_type).to eq :commentable_type expect(subject.key_accessor).to eq :commentable_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_truthy expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end it 'looks up correct inverse model' do expect(@category.associations.detect { |a| a.name == :librarian }.klass).to eq [ARUser] expect(@blog.associations.detect { |a| a.name == :librarian }.klass).to eq [ARProfile] end describe 'on a subclass' do before do class ARReview < ARComment; end allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[ARBlog ARPost ARCategory ARUser ARProfile ARComment ARReview]) end subject { RailsAdmin::AbstractModel.new(ARReview).associations.detect { |a| a.name == :commentable } } it 'returns correct target klasses' do expect(subject.klass).to eq [ARBlog, ARPost] end end end describe 'polymorphic inverse has_many association' do subject { @blog.associations.detect { |a| a.name == :a_r_comments } } it 'returns correct values' do expect(subject.pretty_name).to eq 'A r comments' expect(subject.type).to eq :has_many expect(subject.klass).to eq ARComment expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_type).to be_nil expect(subject.key_accessor).to eq :a_r_comment_ids expect(subject.as).to eq :commentable expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end end end ================================================ FILE: spec/rails_admin/adapters/active_record/object_extension_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::ActiveRecord::ObjectExtension', active_record: true do describe '#assign_attributes' do let(:player) { Player.new } let(:object) { player.extend RailsAdmin::Adapters::ActiveRecord::ObjectExtension } it 'does not cause error with nil' do expect(object.assign_attributes(nil)).to be nil end end end ================================================ FILE: spec/rails_admin/adapters/active_record/property_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'timecop' RSpec.describe 'RailsAdmin::Adapters::ActiveRecord::Property', active_record: true do describe 'string field' do subject { RailsAdmin::AbstractModel.new('Player').properties.detect { |f| f.name == :name } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Name' expect(subject.type).to eq :string expect(subject.length).to eq 100 expect(subject.nullable?).to be_falsey expect(subject.serial?).to be_falsey end end describe 'serialized field' do subject { RailsAdmin::AbstractModel.new('User').properties.detect { |f| f.name == :roles } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Roles' expect(subject.type).to eq :serialized expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey end end describe '#read_only?' do before do class HasReadOnlyColumn < Tableless column :name, :varchar attr_readonly :name end end it 'returns correct values' do expect(RailsAdmin::AbstractModel.new('Player').properties.detect { |f| f.name == :name }).not_to be_read_only expect(RailsAdmin::AbstractModel.new('HasReadOnlyColumn').properties.detect { |f| f.name == :name }).to be_read_only end end end ================================================ FILE: spec/rails_admin/adapters/active_record_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'timecop' RSpec.describe 'RailsAdmin::Adapters::ActiveRecord', active_record: true do let(:activerecord_config) do if ::ActiveRecord::Base.respond_to? :connection_db_config ::ActiveRecord::Base.connection_db_config.configuration_hash else ::ActiveRecord::Base.connection_config end end let(:like) do if %w[postgresql postgis].include? activerecord_config[:adapter] '(field ILIKE ?)' else '(LOWER(field) LIKE ?)' end end let(:not_like) do if %w[postgresql postgis].include? activerecord_config[:adapter] '(field NOT ILIKE ?)' else '(LOWER(field) NOT LIKE ?)' end end def predicates_for(scope) scope.where_clause.instance_variable_get(:@predicates) # .map do |predicate| # if predicate.is_a? Arel::Nodes::BoundSqlLiteral # binds = predicate.positional_binds # predicate.sql_with_placeholders.delete_prefix('(').delete_suffix(')').gsub('?') do |_| # bind = binds.shift # case bind # when Date # "'#{bind.to_fs(:db)}'" # when DateTime, Time # "'#{bind.to_fs(:db)}'" # else # p bind # bind.to_s # end # end # else # predicate # end # end end describe '#associations' do it 'returns Association class' do expect(RailsAdmin::AbstractModel.new(Player).associations.first). to be_a_kind_of RailsAdmin::Adapters::ActiveRecord::Association end end describe '#properties' do it 'returns Property class' do expect(RailsAdmin::AbstractModel.new(Player).properties.first). to be_a_kind_of RailsAdmin::Adapters::ActiveRecord::Property end end describe '#base_class' do it 'returns inheritance base class' do expect(RailsAdmin::AbstractModel.new(Hardball).base_class).to eq Ball end end describe 'data access methods' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Player') } before do @players = FactoryBot.create_list(:player, 3) + [ # Multibyte players FactoryBot.create(:player, name: 'Антоха'), FactoryBot.create(:player, name: 'Петруха'), ] end it '#new returns an ActiveRecord instance' do expect(abstract_model.new).to be_a(ActiveRecord::Base) end it '#get returns an ActiveRecord instance' do expect(abstract_model.get(@players.first.id)).to eq(@players.first) end it '#get returns nil when id does not exist' do expect(abstract_model.get('abc')).to be_nil end it '#get returns an object that can be passed to ActiveJob' do expect { NullJob.perform_later(abstract_model.get(@players.first.id)) }.not_to raise_error end it '#first returns a player' do expect(@players).to include abstract_model.first end describe '#count' do it 'returns count of items' do expect(abstract_model.count).to eq(@players.count) end context 'when default-scoped with select' do before do class PlayerWithDefaultScope < Player self.table_name = 'players' default_scope { select(:id, :name) } end end let(:abstract_model) { RailsAdmin::AbstractModel.new('PlayerWithDefaultScope') } it 'does not break' do expect(abstract_model.count).to eq(@players.count) end end end it '#destroy destroys multiple items' do abstract_model.destroy(@players[0..1]) expect(Player.all).to match_array(@players[2..]) end it '#where returns filtered results' do expect(abstract_model.where(name: @players.first.name)).to eq([@players.first]) end describe '#all' do it 'works without options' do expect(abstract_model.all).to match_array @players end it 'supports eager loading' do expect(abstract_model.all(include: :team).includes_values).to eq([:team]) end it 'supports limiting' do expect(abstract_model.all(limit: 2).size).to eq(2) end it 'supports retrieval by bulk_ids' do expect(abstract_model.all(bulk_ids: @players[0..1].collect(&:id))).to match_array @players[0..1] end it 'supports retrieval by bulk_ids with composite primary keys', composite_primary_keys: true do expect(RailsAdmin::AbstractModel.new(Fanship).all( bulk_ids: %w[1_2 3_4], ).to_sql.tr('`', '"')).to include 'WHERE ("fans_teams"."fan_id" = 1 AND "fans_teams"."team_id" = 2 OR "fans_teams"."fan_id" = 3 AND "fans_teams"."team_id" = 4)' end it 'supports pagination' do expect(abstract_model.all(sort: 'id', page: 2, per: 1)).to eq(@players[-2, 1]) expect(abstract_model.all(sort: 'id', page: 1, per: 2)).to eq(@players[-2, 2].reverse) end it 'supports ordering' do expect(abstract_model.all(sort: 'id', sort_reverse: true)).to eq(@players.sort) expect(abstract_model.all(sort: %w[id name], sort_reverse: true).to_sql.tr('`', '"')).to include('ORDER BY "players"."id" ASC, "players"."name" ASC') expect(abstract_model.all(include: :team, sort: {players: :name, teams: :name}, sort_reverse: true).to_sql.tr('`', '"')).to include('ORDER BY "players"."name" ASC, "teams"."name" ASC') expect { abstract_model.all(sort: 1, sort_reverse: true) }.to raise_error ArgumentError, /Unsupported/ end it 'supports querying' do results = abstract_model.all(query: @players[1].name) expect(results).to eq(@players[1..1]) end it 'supports multibyte querying' do unless activerecord_config[:adapter] == 'sqlite3' results = abstract_model.all(query: @players[4].name) expect(results).to eq(@players[4, 1]) end end it 'supports filtering' do expect(abstract_model.all(filters: {'name' => {'0000' => {o: 'is', v: @players[1].name}}})).to eq(@players[1..1]) end end end describe '#query_scope' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Team') } before do @teams = [{}, {name: 'somewhere foos'}, {manager: 'foo junior'}]. collect { |h| FactoryBot.create :team, h } end it 'makes correct query' do expect(abstract_model.all(query: 'foo')).to match_array @teams[1..2] end context "when field's searchable_columns is empty" do before do RailsAdmin.config do |c| c.model Team do field :players end end end it 'does not break' do expect { abstract_model.all(query: 'foo') }.not_to raise_error end end context 'when parsing is not idempotent' do before do RailsAdmin.config do |c| c.model Team do field :name do def parse_value(value) "#{value}s" end end end end end it 'parses value only once' do expect(abstract_model.all(query: 'foo')).to match_array @teams[1] end end end describe '#filter_scope' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Team') } before do @division = FactoryBot.create :division, name: 'bar division' @teams = [{}, {division: @division}, {name: 'somewhere foos', division: @division}, {name: 'nowhere foos'}]. collect { |h| FactoryBot.create :team, h } end context 'without configuration' do before do allow(Rails.configuration).to receive(:database_configuration) { nil } end after do allow(Rails.configuration).to receive(:database_configuration).and_call_original end it 'does not raise error' do expect { abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'foo'}}}) }.to_not raise_error end end it 'makes correct query' do expect(abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'foo'}}, 'division' => {'0001' => {o: 'like', v: 'bar'}}}, include: :division)).to eq([@teams[2]]) end context 'when parsing is not idempotent' do before do RailsAdmin.config do |c| c.model Team do field :name do def parse_value(value) "some#{value}" end end end end end it 'parses value only once' do expect(abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'where'}}})).to match_array @teams[2] end end context 'when a default_search_operator is set' do before do RailsAdmin.config do |c| c.default_search_operator = 'starts_with' end end it 'only matches on prefix' do # Specified operator is honored and matches expect(abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'where'}}})).to match_array @teams[2..3] # No operator falls back to the default_search_operator(starts_with) and doesn't match NON-PREFIX expect(abstract_model.all(filters: {'name' => {'0000' => {v: 'where'}}})).to be_empty # No operator falls back to the default_search_operator(starts_with) and doesn't match NON-PREFIX expect(abstract_model.all(filters: {'name' => {'0000' => {v: 'somewhere'}}})).to match_array @teams[2] end end end describe '#build_statement' do let(:abstract_model) { RailsAdmin::AbstractModel.new('FieldTest') } def build_statement(type, value, operator) abstract_model.send(:build_statement, :field, type, value, operator) end it "ignores '_discard' operator or value" do [['_discard', ''], ['', '_discard']].each do |value, operator| expect(build_statement(:string, value, operator)).to be_nil end end describe 'string type queries' do it 'supports string type query' do expect(build_statement(:string, '', nil)).to be_nil expect(build_statement(:string, 'foo', 'was')).to be_nil expect(build_statement(:string, 'foo', 'default')).to eq([like, '%foo%']) expect(build_statement(:string, 'foo', 'like')).to eq([like, '%foo%']) expect(build_statement(:string, 'foo', 'not_like')).to eq([not_like, '%foo%']) expect(build_statement(:string, 'foo', 'starts_with')).to eq([like, 'foo%']) expect(build_statement(:string, 'foo', 'ends_with')).to eq([like, '%foo']) expect(build_statement(:string, 'foo', 'is')).to eq(['(field = ?)', 'foo']) end it 'performs case-insensitive searches' do unless %w[postgresql postgis].include?(activerecord_config[:adapter]) expect(build_statement(:string, 'foo', 'default')).to eq([like, '%foo%']) expect(build_statement(:string, 'FOO', 'default')).to eq([like, '%foo%']) end end it 'chooses like statement in per-model basis' do allow(FieldTest.connection).to receive(:adapter_name).and_return('postgresql') expect(build_statement(:string, 'foo', 'default')).to eq(['(field ILIKE ?)', '%foo%']) allow(FieldTest.connection).to receive(:adapter_name).and_return('sqlite3') expect(build_statement(:string, 'foo', 'default')).to eq(['(LOWER(field) LIKE ?)', '%foo%']) end it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(["(field IS NULL OR field = '')"]) end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(["(field IS NOT NULL AND field != '')"]) end end it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(['(field IS NOT NULL)']) end end it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(["(field = '')"]) end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| expect(build_statement(:string, value, operator)).to eq(["(field != '')"]) end end end describe 'boolean type queries' do it 'supports boolean type query' do %w[false f 0].each do |value| expect(build_statement(:boolean, value, nil)).to eq(['(field IS NULL OR field = ?)', false]) end %w[true t 1].each do |value| expect(build_statement(:boolean, value, nil)).to eq(['(field = ?)', true]) end expect(build_statement(:boolean, 'word', nil)).to be_nil end it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NOT NULL)']) end end it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NOT NULL)']) end end it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| expect(build_statement(:boolean, value, operator)).to eq(['(field IS NOT NULL)']) end end end describe 'numeric type queries' do it 'supports integer type query' do expect(build_statement(:integer, '1', nil)).to eq(['(field = ?)', 1]) expect(build_statement(:integer, 'word', nil)).to be_nil expect(build_statement(:integer, '1', 'default')).to eq(['(field = ?)', 1]) expect(build_statement(:integer, 'word', 'default')).to be_nil expect(build_statement(:integer, '1', 'between')).to eq(['(field = ?)', 1]) expect(build_statement(:integer, 'word', 'between')).to be_nil expect(build_statement(:integer, ['6', '', ''], 'default')).to eq(['(field = ?)', 6]) expect(build_statement(:integer, ['7', '10', ''], 'default')).to eq(['(field = ?)', 7]) expect(build_statement(:integer, ['8', '', '20'], 'default')).to eq(['(field = ?)', 8]) expect(build_statement(:integer, %w[9 10 20], 'default')).to eq(['(field = ?)', 9]) end it 'supports integer type range query' do expect(build_statement(:integer, ['', '', ''], 'between')).to be_nil expect(build_statement(:integer, ['2', '', ''], 'between')).to be_nil expect(build_statement(:integer, ['', '3', ''], 'between')).to eq(['(field >= ?)', 3]) expect(build_statement(:integer, ['', '', '5'], 'between')).to eq(['(field <= ?)', 5]) expect(build_statement(:integer, ['', '10', '20'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10, 20]) expect(build_statement(:integer, %w[15 10 20], 'between')).to eq(['(field BETWEEN ? AND ?)', 10, 20]) expect(build_statement(:integer, ['', 'word1', ''], 'between')).to be_nil expect(build_statement(:integer, ['', '', 'word2'], 'between')).to be_nil expect(build_statement(:integer, ['', 'word3', 'word4'], 'between')).to be_nil end it 'supports both decimal and float type queries' do expect(build_statement(:decimal, '1.1', nil)).to eq(['(field = ?)', 1.1]) expect(build_statement(:decimal, 'word', nil)).to be_nil expect(build_statement(:decimal, '1.1', 'default')).to eq(['(field = ?)', 1.1]) expect(build_statement(:decimal, 'word', 'default')).to be_nil expect(build_statement(:decimal, '1.1', 'between')).to eq(['(field = ?)', 1.1]) expect(build_statement(:decimal, 'word', 'between')).to be_nil expect(build_statement(:decimal, ['6.1', '', ''], 'default')).to eq(['(field = ?)', 6.1]) expect(build_statement(:decimal, ['7.1', '10.1', ''], 'default')).to eq(['(field = ?)', 7.1]) expect(build_statement(:decimal, ['8.1', '', '20.1'], 'default')).to eq(['(field = ?)', 8.1]) expect(build_statement(:decimal, ['9.1', '10.1', '20.1'], 'default')).to eq(['(field = ?)', 9.1]) expect(build_statement(:decimal, ['', '', ''], 'between')).to be_nil expect(build_statement(:decimal, ['2.1', '', ''], 'between')).to be_nil expect(build_statement(:decimal, ['', '3.1', ''], 'between')).to eq(['(field >= ?)', 3.1]) expect(build_statement(:decimal, ['', '', '5.1'], 'between')).to eq(['(field <= ?)', 5.1]) expect(build_statement(:decimal, ['', '10.1', '20.1'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10.1, 20.1]) expect(build_statement(:decimal, ['15.1', '10.1', '20.1'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10.1, 20.1]) expect(build_statement(:decimal, ['', 'word1', ''], 'between')).to be_nil expect(build_statement(:decimal, ['', '', 'word2'], 'between')).to be_nil expect(build_statement(:decimal, ['', 'word3', 'word4'], 'between')).to be_nil expect(build_statement(:float, '1.1', nil)).to eq(['(field = ?)', 1.1]) expect(build_statement(:float, 'word', nil)).to be_nil expect(build_statement(:float, '1.1', 'default')).to eq(['(field = ?)', 1.1]) expect(build_statement(:float, 'word', 'default')).to be_nil expect(build_statement(:float, '1.1', 'between')).to eq(['(field = ?)', 1.1]) expect(build_statement(:float, 'word', 'between')).to be_nil expect(build_statement(:float, ['6.1', '', ''], 'default')).to eq(['(field = ?)', 6.1]) expect(build_statement(:float, ['7.1', '10.1', ''], 'default')).to eq(['(field = ?)', 7.1]) expect(build_statement(:float, ['8.1', '', '20.1'], 'default')).to eq(['(field = ?)', 8.1]) expect(build_statement(:float, ['9.1', '10.1', '20.1'], 'default')).to eq(['(field = ?)', 9.1]) expect(build_statement(:float, ['', '', ''], 'between')).to be_nil expect(build_statement(:float, ['2.1', '', ''], 'between')).to be_nil expect(build_statement(:float, ['', '3.1', ''], 'between')).to eq(['(field >= ?)', 3.1]) expect(build_statement(:float, ['', '', '5.1'], 'between')).to eq(['(field <= ?)', 5.1]) expect(build_statement(:float, ['', '10.1', '20.1'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10.1, 20.1]) expect(build_statement(:float, ['15.1', '10.1', '20.1'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10.1, 20.1]) expect(build_statement(:float, ['', 'word1', ''], 'between')).to be_nil expect(build_statement(:float, ['', '', 'word2'], 'between')).to be_nil expect(build_statement(:float, ['', 'word3', 'word4'], 'between')).to be_nil end it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| aggregate_failures do expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end end describe 'date/time type queries' do let(:scope) { FieldTest.all } it 'supports date type query' do expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '2012-02-01', '2012-03-01'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', Date.new(2012, 2, 1), Date.new(2012, 3, 1)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '2012-03-01', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field >= ?)', Date.new(2012, 3, 1)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '', '2012-02-01'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field <= ?)', Date.new(2012, 2, 1)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['2012-02-01'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.new(2012, 2, 1)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'today'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.today))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'yesterday'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.yesterday))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'this_week'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', Date.today.beginning_of_week, Date.today.end_of_week))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'last_week'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', 1.week.ago.to_date.beginning_of_week, 1.week.ago.to_date.end_of_week))) end it 'supports datetime type query' do expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '2012-02-01T12:00:00', '2012-03-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Time.utc(2012, 2, 1, 12), Time.utc(2012, 3, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '2012-03-01T12:00:00', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field >= ?)', Time.utc(2012, 3, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '', '2012-02-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field <= ?)', Time.utc(2012, 2, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['2012-02-01T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field = ?)', Time.utc(2012, 2, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'today'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.today.beginning_of_day, Date.today.end_of_day))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'yesterday'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.yesterday.beginning_of_day, Date.yesterday.end_of_day))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'this_week'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.today.beginning_of_week.beginning_of_day, Date.today.end_of_week.end_of_day))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'last_week'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', 1.week.ago.beginning_of_week, 1.week.ago.end_of_week))) end it 'supports time type query' do expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '2000-01-01T12:00:00', '2000-01-01T14:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field BETWEEN ? AND ?)', Time.utc(2000, 1, 1, 12), Time.utc(2000, 1, 1, 14)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '2000-01-01T14:00:00', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field >= ?)', Time.utc(2000, 1, 1, 14)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '', '2000-01-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field <= ?)', Time.utc(2000, 1, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['2000-01-01T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.time_field = ?)', Time.utc(2000, 1, 1, 12)))) expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['2021-02-03T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.time_field = ?)', Time.utc(2000, 1, 1, 12)))) end end it 'supports enum type query' do expect(build_statement(:enum, '1', nil)).to eq(['(field IN (?))', ['1']]) end describe 'with ActiveRecord native enum' do let(:scope) { FieldTest.all } it 'supports integer enum type query' do expect(predicates_for(abstract_model.send(:filter_scope, scope, 'integer_enum_field' => {'1' => {v: 2, o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.integer_enum_field IN (?))', [2]))) end it 'supports string enum type query' do expect(predicates_for(abstract_model.send(:filter_scope, scope, 'string_enum_field' => {'1' => {v: 'm', o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.string_enum_field IN (?))', ['m']))) end end describe 'uuid type queries' do it 'supports uuid type query' do uuid = SecureRandom.uuid expect(build_statement(:uuid, uuid, nil)).to eq(['(field = ?)', uuid]) end it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) end end it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) end end it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) end end end end describe 'model attribute method' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Player') } it '#scoped returns relation object' do expect(abstract_model.scoped).to be_a_kind_of(ActiveRecord::Relation) end it '#table_name works' do expect(abstract_model.table_name).to eq('players') end end end ================================================ FILE: spec/rails_admin/adapters/mongoid/association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid::Association', mongoid: true do before :all do RailsAdmin::AbstractModel.reset_polymorphic_parents class MongoBlog include Mongoid::Document has_many :mongo_posts has_many :mongo_comments, as: :commentable belongs_to :librarian, polymorphic: true field :mongo_blog_id end class MongoPost include Mongoid::Document belongs_to :mongo_blog has_and_belongs_to_many :mongo_categories has_many :mongo_comments, as: :commentable embeds_one :mongo_note accepts_nested_attributes_for :mongo_note end class MongoCategory include Mongoid::Document has_and_belongs_to_many :mongo_posts belongs_to :librarian, polymorphic: true end class MongoUser include Mongoid::Document has_one :mongo_profile has_many :mongo_categories, as: :librarian embeds_many :mongo_notes accepts_nested_attributes_for :mongo_notes field :name, type: String field :message, type: String field :short_text, type: String validates :short_text, length: {maximum: 255} end class MongoProfile include Mongoid::Document belongs_to :mongo_user has_many :mongo_blogs, as: :librarian end class MongoComment include Mongoid::Document belongs_to :commentable, polymorphic: true end class MongoNote include Mongoid::Document embedded_in :mongo_post embedded_in :mongo_user end @blog = RailsAdmin::AbstractModel.new MongoBlog @post = RailsAdmin::AbstractModel.new MongoPost @category = RailsAdmin::AbstractModel.new MongoCategory @user = RailsAdmin::AbstractModel.new MongoUser @profile = RailsAdmin::AbstractModel.new MongoProfile @comment = RailsAdmin::AbstractModel.new MongoComment end after :all do RailsAdmin::AbstractModel.reset_polymorphic_parents end it 'lists associations' do expect(@post.associations.collect { |a| a.name.to_sym }).to match_array %i[mongo_blog mongo_categories mongo_comments mongo_note] end it 'reads correct and know types in [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do expect((@post.associations + @blog.associations + @user.associations).collect { |a| a.type.to_s }.uniq).to match_array %w[belongs_to has_and_belongs_to_many has_many has_one] end describe 'belongs_to association' do subject { @post.associations.detect { |a| a.name == :mongo_blog } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo blog' expect(subject.type).to eq :belongs_to expect(subject.klass).to eq MongoBlog expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to eq :mongo_blog_id expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to eq :mongo_blog_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end it 'distinguishes foreign_key column' do expect(@post.properties.detect { |f| f.name == :mongo_blog_id }.type).to eq(:bson_object_id) expect(@blog.properties.detect { |f| f.name == :mongo_blog_id }.type).to eq(:string) end end describe 'has_many association' do subject { @blog.associations.detect { |a| a.name == :mongo_posts } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo posts' expect(subject.type).to eq :has_many expect(subject.klass).to eq MongoPost expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to eq :mongo_blog_id expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to eq :mongo_post_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end end describe 'has_and_belongs_to_many association' do subject { @post.associations.detect { |a| a.name == :mongo_categories } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo categories' expect(subject.type).to eq :has_and_belongs_to_many expect(subject.klass).to eq MongoCategory expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to eq :mongo_category_ids expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to eq :mongo_category_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end end describe 'polymorphic belongs_to association' do before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment]) } subject { @comment.associations.detect { |a| a.name == :commentable } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Commentable' expect(subject.type).to eq :belongs_to expect(subject.klass).to eq [MongoBlog, MongoPost] expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to eq :commentable_type expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to eq :commentable_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_truthy expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end describe 'on a subclass' do before do class MongoReview < MongoComment; end allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment MongoReview]) end subject { RailsAdmin::AbstractModel.new(MongoReview).associations.detect { |a| a.name == :commentable } } it 'returns correct target klasses' do expect(subject.klass).to eq [MongoBlog, MongoPost] end end end describe 'polymorphic inverse has_many association' do before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment]) } subject { @blog.associations.detect { |a| a.name == :mongo_comments } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo comments' expect(subject.type).to eq :has_many expect(subject.klass).to eq MongoComment expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to eq :mongo_comment_ids expect(subject.as).to eq :commentable expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end it 'looks up correct inverse model' do expect(@category.associations.detect { |a| a.name == :librarian }.klass).to eq [MongoUser] expect(@blog.associations.detect { |a| a.name == :librarian }.klass).to eq [MongoProfile] end end describe 'embeds_one association' do subject { @post.associations.detect { |a| a.name == :mongo_note } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo note' expect(subject.type).to eq :has_one expect(subject.klass).to eq MongoNote expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to be_nil expect(subject.foreign_key_nullable?).to be_falsey expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil expect(subject.key_accessor).to be_nil expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to eq(allow_destroy: false, update_only: false) end end describe 'embeds_many association' do subject { @user.associations.detect { |a| a.name == :mongo_notes } } it 'returns correct values' do expect(subject.pretty_name).to eq 'Mongo notes' expect(subject.type).to eq :has_many expect(subject.klass).to eq MongoNote expect(subject.primary_key).to eq :_id expect(subject.foreign_key).to be_nil expect(subject.foreign_key_nullable?).to be_falsey expect(subject.foreign_type).to be_nil expect(subject.key_accessor).to be_nil expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to eq(allow_destroy: false, update_only: false) end it 'raises error when embeds_* is used without accepts_nested_attributes_for' do class MongoEmbedsOne include Mongoid::Document embeds_one :mongo_embedded end class MongoEmbedsMany include Mongoid::Document embeds_many :mongo_embeddeds end class MongoEmbedded include Mongoid::Document embedded_in :mongo_embeds_one embedded_in :mongo_embeds_many end class MongoRecursivelyEmbedsOne include Mongoid::Document recursively_embeds_one end class MongoRecursivelyEmbedsMany include Mongoid::Document recursively_embeds_many end expect { RailsAdmin::AbstractModel.new(MongoEmbedsOne).associations.first.nested_options }.to raise_error(RuntimeError, "Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embedded' line to `MongoEmbedsOne' model.\n") expect { RailsAdmin::AbstractModel.new(MongoEmbedsMany).associations.first.nested_options }.to raise_error(RuntimeError, "Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embeddeds' line to `MongoEmbedsMany' model.\n") expect { RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsOne).associations.first.nested_options }.not_to raise_error expect { RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsMany).associations.first.nested_options }.not_to raise_error end it 'works with inherited embeds_many model' do class MongoEmbedsParent include Mongoid::Document embeds_many :mongo_embeddeds accepts_nested_attributes_for :mongo_embeddeds end class MongoEmbedded include Mongoid::Document embedded_in :mongo_embeds_many end class MongoEmbedsChild < MongoEmbedsParent; end expect { RailsAdmin::AbstractModel.new(MongoEmbedsChild).associations }.not_to raise_error end end end ================================================ FILE: spec/rails_admin/adapters/mongoid/object_extension_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid::ObjectExtension', mongoid: true do describe 'has_many association' do let(:players) { FactoryBot.create_list :player, 2 } before do class TeamWithAutoSave < Team has_many :players, inverse_of: :team, autosave: true end end context 'on create' do before do team.player_ids = players.collect(&:id) team.players.each { |player| expect(player).to receive(:save).once.and_call_original } team.save end context 'with autosave: false' do let(:team) { FactoryBot.build(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on save' do expect(team.reload.players).to match_array players end end context 'with autosave: true' do let(:team) { TeamWithAutoSave.new(FactoryBot.attributes_for(:team)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on save' do expect(team.reload.players).to match_array players end end end context 'on update' do let(:team) { FactoryBot.create(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } before do team.player_ids = players.collect(&:id) end context 'with autosave: false' do let(:team) { FactoryBot.create(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on assignment' do expect(team.reload.players).to match_array players end end context 'with autosave: true' do let(:team) { TeamWithAutoSave.create(FactoryBot.attributes_for(:team)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on assignment' do expect(team.reload.players).to match_array players end end end end describe 'has_one association' do let(:draft) { FactoryBot.create(:draft) } before do class PlayerWithAutoSave < Player has_one :draft, inverse_of: :player, autosave: true end end context 'on create' do before do player.draft = draft expect(player.draft._target).to receive(:save).once.and_call_original player.save end context 'with autosave: false' do let(:player) { FactoryBot.build(:player).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on save' do expect(player.reload.draft).to eq draft end end context 'with autosave: true' do let(:player) { PlayerWithAutoSave.new(FactoryBot.attributes_for(:player)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on save' do expect(player.reload.draft).to eq draft end end end context 'on update' do before do player.draft = draft end context 'with autosave: false' do let(:player) { FactoryBot.create(:player).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on assignment' do expect(player.reload.draft).to eq draft end end context 'with autosave: true' do let(:player) { PlayerWithAutoSave.create(FactoryBot.attributes_for(:player)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } it 'persists associated documents changes on assignment' do expect(player.reload.draft).to eq draft end end end end end ================================================ FILE: spec/rails_admin/adapters/mongoid/property_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid::Property', mongoid: true do subject { RailsAdmin::AbstractModel.new(FieldTest).properties.detect { |p| p.name == field } } describe '_id field' do let(:field) { :_id } it 'has correct values' do expect(subject.pretty_name).to eq ' id' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_truthy expect(subject.type).to eq :bson_object_id expect(subject.length).to be_nil end end describe 'array field' do let(:field) { :array_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Array field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :serialized expect(subject.length).to be_nil end end describe 'big decimal field' do let(:field) { :big_decimal_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Big decimal field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :decimal expect(subject.length).to be_nil end end describe 'boolean field' do let(:field) { :boolean_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Boolean field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :boolean expect(subject.length).to be_nil end end describe 'bson object id field' do let(:field) { :bson_object_id_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Bson object id field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :bson_object_id expect(subject.length).to be_nil end end describe 'date field' do let(:field) { :date_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Date field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :date expect(subject.length).to be_nil end end describe 'datetime field' do let(:field) { :datetime_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Datetime field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :datetime expect(subject.length).to be_nil end end describe 'time with zone field' do let(:field) { :time_with_zone_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Time with zone field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :datetime expect(subject.length).to be_nil end end describe 'default field' do let(:field) { :default_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Default field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :string expect(subject.length).to eq 255 end end describe 'float field' do let(:field) { :float_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Float field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :float expect(subject.length).to be_nil end end describe 'hash field' do let(:field) { :hash_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Hash field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :serialized expect(subject.length).to be_nil end end describe 'integer field' do let(:field) { :integer_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Integer field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :integer expect(subject.length).to be_nil end end describe 'object field' do let(:field) { :object_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Object field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :string expect(subject.length).to eq 255 end end describe 'string field' do let(:field) { :string_field } it 'has correct values' do expect(subject.pretty_name).to eq 'String field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :text expect(subject.length).to be_nil end end describe 'symbol field' do let(:field) { :symbol_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Symbol field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :string expect(subject.length).to eq 255 end end describe 'time field' do let(:field) { :time_field } it 'has correct values' do expect(subject.pretty_name).to eq 'Time field' expect(subject.nullable?).to be_truthy expect(subject.serial?).to be_falsey expect(subject.type).to eq :datetime expect(subject.length).to be_nil end end describe 'aliased field' do let(:field) { :aliased_field } it 'has correct values' do expect(subject.name).to eq :aliased_field expect(subject.pretty_name).to eq 'Aliased field' end end describe '#length_validation_lookup' do it 'detects validation length properly' do class LengthValiated include Mongoid::Document field :text, type: String validates :text, length: {maximum: 50} end expect(RailsAdmin::AbstractModel.new('LengthValiated').properties.last.send(:length_validation_lookup)).to eq(50) end it 'does not cause problem with custom validators' do class MyCustomValidator < ActiveModel::Validator def validate(_r); end end class CustomValiated include Mongoid::Document field :text, type: String validates_with MyCustomValidator end expect { RailsAdmin::AbstractModel.new('CustomValiated').properties.last.send(:length_validation_lookup) }.not_to raise_error end end describe '#read_only?' do before do class HasReadOnlyColumn include Mongoid::Document field :name, type: String attr_readonly :name end end it 'returns correct values' do expect(RailsAdmin::AbstractModel.new('Player').properties.detect { |f| f.name == :name }).not_to be_read_only expect(RailsAdmin::AbstractModel.new('HasReadOnlyColumn').properties.detect { |f| f.name == :name }).to be_read_only end end end ================================================ FILE: spec/rails_admin/adapters/mongoid_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid', mongoid: true do describe '#associations' do it 'returns Association class' do expect(RailsAdmin::AbstractModel.new(Player).associations.first). to be_a_kind_of RailsAdmin::Adapters::Mongoid::Association end end describe '#properties' do it 'returns Property class' do expect(RailsAdmin::AbstractModel.new(Player).properties.first). to be_a_kind_of RailsAdmin::Adapters::Mongoid::Property end end describe '#base_class' do it 'returns inheritance base class' do expect(RailsAdmin::AbstractModel.new(Hardball).base_class).to eq Ball end end describe 'data access methods' do before do @players = FactoryBot.create_list(:player, 3) @abstract_model = RailsAdmin::AbstractModel.new('Player') end it '#new returns a Mongoid::Document instance' do expect(@abstract_model.new).to be_a(Mongoid::Document) end it '#get returns a Mongoid::Document instance' do expect(@abstract_model.get(@players.first.id.to_s)).to eq(@players.first) end it '#get returns nil when id does not exist' do expect(@abstract_model.get('4f4f0824dcf2315093000000')).to be_nil end context 'when Mongoid.raise_not_found_error is false' do before { allow(Mongoid).to receive(:raise_not_found_error).and_return(false) } it '#get returns nil when id does not exist' do expect(@abstract_model.get('4f4f0824dcf2315093000000')).to be_nil end end it '#first returns a player' do expect(@players).to include @abstract_model.first end it '#count returns count of items' do expect(@abstract_model.count).to eq(@players.count) end it '#destroy destroys multiple items' do @abstract_model.destroy(@players[0..1]) expect(Player.all).to eq(@players[2..2]) end it '#where returns filtered results' do expect(@abstract_model.where(name: @players.first.name).to_a).to eq([@players.first]) end describe '#all' do it 'works without options' do expect(@abstract_model.all.to_a).to match_array @players end it 'supports eager loading' do expect(@abstract_model.all(include: :team).inclusions.collect(&:class_name)).to eq(['Team']) end it 'supports limiting' do expect(@abstract_model.all(limit: 2).to_a.size).to eq(2) end it 'supports retrieval by bulk_ids' do expect(@abstract_model.all(bulk_ids: @players[0..1].collect { |player| player.id.to_s }).to_a).to match_array @players[0..1] end it 'supports pagination' do expect(@abstract_model.all(sort: 'players._id', page: 2, per: 1).to_a).to eq(@players.sort_by(&:_id)[1..1]) # To prevent RSpec matcher to call Mongoid::Criteria#== method, # (we want to test equality of query result, not of Mongoid criteria) # to_a is added to invoke Mongoid query end it 'supports ordering' do expect(@abstract_model.all(sort: 'players._id', sort_reverse: true).to_a).to eq(@players.sort) expect(@abstract_model.all(sort: 'players._id', sort_reverse: false).to_a).to eq(@players.sort.reverse) end it 'supports querying' do expect(@abstract_model.all(query: @players[1].name)).to eq(@players[1..1]) end it 'supports filtering' do expect(@abstract_model.all(filters: {'name' => {'0000' => {o: 'is', v: @players[1].name}}})).to eq(@players[1..1]) end it 'ignores non-existent field name on filtering' do expect { @abstract_model.all(filters: {'dummy' => {'0000' => {o: 'is', v: @players[1].name}}}) }.not_to raise_error end end end describe 'searching on association' do describe 'whose type is belongs_to' do before do RailsAdmin.config Player do field :team do queryable true end end @players = FactoryBot.create_list(:player, 3) @team = FactoryBot.create :team, name: 'foobar' @team.players << @players[1] @abstract_model = RailsAdmin::AbstractModel.new('Player') end it 'supports querying' do expect(@abstract_model.all(query: 'foobar').to_a).to eq(@players[1..1]) end it 'supports filtering' do expect(@abstract_model.all(filters: {'team' => {'0000' => {o: 'is', v: 'foobar'}}}).to_a).to eq(@players[1..1]) end end describe 'whose type is has_many' do before do RailsAdmin.config Team do field :players do queryable true searchable :name end end @teams = FactoryBot.create_list(:team, 3) @players = [{team: @teams[1]}, {team: @teams[1], name: 'foobar'}, {team: @teams[2]}].collect { |h| FactoryBot.create :player, h } @abstract_model = RailsAdmin::AbstractModel.new('Team') end it 'supports querying' do expect(@abstract_model.all(query: 'foobar').to_a).to eq(@teams[1..1]) end it 'supports filtering' do expect(@abstract_model.all(filters: {'players' => {'0000' => {o: 'is', v: 'foobar'}}}).to_a).to eq(@teams[1..1]) end end describe 'whose type is has_and_belongs_to_many' do before do RailsAdmin.config Team do field :fans do queryable true searchable :name end end @teams = FactoryBot.create_list(:team, 3) @fans = [{}, {name: 'foobar'}, {}].collect { |h| FactoryBot.create :fan, h } @teams[1].fans = [@fans[0], @fans[1]] @teams[2].fans << @fans[2] @abstract_model = RailsAdmin::AbstractModel.new('Team') end it 'supports querying' do expect(@abstract_model.all(query: 'foobar').to_a).to eq(@teams[1..1]) end it 'supports filtering' do expect(@abstract_model.all(filters: {'fans' => {'0000' => {o: 'is', v: 'foobar'}}}).to_a).to eq(@teams[1..1]) end end describe 'whose type is embedded has_many' do before do RailsAdmin.config FieldTest do field :embeds do queryable true searchable :all end end @field_tests = FactoryBot.create_list(:field_test, 3) @field_tests[0].embeds.create name: 'foo' @field_tests[1].embeds.create name: 'bar' @abstract_model = RailsAdmin::AbstractModel.new('FieldTest') end it 'supports querying' do expect(@abstract_model.all(query: 'bar').to_a).to eq(@field_tests[1..1]) end it 'supports filtering' do expect(@abstract_model.all(filters: {'embeds' => {'0000' => {o: 'is', v: 'bar'}}}).to_a).to eq(@field_tests[1..1]) end end end describe '#query_scope' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') @players = [{}, {name: 'Many foos'}, {position: 'foo shortage'}]. collect { |h| FactoryBot.create :player, h } end it 'makes correct query' do expect(@abstract_model.all(query: 'foo').to_a).to match_array @players[1..2] end context 'when parsing is not idempotent' do before do RailsAdmin.config do |c| c.model Player do field :name do def parse_value(value) "#{value}s" end end end end end it 'parses value only once' do expect(@abstract_model.all(query: 'foo')).to match_array @players[1..1] end end end describe '#filter_scope' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') @team = FactoryBot.create :team, name: 'king of bar' @players = [{}, {team: @team}, {name: 'Many foos', team: @team}, {name: 'Great foo'}]. collect { |h| FactoryBot.create :player, h } end it 'makes correct query' do expect(@abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'foo'}}, 'team' => {'0001' => {o: 'like', v: 'bar'}}})).to eq([@players[2]]) end context 'when parsing is not idempotent' do before do RailsAdmin.config do |c| c.model Player do field :name do def parse_value(value) "#{value}s" end end end end end it 'parses value only once' do expect(@abstract_model.all(filters: {'name' => {'0000' => {o: 'like', v: 'foo'}}})).to match_array @players[2] end end end describe '#build_statement' do before do I18n.locale = :en @abstract_model = RailsAdmin::AbstractModel.new('FieldTest') end it "ignores '_discard' operator or value" do [['_discard', ''], ['', '_discard']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to be_nil end end it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: {'$in' => [nil, '']}) end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: {'$nin' => [nil, '']}) end end it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: nil) end end it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: {'$ne' => nil}) end end it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: '') end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| expect(@abstract_model.send(:build_statement, :name, :string, value, operator)).to eq(name: {'$ne' => ''}) end end it 'supports boolean type query' do %w[false f 0].each do |value| expect(@abstract_model.send(:build_statement, :field, :boolean, value, nil)).to eq(field: false) end %w[true t 1].each do |value| expect(@abstract_model.send(:build_statement, :field, :boolean, value, nil)).to eq(field: true) end expect(@abstract_model.send(:build_statement, :field, :boolean, 'word', nil)).to be_nil end it 'supports integer type query' do expect(@abstract_model.send(:build_statement, :field, :integer, '1', nil)).to eq(field: 1) expect(@abstract_model.send(:build_statement, :field, :integer, 'word', nil)).to be_nil end it 'supports integer type range query' do expect(@abstract_model.send(:build_statement, :field, :integer, ['', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['2', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['', '3', ''], 'between')).to eq(field: {'$gte' => 3}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', '', '5'], 'between')).to eq(field: {'$lte' => 5}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', '10', '20'], 'between')).to eq(field: {'$gte' => 10, '$lte' => 20}) expect(@abstract_model.send(:build_statement, :field, :integer, %w[15 10 20], 'between')).to eq(field: {'$gte' => 10, '$lte' => 20}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', 'word1', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['', '', 'word2'], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['', 'word3', 'word4'], 'between')).to be_nil end it 'supports both decimal and float type queries' do expect(@abstract_model.send(:build_statement, :field, :decimal, '1.1', nil)).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :decimal, 'word', nil)).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, '1.1', 'default')).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :decimal, 'word', 'default')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, '1.1', 'between')).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :decimal, 'word', 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, ['6.1', '', ''], 'default')).to eq(field: 6.1) expect(@abstract_model.send(:build_statement, :field, :decimal, ['7.1', '10.1', ''], 'default')).to eq(field: 7.1) expect(@abstract_model.send(:build_statement, :field, :decimal, ['8.1', '', '20.1'], 'default')).to eq(field: 8.1) expect(@abstract_model.send(:build_statement, :field, :decimal, ['9.1', '10.1', '20.1'], 'default')).to eq(field: 9.1) expect(@abstract_model.send(:build_statement, :field, :decimal, ['', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, ['2.1', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, ['', '3.1', ''], 'between')).to eq(field: {'$gte' => 3.1}) expect(@abstract_model.send(:build_statement, :field, :decimal, ['', '', '5.1'], 'between')).to eq(field: {'$lte' => 5.1}) expect(@abstract_model.send(:build_statement, :field, :decimal, ['', '10.1', '20.1'], 'between')).to eq(field: {'$gte' => 10.1, '$lte' => 20.1}) expect(@abstract_model.send(:build_statement, :field, :decimal, ['15.1', '10.1', '20.1'], 'between')).to eq(field: {'$gte' => 10.1, '$lte' => 20.1}) expect(@abstract_model.send(:build_statement, :field, :decimal, ['', 'word1', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, ['', '', 'word2'], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :decimal, ['', 'word3', 'word4'], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, '1.1', nil)).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :float, 'word', nil)).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, '1.1', 'default')).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :float, 'word', 'default')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, '1.1', 'between')).to eq(field: 1.1) expect(@abstract_model.send(:build_statement, :field, :float, 'word', 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, ['6.1', '', ''], 'default')).to eq(field: 6.1) expect(@abstract_model.send(:build_statement, :field, :float, ['7.1', '10.1', ''], 'default')).to eq(field: 7.1) expect(@abstract_model.send(:build_statement, :field, :float, ['8.1', '', '20.1'], 'default')).to eq(field: 8.1) expect(@abstract_model.send(:build_statement, :field, :float, ['9.1', '10.1', '20.1'], 'default')).to eq(field: 9.1) expect(@abstract_model.send(:build_statement, :field, :float, ['', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, ['2.1', '', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, ['', '3.1', ''], 'between')).to eq(field: {'$gte' => 3.1}) expect(@abstract_model.send(:build_statement, :field, :float, ['', '', '5.1'], 'between')).to eq(field: {'$lte' => 5.1}) expect(@abstract_model.send(:build_statement, :field, :float, ['', '10.1', '20.1'], 'between')).to eq(field: {'$gte' => 10.1, '$lte' => 20.1}) expect(@abstract_model.send(:build_statement, :field, :float, ['15.1', '10.1', '20.1'], 'between')).to eq(field: {'$gte' => 10.1, '$lte' => 20.1}) expect(@abstract_model.send(:build_statement, :field, :float, ['', 'word1', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, ['', '', 'word2'], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :float, ['', 'word3', 'word4'], 'between')).to be_nil end it 'supports string type query' do expect(@abstract_model.send(:build_statement, :field, :string, '', nil)).to be_nil expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'was')).to be_nil expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'default')).to eq(field: /foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'like')).to eq(field: /foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'not_like')).to eq(field: /^((?!foo).)*$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'starts_with')).to eq(field: /^foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'ends_with')).to eq(field: /foo$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'is')).to eq(field: 'foo') end it 'supports date type query' do expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-03'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 3)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '2012-01-03', ''], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 3)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '', '2012-01-02'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$lte' => Date.new(2012, 1, 2)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['2012-01-02'], o: 'default'}}).selector).to eq('$and' => [{'date_field' => Date.new(2012, 1, 2)}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'today'}}).selector).to eq('$and' => [{'date_field' => Date.today}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'yesterday'}}).selector).to eq('$and' => [{'date_field' => Date.yesterday}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'this_week'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.today.beginning_of_week, '$lte' => Date.today.end_of_week}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'last_week'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => 1.week.ago.to_date.beginning_of_week, '$lte' => 1.week.ago.to_date.end_of_week}}]) end it 'supports datetime type query' do expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '2012-01-02T12:00:00', '2012-01-03T12:00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 2, 12), '$lte' => Time.zone.local(2012, 1, 3, 12)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '2012-01-03T12:00:00', ''], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 3, 12)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '', '2012-01-02T12:00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$lte' => Time.zone.local(2012, 1, 2, 12)}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['2012-01-02T12:00:00'], o: 'default'}}).selector).to eq('$and' => [{'datetime_field' => Time.zone.local(2012, 1, 2, 12)}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'today'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.today.beginning_of_day, '$lte' => Date.today.end_of_day}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'yesterday'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.yesterday.beginning_of_day, '$lte' => Date.yesterday.end_of_day}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'this_week'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.today.beginning_of_week.beginning_of_day, '$lte' => Date.today.end_of_week.end_of_day}}]) expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'last_week'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => 1.week.ago.to_date.beginning_of_week.beginning_of_day, '$lte' => 1.week.ago.to_date.end_of_week.end_of_day}}]) end it 'supports enum type query' do expect(@abstract_model.send(:build_statement, :field, :enum, '1', nil)).to eq(field: {'$in' => ['1']}) end end describe 'model attribute method' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') end it '#scoped returns relation object' do expect(@abstract_model.scoped).to be_instance_of(Mongoid::Criteria) end it '#table_name works' do expect(@abstract_model.table_name).to eq('players') end end describe 'serialization' do before do @abstract_model = RailsAdmin::AbstractModel.new('FieldTest') @controller = RailsAdmin::MainController.new end it 'accepts array value' do params = HashWithIndifferentAccess.new(array_field: '[1, 3]') @controller.send(:sanitize_params_for!, 'create', @abstract_model.config, params) expect(params[:array_field]).to eq([1, 3]) end it 'accepts hash value' do params = HashWithIndifferentAccess.new(hash_field: '{a: 1, b: 3}') @controller.send(:sanitize_params_for!, 'create', @abstract_model.config, params) expect(params[:hash_field]).to eq('a' => 1, 'b' => 3) end end end ================================================ FILE: spec/rails_admin/config/actions/base_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Actions::Base do describe '#enabled?' do it 'excludes models not referenced in the only array' do RailsAdmin.config do |config| config.actions do index do only [Player, Cms::BasicPage] end end end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player))).to be_enabled expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_nil expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Cms::BasicPage))).to be_enabled end it 'excludes models referenced in the except array' do RailsAdmin.config do |config| config.actions do index do except [Player, Cms::BasicPage] end end end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player))).to be_nil expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_enabled expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Cms::BasicPage))).to be_nil end it 'is always true for a writable model' do RailsAdmin.config do |config| config.actions do index show new edit delete end end %i[index show new edit delete].each do |action| expect(RailsAdmin::Config::Actions.find(action, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player), object: Player.new)).to be_enabled end end it 'is false for write operations of a read-only model' do RailsAdmin.config do |config| config.actions do index show new edit delete end end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment))).to be_enabled expect(RailsAdmin::Config::Actions.find(:show, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_enabled expect(RailsAdmin::Config::Actions.find(:new, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment))).to be_enabled expect(RailsAdmin::Config::Actions.find(:edit, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_nil expect(RailsAdmin::Config::Actions.find(:delete, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_nil end end end ================================================ FILE: spec/rails_admin/config/actions_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Actions do describe 'default' do it 'is as before' do expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index show new edit export delete bulk_delete history_show history_index show_in_app]) end end describe 'find' do it 'finds by custom key' do RailsAdmin.config do |config| config.actions do dashboard do custom_key :custom_dashboard end collection :custom_collection, :index show end end expect(RailsAdmin::Config::Actions.find(:custom_dashboard)).to be_a(RailsAdmin::Config::Actions::Dashboard) expect(RailsAdmin::Config::Actions.find(:custom_collection)).to be_a(RailsAdmin::Config::Actions::Index) expect(RailsAdmin::Config::Actions.find(:show)).to be_a(RailsAdmin::Config::Actions::Show) end it 'returns nil when no action is found by the custom key' do expect(RailsAdmin::Config::Actions.find(:non_existent_action_key)).to be_nil end it 'returns visible action passing binding if controller binding is given, and pass action visible or not if no' do RailsAdmin.config do |config| config.actions do root :custom_root do visible do bindings[:controller] == 'controller' end end end end expect(RailsAdmin::Config::Actions.find(:custom_root)).to be_a(RailsAdmin::Config::Actions::Base) expect(RailsAdmin::Config::Actions.find(:custom_root, controller: 'not_controller')).to be_nil expect(RailsAdmin::Config::Actions.find(:custom_root, controller: 'controller')).to be_a(RailsAdmin::Config::Actions::Base) end it "ignores bindings[:abstract_model] visibility while checking action\'s visibility" do RailsAdmin.config Team do hide end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Comment))).to be_a(RailsAdmin::Config::Actions::Index) # decoy expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_a(RailsAdmin::Config::Actions::Index) end it "checks bindings[:abstract_model] presence while checking action\'s visibility" do RailsAdmin.config do |config| config.excluded_models << Team end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Comment))).to be_a(RailsAdmin::Config::Actions::Index) # decoy expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_nil end end describe 'all' do it 'returns all defined actions' do RailsAdmin.config do |config| config.actions do dashboard index end end expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index]) end it 'restricts by scope' do RailsAdmin.config do |config| config.actions do root :custom_root collection :custom_collection member :custom_member end end expect(RailsAdmin::Config::Actions.all(:root).collect(&:key)).to eq([:custom_root]) expect(RailsAdmin::Config::Actions.all(:collection).collect(&:key)).to eq([:custom_collection]) expect(RailsAdmin::Config::Actions.all(:member).collect(&:key)).to eq([:custom_member]) end it 'returns all visible actions passing binding if controller binding is given, and pass all actions if no' do RailsAdmin.config do |config| config.actions do root :custom_root do visible do bindings[:controller] == 'controller' end end end end expect(RailsAdmin::Config::Actions.all(:root).collect(&:custom_key)).to eq([:custom_root]) expect(RailsAdmin::Config::Actions.all(:root, controller: 'not_controller').collect(&:custom_key)).to eq([]) expect(RailsAdmin::Config::Actions.all(:root, controller: 'controller').collect(&:custom_key)).to eq([:custom_root]) end end describe 'customized through DSL' do it 'adds the one asked' do RailsAdmin.config do |config| config.actions do dashboard index show end end expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index show]) end it 'allows to customize the custom_key when customizing an existing action' do RailsAdmin.config do |config| config.actions do dashboard do custom_key :my_dashboard end end end expect(RailsAdmin::Config::Actions.all.collect(&:custom_key)).to eq([:my_dashboard]) expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:dashboard]) end it 'allows to change the key and the custom_key when subclassing an existing action' do RailsAdmin.config do |config| config.actions do root :my_dashboard_key, :dashboard do custom_key :my_dashboard_custom_key end end end expect(RailsAdmin::Config::Actions.all.collect(&:custom_key)).to eq([:my_dashboard_custom_key]) expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:my_dashboard_key]) expect(RailsAdmin::Config::Actions.all.collect(&:class)).to eq([RailsAdmin::Config::Actions::Dashboard]) end it 'does not add the same custom_key twice' do expect do RailsAdmin.config do |config| config.actions do dashboard dashboard end end end.to raise_error('Action dashboard already exists. Please change its custom key.') expect do RailsAdmin.config do |config| config.actions do index collection :index end end end.to raise_error('Action index already exists. Please change its custom key.') end it 'adds the same key with different custom key' do RailsAdmin.config do |config| config.actions do dashboard dashboard do custom_key :my_dashboard end end end expect(RailsAdmin::Config::Actions.all.collect(&:custom_key)).to eq(%i[dashboard my_dashboard]) expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard dashboard]) end end end ================================================ FILE: spec/rails_admin/config/configurable_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Configurable do class ConfigurableTest include RailsAdmin::Config::Configurable register_instance_option :foo do 'default' end end subject { ConfigurableTest.new } describe 'recursion tracking' do it 'works and use default value' do subject.instance_eval do foo { foo } end expect(subject.foo).to eq 'default' end describe 'with parallel execution' do before do subject.instance_eval do foo do sleep 0.15 'value' end end end it 'ensures thread-safety' do threads = Array.new(2) do |i| Thread.new do sleep i * 0.1 expect(subject.foo).to eq 'value' end end threads.each(&:join) end end end end ================================================ FILE: spec/rails_admin/config/const_load_suppressor_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::ConstLoadSuppressor do describe '.suppressing' do it 'suppresses constant loading' do expect do subject.suppressing { UnknownConstant } end.not_to raise_error end it 'raises the error on recursion' do expect do subject.suppressing do subject.suppressing {} end end.to raise_error(/already suppressed/) end end describe '.allowing' do it 'suspends constant loading suppression' do expect do subject.suppressing do subject.allowing { UnknownConstant } end end.to raise_error NameError end it 'does not break when suppression is disabled' do expect do subject.allowing {} end.not_to raise_error end end end ================================================ FILE: spec/rails_admin/config/fields/association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Association do describe '#pretty_value' do let(:player) { FactoryBot.create(:player, name: '
    ', team: FactoryBot.create(:team)) } let(:field) { RailsAdmin.config('Team').fields.detect { |f| f.name == :players } } let(:view) { ActionView::Base.empty } subject { field.with(object: player.team, view: view).pretty_value } context 'when the link is disabled' do let(:view) { ActionView::Base.empty.tap { |d| allow(d).to receive(:action).and_return(nil) } } it 'does not expose non-HTML-escaped string' do is_expected.to be_html_safe is_expected.to eq '<br />' end end context 'when the value is empty' do let(:team) { FactoryBot.build :team } subject { field.with(object: team, view: view).pretty_value } it "returns '-' to show emptiness" do is_expected.to eq '-' end end end describe '#dynamic_scope_relationships' do let(:player) { FactoryBot.create(:player, team: FactoryBot.create(:team)) } let(:field) { RailsAdmin.config('Draft').fields.detect { |f| f.name == :player } } it 'returns the relationship of fields in this model and in the associated model' do RailsAdmin.config Draft do field :team field :player do dynamically_scope_by :team end end expect(field.dynamic_scope_relationships).to eq({team_id: :team}) end it 'accepts Array' do RailsAdmin.config Draft do field :team field :notes field :player do dynamically_scope_by %i[team notes] end end expect(field.dynamic_scope_relationships).to eq({team_id: :team, notes: :notes}) end it 'accepts Hash' do RailsAdmin.config Draft do field :round field :player do dynamically_scope_by({round: :number}) end end expect(field.dynamic_scope_relationships).to eq({round: :number}) end it 'accepts mixture of Array and Hash' do RailsAdmin.config Draft do field :team field :round field :player do dynamically_scope_by [:team, {round: :number}] end end expect(field.dynamic_scope_relationships).to eq({team_id: :team, round: :number}) end it 'raises error if the field does not exist in this model' do RailsAdmin.config Draft do field :player do dynamically_scope_by :team end end expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' was given for #dynamically_scope_by but not found in 'Draft'" end it 'raises error if the field does not exist in the associated model' do RailsAdmin.config Player do field :name end RailsAdmin.config Draft do field :team field :player do dynamically_scope_by :team end end expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' was given for #dynamically_scope_by but not found in 'Player'" end it 'raises error if the target field is not filterable' do RailsAdmin.config Player do field :name field :team do filterable false end end RailsAdmin.config Draft do field :team field :player do dynamically_scope_by :team end end expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' in 'Player' can't be used for dynamic scoping because it's not filterable" end end describe '#removable?', active_record: true do context 'with non-nullable foreign key' do let(:field) { RailsAdmin.config('FieldTest').fields.detect { |f| f.name == :nested_field_tests } } it 'is false' do expect(field.removable?).to be false end end context 'with nullable foreign key' do let(:field) { RailsAdmin.config('Team').fields.detect { |f| f.name == :players } } it 'is true' do expect(field.removable?).to be true end end context 'with polymorphic has_many' do let(:field) { RailsAdmin.config('Player').fields.detect { |f| f.name == :comments } } it 'does not break' do expect(field.removable?).to be true end end context 'with has_many through' do before do class TeamWithHasManyThrough < Team has_many :drafts has_many :draft_players, through: :drafts, source: :player end end let(:field) { RailsAdmin.config('TeamWithHasManyThrough').fields.detect { |f| f.name == :draft_players } } it 'does not break' do expect(field.removable?).to be true end end end describe '#value' do context 'when using `system` as the association name' do before do class System < Tableless; end class BelongsToSystem < Tableless column :system_id, :integer belongs_to :system end end let(:record) { BelongsToSystem.new(system: System.new) } let(:field) { RailsAdmin.config(BelongsToSystem).fields.detect { |f| f.name == :system } } subject { field.with(object: record).value } it 'does not break' do expect(subject).to be_a_kind_of System end end end end ================================================ FILE: spec/rails_admin/config/fields/base_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Base do describe '#required' do it 'reads the on: :create/:update validate option' do RailsAdmin.config Ball do field 'color' end expect(RailsAdmin.config('Ball').fields.first.with(object: Ball.new)).to be_required expect(RailsAdmin.config('Ball').fields.first.with(object: FactoryBot.create(:ball))).not_to be_required end context 'without validation' do it 'is optional' do # draft.notes is nullable and has no validation field = RailsAdmin.config('Draft').edit.fields.detect { |f| f.name == :notes } expect(field.properties.nullable?).to be_truthy expect(field.required?).to be_falsey end end context 'with presence validation' do it 'is required' do # draft.date is nullable in the schema but has an AR # validates_presence_of validation that makes it required field = RailsAdmin.config('Draft').edit.fields.detect { |f| f.name == :date } expect(field.properties.nullable?).to be_truthy expect(field.required?).to be_truthy end end context 'with numericality validation' do it 'is required' do # draft.round is nullable in the schema but has an AR # validates_numericality_of validation that makes it required field = RailsAdmin.config('Draft').edit.fields.detect { |f| f.name == :round } expect(field.properties.nullable?).to be_truthy expect(field.required?).to be_truthy end end context 'with validation marked as allow_nil or allow_blank' do it 'is optional' do # team.revenue is nullable in the schema but has an AR # validates_numericality_of validation that allows nil field = RailsAdmin.config('Team').edit.fields.detect { |f| f.name == :revenue } expect(field.properties.nullable?).to be_truthy expect(field.required?).to be_falsey # team.founded is nullable in the schema but has an AR # validates_numericality_of validation that allows blank field = RailsAdmin.config('Team').edit.fields.detect { |f| f.name == :founded } expect(field.properties.nullable?).to be_truthy expect(field.required?).to be_falsey end end context 'with conditional validation' do before do class ConditionalValidationTest < Tableless column :foo, :varchar column :bar, :varchar validates :foo, presence: true, if: :persisted? validates :bar, presence: true, unless: :persisted? end end it 'is optional' do expect(RailsAdmin.config('ConditionalValidationTest').fields.detect { |f| f.name == :foo }).not_to be_required expect(RailsAdmin.config('ConditionalValidationTest').fields.detect { |f| f.name == :bar }).not_to be_required end end context 'on a Paperclip installation' do it 'should detect required fields' do expect(RailsAdmin.config('Image').fields.detect { |f| f.name == :file }.with(object: Image.new)).to be_required end end describe 'associations' do before do class RelTest < Tableless column :league_id, :integer column :division_id, :integer, nil, false column :player_id, :integer belongs_to :league, optional: true belongs_to :division, optional: true belongs_to :player, optional: true validates_numericality_of(:player_id, only_integer: true) end @fields = RailsAdmin.config(RelTest).create.fields end describe 'for column with nullable foreign key and no model validations' do it 'is optional' do expect(@fields.detect { |f| f.name == :league }.required?).to be_falsey end end describe 'for column with non-nullable foreign key and no model validations' do it 'is optional' do expect(@fields.detect { |f| f.name == :division }.required?).to be_falsey end end describe 'for column with nullable foreign key and a numericality model validation' do it 'is required' do expect(@fields.detect { |f| f.name == :player }.required?).to be_truthy end end end end describe '#name' do it 'is normalized to Symbol' do RailsAdmin.config Team do field 'name' end expect(RailsAdmin.config('Team').fields.first.name).to eq(:name) end end describe '#children_fields' do POLYMORPHIC_CHILDREN = %i[commentable_id commentable_type].freeze it 'is empty by default' do expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :name }.children_fields).to eq([]) end it 'contains child key for belongs to associations' do expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.children_fields).to eq([:division_id]) end it 'contains child keys for polymorphic belongs to associations' do expect(RailsAdmin.config(Comment).fields.detect { |f| f.name == :commentable }.children_fields).to match_array POLYMORPHIC_CHILDREN end it 'has correct fields when polymorphic_type column comes ahead of polymorphic foreign_key column' do class CommentReversed < Tableless column :commentable_type, :varchar column :commentable_id, :integer belongs_to :commentable, polymorphic: true end expect(RailsAdmin.config(CommentReversed).fields.collect { |f| f.name.to_s }.select { |f| /^comment/ =~ f }).to match_array ['commentable'].concat(POLYMORPHIC_CHILDREN.collect(&:to_s)) end context 'of a Paperclip installation' do it 'is a _file_name field' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :paperclip_asset }.children_fields.include?(:paperclip_asset_file_name)).to be_truthy end it 'is hidden, not filterable' do field = RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :paperclip_asset_file_name } expect(field.hidden?).to be_truthy expect(field.filterable?).to be_falsey end end context 'of a Dragonfly installation' do it 'is a _name field and _uid field' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.children_fields).to eq(%i[dragonfly_asset_name dragonfly_asset_uid]) end end context 'of a Carrierwave installation' do it 'is the parent field itself' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_asset }.children_fields).to eq([:carrierwave_asset]) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_asset }.hidden?).to be_falsey end end context 'of a Carrierwave installation with multiple file support' do it 'is the parent field itself' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_assets }.children_fields).to eq([:carrierwave_assets]) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_assets }.hidden?).to be_falsey end end if defined?(ActiveStorage) context 'of a ActiveStorage installation' do it 'is _attachment and _blob fields' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_asset }.children_fields).to match_array %i[active_storage_asset_attachment active_storage_asset_blob] end it 'is hidden, not filterable' do fields = RailsAdmin.config(FieldTest).fields.select { |f| %i[active_storage_asset_attachment active_storage_asset_blob].include?(f.name) } expect(fields).to all(be_hidden) expect(fields).not_to include(be_filterable) end end context 'of a ActiveStorage installation with multiple file support' do it 'is _attachment and _blob fields' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_assets }.children_fields).to match_array %i[active_storage_assets_attachments active_storage_assets_blobs] end it 'is hidden, not filterable' do fields = RailsAdmin.config(FieldTest).fields.select { |f| %i[active_storage_assets_attachments active_storage_assets_blobs].include?(f.name) } expect(fields).to all(be_hidden) expect(fields).not_to include(be_filterable) end end end if defined?(Shrine) context 'of a Shrine installation' do it 'is the parent field itself' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :shrine_asset }.children_fields).to eq([:shrine_asset_data]) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :shrine_asset }.hidden?).to be_falsey end it 'is not filterable' do fields = RailsAdmin.config(FieldTest).fields.select { |f| [:shrine_asset_data].include?(f.name) } expect(fields).to all(be_hidden) expect(fields).not_to include(be_filterable) end end end end describe '#form_default_value' do it 'is default_value for new records when value is nil' do RailsAdmin.config Team do list do field :name do default_value 'default value' end end end @team = Team.new expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.with(object: @team).form_default_value).to eq('default value') @team.name = 'set value' expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.with(object: @team).form_default_value).to be_nil @team = FactoryBot.create :team @team.name = nil expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.with(object: @team).form_default_value).to be_nil end end describe '#default_value' do it 'is nil by default' do expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.default_value).to be_nil end end describe '#hint' do it 'is user customizable' do RailsAdmin.config Team do list do field :division do hint 'Great Division' end field :name end end expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :division }.hint).to eq('Great Division') # custom expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.hint).to eq('') # default end end describe '#help' do it 'has a default and be user customizable via i18n' do RailsAdmin.config Team do list do field :division field :name end end field_specific_i18n = RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name } expect(field_specific_i18n.help).to eq(I18n.translate('admin.help.team.name')) # custom via locales yml field_no_specific_i18n = RailsAdmin.config('Team').list.fields.detect { |f| f.name == :division } expect(field_no_specific_i18n.help).to eq(field_no_specific_i18n.generic_help) # rails_admin generic fallback end end describe '#css_class' do it 'has a default and be user customizable' do RailsAdmin.config Team do list do field :division do css_class 'custom' end field :name end end expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :division }.css_class).to eq('custom') # custom expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :division }.type_css_class).to eq('belongs_to_association_type') # type css class, non-customizable expect(RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name }.css_class).to eq('name_field') # default end end describe '#associated_collection_cache_all' do it 'defaults to true if associated collection count < 100' do expect(RailsAdmin.config(Team).edit.fields.detect { |f| f.name == :players }.associated_collection_cache_all).to be_truthy end it 'defaults to false if associated collection count >= 100' do @players = Array.new(100) do FactoryBot.create :player end expect(RailsAdmin.config(Team).edit.fields.detect { |f| f.name == :players }.associated_collection_cache_all).to be_falsey end context 'with custom configuration' do before do RailsAdmin.config.default_associated_collection_limit = 5 end it 'defaults to true if associated collection count less than than limit' do @players = Array.new(4) do FactoryBot.create :player end expect(RailsAdmin.config(Team).edit.fields.detect { |f| f.name == :players }.associated_collection_cache_all).to be_truthy end it 'defaults to false if associated collection count >= that limit' do @players = Array.new(5) do FactoryBot.create :player end expect(RailsAdmin.config(Team).edit.fields.detect { |f| f.name == :players }.associated_collection_cache_all).to be_falsey end end end describe '#searchable_columns' do describe 'for belongs_to fields' do it 'finds label method on the opposite side for belongs_to associations by default' do expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.searchable_columns.collect { |c| c[:column] }).to eq(['divisions.name', 'teams.division_id']) end it 'searches on opposite table for belongs_to' do RailsAdmin.config(Team) do field :division do searchable :custom_id end end expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.searchable_columns.collect { |c| c[:column] }).to eq(['divisions.custom_id']) end it 'searches on asked table with model name' do RailsAdmin.config(Team) do field :division do searchable League => :name end end expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.searchable_columns).to eq([{column: 'leagues.name', type: :string}]) end it 'searches on asked table with table name' do RailsAdmin.config(Team) do field :division do searchable leagues: :name end end expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.searchable_columns).to eq([{column: 'leagues.name', type: :string}]) end end describe 'for basic type fields' do it 'uses base table and find correct column type' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :text_field }.searchable_columns).to eq([{column: 'field_tests.text_field', type: :text}]) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :integer_field }.searchable_columns).to eq([{column: 'field_tests.integer_field', type: :integer}]) end it 'is customizable to another field on the same table' do RailsAdmin.config(FieldTest) do field :time_field do searchable :date_field end end expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :time_field }.searchable_columns).to eq([{column: 'field_tests.date_field', type: :date}]) end it 'is customizable to another field on another table with :table_name' do RailsAdmin.config(FieldTest) do field :string_field do searchable nested_field_tests: :title end end expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :string_field }.searchable_columns).to eq([{column: 'nested_field_tests.title', type: :string}]) end it 'is customizable to another field on another model with ModelClass' do RailsAdmin.config(FieldTest) do field :string_field do searchable NestedFieldTest => :title end end expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :string_field }.searchable_columns).to eq([{column: 'nested_field_tests.title', type: :string}]) end end describe 'for mapped fields' do it 'of paperclip should find the underlying column on the base table' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :paperclip_asset }.searchable_columns.collect { |c| c[:column] }).to eq(['field_tests.paperclip_asset_file_name']) end it 'of dragonfly should find the underlying column on the base table' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.searchable_columns.collect { |c| c[:column] }).to eq(['field_tests.dragonfly_asset_name']) end it 'of carrierwave should find the underlying column on the base table' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_asset }.searchable_columns.collect { |c| c[:column] }).to eq(['field_tests.carrierwave_asset']) end end end describe '#searchable and #sortable' do it 'is false if column is virtual, true otherwise' do RailsAdmin.config League do field :virtual_column field :name end @league = FactoryBot.create :league expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :virtual_column }.sortable).to be_falsey expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :virtual_column }.searchable).to be_falsey expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :name }.sortable).to be_truthy expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :name }.searchable).to be_truthy end context 'of a virtual field with children fields' do it 'of paperclip should target the first children field' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :paperclip_asset }.searchable).to eq(:paperclip_asset_file_name) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :paperclip_asset }.sortable).to eq(:paperclip_asset_file_name) end it 'of dragonfly should target the first children field' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.searchable).to eq(:dragonfly_asset_name) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.sortable).to eq(:dragonfly_asset_name) end it 'of carrierwave should target the first children field' do expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_asset }.searchable).to eq(:carrierwave_asset) expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :carrierwave_asset }.sortable).to eq(:carrierwave_asset) end end end describe '#virtual?' do it 'is true if column has no properties, false otherwise' do RailsAdmin.config League do field :virtual_column field :name end @league = FactoryBot.create :league expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :virtual_column }.virtual?).to be_truthy expect(RailsAdmin.config('League').export.fields.detect { |f| f.name == :name }.virtual?).to be_falsey end end describe '#default_search_operator' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Player') } let(:model_config) { RailsAdmin.config(abstract_model) } let(:queryable_fields) { model_config.list.fields.select(&:queryable?) } context 'when no search operator is specified for the field' do it "uses 'default' search operator" do expect(queryable_fields.size).to be >= 1 expect(queryable_fields.first.search_operator).to eq(RailsAdmin::Config.default_search_operator) end it 'uses config.default_search_operator if set' do RailsAdmin.config do |config| config.default_search_operator = 'starts_with' end expect(queryable_fields.size).to be >= 1 expect(queryable_fields.first.search_operator).to eq(RailsAdmin::Config.default_search_operator) end end context 'when search operator is specified for the field' do it 'uses specified search operator' do RailsAdmin.config Player do list do fields do search_operator 'starts_with' end end end expect(queryable_fields.size).to be >= 1 expect(queryable_fields.first.search_operator).to eq('starts_with') end it 'uses specified search operator even if config.default_search_operator set' do RailsAdmin.config do |config| config.default_search_operator = 'starts_with' config.model Player do list do fields do search_operator 'ends_with' end end end end expect(queryable_fields.size).to be >= 1 expect(queryable_fields.first.search_operator).to eq('ends_with') end end end describe '#render' do it 'is configurable' do RailsAdmin.config Team do field :name do render do 'rendered' end end end expect(RailsAdmin.config(Team).field(:name).render).to eq('rendered') end end describe '#active' do it 'is false by default' do expect(RailsAdmin.config(Team).field(:division).active?).to be_falsey end end describe '#visible?' do it 'is false when fields have specific name ' do class FieldVisibilityTest < Tableless column :id, :integer column :_id, :integer column :_type, :varchar column :name, :varchar column :created_at, :timestamp column :updated_at, :timestamp column :deleted_at, :timestamp column :created_on, :timestamp column :updated_on, :timestamp column :deleted_on, :timestamp end expect(RailsAdmin.config(FieldVisibilityTest).base.fields.select(&:visible?).collect(&:name)).to match_array %i[_id created_at created_on deleted_at deleted_on id name updated_at updated_on] expect(RailsAdmin.config(FieldVisibilityTest).list.fields.select(&:visible?).collect(&:name)).to match_array %i[_id created_at created_on deleted_at deleted_on id name updated_at updated_on] expect(RailsAdmin.config(FieldVisibilityTest).edit.fields.select(&:visible?).collect(&:name)).to match_array [:name] expect(RailsAdmin.config(FieldVisibilityTest).show.fields.select(&:visible?).collect(&:name)).to match_array [:name] end end describe '#allowed_methods' do it 'includes method_name' do RailsAdmin.config do |config| config.model Team do field :name end end expect(RailsAdmin.config(Team).field(:name).allowed_methods).to eq [:name] end end describe '#default_filter_operator' do it 'has a default and be user customizable' do RailsAdmin.config Team do list do field :division field :name do default_filter_operator 'is' end end end name_field = RailsAdmin.config('Team').list.fields.detect { |f| f.name == :name } expect(name_field.default_filter_operator).to eq('is') # custom via user specification division_field = RailsAdmin.config('Team').list.fields.detect { |f| f.name == :division } expect(division_field.default_filter_operator).to be nil # rails_admin generic fallback end end describe '#eager_load' do let(:field) { RailsAdmin.config('Team').fields.detect { |f| f.name == :players } } it 'can be set to true' do RailsAdmin.config Team do field :players do eager_load true end end expect(field.eager_load_values).to eq [:players] end it 'can be set to false' do RailsAdmin.config Team do field :players do eager_load false end end expect(field.eager_load_values).to eq [] end it 'can be set to a custom value' do RailsAdmin.config Team do field :players do eager_load [{players: :draft}, :fans] end end expect(field.eager_load_values).to eq [{players: :draft}, :fans] end end end ================================================ FILE: spec/rails_admin/config/fields/types/action_text_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' if defined?(ActionText) RSpec.describe RailsAdmin::Config::Fields::Types::ActionText do it_behaves_like 'a generic field type', :action_text_field it_behaves_like 'a string-like field type', :action_text_field end end ================================================ FILE: spec/rails_admin/config/fields/types/active_record_enum_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::ActiveRecordEnum, active_record: true do it_behaves_like 'a generic field type', :string_enum_field describe '#pretty_value' do context 'when column name is format' do before do class FormatAsEnum < FieldTest if ActiveRecord.gem_version >= Gem::Version.new('7.0') enum :format, {Text: 'txt', Markdown: 'md'} else enum format: {Text: 'txt', Markdown: 'md'} end end end let(:field) do RailsAdmin.config(FormatAsEnum).fields.detect do |f| f.name == :format end.with(object: FormatAsEnum.new(format: 'md')) end it 'does not break' do expect(field.pretty_value).to eq 'Markdown' end end end end ================================================ FILE: spec/rails_admin/config/fields/types/active_storage_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' if defined?(ActiveStorage) RSpec.describe RailsAdmin::Config::Fields::Types::ActiveStorage do it_behaves_like 'a generic field type', :string_field, :active_storage let(:record) { FactoryBot.create :field_test } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :active_storage_asset end.with(object: record) end describe '#thumb_method' do it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do expect(field.thumb_method).to eq(resize_to_limit: [100, 100]) end end describe '#image?' do context 'configured Mime::Types' do before { Mime::Type.register 'image/webp', :webp } after { Mime::Type.unregister :webp } %w[jpg jpeg png gif svg webp].each do |image_type_ext| context "when attachment is a '#{image_type_ext}' file" do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.#{image_type_ext}"} } it 'returns true' do expect(field.image?).to be_truthy end end end end context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'} } it 'returns false' do expect(field.image?).to be_falsy end end context 'when attachment is a PDF file' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'} } before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } it 'returns true' do expect(field.image?).to be_truthy end end end describe '#resource_url' do context 'when calling with thumb = false' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } it 'returns original url' do expect(field.resource_url).not_to match(/representations/) end end context 'when attachment is an image' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } it 'returns variant\'s url' do expect(field.resource_url(true)).to match(/representations/) end end context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'} } it 'returns original url' do expect(field.resource_url(true)).not_to match(/representations/) end end context 'when attachment is a PDF file' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'} } before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } it 'returns variant\'s url' do expect(field.resource_url(true)).to match(/representations/) end end end describe '#value' do context 'when attachment exists' do let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } it 'returns attached object' do expect(field.value).to be_a(ActiveStorage::Attached::One) end end context 'when attachment does not exist' do let(:record) { FactoryBot.create :field_test } it 'returns nil' do expect(field.value).to be_nil end end end describe '#eager_load' do it 'points to associations to be eager-loaded' do expect(field.eager_load).to eq({active_storage_asset_attachment: :blob}) end end describe '#direct' do let(:view) { double } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :active_storage_asset end.with(view: view) end before do allow(view).to receive_message_chain(:main_app, :rails_direct_uploads_url) { 'http://www.example.com/rails/active_storage/direct_uploads' } end context 'when false' do it "doesn't put the direct upload url in html_attributes" do expect(field.html_attributes[:data]&.[](:direct_upload_url)).to be_nil end end context 'when true' do before do RailsAdmin.config FieldTest do field(:active_storage_asset) { direct true } end end it 'puts the direct upload url in html_attributes' do expect(field.html_attributes[:data]&.[](:direct_upload_url)).to eq 'http://www.example.com/rails/active_storage/direct_uploads' end end end describe '#searchable' do it 'is false' do expect(field.searchable).to be false end end describe '#sortable' do it 'is false' do expect(field.sortable).to be false end end end end ================================================ FILE: spec/rails_admin/config/fields/types/belongs_to_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::BelongsToAssociation do it_behaves_like 'a generic field type', :integer_field, :belongs_to_association end ================================================ FILE: spec/rails_admin/config/fields/types/boolean_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Boolean do it_behaves_like 'a generic field type', :boolean_field, :boolean subject do RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :boolean_field end.with(object: test_object) end describe '#pretty_value' do { false => %(), true => %(), nil => %(), }.each do |field_value, expected_result| context "when field value is '#{field_value.inspect}'" do let(:test_object) { FieldTest.new(boolean_field: field_value) } it 'returns the appropriate html result' do expect(subject.pretty_value).to eq(expected_result) end end end end end ================================================ FILE: spec/rails_admin/config/fields/types/bson_object_id_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::BsonObjectId do it_behaves_like 'a generic field type', :string_field, :bson_object_id describe '#parse_value' do let(:bson) { RailsAdmin::Adapters::Mongoid::Bson::OBJECT_ID.new } let(:field) do RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :bson_object_id_field end end before :each do RailsAdmin.config do |config| config.model FieldTest do field :bson_object_id_field, :bson_object_id end end end it 'parse valid bson_object_id', mongoid: true do expect(field.parse_value(bson.to_s)).to eq bson end end end ================================================ FILE: spec/rails_admin/config/fields/types/carrierwave_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Carrierwave do it_behaves_like 'a generic field type', :string_field, :carrierwave describe '#thumb_method' do before do RailsAdmin.config FieldTest do field :carrierwave_asset, :carrierwave end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :carrierwave_asset end.with( object: FieldTest.new(string_field: 'dummy.txt'), view: ApplicationController.new.view_context, ) end it 'auto-detects thumb-like version name' do expect(rails_admin_field.thumb_method).to eq :thumb end end end ================================================ FILE: spec/rails_admin/config/fields/types/citext_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Citext do it_behaves_like 'a generic field type', :string_field it_behaves_like 'a string-like field type', :string_field end ================================================ FILE: spec/rails_admin/config/fields/types/ck_editor_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::CKEditor do it_behaves_like 'a generic field type', :text_field, :ck_editor it_behaves_like 'a string-like field type', :text_field, :ck_editor end ================================================ FILE: spec/rails_admin/config/fields/types/code_mirror_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::CodeMirror do it_behaves_like 'a generic field type', :text_field, :code_mirror it_behaves_like 'a string-like field type', :text_field, :code_mirror end ================================================ FILE: spec/rails_admin/config/fields/types/color_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Color do it_behaves_like 'a generic field type', :string_field, :color it_behaves_like 'a string-like field type', :string_field, :color end ================================================ FILE: spec/rails_admin/config/fields/types/date_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Date do it_behaves_like 'a generic field type', :date_field, :date describe '#formatted_value' do it 'gets object value' do field = RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :date_field end.with(object: FieldTest.new(date_field: DateTime.parse('02/01/2012'))) expect(field.formatted_value).to eq 'January 02, 2012' end it 'gets default value for new objects if value is nil' do RailsAdmin.config(FieldTest) do |_config| field :date_field do default_value DateTime.parse('01/01/2012') end end field = RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :date_field end.with(object: FieldTest.new) expect(field.formatted_value).to eq 'January 01, 2012' end end describe '#parse_input' do let(:field) { RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :date_field } } before :each do @object = FactoryBot.create(:field_test) @time = ::Time.now.getutc end after :each do Time.zone = 'UTC' end it 'reads %B %d, %Y by default' do @object.date_field = field.parse_input(date_field: @time.strftime('%B %d, %Y')) expect(@object.date_field).to eq(::Date.parse(@time.to_s)) end it 'covers a timezone lag even if in UTC+n:00 timezone.' do Time.zone = 'Tokyo' # +09:00 @object.date_field = field.parse_input(date_field: @time.strftime('%B %d, %Y')) expect(@object.date_field).to eq(::Date.parse(@time.to_s)) end it 'has a simple customization option' do RailsAdmin.config FieldTest do field :date_field do date_format do :default end end end @object.date_field = field.parse_input(date_field: @time.strftime('%Y-%m-%d')) expect(@object.date_field).to eq(::Date.parse(@time.to_s)) end it 'has a customization option' do RailsAdmin.config FieldTest do field :date_field do strftime_format '%Y/%m/%d' end end @object.date_field = field.parse_input(date_field: @time.strftime('%Y/%m/%d')) expect(@object.date_field).to eq(::Date.parse(@time.to_s)) end end describe '#default_value' do let(:field) do field = RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :date_field } field.bindings = {object: @object} field end before :each do RailsAdmin.config FieldTest do field :date_field do default_value Date.current end end @object = FactoryBot.create(:field_test) @time = ::Time.now.getutc end it 'should contain the default value' do expect(field.default_value).to eq(Date.current) end it 'should propagate to the field formatted_value when the object is a new record' do object = FactoryBot.build(:field_test) field.bindings = {object: object} expect(field.formatted_value).to eq(Date.current.strftime('%B %d, %Y')) end end end ================================================ FILE: spec/rails_admin/config/fields/types/datetime_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Datetime do it_behaves_like 'a generic field type', :datetime_field, :datetime describe '#formatted_value' do it 'gets object value' do field = RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :datetime_field end.with(object: FieldTest.new(datetime_field: DateTime.parse('02/01/2012'))) expect(field.formatted_value).to eq 'January 02, 2012 00:00' end it 'gets default value for new objects if value is nil' do RailsAdmin.config(FieldTest) do |_config| field :datetime_field do default_value DateTime.parse('01/01/2012') end end field = RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :datetime_field end.with(object: FieldTest.new) expect(field.formatted_value).to eq 'January 01, 2012 00:00' end end describe '#parse_input' do let(:field) { RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :datetime_field } } before :each do @object = FactoryBot.create(:field_test) @time = ::Time.now.getutc end after :each do Time.zone = 'UTC' end it 'is able to read %B %-d, %Y %H:%M' do @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime('%B %-d, %Y %H:%M')) expect(@object.datetime_field.strftime('%Y-%m-%d %H:%M')).to eq(@time.strftime('%Y-%m-%d %H:%M')) end it 'is able to read %a, %d %b %Y %H:%M:%S %z' do RailsAdmin.config FieldTest do field :datetime_field do date_format do :default end end end @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime('%a, %d %b %Y %H:%M:%S %z')) expect(@object.datetime_field.to_formatted_s(:rfc822)).to eq(@time.to_formatted_s(:rfc822)) end it 'has a customization option' do RailsAdmin.config FieldTest do field :datetime_field do strftime_format do '%Y-%m-%d %H:%M:%S' end end end @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime('%Y-%m-%d %H:%M:%S')) expect(@object.datetime_field.to_formatted_s(:rfc822)).to eq(@time.to_formatted_s(:rfc822)) end it 'does round-trip saving properly with non-UTC timezones' do RailsAdmin.config FieldTest do field :datetime_field do date_format do :default end end end Time.zone = 'Vienna' @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: 'Sat, 01 Sep 2012 12:00:00 +0200') expect(@object.datetime_field).to eq(Time.zone.parse('2012-09-01 12:00:00 +02:00')) end it 'changes formats when the locale changes' do french_format = '%A %d %B %Y %H:%M' allow(I18n).to receive(:t).with(:long, scope: %i[time formats], raise: true).and_return(french_format) @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime(french_format)) expect(@object.datetime_field.strftime(french_format)).to eq(@time.strftime(french_format)) american_format = '%B %d, %Y %H:%M' allow(I18n).to receive(:t).with(:long, scope: %i[time formats], raise: true).and_return(american_format) @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime(american_format)) expect(@object.datetime_field.strftime(american_format)).to eq(@time.strftime(american_format)) end end end ================================================ FILE: spec/rails_admin/config/fields/types/decimal_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Decimal do it_behaves_like 'a float-like field type', :float_field end ================================================ FILE: spec/rails_admin/config/fields/types/drangonfly_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Dragonfly do it_behaves_like 'a generic field type', :string_field, :dragonfly let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :dragonfly_asset end.with(object: record) end describe '#image?' do let(:file) { File.open(file_path('test.jpg')) } let(:record) { FactoryBot.create :field_test, dragonfly_asset: file } it 'returns true' do expect(field.image?).to be true end context 'with non-image' do let(:file) { File.open(file_path('test.txt')) } it 'returns false' do expect(field.image?).to be false end end end describe 'with a model which does not extend Dragonfly::Model' do before do class NonDragonflyTest < Tableless column :asset_uid, :varchar end end it 'does not break' do expect { RailsAdmin.config(NonDragonflyTest).fields }.not_to raise_error end end end ================================================ FILE: spec/rails_admin/config/fields/types/enum_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Enum do it_behaves_like 'a generic field type', :string_field, :enum subject { RailsAdmin.config(Team).field(:color) } describe "when object responds to '\#{method}_enum'" do before do allow_any_instance_of(Team).to receive(:color_enum).and_return(%w[blue green red]) RailsAdmin.config Team do edit do field :color end end end it 'auto-detects enumeration' do is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) is_expected.not_to be_multiple expect(subject.with(object: Team.new).enum).to eq %w[blue green red] end end describe "when class responds to '\#{method}_enum'" do before do allow(Team).to receive(:color_enum).and_return(%w[blue green red]) Team.instance_eval do def color_enum %w[blue green red] end end RailsAdmin.config Team do edit do field :color end end end it 'auto-detects enumeration' do is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) expect(subject.with(object: Team.new).enum).to eq %w[blue green red] end end describe 'the enum instance method' do before do Team.class_eval do def color_list %w[blue green red] end end RailsAdmin.config Team do field :color, :enum do enum_method :color_list end end end after do Team.send(:remove_method, :color_list) end it 'allows configuration' do is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) expect(subject.with(object: Team.new).enum).to eq %w[blue green red] end end describe 'the enum class method' do before do Team.instance_eval do def color_list %w[blue green red] end end RailsAdmin.config Team do field :color, :enum do enum_method :color_list end end end after do Team.instance_eval { undef :color_list } end it 'allows configuration' do is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) expect(subject.with(object: Team.new).enum).to eq %w[blue green red] end end describe 'when overriding enum configuration' do before do Team.class_eval do def color_list %w[blue green red] end end RailsAdmin.config Team do field :color, :enum do enum_method :color_list enum do %w[yellow black] end end end end after do Team.send(:remove_method, :color_list) end it 'allows direct listing of enumeration options and override enum method' do is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) expect(subject.with(object: Team.new).enum).to eq %w[yellow black] end end describe 'when serialize is enabled in ActiveRecord model', active_record: true do subject { RailsAdmin.config(TeamWithSerializedEnum).field(:color) } before do class TeamWithSerializedEnum < Team self.table_name = 'teams' if ActiveRecord.gem_version < Gem::Version.new('7.1') serialize :color else serialize :color, coder: JSON end def color_enum %w[blue green red] end end RailsAdmin.config do |c| c.included_models = [TeamWithSerializedEnum] end end it 'makes enumeration multi-selectable' do is_expected.to be_multiple end end describe 'when serialize is enabled in Mongoid model', mongoid: true do before do allow(Team).to receive(:color_enum).and_return(%w[blue green red]) Team.instance_eval do field :color, type: Array end end after do Team.instance_eval do field :color, type: String end end it 'makes enumeration multi-selectable' do is_expected.to be_multiple end end end ================================================ FILE: spec/rails_admin/config/fields/types/file_upload_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::FileUpload do it_behaves_like 'a generic field type', :string_field, :file_upload describe '#allowed_methods' do it 'includes delete_method and cache_method' do RailsAdmin.config do |config| config.model FieldTest do field :carrierwave_asset field :dragonfly_asset field :paperclip_asset do delete_method :delete_paperclip_asset end if defined?(ActiveStorage) field :active_storage_asset do delete_method :remove_active_storage_asset end end if defined?(Shrine) field :shrine_asset do delete_method :remove_shrine_asset cache_method :cached_shrine_asset_data end end end end expect(RailsAdmin.config(FieldTest).field(:carrierwave_asset).allowed_methods.collect(&:to_s)).to eq %w[carrierwave_asset remove_carrierwave_asset carrierwave_asset_cache] expect(RailsAdmin.config(FieldTest).field(:dragonfly_asset).allowed_methods.collect(&:to_s)).to eq %w[dragonfly_asset remove_dragonfly_asset retained_dragonfly_asset] expect(RailsAdmin.config(FieldTest).field(:paperclip_asset).allowed_methods.collect(&:to_s)).to eq %w[paperclip_asset delete_paperclip_asset] expect(RailsAdmin.config(FieldTest).field(:active_storage_asset).allowed_methods.collect(&:to_s)).to eq %w[active_storage_asset remove_active_storage_asset] if defined?(ActiveStorage) expect(RailsAdmin.config(FieldTest).field(:shrine_asset).allowed_methods.collect(&:to_s)).to eq %w[shrine_asset remove_shrine_asset cached_shrine_asset_data] if defined?(Shrine) end end describe '#html_attributes' do context 'when the field is required and value is already set' do before do RailsAdmin.config FieldTest do field :string_field, :file_upload do required true end end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with(object: FieldTest.new(string_field: 'dummy.jpg')) end it 'does not have a required attribute' do expect(rails_admin_field.html_attributes[:required]).to be_falsy end end end describe '#pretty_value' do context 'when the field is not image' do before do RailsAdmin.config FieldTest do field :string_field, :file_upload do def resource_url 'http://example.com/dummy.txt' end end end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with( object: FieldTest.new(string_field: 'dummy.txt'), view: ApplicationController.new.view_context, ) end it 'uses filename as link text' do expect(Nokogiri::HTML(rails_admin_field.pretty_value).text).to eq 'dummy.txt' end end end describe '#image?' do let(:filename) { 'dummy.txt' } let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with( object: FieldTest.new(string_field: filename), view: ApplicationController.new.view_context, ) end before do RailsAdmin.config FieldTest do field :string_field, :file_upload do def resource_url "http://example.com/#{value}" end end end end context 'when the file is not an image' do let(:filename) { 'dummy.txt' } it 'returns false' do expect(rails_admin_field.image?).to be false end end context 'when the file is an image' do let(:filename) { 'dummy.jpg' } it 'returns true' do expect(rails_admin_field.image?).to be true end end context 'when the file is an image but suffixed with a query string' do let(:filename) { 'dummy.jpg?foo=bar' } it 'returns true' do expect(rails_admin_field.image?).to be true end end context "when the filename can't be represented as a valid URI" do let(:filename) { 'du mmy.jpg' } it 'returns false' do expect(rails_admin_field.image?).to be false end end end end ================================================ FILE: spec/rails_admin/config/fields/types/float_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Float do it_behaves_like 'a float-like field type', :float_field end ================================================ FILE: spec/rails_admin/config/fields/types/froala_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Froala do it_behaves_like 'a generic field type', :text_field, :froala it_behaves_like 'a string-like field type', :text_field, :froala end ================================================ FILE: spec/rails_admin/config/fields/types/has_and_belongs_to_many_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasAndBelongsToManyAssociation do it_behaves_like 'a generic field type', :integer_field, :has_and_belongs_to_many_association end ================================================ FILE: spec/rails_admin/config/fields/types/has_many_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasManyAssociation do it_behaves_like 'a generic field type', :integer_field, :has_many_association end ================================================ FILE: spec/rails_admin/config/fields/types/has_one_association_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasOneAssociation do it_behaves_like 'a generic field type', :integer_field, :has_one_association end ================================================ FILE: spec/rails_admin/config/fields/types/hidden_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Hidden do it_behaves_like 'a generic field type', :integer_field, :hidden it_behaves_like 'a string-like field type', :string_field, :hidden end ================================================ FILE: spec/rails_admin/config/fields/types/inet_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Inet do it_behaves_like 'a generic field type', :string_field, :inet end ================================================ FILE: spec/rails_admin/config/fields/types/integer_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Integer do it_behaves_like 'a generic field type', :integer_field, :integer end ================================================ FILE: spec/rails_admin/config/fields/types/json_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Json do let(:field) { RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :json_field } } let(:object) { FieldTest.new } let(:bindings) do { object: object, view: ApplicationController.new.view_context, } end describe '#formatted_value' do before do RailsAdmin.config do |config| config.model FieldTest do field :json_field, :json end end end it 'returns correct value for empty json' do allow(object).to receive(:json_field) { {} } actual = field.with(bindings).formatted_value expect(actual).to match(/{\n*}/) end it 'retuns correct value' do allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).formatted_value expected = [ '{', ' "sample_key": "sample_value"', '}', ].join("\n") expect(actual).to eq(expected) end end describe '#pretty_value' do before do RailsAdmin.config do |config| config.model FieldTest do field :json_field, :json end end end it 'retuns correct value' do allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).pretty_value expected = [ '
    {',
            '  "sample_key": "sample_value"',
            '}
    ', ].join("\n") expect(actual).to eq(expected) end end describe '#export_value' do before do RailsAdmin.config do |config| config.model FieldTest do field :json_field, :json end end end it 'returns correct value for empty json' do allow(object).to receive(:json_field) { {} } actual = field.with(bindings).export_value expect(actual).to match(/{\n*}/) end it 'returns correct value' do allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).export_value expected = [ '{', ' "sample_key": "sample_value"', '}', ].join("\n") expect(actual).to eq(expected) end end describe '#parse_input' do before :each do RailsAdmin.config do |config| config.model FieldTest do field :json_field, :json end end end it 'parse valid json string' do data = {string: 'string', integer: 1, array: [1, 2, 3], object: {bla: 'foo'}}.as_json expect(field.parse_input(json_field: data.to_json)).to eq data end it 'raise JSON::ParserError with invalid json string' do expect { field.parse_input(json_field: '{{') }.to raise_error(JSON::ParserError) end end describe 'aliasing' do before :each do RailsAdmin.config do |config| config.model FieldTest do field :json_field, :jsonb end end end it 'allows use of :jsonb fieldtype' do expect(field.class).to eq RailsAdmin::Config::Fields::Types::Json end end it_behaves_like 'a generic field type', :text, :json end ================================================ FILE: spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' if defined?(ActiveStorage) RSpec.describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage do it_behaves_like 'a generic field type', :string_field, :multiple_active_storage let(:record) { FactoryBot.create :field_test } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :active_storage_assets end.with( object: record, view: ApplicationController.new.view_context, ) end describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage::ActiveStorageAttachment do describe '#thumb_method' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } subject { field.attachments[0] } it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do expect(subject.thumb_method).to eq(resize_to_limit: [100, 100]) end end describe '#pretty_value' do subject { field.pretty_value } context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } it 'uses filename as link text' do expect(Nokogiri::HTML(subject).text).to eq 'test.txt' end end context 'when the field is an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } it 'shows thumbnail image with a link' do expect(Nokogiri::HTML(subject).css('img').attribute('src').value).to match(%r{rails/active_storage/representations}) expect(Nokogiri::HTML(subject).css('a').attribute('href').value).to match(%r{rails/active_storage/blobs}) end end end describe '#image?' do context 'when attachment is an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } it 'returns true' do expect(field.attachments[0].image?).to be_truthy end end context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } it 'returns false' do expect(field.attachments[0].image?).to be_falsy end end context 'when attachment is a PDF file' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'}] } before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } it 'returns true' do expect(field.attachments[0].image?).to be_truthy end end end describe '#resource_url' do context 'when calling with thumb = false' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } it 'returns original url' do expect(field.attachments[0].resource_url).not_to match(/representations/) end end context 'when attachment is an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } it 'returns variant\'s url' do expect(field.attachments[0].resource_url(true)).to match(/representations/) end end context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } it 'returns original url' do expect(field.attachments[0].resource_url(true)).not_to match(/representations/) end end context 'when attachment is a PDF file' do let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'}] } before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } it 'returns variant\'s url' do expect(field.attachments[0].resource_url(true)).to match(/representations/) end end end end describe '#eager_load' do it 'points to associations to be eager-loaded' do expect(field.eager_load).to eq({active_storage_assets_attachments: :blob}) end end describe '#direct' do let(:view) { double } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :active_storage_assets end.with(view: view) end before do allow(view).to receive_message_chain(:main_app, :rails_direct_uploads_url) { 'http://www.example.com/rails/active_storage/direct_uploads' } end context 'when false' do it "doesn't put the direct upload url in html_attributes" do expect(field.html_attributes[:data]&.[](:direct_upload_url)).to be_nil end end context 'when true' do before do RailsAdmin.config FieldTest do field(:active_storage_assets) { direct true } end end it 'puts the direct upload url in html_attributes' do expect(field.html_attributes[:data]&.[](:direct_upload_url)).to eq 'http://www.example.com/rails/active_storage/direct_uploads' end end end describe '#searchable' do it 'is false' do expect(field.searchable).to be false end end describe '#sortable' do it 'is false' do expect(field.sortable).to be false end end end end ================================================ FILE: spec/rails_admin/config/fields/types/multiple_carrierwave_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'base64' RSpec.describe RailsAdmin::Config::Fields::Types::MultipleCarrierwave do it_behaves_like 'a generic field type', :string_field, :multiple_carrierwave describe '#thumb_method' do before do RailsAdmin.config FieldTest do field :carrierwave_assets, :multiple_carrierwave end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :carrierwave_assets end.with( object: FieldTest.new(carrierwave_assets: [File.open(file_path('test.jpg'))]), view: ApplicationController.new.view_context, ) end it 'auto-detects thumb-like version name' do expect(rails_admin_field.attachments.map(&:thumb_method)).to eq [:thumb] end end describe '#delete_value', active_record: true do before do RailsAdmin.config FieldTest do field :carrierwave_assets, :multiple_carrierwave end end let :file do CarrierWave::SanitizedFile.new( tempfile: StringIO.new(Base64.decode64('R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=')), filename: 'dummy.gif', ) end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :carrierwave_assets end.with( object: FieldTest.create(carrierwave_assets: [file]), view: ApplicationController.new.view_context, ) end it 'does not use file.identifier, which is not available for Fog files' do expect_any_instance_of(CarrierWave::SanitizedFile).not_to receive :identifier expect(rails_admin_field.attachments.map(&:delete_value)).to eq ['dummy.gif'] end end end ================================================ FILE: spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::MultipleFileUpload do it_behaves_like 'a generic field type', :string_field, :multiple_file_upload describe '#allowed_methods' do it 'includes delete_method and cache_method' do RailsAdmin.config do |config| config.model FieldTest do field :carrierwave_assets, :multiple_carrierwave if defined?(ActiveStorage) field :active_storage_assets, :multiple_active_storage do delete_method :remove_active_storage_assets end end end end expect(RailsAdmin.config(FieldTest).field(:carrierwave_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w[carrierwave_assets] expect(RailsAdmin.config(FieldTest).field(:active_storage_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w[active_storage_assets remove_active_storage_assets] if defined?(ActiveStorage) end end describe '#html_attributes' do context 'when the field is required and value is already set' do before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload do required true end end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with(object: FieldTest.new(string_field: 'dummy.jpg')) end it 'does not have a required attribute' do expect(rails_admin_field.html_attributes[:required]).to be_falsy end end end describe '#pretty_value' do before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload do attachment do thumb_method 'thumb' def resource_url(thumb = false) if thumb "http://example.com/#{thumb}/#{value}" else "http://example.com/#{value}" end end end end end end let(:filename) { '' } let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with( object: FieldTest.new(string_field: filename), view: ApplicationController.new.view_context, ) end context 'when the field is not an image' do let(:filename) { 'dummy.txt' } it 'uses filename as link text' do expect(Nokogiri::HTML(rails_admin_field.pretty_value).text).to eq 'dummy.txt' end end context 'when the field is an image' do let(:filename) { 'dummy.jpg' } subject { Nokogiri::HTML(rails_admin_field.pretty_value) } it 'shows thumbnail image with a link' do expect(subject.css('img').attribute('src').value).to eq 'http://example.com/thumb/dummy.jpg' expect(subject.css('a').attribute('href').value).to eq 'http://example.com/dummy.jpg' end end end describe '#attachment' do before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload do attachment do delete_value 'something' def resource_url(_thumb = false) "http://example.com/#{value}" end end def value ['foo.jpg'] end end end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with( view: ApplicationController.new.view_context, ) end it 'enables configuration' do expect(rails_admin_field.attachments.map(&:delete_value)).to eq ['something'] expect(rails_admin_field.attachments.map(&:resource_url)).to eq ['http://example.com/foo.jpg'] expect(rails_admin_field.pretty_value).to match(%r{src="http://example.com/foo.jpg"}) end end describe '#attachments' do before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload end end let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end end it 'wraps value with Array()' do expect(rails_admin_field.with(object: FieldTest.new(string_field: nil)).attachments).to eq [] expect(rails_admin_field.with(object: FieldTest.new(string_field: 'dummy.txt')).attachments.map(&:value)).to eq ['dummy.txt'] end end describe '#image?' do let(:filename) { 'dummy.txt' } let :rails_admin_field do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with( object: FieldTest.new(string_field: filename), view: ApplicationController.new.view_context, ) end before do RailsAdmin.config FieldTest do field :string_field, :multiple_file_upload do attachment do def resource_url "http://example.com/#{value}" end end end end end context 'when the file is not an image' do let(:filename) { 'dummy.txt' } it 'returns false' do expect(rails_admin_field.attachments.first.image?).to be false end end context 'when the file is an image' do let(:filename) { 'dummy.jpg' } it 'returns true' do expect(rails_admin_field.attachments.first.image?).to be true end end context 'when the file is an image but suffixed with a query string' do let(:filename) { 'dummy.jpg?foo=bar' } it 'returns true' do expect(rails_admin_field.attachments.first.image?).to be true end end context "when the filename can't be represented as a valid URI" do let(:filename) { 'du mmy.jpg' } it 'returns false' do expect(rails_admin_field.attachments.first.image?).to be false end end end end ================================================ FILE: spec/rails_admin/config/fields/types/numeric_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Numeric do it_behaves_like 'a generic field type', :integer_field, :integer subject do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :integer_field end.with(object: FieldTest.new) end describe '#view_helper' do it "uses the 'number' type input tag" do expect(subject.view_helper).to eq(:number_field) end end end ================================================ FILE: spec/rails_admin/config/fields/types/paperclip_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Paperclip do it_behaves_like 'a generic field type', :string_field, :paperclip context 'when a *_file_name field exists but not declared as has_attached_file' do before do class PaperclipTest < Tableless column :some_file_name, :varchar end RailsAdmin.config.included_models = [PaperclipTest] end it 'does not break' do expect { RailsAdmin.config(PaperclipTest).fields }.not_to raise_error end end end ================================================ FILE: spec/rails_admin/config/fields/types/password_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Password do it_behaves_like 'a generic field type', :string_field, :password describe '#parse_input' do let(:field) do RailsAdmin.config(User).fields.detect do |f| f.name == :password end end context 'if password is not present' do let(:nil_params) { {password: nil} } let(:blank_params) { {password: ''} } it 'cleans nil' do field.parse_input(nil_params) expect(nil_params).to eq({}) field.parse_input(blank_params) expect(blank_params).to eq({}) end end context 'if password is present' do let(:params) { {password: 'aaa'} } it 'keeps the value' do field.parse_input(params) expect(params).to eq(password: 'aaa') end end end end ================================================ FILE: spec/rails_admin/config/fields/types/serialized_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Serialized do it_behaves_like 'a generic field type', :text_field, :serialized end ================================================ FILE: spec/rails_admin/config/fields/types/shrine_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' if defined?(Shrine) RSpec.describe RailsAdmin::Config::Fields::Types::Shrine do context 'when asset is an image with versions' do let(:record) { FactoryBot.create :field_test, shrine_versioning_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :shrine_versioning_asset end.with(object: record) end before do if Gem.loaded_specs['shrine'].version >= Gem::Version.create('3') if record.shrine_versioning_asset record.shrine_versioning_asset_derivatives! record.save end else skip end end describe '#image?' do it 'returns true' do expect(field.image?).to be_truthy end end describe '#link_name' do it 'returns filename' do expect(field.link_name).to eq('test.jpg') end end describe '#value' do context 'when attachment exists' do it 'returns attached object' do expect(field.value).to be_a(ShrineVersioningUploader::UploadedFile) end end context 'when attachment does not exist' do let(:record) { FactoryBot.create :field_test } it 'returns nil' do expect(field.value).to be_nil end end end describe '#thumb_method' do it 'returns :thumb' do expect(field.thumb_method).to eq(:thumb) end end describe '#resource_url' do context 'when calling without thumb' do it 'returns original url' do expect(field.resource_url).to_not match(/thumb-/) end end context 'when calling with thumb' do it 'returns thumb url' do expect(field.resource_url(field.thumb_method)).to match(/thumb-/) end end end end context 'when asset without versions' do let(:record) { FactoryBot.create :field_test } let(:field) do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :shrine_asset end.with(object: record) end describe '#image?' do context 'when attachment is an image' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } it 'returns true' do expect(field.image?).to be_truthy end end context 'when attachment is not an image' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } it 'returns false' do expect(field.image?).to be_falsy end end end describe '#value' do context 'when attachment exists' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } it 'returns attached object' do expect(field.value).to be_a(ShrineUploader::UploadedFile) end end context 'when attachment does not exist' do it 'returns nil' do expect(field.value).to be_nil end end end describe '#thumb_method' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } it 'returns nil' do expect(field.thumb_method).to eq(nil) end end describe '#resource_url' do context 'when calling without thumb' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } it 'returns url' do expect(field.resource_url).not_to be_nil end end context 'when calling with thumb' do let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } it 'returns url' do expect(field.resource_url(field.thumb_method)).not_to be_nil end end context 'when attachment does not exist' do it 'returns nil' do expect(field.resource_url).to be_nil end end end end end end ================================================ FILE: spec/rails_admin/config/fields/types/simple_mde_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::SimpleMDE do it_behaves_like 'a generic field type', :text_field, :simple_mde it_behaves_like 'a string-like field type', :text_field, :simple_mde end ================================================ FILE: spec/rails_admin/config/fields/types/string_like_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::StringLike do describe '#treat_empty_as_nil?', active_record: true do context 'with a nullable field' do subject do RailsAdmin.config('Team').fields.detect do |f| f.name == :name end.with(object: Team.new) end it 'is true' do expect(subject.treat_empty_as_nil?).to be true end end context 'with a non-nullable field' do subject do RailsAdmin.config('Team').fields.detect do |f| f.name == :manager end.with(object: Team.new) end it 'is false' do expect(subject.treat_empty_as_nil?).to be false end end end describe '#parse_input' do subject do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == :string_field end.with(object: FieldTest.new) end context 'with treat_empty_as_nil being true' do before do RailsAdmin.config FieldTest do field :string_field do treat_empty_as_nil true end end end context 'when value is empty' do let(:params) { {string_field: ''} } it 'makes the value nil' do subject.parse_input(params) expect(params.key?(:string_field)).to be true expect(params[:string_field]).to be nil end end context 'when value does not exist in params' do let(:params) { {} } it 'does not touch params' do subject.parse_input(params) expect(params.key?(:string_field)).to be false end end end context 'with treat_empty_as_nil being false' do before do RailsAdmin.config FieldTest do field :string_field do treat_empty_as_nil false end end end let(:params) { {string_field: ''} } it 'keeps the value untouched' do subject.parse_input(params) expect(params.key?(:string_field)).to be true expect(params[:string_field]).to eq '' end end end end ================================================ FILE: spec/rails_admin/config/fields/types/string_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::String do describe '#html_attributes' do before :each do RailsAdmin.config Ball do field 'color', :string end end let(:string_field) do RailsAdmin.config('Ball').fields.detect do |f| f.name == :color end.with(object: Ball.new) end it 'should contain a size attribute' do expect(string_field.html_attributes[:size]).to be_present end it 'should not contain a size attribute valorized with 0' do expect(string_field.html_attributes[:size]).to_not be_zero end end it_behaves_like 'a generic field type', :string_field it_behaves_like 'a string-like field type', :string_field end ================================================ FILE: spec/rails_admin/config/fields/types/text_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Text do it_behaves_like 'a generic field type', :text_field it_behaves_like 'a string-like field type', :text_field end ================================================ FILE: spec/rails_admin/config/fields/types/time_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Time, active_record: true do it_behaves_like 'a generic field type', :time_field, :time describe '#parse_input' do let(:field) do RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :time_field end end before do RailsAdmin.config(FieldTest) do field :time_field, :time end end before :each do @object = FactoryBot.create(:field_test) @time = ::Time.new(2000, 1, 1, 3, 45) end after :each do Time.zone = 'UTC' end it 'reads %H:%M' do @object.time_field = field.parse_input(time_field: @time.strftime('%H:%M')) expect(@object.time_field.strftime('%H:%M')).to eq(@time.strftime('%H:%M')) end it 'interprets time value as local time when timezone is specified' do Time.zone = 'Eastern Time (US & Canada)' # -05:00 @object.time_field = field.parse_input(time_field: '2000-01-01T03:45:00') expect(@object.time_field.strftime('%H:%M')).to eq('03:45') end context 'with a custom strftime_format' do let(:field) do RailsAdmin.config(FieldTest).fields.detect do |f| f.name == :time_field end end before do RailsAdmin.config(FieldTest) do field :time_field, :time do strftime_format '%I:%M %p' end end end it 'has a customization option' do @object.time_field = field.parse_input(time_field: @time.strftime('%I:%M %p')) expect(@object.time_field.strftime('%H:%M')).to eq(@time.strftime('%H:%M')) end end end end ================================================ FILE: spec/rails_admin/config/fields/types/timestamp_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Timestamp, active_record: true do it_behaves_like 'a generic field type', :timestamp_field, :timestamp describe '#parse_input' do before :each do @object = FactoryBot.create(:field_test) @time = ::Time.now.getutc @field = RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :timestamp_field } end after :each do Time.zone = 'UTC' end it 'reads %B %d, %Y %H:%M' do @object.timestamp_field = @field.parse_input(timestamp_field: @time.strftime('%B %d, %Y %H:%M')) expect(@object.timestamp_field.strftime('%Y-%m-%d %H:%M')).to eq(@time.strftime('%Y-%m-%d %H:%M')) end end end ================================================ FILE: spec/rails_admin/config/fields/types/uuid_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Uuid do let(:uuid) { SecureRandom.uuid } let(:object) { FactoryBot.create(:field_test) } let(:field) { RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :uuid_field } } before do RailsAdmin.config do |config| config.model FieldTest do field :uuid_field, :uuid end end allow(object).to receive(:uuid_field).and_return uuid field.bindings = {object: object} end it 'field is a Uuid fieldtype' do expect(field.class).to eq RailsAdmin::Config::Fields::Types::Uuid end it 'handles uuid string' do expect(field.value).to eq uuid end it_behaves_like 'a generic field type', :string_field, :uuid end ================================================ FILE: spec/rails_admin/config/fields/types/wysihtml5_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Wysihtml5 do it_behaves_like 'a generic field type', :text_field, :wysihtml5 it_behaves_like 'a string-like field type', :text_field, :wysihtml5 end ================================================ FILE: spec/rails_admin/config/fields_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields, mongoid: true do describe '.factory for self.referentials belongs_to' do it 'associates belongs_to _id foreign_key to a belongs_to association' do class MongoTree include Mongoid::Document has_many :children, class_name: name, foreign_key: :parent_id belongs_to :parent, class_name: name end expect(RailsAdmin.config(MongoTree).fields.detect { |f| f.name == :parent }.type).to eq :belongs_to_association end end end ================================================ FILE: spec/rails_admin/config/has_description_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::HasDescription do it 'shows description message when added through the DSL' do RailsAdmin.config do |config| config.model Team do desc 'Description of Team model' end end expect(RailsAdmin.config(Team).description).to eq('Description of Team model') end end ================================================ FILE: spec/rails_admin/config/has_fields_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::HasFields do it 'shows hidden fields when added through the DSL' do expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division_id }).not_to be_visible RailsAdmin.config do |config| config.model Team do field :division_id end end expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :division_id }).to be_visible end it 'does not set visibility for fields with bindings' do RailsAdmin.config do |config| config.model Team do field :division do visible do bindings[:controller].current_user.email == 'test@email.com' end end end end expect { RailsAdmin.config(Team).fields.detect { |f| f.name == :division } }.not_to raise_error expect { RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.visible? }.to raise_error(/undefined method [`']\[\]' for nil/) end it 'assigns properties to new one on overriding existing field' do RailsAdmin.config do |config| config.model Team do field :players, :has_and_belongs_to_many_association end end expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :players }.properties).not_to be_nil end describe '#configure' do it 'does not change the order of existing fields, if some field types of them are changed' do original_fields_order = RailsAdmin.config(Team).fields.map(&:name) RailsAdmin.config do |config| config.model Team do configure :players, :enum do enum { [] } end configure :revenue, :integer end end expect(RailsAdmin.config(Team).fields.map(&:name)).to eql(original_fields_order) end it 'allows passing multiple fields to share the same configuration' do target_field_names = %i[players revenue] original_config = RailsAdmin.config(Team).fields.select { |field| target_field_names.include?(field.name) } original_config.each { |field| expect(field).to be_visible } RailsAdmin.config do |config| config.model Team do configure target_field_names do visible false end end end updated_config = RailsAdmin.config(Team).fields.select { |field| target_field_names.include?(field.name) } updated_config.each { |field| expect(field).to_not be_visible } end end describe '#_fields' do let(:config) { RailsAdmin.config(Team) } before do RailsAdmin.config(Team) do field :id field :wins, :boolean end end it "does not cause FrozenError by changing exiting field's type" do # Reference the fields for readonly config.edit.send(:_fields, true) RailsAdmin.config(Team) do field :wins, :integer end expect(config.fields.map(&:name)).to match_array %i[id wins] end end end ================================================ FILE: spec/rails_admin/config/lazy_model_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::LazyModel do subject { RailsAdmin::Config::LazyModel.new(:Team, &block) } let(:block) { proc { register_instance_option('parameter') } } # an arbitrary instance method we can spy on describe '#initialize' do it "doesn't evaluate the block immediately" do expect_any_instance_of(RailsAdmin::Config::Model).not_to receive(:register_instance_option) subject end it 'evaluates block when reading' do expect_any_instance_of(RailsAdmin::Config::Model).to receive(:register_instance_option).with('parameter') subject.groups # an arbitrary instance method on RailsAdmin::Config::Model to wake up lazy_model end it 'evaluates config block only once' do expect_any_instance_of(RailsAdmin::Config::Model).to receive(:register_instance_option).once.with('parameter') subject.groups subject.groups end end describe '#add_deferred_block' do let(:another_block) { proc { register_instance_option('parameter2') } } it "doesn't evaluate the block immediately" do expect_any_instance_of(RailsAdmin::Config::Model).not_to receive(:register_instance_option).with('parameter2') subject.add_deferred_block(&another_block) end it 'evaluates the block immediately after initialization' do subject.target expect_any_instance_of(RailsAdmin::Config::Model).to receive(:register_instance_option).with('parameter2') subject.add_deferred_block(&another_block) end end context 'when a method is defined in Kernel' do before do Kernel.module_eval do def weight 42 end end end after do Kernel.module_eval do undef weight end end it 'proxies calls for the method to @object' do expect(subject.weight).to eq 0 end end end ================================================ FILE: spec/rails_admin/config/model_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Model do describe '#excluded?' do before do RailsAdmin.config do |config| config.included_models = [Comment] end end it 'returns false when included, true otherwise' do allow(RailsAdmin::AbstractModel).to receive(:all).and_call_original player_config = RailsAdmin.config(Player) expect(player_config.excluded?).to be_truthy expect(RailsAdmin::AbstractModel).to have_received(:all).once # Calling a second time uses the cached value. expect(player_config.excluded?).to be_truthy expect(RailsAdmin::AbstractModel).to have_received(:all).once comment_config = RailsAdmin.config(Comment) expect(comment_config.excluded?).to be_falsey expect(RailsAdmin::AbstractModel).to have_received(:all).twice # Calling a second time uses the cached value. expect(comment_config.excluded?).to be_falsey expect(RailsAdmin::AbstractModel).to have_received(:all).twice end end describe '#object_label' do before do RailsAdmin.config(Comment) do object_label_method :content end end it 'sends object_label_method to binding[:object]' do c = Comment.new(content: 'test') expect(RailsAdmin.config(Comment).with(object: c).object_label).to eq('test') end context 'when the object_label_method is blank' do it 'uses the rails admin default' do c = Comment.create(content: '', id: '1') expect(RailsAdmin.config(Comment).with(object: c).object_label).to eq('Comment #1') end end end describe '#object_label_method' do it 'is first of Config.label_methods if found as a column on model, or :rails_admin_default_object_label_method' do expect(RailsAdmin.config(Comment).object_label_method).to eq(:rails_admin_default_object_label_method) expect(RailsAdmin.config(Division).object_label_method).to eq(:name) end end describe '#label' do it 'is pretty' do expect(RailsAdmin.config(Comment).label).to eq('Comment') end end describe '#label_plural' do it 'is pretty' do expect(RailsAdmin.config(Comment).label_plural).to eq('Comments') end context 'when using i18n as label source', skip_mongoid: true do around do |example| I18n.config.available_locales = I18n.config.available_locales + [:xx] I18n.backend.class.send(:include, I18n::Backend::Pluralization) I18n.backend.store_translations :xx, activerecord: { models: { comment: { one: 'one', two: 'two', other: 'other' }, }, } I18n.locale = :xx example.run I18n.locale = :en I18n.config.available_locales = I18n.config.available_locales - [:xx] end context 'and the locale uses a specific pluralization rule' do before do I18n.backend.store_translations :xx, i18n: { plural: { rule: ->(count) do case count when 0 :zero when 1 :one when 2 :two else :other end end, }, } end it 'always uses :other as pluralization key' do expect(RailsAdmin.config(Comment).label_plural).to eq('other') end end end end describe '#weight' do it 'is 0' do expect(RailsAdmin.config(Comment).weight).to eq(0) end end describe '#parent' do it 'is nil for ActiveRecord::Base inherited models' do expect(RailsAdmin.config(Comment).parent).to be_nil end it 'is parent model otherwise' do expect(RailsAdmin.config(Hardball).parent).to eq(Ball) end end describe '#navigation_label' do it 'is nil if parent module is Object' do expect(RailsAdmin.config(Comment).navigation_label).to be_nil end it 'is parent module otherwise' do expect(RailsAdmin.config(Cms::BasicPage).navigation_label).to eq('Cms') end end describe '#last_created_at', active_record: true do let!(:teams) do [FactoryBot.create(:team, created_at: 1.day.ago), FactoryBot.create(:team, created_at: 2.days.ago)] end before do RailsAdmin.config(Team) do last_created_at { abstract_model.model.maximum(:created_at) } end end it 'allow customization' do expect(RailsAdmin.config(Team).last_created_at.to_date).to eq 1.day.ago.to_date end end end ================================================ FILE: spec/rails_admin/config/proxyable_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Proxyable do class ProxyableTest include RailsAdmin::Config::Proxyable def boo sleep 0.15 bindings[:foo] end def qux 'foobar' end end let!(:proxyable_test) { ProxyableTest.new } subject do proxyable_test.bindings = {foo: 'bar'} proxyable_test end it 'proxies method calls to @object' do expect(subject.bindings).to eq foo: 'bar' end it 'preserves initially set @bindings' do expect(subject.with(foo: 'baz').tap(&:qux).bindings).to eq foo: 'bar' end context 'when a method is defined in Kernel' do before do Kernel.module_eval do def qux 'quux' end end end after do Kernel.module_eval do undef qux end end it 'proxies calls for the method to @object' do expect(subject.qux).to eq 'foobar' end end describe 'with parallel execution' do it 'ensures thread-safety' do threads = Array.new(2) do |i| Thread.new do value = %w[a b][i] proxy = proxyable_test.with foo: value sleep i * 0.1 expect(proxy.boo).to eq value end end threads.each(&:join) end end end ================================================ FILE: spec/rails_admin/config/sections/list_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Sections::List do describe '#fields_for_table' do subject { RailsAdmin.config(Player).list } it 'brings sticky fields first' do RailsAdmin.config Player do list do field(:number) field(:id) field(:name) { sticky true } end end expect(subject.fields_for_table.map(&:name)).to eq %i[name number id] end it 'keep the original order except for stickey ones' do RailsAdmin.config Player do list do configure(:number) { sticky true } end end expect(subject.fields_for_table.map(&:name)).to eq %i[number] + (subject.visible_fields.map(&:name) - %i[number]) end end end ================================================ FILE: spec/rails_admin/config/sections_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config::Sections do describe 'configure' do it 'configures without changing the section default list' do RailsAdmin.config Team do edit do configure :name do label 'Renamed' end end end fields = RailsAdmin.config(Team).edit.fields expect(fields.detect { |f| f.name == :name }.label).to eq('Renamed') expect(fields.count).to be >= 19 # not 1 end it 'does not change the section list if set' do RailsAdmin.config Team do edit do field :manager configure :name do label 'Renamed' end end end fields = RailsAdmin.config(Team).edit.fields expect(fields.first.name).to eq(:manager) expect(fields.count).to eq(1) # not 19 end end describe 'DSL field inheritance' do it 'is tested' do RailsAdmin.config do |config| config.model Fan do field :name do label do @label ||= "modified base #{label}" end end list do field :name do label do @label ||= "modified list #{label}" end end end edit do field :name do label do @label ||= "modified edit #{label}" end end end create do field :name do label do @label ||= "modified create #{label}" end end end end end expect(RailsAdmin.config(Fan).visible_fields.count).to eq(1) expect(RailsAdmin.config(Fan).visible_fields.first.label).to eq('modified base Their Name') expect(RailsAdmin.config(Fan).list.visible_fields.first.label).to eq('modified list Their Name') expect(RailsAdmin.config(Fan).export.visible_fields.first.label).to eq('modified base Their Name') expect(RailsAdmin.config(Fan).edit.visible_fields.first.label).to eq('modified edit Their Name') expect(RailsAdmin.config(Fan).create.visible_fields.first.label).to eq('modified create Their Name') expect(RailsAdmin.config(Fan).update.visible_fields.first.label).to eq('modified edit Their Name') end end describe 'DSL group inheritance' do it 'is tested' do RailsAdmin.config do |config| config.model Team do list do group 'a' do field :founded end group 'b' do field :name field :wins end end edit do group 'a' do field :name end group 'c' do field :founded field :wins end end update do group 'd' do field :wins end group 'e' do field :losses end end end end expect(RailsAdmin.config(Team).list.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:founded], %i[name wins]]) expect(RailsAdmin.config(Team).edit.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], %i[founded wins]]) expect(RailsAdmin.config(Team).create.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], %i[founded wins]]) expect(RailsAdmin.config(Team).update.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], [:founded], [:wins], [:losses]]) expect(RailsAdmin.config(Team).visible_groups.collect { |g| g.visible_fields.collect(&:name) }.flatten.count).to eq(20) expect(RailsAdmin.config(Team).export.visible_groups.collect { |g| g.visible_fields.collect(&:name) }.flatten.count).to eq(20) end end end ================================================ FILE: spec/rails_admin/config_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Config do describe '.included_models' do it 'only uses included models' do RailsAdmin.config.included_models = [Team, League] expect(RailsAdmin::AbstractModel.all.collect(&:model)).to eq([League, Team]) # it gets sorted end it 'does not restrict models if included_models is left empty' do RailsAdmin.config.included_models = [] expect(RailsAdmin::AbstractModel.all.collect(&:model)).to include(Team, League) end it 'removes excluded models (whitelist - blacklist)' do RailsAdmin.config.excluded_models = [Team] RailsAdmin.config.included_models = [Team, League] expect(RailsAdmin::AbstractModel.all.collect(&:model)).to eq([League]) end it 'excluded? returns true for any model not on the list' do RailsAdmin.config.included_models = [Team, League] team_config = RailsAdmin::AbstractModel.new('Team').config fan_config = RailsAdmin::AbstractModel.new('Fan').config expect(fan_config).to be_excluded expect(team_config).not_to be_excluded end end describe '.add_extension' do before do silence_warnings do RailsAdmin.const_set('EXTENSIONS', []) end end it 'registers the extension with RailsAdmin' do RailsAdmin.add_extension(:example, ExampleModule) expect(RailsAdmin::EXTENSIONS.count { |name| name == :example }).to eq(1) end context 'given an extension with an authorization adapter' do it 'registers the adapter' do RailsAdmin.add_extension(:example, ExampleModule, authorization: true) expect(RailsAdmin::AUTHORIZATION_ADAPTERS[:example]).to eq(ExampleModule::AuthorizationAdapter) end end context 'given an extension with an auditing adapter' do it 'registers the adapter' do RailsAdmin.add_extension(:example, ExampleModule, auditing: true) expect(RailsAdmin::AUDITING_ADAPTERS[:example]).to eq(ExampleModule::AuditingAdapter) end end context 'given an extension with a configuration adapter' do it 'registers the adapter' do RailsAdmin.add_extension(:example, ExampleModule, configuration: true) expect(RailsAdmin::CONFIGURATION_ADAPTERS[:example]).to eq(ExampleModule::ConfigurationAdapter) end end end describe '.main_app_name' do it 'as a default meaningful dynamic value' do expect(RailsAdmin.config.main_app_name.call).to eq(['Dummy App', 'Admin']) end it 'can be configured' do RailsAdmin.config do |config| config.main_app_name = %w[static value] end expect(RailsAdmin.config.main_app_name).to eq(%w[static value]) end end describe '.authorize_with' do context 'given a key for a extension with authorization' do before do RailsAdmin.add_extension(:example, ExampleModule, authorization: true) end it 'initializes the authorization adapter' do expect(ExampleModule::AuthorizationAdapter).to receive(:new).with(RailsAdmin::Config) RailsAdmin.config do |config| config.authorize_with(:example) end RailsAdmin.config.authorize_with.call end it 'passes through any additional arguments to the initializer' do options = {option: true} expect(ExampleModule::AuthorizationAdapter).to receive(:new).with(RailsAdmin::Config, options) RailsAdmin.config do |config| config.authorize_with(:example, options) end RailsAdmin.config.authorize_with.call end end end describe '.audit_with' do context 'given a key for a extension with auditing' do before do RailsAdmin.add_extension(:example, ExampleModule, auditing: true) end it 'initializes the auditing adapter' do expect(ExampleModule::AuditingAdapter).to receive(:new).with(RailsAdmin::Config) RailsAdmin.config do |config| config.audit_with(:example) end RailsAdmin.config.audit_with.call end it 'passes through any additional arguments to the initializer' do options = {option: true} expect(ExampleModule::AuditingAdapter).to receive(:new).with(RailsAdmin::Config, options) RailsAdmin.config do |config| config.audit_with(:example, options) end RailsAdmin.config.audit_with.call end end context 'given paper_trail as the extension for auditing', active_record: true do before do class ControllerMock def set_paper_trail_whodunnit; end end module PaperTrail; end class Version; end RailsAdmin.add_extension(:example, RailsAdmin::Extensions::PaperTrail, auditing: true) end it 'initializes the auditing adapter' do RailsAdmin.config do |config| config.audit_with(:example) end expect { ControllerMock.new.instance_eval(&RailsAdmin.config.audit_with) }.not_to raise_error end end end describe '.configure_with' do context 'given a key for a extension with configuration' do before do RailsAdmin.add_extension(:example, ExampleModule, configuration: true) end it 'initializes configuration adapter' do expect(ExampleModule::ConfigurationAdapter).to receive(:new) RailsAdmin.config do |config| config.configure_with(:example) end end it 'yields the (optionally) provided block, passing the initialized adapter' do configurator = nil RailsAdmin.config do |config| config.configure_with(:example) do |configuration_adapter| configurator = configuration_adapter end end expect(configurator).to be_a(ExampleModule::ConfigurationAdapter) end end end describe '.config' do context '.default_search_operator' do it 'sets the default_search_operator' do RailsAdmin.config do |config| config.default_search_operator = 'starts_with' end expect(RailsAdmin::Config.default_search_operator).to eq('starts_with') end it 'errors on unrecognized search operator' do expect do RailsAdmin.config do |config| config.default_search_operator = 'random' end end.to raise_error(ArgumentError, "Search operator 'random' not supported") end it "defaults to 'default'" do expect(RailsAdmin::Config.default_search_operator).to eq('default') end end end describe '.visible_models' do it 'passes controller bindings, find visible models, order them' do RailsAdmin.config do |config| config.included_models = [Player, Fan, Comment, Team] config.model Player do hide end config.model Fan do weight(-1) show end config.model Comment do visible do bindings[:controller]._current_user.role == :admin end end config.model Team do visible do bindings[:controller]._current_user.role != :admin end end end expect(RailsAdmin.config.visible_models(controller: double(_current_user: double(role: :admin), authorized?: true)).collect(&:abstract_model).collect(&:model)).to match_array [Fan, Comment] end it 'hides unallowed models' do RailsAdmin.config do |config| config.included_models = [Comment] end expect(RailsAdmin.config.visible_models(controller: double(authorization_adapter: double(authorized?: true))).collect(&:abstract_model).collect(&:model)).to eq([Comment]) expect(RailsAdmin.config.visible_models(controller: double(authorization_adapter: double(authorized?: false))).collect(&:abstract_model).collect(&:model)).to eq([]) end it 'does not contain embedded model', mongoid: true do RailsAdmin.config do |config| config.included_models = [FieldTest, Comment, Embed] end expect(RailsAdmin.config.visible_models(controller: double(_current_user: double(role: :admin), authorized?: true)).collect(&:abstract_model).collect(&:model)).to match_array [FieldTest, Comment] end it 'basically does not contain embedded model except model using recursively_embeds_many or recursively_embeds_one', mongoid: true do class RecursivelyEmbedsOne include Mongoid::Document recursively_embeds_one end class RecursivelyEmbedsMany include Mongoid::Document recursively_embeds_many end RailsAdmin.config do |config| config.included_models = [FieldTest, Comment, Embed, RecursivelyEmbedsMany, RecursivelyEmbedsOne] end expect(RailsAdmin.config.visible_models(controller: double(_current_user: double(role: :admin), authorized?: true)).collect(&:abstract_model).collect(&:model)).to match_array [FieldTest, Comment, RecursivelyEmbedsMany, RecursivelyEmbedsOne] end end describe '.models_pool' do it 'should not include classnames start with Concerns::' do expect(RailsAdmin::Config.models_pool.select { |m| m.match(/^Concerns::/) }).to be_empty end it 'includes models in the directory added by config.eager_load_paths' do expect(RailsAdmin::Config.models_pool).to include('Basketball') end it 'should include a model which was configured explicitly' do RailsAdmin::Config.model 'PaperTrail::Version' do visible false end expect(RailsAdmin::Config.models_pool).to include('PaperTrail::Version') end end describe '.parent_controller' do before do class TestController < ActionController::Base; end end it 'uses default class' do expect(RailsAdmin.config.parent_controller).to eq '::ActionController::Base' end it 'uses other class' do RailsAdmin.config do |config| config.parent_controller = 'TestController' end expect(RailsAdmin.config.parent_controller).to eq 'TestController' end end describe '.parent_controller=' do context 'if RailsAdmin::ApplicationController is already loaded' do before do # preload controllers (e.g. when config.eager_load = true) RailsAdmin::MainController end after do RailsAdmin::Config.reset RailsAdmin.send(:remove_const, :ApplicationController) load RailsAdmin::Engine.root.join('app/controllers/rails_admin/application_controller.rb') end it 'can be changed' do RailsAdmin.config.parent_controller = 'ApplicationController' expect(RailsAdmin::ApplicationController.superclass).to eq ApplicationController expect(RailsAdmin::MainController.superclass.superclass).to eq ApplicationController end end end describe '.forgery_protection_settings' do it 'uses with: :exception by default' do expect(RailsAdmin.config.forgery_protection_settings).to eq(with: :exception) end it 'allows to customize settings' do RailsAdmin.config do |config| config.forgery_protection_settings = {with: :null_session} end expect(RailsAdmin.config.forgery_protection_settings).to eq(with: :null_session) end end describe '.model' do let(:fields) { described_class.model(Team).fields } before do described_class.model Team do field :players do visible false end end end context 'when model expanded' do before do described_class.model(Team) do field :fans end end it 'execute all passed blocks' do expect(fields.map(&:name)).to match_array %i[players fans] end end context 'when expand redefine behavior' do before do described_class.model Team do field :players end end it 'execute all passed blocks' do expect(fields.find { |f| f.name == :players }.visible).to be true end end context 'when model has no table yet', active_record: true do it 'does not try to apply the configuration block' do described_class.model(WithoutTable) do include_all_fields end end end end describe '.reset' do before do RailsAdmin.config do |config| config.included_models = %w[Player Team] end RailsAdmin::AbstractModel.all RailsAdmin::Config.reset RailsAdmin.config do |config| config.excluded_models = ['Player'] end end subject { RailsAdmin::AbstractModel.all.map { |am| am.model.name } } it 'refreshes the result of RailsAdmin::AbstractModel.all' do expect(subject).not_to include 'Player' expect(subject).to include 'Team' end end describe '.reload!' do before do RailsAdmin.config Player do field :name end RailsAdmin.config Team do field :color, :integer end end it 'clears current configuration' do RailsAdmin::Config.reload! expect(RailsAdmin::Config.model(Player).fields.map(&:name)).to include :number end it 'reloads the configuration from the initializer' do RailsAdmin::Config.reload! expect(RailsAdmin::Config.model(Team).fields.find { |f| f.name == :color }.type).to eq :hidden end end end module ExampleModule class AuthorizationAdapter; end class ConfigurationAdapter; end class AuditingAdapter; end end ================================================ FILE: spec/rails_admin/engine_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Engine do context 'on class unload' do let(:fields) { RailsAdmin.config(Player).edit.fields } before do Rails.application.config.cache_classes = false RailsAdmin.config(Player) do field :name field :number end end after { Rails.application.config.cache_classes = true } it 'triggers RailsAdmin config to be reloaded' do # this simulates rails code reloading RailsAdmin::Engine.initializers.find do |i| i.name == 'RailsAdmin reload config in development' end.block.call(Rails.application) Rails.application.executor.wrap do ActiveSupport::Reloader.new.tap(&:class_unload!).complete! end RailsAdmin.config(Player) do field :number end expect(fields.map(&:name)).to match_array %i[number] end end end ================================================ FILE: spec/rails_admin/extentions/cancancan/authorization_adapter_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Extensions::CanCanCan::AuthorizationAdapter do let(:user) { double } let(:controller) { double(_current_user: user, current_ability: MyAbility.new(user)) } class MyAbility include CanCan::Ability def initialize(_user) can :access, :rails_admin can :manage, :all end end describe '#initialize' do it 'accepts the ability class as an argument' do expect(described_class.new(controller, MyAbility).ability_class).to eq MyAbility end it 'supports block DSL' do adapter = described_class.new(controller) do ability_class MyAbility end expect(adapter.ability_class).to eq MyAbility end end end ================================================ FILE: spec/rails_admin/extentions/paper_trail/auditing_adapter_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Extensions::PaperTrail::AuditingAdapter, active_record: true do let(:controller) { double(set_paper_trail_whodunnit: nil) } describe '#initialize' do it 'accepts the user and version classes as arguments' do adapter = described_class.new(controller, User::Confirmed, Trail) expect(adapter.user_class).to eq User::Confirmed expect(adapter.version_class).to eq Trail end it 'supports block DSL' do adapter = described_class.new(controller) do user_class User::Confirmed version_class Trail sort_by(created_at: :asc) end expect(adapter.user_class).to eq User::Confirmed expect(adapter.version_class).to eq Trail expect(adapter.sort_by).to eq({created_at: :asc}) end end describe '#listing_for_model' do subject { RailsAdmin::Extensions::PaperTrail::AuditingAdapter.new(nil) } let(:model) { RailsAdmin::AbstractModel.new(PaperTrailTest) } it 'uses the given sort order' do expect_any_instance_of(ActiveRecord::Relation).to receive(:order).with(whodunnit: :asc).and_call_original subject.listing_for_model model, nil, :username, false, true, nil, 20 end it 'uses the default order when sort is not given' do expect_any_instance_of(ActiveRecord::Relation).to receive(:order).with(id: :desc).and_call_original subject.listing_for_model model, nil, false, false, true, nil, 20 end end end ================================================ FILE: spec/rails_admin/extentions/paper_trail/version_proxy_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Extensions::PaperTrail::VersionProxy, active_record: true do describe '#username' do subject { described_class.new(version, user_class).username } let(:version) { double(whodunnit: :the_user) } let(:user_class) { double(find: user) } context 'when found user has email' do let(:user) { double(email: :mail) } it { is_expected.to eq(:mail) } end context 'when found user does not have email' do let(:user) { double } # no email method it { is_expected.to eq(:the_user) } end end end ================================================ FILE: spec/rails_admin/install_generator_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'generators/rails_admin/install_generator' RSpec.describe RailsAdmin::InstallGenerator, type: :generator do destination File.expand_path('../dummy_app/tmp/generator', __dir__) arguments ['admin', "--asset=#{CI_ASSET}", '--force'] before do prepare_destination File.write(File.join(destination_root, 'package.json'), '{"license": "MIT"}') FileUtils.touch File.join(destination_root, 'Gemfile') FileUtils.mkdir_p(File.join(destination_root, 'config/initializers')) File.write(File.join(destination_root, 'config/routes.rb'), <<~RUBY) Rails.application.routes.draw do # empty end RUBY File.write(File.join(destination_root, 'Rakefile'), <<-RUBY.gsub(/^ {4}/, '')) desc 'Stub for testing' task 'css:install:sass' RUBY end after do FileUtils.rm_rf(destination_root) end it 'mounts RailsAdmin as Engine and generates RailsAdmin Initializer' do Dir.chdir(destination_root) do run_generator end expect(destination_root).to( have_structure do directory 'config' do directory 'initializers' do file 'rails_admin.rb' do contains 'RailsAdmin.config' contains 'asset_source =' end end file 'routes.rb' do contains "mount RailsAdmin::Engine => '/admin', as: 'rails_admin'" end end case CI_ASSET when :webpacker file 'app/javascript/packs/rails_admin.js' do contains 'import "rails_admin/src/rails_admin/base"' end file 'app/javascript/stylesheets/rails_admin.scss' do contains '@import "rails_admin/src/rails_admin/styles/base"' end when :sprockets file 'Gemfile' do contains 'sassc-rails' end when :importmap file 'app/javascript/rails_admin.js' do contains 'import "rails_admin/src/rails_admin/base"' end file 'app/assets/stylesheets/rails_admin.scss' do contains '$fa-font-path: ".";' contains '@import "rails_admin/src/rails_admin/styles/base"' end file 'config/importmap.rails_admin.rb' do contains 'pin "rails_admin", preload: true' contains 'pin "rails_admin/src/rails_admin/base", to: "https://ga.jspm.io/npm:rails_admin@' contains 'pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@' end file 'config/initializers/assets.rb' do contains 'Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")' end file 'package.json' do contains 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css' end when :webpack file 'app/javascript/rails_admin.js' do contains 'import "rails_admin/src/rails_admin/base"' end file 'app/assets/stylesheets/rails_admin.scss' do contains '$fa-font-path: ".";' contains '@import "rails_admin/src/rails_admin/styles/base"' end file 'package.json' do contains 'webpack --config webpack.config.js' contains 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css' end when :vite file 'app/frontend/entrypoints/rails_admin.js' do contains 'import "~/stylesheets/rails_admin.scss"' contains 'import "rails_admin/src/rails_admin/base"' end file 'app/frontend/stylesheets/rails_admin.scss' do contains '$fa-font-path: "@fortawesome/fontawesome-free/webfonts";' contains '@import "rails_admin/src/rails_admin/styles/base"' end file 'package.json' do contains 'sass' end end end, ) end it 'inserts asset_source option to RailsAdmin Initializer' do File.write(File.join(destination_root, 'config/initializers/rails_admin.rb'), <<~RUBY) RailsAdmin.config do |config| # empty end RUBY Dir.chdir(destination_root) do run_generator end expect(File.read(File.join(destination_root, 'config/initializers/rails_admin.rb'))).to include 'config.asset_source =' end end ================================================ FILE: spec/rails_admin/support/csv_converter_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::CSVConverter do it 'keeps headers ordering' do RailsAdmin.config(Player) do export do field :number field :name end end FactoryBot.create :player objects = Player.all schema = {only: %i[number name]} expect(RailsAdmin::CSVConverter.new(objects, schema).to_csv({})[2]).to match(/Number,Name/) end describe '#generate_csv_header' do let(:objects) { FactoryBot.create_list :player, 1 } before do RailsAdmin.config(Player) do export do field :number field :name end end end it 'does not break when non-existent fields are given' do expect(RailsAdmin::CSVConverter.new(objects, {only: %i[name foo], include: {bar: :baz}}).send(:generate_csv_header)). to eq ['Name'] end it 'does not break when non-association fields are given to :include' do expect(RailsAdmin::CSVConverter.new(objects, {only: %i[name foo], include: {name: :name}}).send(:generate_csv_header)). to eq ['Name'] end end describe '#to_csv' do before do RailsAdmin.config(Player) do export do field :number field :name end end end let(:objects) { FactoryBot.create_list :player, 1, number: 1, name: 'なまえ' } let(:schema) { {only: %i[number name]} } let(:options) { {encoding_to: encoding} } subject { RailsAdmin::CSVConverter.new(objects, schema).to_csv(options) } context 'when encoding FROM latin1', active_record: true do let(:connection_config) do if ActiveRecord::Base.respond_to?(:connection_db_config) ActiveRecord::Base.connection_db_config.configuration_hash else ActiveRecord::Base.connection_config end end let(:encoding) { '' } let(:objects) { FactoryBot.create_list :player, 1, number: 1, name: 'Josè'.encode('ISO-8859-1') } before do case connection_config[:adapter] when 'postgresql' @connection = ActiveRecord::Base.connection.raw_connection @connection.set_client_encoding('latin1') when 'mysql2' ActiveRecord::Base.connection.execute('SET NAMES latin1;') end end after do case connection_config[:adapter] when 'postgresql' @connection.set_client_encoding('utf8') when 'mysql2' ActiveRecord::Base.connection.execute('SET NAMES utf8;') end end it 'exports to ISO-8859-1' do expect(subject[1]).to eq 'ISO-8859-1' expect(subject[2].encoding).to eq Encoding::ISO_8859_1 expect(subject[2].unpack1('H*')). to eq '4e756d6265722c4e616d650a312c4a6f73e80a' end end context 'when encoding to UTF-8' do let(:encoding) { 'UTF-8' } it 'exports to UTF-8 with BOM' do expect(subject[1]).to eq 'UTF-8' expect(subject[2].encoding).to eq Encoding::UTF_8 expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265722c4e616d650a312ce381aae381bee381880a' # have BOM end end context 'when encoding to Shift_JIS' do let(:encoding) { 'Shift_JIS' } it 'exports to Shift_JIS' do expect(subject[1]).to eq 'Shift_JIS' expect(subject[2].encoding).to eq Encoding::Shift_JIS expect(subject[2].unpack1('H*')). to eq '4e756d6265722c4e616d650a312c82c882dc82a60a' end end context 'when encoding to UTF-16(ASCII-incompatible)' do let(:encoding) { 'UTF-16' } it 'encodes to expected byte sequence' do expect(subject[1]).to eq 'UTF-16' expect(subject[2].encoding).to eq Encoding::UTF_16 expect(subject[2].unpack1('H*').force_encoding('US-ASCII')). to eq 'feff004e0075006d006200650072002c004e0061006d0065000a0031002c306a307e3048000a' end end context 'when specifying a column separator' do context 'when options keys are symbolized' do let(:options) { {encoding_to: 'UTF-8', generator: {col_sep: '___'}} } it 'uses the column separator specified' do expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265725f5f5f4e616d650a315f5f5fe381aae381bee381880a' end end context 'when options keys are string' do let(:options) { {'encoding_to' => 'UTF-8', 'generator' => {'col_sep' => '___'}} } it 'uses the column separator specified' do expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265725f5f5f4e616d650a315f5f5fe381aae381bee381880a' end end end context 'when objects is empty' do let(:objects) { [] } let(:options) { {} } it 'generates an empty csv' do expect(subject[2]).to eq("\n") end end end end ================================================ FILE: spec/rails_admin/support/datetime_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Support::Datetime do describe '#to_flatpickr_format' do { '%D de %M de %Y, %H:%M:%S' => 'm/d/y \d\e i \d\e Y, H:i:S', '%d/%-m/%Y, %H:%M:%S' => 'd/n/Y, H:i:S', '%d de %B de %Y' => 'd \d\e F \d\e Y', '%-d %B %Y' => 'j F Y', '%F %T' => 'Y-m-d H:i:S', '%Y-%m-%dT%H:%M:%S%:z' => 'Y-m-d\TH:i:S+00:00', '%HH%MM%SS' => 'H\Hi\MS\S', 'a%-Ha%-Ma%-Sa%:za' => '\aH\ai\as\a+00:00\a', '%B %-d at %-l:%M %p' => 'F j \a\t h:i K', }.each do |strftime_format, flatpickr_format| it "convert strftime_format to flatpickr_format - example #{strftime_format}" do expect(RailsAdmin::Support::Datetime.to_flatpickr_format(strftime_format)).to eq flatpickr_format end end it 'raises an error with unsupported directive' do expect do RailsAdmin::Support::Datetime.to_flatpickr_format('%C') end.to raise_error(/Unsupported/) end end end ================================================ FILE: spec/rails_admin/support/hash_helper_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::HashHelper do let(:hash) do { 'subject' => 'Test', 'user' => { name: 'Dirk', 'title' => 'Holistic Detective', 'clients' => [ {name: 'Zaphod'}, {'name' => 'Arthur'}, ], }, } end describe 'symbolize' do let(:symbolized_hash) { RailsAdmin::HashHelper.symbolize(hash) } it 'symbolizes top-level hash keys' do %i[subject user].each do |key| expect(symbolized_hash.keys).to include(key) end end it 'symbolizes nested hashes' do %i[name title clients].each do |key| expect(symbolized_hash[:user].keys).to include(key) end end it 'symbolizes nested hashes inside of array values' do clients = symbolized_hash[:user][:clients] expect(clients.length).to eq(2) expect(clients[0][:name]).to eq(:Zaphod) expect(clients[1][:name]).to eq(:Arthur) end end end ================================================ FILE: spec/rails_admin/version_spec.rb ================================================ # frozen_string_literal: true require 'fileutils' require 'spec_helper' RSpec.describe 'RailsAdmin::Version' do describe '#warn_with_js_version' do it 'does nothing when the versions match' do allow(RailsAdmin::Version).to receive(:actual_js_version).and_return('3.0.0') allow(RailsAdmin::Version).to receive(:js).and_return('3.0.0') expect(RailsAdmin::Version).not_to receive(:warn) RailsAdmin::Version.warn_with_js_version end it "shows a warning when actual_js_version couldn't detected" do allow(RailsAdmin::Version).to receive(:actual_js_version).and_return(nil) allow(RailsAdmin::Version).to receive(:js).and_return('3.0.1') expect(RailsAdmin::Version).to receive(:warn).with(/yarn install/) RailsAdmin::Version.warn_with_js_version end it 'shows a warning when the versions do not match' do allow(RailsAdmin::Version).to receive(:actual_js_version).and_return('3.0.0') allow(RailsAdmin::Version).to receive(:js).and_return('3.0.1') expect(RailsAdmin::Version).to receive(:warn).with(/inconsistency/) RailsAdmin::Version.warn_with_js_version end end describe '#js_version_from_node_modules' do unless CI_ASSET == :sprockets let(:path) { Rails.root.join('node_modules/rails_admin/package.json') } before do @backup = File.read(path) FileUtils.rm(path) end after { File.write(path, @backup) } it 'returns nil when RailsAdmin package.json is not found' do expect(RailsAdmin::Version.send(:js_version_from_node_modules)).to be_nil end it 'shows a warning when RailsAdmin package.json is not found' do File.write(path, '{"version": "0.1.0-alpha"}') expect(RailsAdmin::Version.send(:js_version_from_node_modules)).to eq '0.1.0-alpha' end end end end ================================================ FILE: spec/shared_examples/shared_examples_for_field_types.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.shared_examples 'a generic field type' do |column_name, field_type| describe '#html_attributes' do context 'when the field is required' do before do RailsAdmin.config FieldTest do field column_name, field_type do required true end end end subject do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == column_name end.with(object: FieldTest.new) end it 'should contain a required attribute with the string "required" as value' do expect(subject.html_attributes[:required]).to be_truthy end end end end RSpec.shared_examples 'a string-like field type' do |column_name, _| subject do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == column_name end.with(object: FieldTest.new) end it 'is a StringLike field' do expect(subject).to be_a(RailsAdmin::Config::Fields::Types::StringLike) end end RSpec.shared_examples 'a float-like field type' do |column_name| subject do RailsAdmin.config('FieldTest').fields.detect do |f| f.name == column_name end.with(object: FieldTest.new) end describe '#html_attributes' do it 'should contain a step attribute' do expect(subject.html_attributes[:step]).to eq('any') end it 'should contain a falsey required attribute' do expect(subject.html_attributes[:required]).to be_falsey end context 'when the field is required' do before do RailsAdmin.config FieldTest do field column_name, :float do required true end end end it 'should contain a truthy required attribute' do expect(subject.html_attributes[:required]).to be_truthy end end end describe '#view_helper' do it "uses the 'number' type input tag" do expect(subject.view_helper).to eq(:number_field) end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true # Configure Rails Environment ENV['RAILS_ENV'] = 'test' CI_ORM = (ENV['CI_ORM'] || :active_record).to_sym PK_COLUMN = {active_record: :id, mongoid: :_id}[CI_ORM] if RUBY_ENGINE == 'jruby' # Workaround for JRuby CI failure https://github.com/jruby/jruby/issues/6547#issuecomment-774104996 require 'i18n/backend' require 'i18n/backend/simple' end require 'simplecov' require 'simplecov-lcov' SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter] SimpleCov.start do add_filter '/spec/' add_filter '/vendor/bundle/' end SimpleCov::Formatter::LcovFormatter.config do |c| c.report_with_single_file = true c.single_report_path = 'coverage/lcov.info' end require File.expand_path('dummy_app/config/environment', __dir__) require 'rspec/rails' require 'factory_bot' require 'factories' require 'policies' require "database_cleaner/#{CI_ORM}" require "orm/#{CI_ORM}" require 'paper_trail/frameworks/rspec' if defined?(PaperTrail) Dir[File.expand_path('support/**/*.rb', __dir__), File.expand_path('shared_examples/**/*.rb', __dir__)].sort.each { |f| require f } ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.default_url_options[:host] = 'example.com' Rails.backtrace_cleaner.remove_silencers! require 'capybara/cuprite' Capybara.javascript_driver = :cuprite Capybara.register_driver(:cuprite) do |app| # Refs. https://github.com/rubycdp/ferrum/issues/470 Capybara::Cuprite::Driver.new(app, flatten: RUBY_ENGINE != 'jruby', js_errors: true, logger: ConsoleLogger) end Capybara.server = :webrick RailsAdmin.setup_all_extensions RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = :expect end config.disable_monkey_patching! config.include RSpec::Matchers config.include RailsAdmin::Engine.routes.url_helpers config.include Warden::Test::Helpers config.include Capybara::DSL, type: :request config.include Capybara::RSpecMatchers, type: :request config.verbose_retry = true config.display_try_failure_messages = true config.around :each, :js do |example| example.run_with_retry retry: (ENV['CI'] && RUBY_ENGINE == 'jruby' ? 3 : 2) end config.retry_callback = proc do |example| example.metadata[:retry] = 6 if [Ferrum::DeadBrowserError, Ferrum::NoExecutionContextError, Ferrum::TimeoutError].include?(example.exception.class) if example.metadata[:js] attempt = 0 begin Capybara.reset! rescue Ferrum::TimeoutError, Ferrum::NoExecutionContextError attempt += 1 raise if attempt >= 5 retry end end end config.before(:all) do case CI_ASSET when :webpacker Webpacker.instance.compiler.compile when :vite ViteRuby.instance.commands.build end end config.before do |example| DatabaseCleaner.strategy = if CI_ORM == :mongoid || example.metadata[:js] :deletion else :transaction end DatabaseCleaner.start RailsAdmin::Config.reset RailsAdmin::Config.asset_source = CI_ASSET end config.after(:each) do Warden.test_reset! DatabaseCleaner.clean end CI_TARGET_ORMS.each do |orm| if orm == CI_ORM config.filter_run_excluding "skip_#{orm}": true else config.filter_run_excluding orm => true end end config.filter_run_excluding composite_primary_keys: true unless defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) end ================================================ FILE: spec/support/cuprite_logger.rb ================================================ # frozen_string_literal: true class ConsoleLogger def self.puts(message) warn(message) unless message.start_with?(' ◀', "\n\n▶") end end ================================================ FILE: spec/support/fakeio.rb ================================================ # frozen_string_literal: true require 'forwardable' require 'stringio' class FakeIO attr_reader :original_filename, :content_type def initialize(content, filename: nil, content_type: nil) @io = StringIO.new(content) @original_filename = filename @content_type = content_type end extend Forwardable delegate %i[read rewind eof? close size] => :@io end ================================================ FILE: spec/support/fixtures.rb ================================================ # frozen_string_literal: true def file_path(*paths) File.expand_path(File.join(File.dirname(__FILE__), '../fixtures', *paths)) end ================================================ FILE: spec/support/jquery.simulate.drag-sortable.js ================================================ (function($) { /* * Simulate drag of a JQuery UI sortable list * Repository: https://github.com/mattheworiordan/jquery.simulate.drag-sortable.js * Author: http://mattheworiordan.com * * options are: * - move: move item up (positive) or down (negative) by Integer amount * - dropOn: move item to a new linked list, move option now represents position in the new list (zero indexed) * - handle: selector for the draggable handle element (optional) * - listItem: selector to limit which sibling items can be used for reordering * - placeHolder: if a placeholder is used during dragging, we need to consider it's height * - tolerance: (optional) number of pixels to overlap by instead of the default 50% of the element height * */ $.fn.simulateDragSortable = function(options) { // build main options before element iteration var opts = $.extend({}, $.fn.simulateDragSortable.defaults, options); applyDrag = function(options) { // allow for a drag handle if item is not draggable var that = this, options = options || opts, // default to plugin opts unless options explicitly provided handle = options.handle ? $(this).find(options.handle)[0] : $(this)[0], listItem = options.listItem, placeHolder = options.placeHolder, sibling = $(this), moveCounter = Math.floor(options.move), direction = moveCounter > 0 ? 'down' : 'up', moveVerticalAmount = 0, initialVerticalPosition = 0, extraDrag = !isNaN(parseInt(options.tolerance, 10)) ? function() { return Number(options.tolerance); } : function(obj) { return ($(obj).outerHeight() / 2) + 5; }, dragPastBy = 0, // represents the additional amount one drags past an element and bounce back dropOn = options.dropOn ? $(options.dropOn) : false, center = findCenter(handle), x = Math.floor(center.x), y = Math.floor(center.y), mouseUpAfter = (opts.debug ? 2500 : 10); if (dropOn) { if (dropOn.length === 0) { if (console && console.log) { console.log('simulate.drag-sortable.js ERROR: Drop on target could not be found'); console.log(options.dropOn); } return; } sibling = dropOn.find('>*:last'); moveCounter = -(dropOn.find('>*').length + 1) + (moveCounter + 1); // calculate length of list after this move, use moveCounter as a positive index position in list to reverse back up if (dropOn.offset().top - $(this).offset().top < 0) { // moving to a list above this list, so move to just above top of last item (tried moving to top but JQuery UI wouldn't bite) initialVerticalPosition = sibling.offset().top - $(this).offset().top - extraDrag(this); } else { // moving to a list below this list, so move to bottom and work up (JQuery UI does not trigger new list below unless you move past top item first) initialVerticalPosition = sibling.offset().top - $(this).offset().top - $(this).height(); } } else if (moveCounter === 0) { if (console && console.log) { console.log('simulate.drag-sortable.js WARNING: Drag with move set to zero has no effect'); } return; } else { while (moveCounter !== 0) { if (direction === 'down') { if (sibling.next(listItem).length) { sibling = sibling.next(listItem); moveVerticalAmount += sibling.outerHeight(); } moveCounter -= 1; } else { if (sibling.prev(listItem).length) { sibling = sibling.prev(listItem); moveVerticalAmount -= sibling.outerHeight(); } moveCounter += 1; } } } dispatchEvent(handle, 'mousedown', createEvent('mousedown', handle, { clientX: x, clientY: y })); // simulate drag start dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x+1, clientY: y+1 })); if (dropOn) { // jump to top or bottom of new list but do it in increments so that JQuery UI registers the drag events slideUpTo(x, y, initialVerticalPosition); // reset y position to top or bottom of list and move from there y += initialVerticalPosition; // now call regular shift/down in a list options = jQuery.extend(options, { move: moveCounter }); delete options.dropOn; // add some delays to allow JQuery UI to catch up setTimeout(function() { dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y })); }, 5); setTimeout(function() { dispatchEvent(handle, 'mouseup', createEvent('mouseup', handle, { clientX: x, clientY: y })); setTimeout(function() { if (options.move) { applyDrag.call(that, options); } }, 5); }, mouseUpAfter); // stop execution as applyDrag has been called again return; } // Sortable is using a fixed height placeholder meaning items jump up and down as you drag variable height items into fixed height placeholder placeHolder = placeHolder && $(this).parent().find(placeHolder); if (!placeHolder && (direction === 'down')) { // need to move at least as far as this item and or the last sibling if ($(this).outerHeight() > $(sibling).outerHeight()) { moveVerticalAmount += $(this).outerHeight() - $(sibling).outerHeight(); } moveVerticalAmount += extraDrag(sibling); dragPastBy += extraDrag(sibling); } else if (direction === 'up') { // move a little extra to ensure item clips into next position moveVerticalAmount -= Math.max(extraDrag(this), 5); } else if (direction === 'down') { // moving down with a place holder if (placeHolder.height() < $(this).height()) { moveVerticalAmount += Math.max(placeHolder.height(), 5); } else { moveVerticalAmount += extraDrag(sibling); } } if (sibling[0] !== $(this)[0]) { // step through so that the UI controller can determine when to show the placeHolder slideUpTo(x, y, moveVerticalAmount, dragPastBy); } else { if (window.console) { console.log('simulate.drag-sortable.js WARNING: Could not move as at top or bottom already'); } } setTimeout(function() { dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y + moveVerticalAmount })); }, 5); setTimeout(function() { dispatchEvent(handle, 'mouseup', createEvent('mouseup', handle, { clientX: x, clientY: y + moveVerticalAmount })); }, mouseUpAfter); }; // iterate and move each matched element return this.each(applyDrag); }; // fire mouse events, go half way, then the next half, so small mouse movements near target and big at the start function slideUpTo(x, y, targetOffset, goPastBy) { var moveBy, offset; if (!goPastBy) { goPastBy = 0; } if ((targetOffset < 0) && (goPastBy > 0)) { goPastBy = -goPastBy; } // ensure go past is in the direction as often passed in from object height so always positive // go forwards including goPastBy for (offset = 0; Math.abs(offset) + 1 < Math.abs(targetOffset + goPastBy); offset += ((targetOffset + goPastBy - offset)/2) ) { dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y + Math.ceil(offset) })); } offset = targetOffset + goPastBy; dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y + offset })); // now bounce back for (; Math.abs(offset) - 1 >= Math.abs(targetOffset); offset += ((targetOffset - offset)/2) ) { dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y + Math.ceil(offset) })); } dispatchEvent(document, 'mousemove', createEvent('mousemove', document, { clientX: x, clientY: y + targetOffset })); } function createEvent(type, target, options) { var evt; var e = $.extend({ target: target, preventDefault: function() { }, stopImmediatePropagation: function() { }, stopPropagation: function() { }, isPropagationStopped: function() { return true; }, isImmediatePropagationStopped: function() { return true; }, isDefaultPrevented: function() { return true; }, bubbles: true, cancelable: (type != "mousemove"), view: window, detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0, relatedTarget: undefined }, options || {}); if ($.isFunction(document.createEvent)) { evt = document.createEvent("MouseEvents"); evt.initMouseEvent(type, e.bubbles, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget || document.body.parentNode); } else if (document.createEventObject) { evt = document.createEventObject(); $.extend(evt, e); evt.button = { 0:1, 1:4, 2:2 }[evt.button] || evt.button; } return evt; } function dispatchEvent(el, type, evt) { if (el.dispatchEvent) { el.dispatchEvent(evt); } else if (el.fireEvent) { el.fireEvent('on' + type, evt); } return evt; } function findCenter(el) { var elm = $(el), o = elm.offset(); return { x: o.left + elm.outerWidth() / 2, y: o.top + elm.outerHeight() / 2 }; } // // plugin defaults // $.fn.simulateDragSortable.defaults = { move: 0 }; })(jQuery); ================================================ FILE: src/rails_admin/abstract-select.js ================================================ import jQuery from "jquery"; import "jquery-ui/ui/widget.js"; (function ($) { "use strict"; $.widget("ra.abstractSelect", { options: { createQuery: function (query) { if ($.isEmptyObject(this.scopeBy)) { return { query: query }; } else { const filterQuery = {}; for (var field in this.scopeBy) { const targetField = this.scopeBy[field]; const targetValue = $(`[name$="[${field}]"]`).val(); if (!filterQuery[targetField]) { filterQuery[targetField] = []; } filterQuery[targetField].push( targetValue ? { o: "is", v: targetValue } : { o: "_blank" } ); } return { query: query, f: filterQuery }; } }, scopeBy: {}, }, }); })(jQuery); ================================================ FILE: src/rails_admin/base.js ================================================ import Rails from "@rails/ujs"; import "@hotwired/turbo-rails"; import "./jquery"; import "./vendor/jquery_nested_form"; import "bootstrap"; // These jQuery-UI indirect dependencies need to be preloaded to be used within Import maps import "jquery-ui/ui/version.js"; import "jquery-ui/ui/keycode.js"; import "jquery-ui/ui/position.js"; import "jquery-ui/ui/safe-active-element.js"; import "jquery-ui/ui/data.js"; import "jquery-ui/ui/ie.js"; import "jquery-ui/ui/scroll-parent.js"; import "jquery-ui/ui/unique-id.js"; import "jquery-ui/ui/widget.js"; import "jquery-ui/ui/widgets/menu.js"; import "jquery-ui/ui/widgets/mouse.js"; import "./abstract-select"; import "./filter-box"; import "./filtering-multiselect"; import "./filtering-select"; import "./nested-form-hooks"; import "./remote-form"; import "./sidescroll"; import "./ui"; import "./widgets"; if (!window._rails_loaded) { Rails.start(); } ================================================ FILE: src/rails_admin/filter-box.js ================================================ import jQuery from "jquery"; import I18n from "./i18n"; import flatpickr from "flatpickr"; (function ($) { var filters; $.filters = filters = { append: function (options) { var field_label = options["label"]; var field_name = options["name"]; var field_type = options["type"]; var field_value = options["value"] || ""; var field_operator = options["operator"]; var operators = options["operators"]; var index = options["index"]; var value_name = "f[" + field_name + "][" + index + "][v]"; var operator_name = "f[" + field_name + "][" + index + "][o]"; var control = null; var additional_control = null; if (operators.length > 0) { control = $( '' ).prop("name", operator_name); operators.forEach((operator) => { var element = this.build_operator(operator, options); if (!element) { return; } if (element.prop("value") === field_operator) { element.prop("selected", true); } control.append(element); }); if (control.find("[data-additional-fieldset]").length > 0) { control.addClass("switch-additional-fieldsets"); } } switch (field_type) { case "boolean": control && control .prop("name", value_name) .find("option") .each(function () { if ($(this).attr("value") === field_value) $(this).attr("selected", true); }); break; case "date": case "datetime": case "timestamp": case "time": additional_control = $.map( [undefined, "-∞", "∞"], function (placeholder, index) { var visible = index == 0 ? !field_operator || field_operator == "default" : field_operator == "between"; return $('') .addClass(index == 0 ? "default" : "between") .css("display", visible ? "inline-block" : "none") .html( $( '' ) .addClass(field_type == "date" ? "date" : "datetime") .prop("name", value_name + "[]") .prop("value", field_value[index] || "") .prop( "size", field_type == "date" || field_type == "time" ? 20 : 25 ) .prop("placeholder", placeholder) ); } ); break; case "enum": if (control) { var multiple = field_value instanceof Array; control = control .prop("name", multiple ? value_name + "[]" : value_name) .prop("multiple", multiple) .add( $('').append( $("").addClass( "fas fa-" + (multiple ? "minus" : "plus") ) ) ); control.find("option").each(function () { const value = $(this).attr("value"); if ( multiple ? field_value.includes(value) : value === field_value ) $(this).attr("selected", true); }); if (multiple) control.find("option[value^=_],option[disabled]").hide(); } break; case "citext": case "string": case "text": case "belongs_to_association": case "has_one_association": additional_control = $( '' ) .css( "display", field_operator == "_present" || field_operator == "_blank" ? "none" : "inline-block" ) .prop("name", value_name) .prop("value", field_value); break; case "integer": case "decimal": case "float": additional_control = $( '' ) .css( "display", !field_operator || field_operator == "default" ? "inline-block" : "none" ) .prop("type", field_type) .prop("name", value_name + "[]") .prop("value", field_value[0] || "") .add( $( '' ) .css( "display", field_operator == "between" ? "inline-block" : "none" ) .prop("type", field_type) .prop("name", value_name + "[]") .prop("value", field_value[1] || "") ) .add( $( '' ) .css( "display", field_operator == "between" ? "inline-block" : "none" ) .prop("type", field_type) .prop("name", value_name + "[]") .prop("value", field_value[2] || "") ); break; default: control = $( '' ) .prop("name", value_name) .prop("value", field_value); break; } var filterContainerId = field_name + "-" + index + "-filter-container"; $("#" + filterContainerId).remove(); var $content = $("
    ") .attr("id", filterContainerId) .addClass("filter d-inline-block my-1") .append( $( '' ) .append('') .append(document.createTextNode(field_label)) ) .append(" ") .append(control) .append(" ") .append(additional_control); $("#filters_box").append($content); $content.find(".date, .datetime").each(function () { flatpickr( this, $.extend( { dateFormat: "Y-m-dTH:i:S", altInput: true, locale: I18n.locale, }, options["datetimepicker_options"] ) ); }); $("hr.filters_box:hidden").show("slow"); }, build_operator: function (operator, options) { if (operator instanceof Object) { var element = $(""); element.text(operator.label); delete operator.label; for (const key in operator) { element.attr(key, operator[key]); } return element; } switch (operator) { case "_discard": return $(''); case "_separator": return $(''); case "_present": return $('').text( I18n.t("is_present") ); case "_blank": return $('').text(I18n.t("is_blank")); case "_not_null": return $('').text( I18n.t("is_present") ); case "_null": return $('').text(I18n.t("is_blank")); case "true": return $('').text(I18n.t("true")); case "false": return $('').text(I18n.t("false")); case "today": return $('').text(I18n.t("today")); case "yesterday": return $('').text( I18n.t("yesterday") ); case "this_week": return $('').text( I18n.t("this_week") ); case "last_week": return $('').text( I18n.t("last_week") ); case "like": return $( '' ).text(I18n.t("contains")); case "not_like": return $( '' ).text(I18n.t("does_not_contain")); case "is": return $( '' ).text(I18n.t("is_exactly")); case "starts_with": return $( '' ).text(I18n.t("starts_with")); case "ends_with": return $( '' ).text(I18n.t("ends_with")); case "default": var label; switch (options.type) { case "date": case "datetime": case "timestamp": label = I18n.t("date"); break; case "time": label = I18n.t("time"); break; case "integer": case "decimal": case "float": label = I18n.t("number"); break; } return $( '' ).text(label); case "between": return $( '' ).text(I18n.t("between_and_")); default: return null; } }, }; $(document).on("click", "#filters a", function (e) { e.preventDefault(); $.filters.append( $.extend( { index: $.now().toString().slice(6, 11) }, $(this).data("options") ) ); }); $(document).on("click", "#filters_box .delete", function (e) { e.preventDefault(); var form = $(this).parents("form"); $(this).parents(".filter").remove(); !$("#filters_box").children().length && $("hr.filters_box:visible").hide("slow"); }); $(document).on("click", "#filters_box .switch-select", function (e) { e.preventDefault(); var select = $(this).siblings("select"); select.attr("multiple", !select.attr("multiple")); select.attr( "name", select.attr("multiple") ? select.attr("name") + "[]" : select.attr("name").replace(/\[\]$/, "") ); select.find("option[value^=_],option[disabled]").toggle(); $(this).find("i").toggleClass("fa-plus fa-minus"); }); $(document).on( "change", "#filters_box .switch-additional-fieldsets", function (e) { var selected_option = $(this).find("option:selected"); var klass = $(selected_option).data("additional-fieldset"); if (klass) { $(this) .siblings(".additional-fieldset:not(." + klass + ")") .hide("slow"); $(this) .siblings("." + klass) .show("slow"); } else { $(this).siblings(".additional-fieldset").hide("slow"); } } ); })(jQuery); ================================================ FILE: src/rails_admin/filtering-multiselect.js ================================================ import jQuery from "jquery"; import "jquery-ui/ui/widget.js"; import I18n from "./i18n"; (function ($) { $.widget("ra.filteringMultiselect", $.ra.abstractSelect, { _cache: {}, options: { sortable: false, removable: true, regional: { add: "Add", chooseAll: "Choose all", clearAll: "Clear all", down: "Down", remove: "Remove", search: "Search", up: "Up", }, searchDelay: 400, remote_source: null, xhr: false, }, wrapper: null, filter: null, collection: null, addAll: null, add: null, remove: null, up: null, down: null, selection: null, removeAll: null, _create: function () { this._cache = {}; this._build(); this._buildCache(); this._bindEvents(); }, _build: function () { var i; this.wrapper = this.element.siblings( '.ra-multiselect[data-input-for="' + this.element.attr("id") + '"]' ); // Prevent duplication on browser back if (this.wrapper.length > 0) { this.filter = this.wrapper.find("input.ra-multiselect-search"); this.collection = this.wrapper.find("select.ra-multiselect-collection"); this.addAll = this.wrapper.find("a.ra-multiselect-item-add-all"); this.add = this.wrapper.find("a.ra-multiselect-item-add"); this.remove = this.wrapper.find("a.ra-multiselect-item-remove"); this.up = this.wrapper.find("a.ra-multiselect-item-up"); this.down = this.wrapper.find("a.ra-multiselect-item-down"); this.selection = this.wrapper.find("select.ra-multiselect-selection"); this.removeAll = this.wrapper.find("a.ra-multiselect-item-remove-all"); return; } this.wrapper = $('
    ').attr( "data-input-for", this.element.attr("id") ); this.wrapper.insertAfter(this.element); var header = $('
    '); this.filter = $( '' ); header.append(this.filter); this.wrapper.append(header); var columns = { left: $('
    '), center: $('
    '), right: $('
    '), }; for (i in columns) { if (columns.hasOwnProperty(i)) { this.wrapper.append(columns[i]); } } this.collection = $(''); this.collection.addClass("form-control ra-multiselect-collection"); this.addAll = $( '' + this.options.regional.chooseAll + "" ); columns.left.html(this.collection).append(this.addAll); this.collection.wrap('
    '); this.add = $( '' ).attr("title", this.options.regional.add); columns.center.append(this.add); if (this.options.removable) { this.remove = $( '' ).attr("title", this.options.regional.remove); columns.center.append(this.remove); } if (this.options.sortable) { this.up = $( '' ).attr("title", this.options.regional.up); this.down = $( '' ).attr("title", this.options.regional.down); columns.center.append(this.up).append(this.down); } this.selection = $( '' ); columns.right.append(this.selection); if (this.options.removable) { this.removeAll = $( '' + this.options.regional.clearAll + "" ); columns.right.append(this.removeAll); } this.selection.wrap('
    '); this.element.css({ display: "none" }); this.tooManyObjectsPlaceholder = $('") .prop("value", matches[filtered[i]].id) .prop("title", matches[filtered[i]].label) .text(matches[filtered[i]].label); widget.collection.append(newOptions); } } else { widget.collection.html(widget.noObjectsPlaceholder); } }); }, /* * Cache key is stored in the format `o_") .prop("value", option.value) .prop("selected", true) ); } }); $(options).appendTo(this.selection).prop("selected", false); }, _move: function (direction, options) { var widget = this; if (direction == "up") { options.each(function (i, option) { var prev = $(option).prev(); if (prev.length > 0) { var el = widget.element.find( 'option[value="' + option.value + '"]' ); var el_prev = widget.element.find( 'option[value="' + prev[0].value + '"]' ); el_prev.before(el); prev.before($(option)); } }); } else { $.fn.reverse = [].reverse; // needed to lower last items first options.reverse().each(function (i, option) { var next = $(option).next(); if (next.length > 0) { var el = widget.element.find( 'option[value="' + option.value + '"]' ); var el_next = widget.element.find( 'option[value="' + next[0].value + '"]' ); el_next.after(el); next.after($(option)); } }); } }, selected: function (value) { if ( this.selection[0].querySelectorAll('option[value="' + value + '"]')[0] ) { return true; } }, destroy: function () { this.wrapper.remove(); this.element.css({ display: "inline" }); $.Widget.prototype.destroy.apply(this, arguments); }, }); })(jQuery); ================================================ FILE: src/rails_admin/filtering-select.js ================================================ import jQuery from "jquery"; import "jquery-ui/ui/widget.js"; import "jquery-ui/ui/widgets/autocomplete.js"; import I18n from "./i18n"; (function ($) { "use strict"; $.widget("ra.filteringSelect", $.ra.abstractSelect, { options: { minLength: 0, searchDelay: 200, remote_source: null, source: null, xhr: false, }, button: null, input: null, select: null, filtering_select: null, _create: function () { this.filtering_select = this.element.siblings( '[data-input-for="' + this.element.attr("id") + '"]' ); // When using the browser back and forward buttons, it is possible that // the autocomplete field will be cached which causes duplicate fields // to be generated. if (this.filtering_select.length > 0) { this.input = this.filtering_select.children("input"); this.button = this.filtering_select.children(".input-group-btn"); } else { this.element.hide(); this.filtering_select = this._inputGroup(this.element.attr("id")); this.input = this._inputField(); this.button = this._buttonField(); } this.clearOption = $('').append( ' ' + $("").text(I18n.t("clear")).html() ); this.noObjectsPlaceholder = $('